SLOPSHOPPER

Useful Skills

Claude Code engineering workflows, safety reviews, and local memory tools

newcommandtoastprocess
v0.2.36GPL-3.0-onlyupdated 2026-10-08MSpiechowicz/harness-useful-skills
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · useful-skills
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /useful-skills ⎿ useful-skills: dev ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Useful Skills for Oh My Pi

From a natural-language request to reviewed, verified work.

Owned us-* skills for development, focused audits, delivery, and project memory. OMP discovers their descriptions; the model follows the relevant instructions using native tools and workers.

DevelopInspectDeliverRemember
Clarify, research, approve, implement, reviewFocused security, performance, and quality auditsExplicitly authorized feature-branch PR deliveryDurable facts alongside code relationships

Start here: Installation · Claude Code · Workflow & skills · Model configuration · Memory

Reference: Management · Audits & delivery · Updates · Development · Attribution

No Anvil dependency, custom workflow executor, model router, phase database, or stop-hook continuation loop.

Installation

Requires OMP with marketplace support and Node.js 22+. No npm dependencies, build step, host configuration, prompt file, or startup flag is required.

omp plugin marketplace add MSpiechowicz/harness-useful-skills
omp plugin install harness-useful-skills@harness-useful-skills --scope user

These commands and ./useful-skills install below require a published release of the new catalog; until then, load this source checkout with omp -e /absolute/path/to/harness-useful-skills as shown below.

Restart OMP after installation or update. Ordinary startup adds discovery and, when enabled for the current repository in the active profile, narrowly scoped session-local workflow-stage guidance. Use --scope project from the project directory for a project installation, or prefix both commands with omp --profile NAME for an isolated profile. Installation scope is separate from the repository-local workflow master and six profile-wide stage preferences. The marketplace and plugin are both named harness-useful-skills; the OMP plugin ID is harness-useful-skills@harness-useful-skills. OMP does not import this plugin's Claude Code MCP server: its manifest declares empty mcpServers. Claude Code loads its own separate MCP tools when installed there.

Remove or migrate an OMP installation

Uninstall each historic ID that is installed in the relevant scope; check omp plugin list --json if unsure. These commands remove installed plugins, not their marketplace registration:

omp plugin uninstall oh-my-pi-useful-skills@omp-useful-skills --scope user
omp plugin uninstall useful-skills@omp-useful-skills --scope user

An old installation is not renamed by updating a checkout or publishing a release, and loading the checkout does not remove its startup message. You can uninstall it before the new catalog is published. If no plugins need the old marketplace, optionally remove its registration with omp plugin marketplace remove omp-useful-skills. After the new catalog is published, use the installation commands above to add the new marketplace and install the new ID. The updater does not migrate old installations or marketplace registrations.

For a project installation, run these commands from that project directory with --scope project instead of --scope user. For an isolated profile, prefix each command with omp --profile NAME instead of omp, retaining the original scope and project directory. Restart OMP after installation.

For local development, load the package directory, not the extension file alone:

omp -e /absolute/path/to/harness-useful-skills

./useful-skills install [--scope user|project] [--profile NAME] installs the published marketplace plugin, not the checkout itself. Source checkouts are updated with git pull --ff-only; the updater never overwrites them.

Claude Code

The Claude Code adapter requires Node.js 22+ and a marketplace installation. For a local checkout, use its absolute path:

claude plugin marketplace add /absolute/path/to/harness-useful-skills --scope user
claude plugin install useful-skills@useful-skills-local --scope user

Or add the same repository as a GitHub marketplace:

claude plugin marketplace add MSpiechowicz/harness-useful-skills
claude plugin install useful-skills@useful-skills-local --scope user

The Claude plugin ID remains useful-skills@useful-skills-local in either case. Install through the marketplace, not claude --plugin-dir .: that loads the repository-root OMP skills/ instead of the marketplace's selected Claude skills. Restart Claude Code after installation. The OMP installation, commands, and profile behavior above remain unchanged.

Claude exposes 17 namespaced skills (for example, /useful-skills:us-workflow) and seven native agents: planner, scout, frontend, backend, general-purpose, reviewer, and security-reviewer. All declare model: inherit; per-role models come from a Claude-owned config file instead (see Claude Code model roles). Permissions and stronger host rules remain authoritative. Plugin userConfig has seven default-true booleans: workflow, research, plan, review, security_review, backend_memory, and graphify_memory. Configure them in Claude Code's /plugin configure flow or at install time with repeatable --config key=false flags (for example, --config workflow=false). Disabling workflow suppresses automatic stages without changing the other saved values. A direct /useful-skills:us-ignore-workflow invocation bypasses package-mandatory stages for one request only; quoted or copied text is not an opt-out. With a recent Claude Code that supports mods, /useful-skills is also available: without arguments it opens a menu (Settings, Status, Browse, Help); with arguments it runs the commands in Management. Saved settings mirror OMP's layout under the Claude config directory ($CLAUDE_CONFIG_DIR or ~/.claude): the per-repository workflow master is useful-skills/workflow-settings/repositories/<sha256(scopeRoot)>/workflow.json (same repository scope rules as OMP) and the six profile-wide stage switches are useful-skills/workflow-settings/<stage>.json, all private. For each setting a saved file wins, then the /plugin option, then enabled; an unreadable or unsafe saved file makes that setting unknown with no fallback. /plugin options remain fallbacks, and the only control on Claude Code versions without mods.

The Claude plugin adds a separate local stdio MCP server with five tools: memory_status, memory_search, memory_save, graph_build, and graph_query. Facts are project-scoped private files under Claude plugin data, searched by literal text; they are not OMP's native Mnemopi facts and are not shared with them. Graphify is the shared code-relationship implementation, with lazy managed setup on explicit graph actions rather than at startup. Do not save credentials, personal data, transcripts, or tool dumps; secret detection is only a backstop. A Bash pre-tool guard denies recognized dangerous commands, and successful tool responses can be redacted by PostToolUse; failed third-party tool responses (PostToolUseFailure) cannot receive equivalent redaction. These protections are not a sandbox or a substitute for Claude's permissions, user approval, or publication controls.

In Claude Code, /useful-skills doctor also prepares memory for the session's project (see Management); it never writes the workflow or stage settings files.

To submit Claude Code to the Claude plugin directory, node scripts/build-claude-plugin.js generates a Claude-only plugin bundle at plugins/claude/; the self-hosted marketplace install above is unchanged. plugins/claude/ is generated: run the script after editing Claude runtime files or skills (CI runs it with --check). Only plugins/claude/README.md is hand-written, and generated output is outside the 500-line cap.

OMP's updater and release process do not update the Claude installation automatically. There is no Codex runtime adapter yet.

Skill-led development

Ask naturally: “I want to build a rocket simulator.” Before composing stages, us-workflow inspects current source, relevant callers, acceptance, authorization, and risk, then briefly explains the selected lane. With the repository master enabled, the default full stages apply to normal work; a clear bounded low-risk correction within an authorized outcome can instead use focused repair. Size, urgency, a stale plan, or “already delivered” alone does not qualify.

flowchart TD
    request["Development request"] --> settings{"Repository master<br/>or explicit ignore?"}
    settings -->|Disabled / direct ignore| fast["Fast lane<br/>Inspect, fix, verify, report"]
    settings -->|Enabled| inspect["Inspect current source/callers<br/>Acceptance, authorization, risk"]
    inspect --> triage{"Bounded authorized<br/>low-risk correction?"}
    triage -->|Yes| focused["Focused repair<br/>Inline scoped fix permitted"]
    focused --> extra["Select only necessary eligible stages<br/>Uncertainty, contracts, findings, impact"]
    extra --> smoke["Verify changed path<br/>Fresh selected reviews"]
    smoke --> report["Truthful report<br/>Omitted stages are skipped"]
    triage -->|No / explicit full workflow| normal["Normal applicable effective stages"]
    normal --> research["Clarify + research"]
    research --> plan["Planner draft + approval<br/>when applicable"]
    plan --> implement["Classified implementation workers"]
    implement --> verify["Verify behavior"]
    verify --> reviews["Fresh required correctness/security reviews"]
    reviews --> outcome{"Material findings?"}
    outcome -->|No| finish["Summary + applicable memory"]
    outcome -->|Yes| limit{"Repair limit?"}
    limit -->|No| repair["Repair within approval<br/>No routine re-planning"]
    repair --> verify
    limit -->|Reached| blocked["Keep work, report blocker"]

Focused repair requires current source/caller inspection, an authorized scoped fix, observable changed-path verification, and a truthful report. A current bounded repair request or prior in-scope approval can authorize it without routine new planning/approval. The parent may fix inline; enabled switches alone do not require a new scout, planner, implementation worker, full reviews, or memory cycle. Select extra eligible stages for actual uncertainty, affected contracts, findings, or security/memory impact; follow each selected stage's own rules without automatically restarting the full flow. Disabled/unknown automatic stages are never silently activated, and explicit audits/full-workflow requests and independent controls remain binding.

Use normal composition for new or material behavior, architecture, broad/ambiguous failures, uncertain scope, security-sensitive changes, or an explicit full-workflow request. Investigate ambiguity before calling a failure small. In an already active normal workflow, repairs retain its required workers and fresh reviews; do not retroactively switch lanes to omit them. Prior approval covers in-scope repair without a new planner/approval cycle. Verification failures return to implementation; every required or selected review must be fresh after affected changes, and prior reviews do not cover newly changed code. Three unsuccessful repair rounds or repeated no progress stop the loop. Skipped stages are never called complete; an applicable required memory failure prevents claiming full completion. Publication is separate and explicitly authorized.

The parent owns objective, lane selection, research scope, native todo, acceptance, applicable approval, integration, and combined verification. In normal composition, enabled research uses a read-only worker and enabled planning uses native planner after parent inspection, followed by one approval. With no applicable plan stage, a bounded request can authorize implementation, subject to independent requirements. Required implementation packages—including one package—use frontend, backend, or native task for genuinely neither (documentation/tooling); eligible focused repair may be inline. Split coherent mixed boundaries, serialize dependencies/shared files, and parallelize only ready disjoint packages. Selected focused stages retain their own dispatch rules. Current source, not historical plans/transcripts alone, governs interrupted work.

Ordinary installed startup supplies discovery and a narrow session-local stage policy while the effective repository workflow is enabled. Each prompt reconciles owned instructions using the active workspace's master and profile stage preferences, preserving unrelated guidance. Enabled settings mean stage eligibility, not automatic execution; triage permits focused inline repair. A required applicable plan permits bounded planner dispatch after inspection; required classified implementation dispatch follows appropriate authorization, including a single package. No global repository is selected and no general extra delegation is authorized. Disabled master supplies fast-lane guidance. Native /skill:us-ignore-workflow selects a one-request exception separately: the extension does not classify prompt text or persist that choice. Startup never dispatches workers, builds Graphify, writes workflow state, or starts a model workflow.

These are prompt instructions, not enforced stage transitions. They cannot guarantee model compliance or override stronger safety constraints, existing mappings, permissions, independent approval boundaries, or publication rules. Ordinary explanations and explicitly focused audits do not start the full development workflow. Plan approval does not authorize publication.

For a persistent development fast lane, run /useful-skills workflow disabled from the intended repository; /useful-skills workflow enabled restores that repository's master in the active OMP profile. The choice survives sessions/restarts without affecting other repositories. The nearest canonical Git checkout root is the scope: root, subdirectories, and symlink aliases share; separate checkouts and linked worktrees isolate, as do nested checkouts. Outside Git, canonical cwd is the scope, so distinct non-Git directories are separate. Moving a checkout changes its path-derived identity; no relocation migration is performed.

The master is stored privately at <agentDir>/useful-skills/workflow-settings/repositories/<sha256(scopeRoot)>/workflow.json; six stage files remain in <agentDir>/useful-skills/workflow-settings/. Reads create nothing. Storage retains private directories/files, bounded no-follow reads, and atomic writes. Identity discovery inspects filesystem Git markers without executing project code/hooks or using Graphify. Unreadable/unsafe identity makes only the master unknown while independently readable profile stages remain visible; unknown automatic stages do not run. Status exposes canonical scope, master file, profile stage directory, saved/effective selections, and scoped errors.

Legacy master: the old profile-level <agentDir>/useful-skills/workflow-settings/workflow.json is untouched and ignored, with no fallback, copying, or automatic migration. A missing new repository master defaults enabled. To retain an old disabled choice, enter the intended repository and reapply /useful-skills workflow disabled.

Alternatively, directly invoke native /skill:us-ignore-workflow for one request without changing saved settings. Quoted/copied invocations are not selection. The disabled-master/direct-ignore fast lane still inspects current code/callers, implements and exercises the changed path, and reports evidence, but skips package-mandatory stages. It is distinct from enabled focused repair and does not waive stronger safety/permissions, independent authorization, explicit audits, or publication controls.

Six retained profile-wide switches control eligibility for individual automatic stages across repositories; all default enabled and changing one leaves the others intact. In normal composition, apply effective stages. Focused repair selects only necessary eligible extras rather than inheriting the whole cycle. Use /useful-skills stage <name> enabled|disabled with a name from this table:

Stage nameWhen disabled
researchSkip the package-mandatory research stage; still inspect essential source code, callers, and behavior needed for the request.
planSkip the package-mandatory planner draft and plan approval; independently required authorization still applies.
reviewSkip the automatic correctness review; security review remains independently controlled.
security-reviewSkip the automatic security review; correctness review remains independently controlled.
backend-memorySkip automatic native-memory fact search/save guidance, not explicit memory actions or host retention.
graphify-memorySkip automatic Graphify graph build/query guidance, not explicit graph actions.

Disabling a repository master suppresses all automatic stages there without changing the six profile-wide saved preferences or any other repository's master. Re-enabling restores their eligibility, not proof of execution; a disabled stage remains disabled. Direct one-request ignore takes precedence for that request only. Memory opt-outs are guidance, not provider/tool bans: explicit us_memory, graph test, doctor, and native /memory remain available and do not alter backend settings or host autoRetain. Configure host retention separately if unwanted. Prompt guidance cannot guarantee model compliance.

SkillSelection and responsibility
us-workflowNatural development requests; pre-composition risk triage and applicable effective stages
us-ignore-workflowExplicit one-request fast lane; changes no repository/profile settings
us-grill-meConsequential unresolved product/engineering choices; inspect before asking
us-researchCurrent source/callers, conventions, documentation, and applicable memory when selected
us-planApplicable planner draft, concrete acceptance/ownership, one approval
us-implementAuthorized implementation/repairs, inline focused or required classified workers
us-reviewFresh required/selected evidence-based correctness review
us-memoryExplicit or applicable automatic recall and verified graph/fact updates
us-conciseCaveman-lite context/prose guidance; preserves technical meaning and exact text
us-check-securityRead-only trust-boundary audit or post-correctness security review
us-check-performanceMeasured representative performance audit, not speculative optimization
us-check-code-qualityEvidence-backed cohesion/duplication audit and explicit guidance merge
us-improve-codebase-architectureRead-only architectural opportunities, then approved focused refactoring
us-improve-code-coverageMeasured coverage gaps, then approved behavior-focused test improvements
us-ship-backlog-itemOne item, isolated feature branch, reviewed linked PR, and Done only after confirmed merge
us-open-prExplicitly authorized reviewed feature-branch PR delivery
us-approve-workExplicitly authorized approved-only commit and safe feature-branch PR against the verified GitHub default branch; commit-only stays local

Direct invocation uses OMP's actual syntax:

/skill:us-ignore-workflow
/skill:us-check-security
/skill:us-check-performance
/skill:us-check-code-quality
/skill:us-improve-codebase-architecture
/skill:us-improve-code-coverage
/skill:us-ship-backlog-item
/skill:us-open-pr
/skill:us-approve-work

No extension-command wrapper is installed for each skill. Add a matching skills/us-example/SKILL.md with a concrete single-line frontmatter description; native shallow discovery and the catalog need no JavaScript registry update. Keep substantial checklists/worker briefs in skill-local references linked through skill://us-<name>.

Jev assessment — no integration

Assessed 2026-09-23: TypeSafe's coding-agent guidance positions Jev for narrow typed decisions inside an application, not as a drop-in coding agent. It could advise on bounded classifications, but is not a workflow executor or an authority for agent dispatch or approval.

The official API documents POST https://api.typesafe.ai/v1/systemone with state and typed questions; Jev 1.13 model limits are 64k tokens total per request and 32k tokens for the state plus longest question. The community Jev API page separately claims a https://www.jevai.org/api/v1/decisions/route endpoint and a 32 KiB HTTP body cap; neither is corroborated by the official TypeSafe docs.

Sending task context would disclose prompt data to the service. TypeSafe's privacy policy says inputs are not used to train or fine-tune models but gives no fixed API-log retention period. Its Master Customer Agreement permits Customer Data processing during the term and in perpetuity for telemetry, fraud prevention, and legal purposes; telemetry may be processed without restriction, including to improve services and products. API calls consume credits under that agreement.

No Jev integration is included and no task context is sent. Any future integration requires separate explicit approval and opt-in context transmission; its output must remain advisory and cannot replace parent judgment or human approval.

Native model configuration

Useful Skills bundles native planner, frontend, and backend agents; it does not add a model router or workflow executor. Ordinary startup needs no role mapping, global host override, prompt file, or extra flag for its narrow session-local policy. The repository master and profile stage preferences determine eligibility; pre-composition triage determines applicability. Existing OMP mappings remain authoritative. Optional distinct mappings use native roles in the active profile, project configuration, overlays, or runtime options; Useful Skills never changes them.

Open /model, then the Roles view, to optionally assign and persist role mappings. This example uses only OpenAI Codex selectors; availability depends on your OMP model catalog and credentials, not on this table.

RoleNative task agentExample model
researchscoutopenai-codex/gpt-5.6-luna:xhigh
frontendfrontendopenai-codex/gpt-5.6-terra:high
backendbackendopenai-codex/gpt-5.6-terra:high
implementationtaskopenai-codex/gpt-5.6-terra:high
reviewrevieweropenai-codex/gpt-6-astra:medium
securitysecurity-revieweropenai-codex/gpt-5.6-sol:high
planplanneropenai-codex/gpt-6-astra:high

task is reserved for work genuinely neither frontend nor backend.

planner is bundled and read-only.

modelRoles:
  research: openai-codex/gpt-5.6-luna:xhigh
  frontend: openai-codex/gpt-5.6-terra:high
  backend: openai-codex/gpt-5.6-terra:high
  implementation: openai-codex/gpt-5.6-terra:high
  review: openai-codex/gpt-6-astra:medium
  security: openai-codex/gpt-5.6-sol:high
  plan: openai-codex/gpt-6-astra:high

task:
  agentModelOverrides:
    scout: "@research"
    frontend: "@frontend"
    backend: "@backend"
    task: "@implementation"
    reviewer: "@review"
    security-reviewer: "@security"
    planner:
Source 1 files
hooks/register.ts 375 lines
1import type { EngineInterface, PluginOptions, Register } from 'claude-code'
2
3// Claude Code function-hooks module: `/useful-skills`, backed by claude/command.js.
4
5const COMMAND_TIMEOUT_MS = 60_000
6const DOCTOR_TIMEOUT_MS = 600_000
7const MAX_ARGUMENTS = 8
8
9const NODE_REQUIRED = 'Useful Skills needs Node.js 22+ on PATH to run /useful-skills.'
10const FAILED = 'Useful Skills could not complete /useful-skills.'
11const CANCELLED = 'Cancelled; no settings changed.'
12const NO_CHANGE = 'No change.'
13const NO_OUTPUT = 'Useful Skills command produced no output; nothing was confirmed.'
14const WRITE_REFUSED = 'Workflow settings can only be changed from your own prompt.'
15const DOCTOR_PREPARING = 'Useful Skills doctor: preparing memory; first setup can take several minutes'
16const DOCTOR_TIMED_OUT = 'Useful Skills doctor did not finish within 10 minutes; setup may be partial — run it again.'
17const USAGE = [
18  'Usage: /useful-skills [status|workflow|stage|list|doctor|help]',
19  'Run /useful-skills help for the full list.',
20].join('\n')
21const TOO_MANY_ARGUMENTS = [`Too many arguments (at most ${MAX_ARGUMENTS}).`, USAGE].join('\n')
22
23// `doctor` alone prepares memory (writes); `doctor --check` only reports.
24const DOCTOR_SETUP: readonly string[] = ['doctor']
25const DOCTOR_CHECK: readonly string[] = ['doctor', '--check']
26
27// Boolean plugin options travel to the CLI as the env vars Claude gives hook processes.
28const OPTION_ENV: Readonly<Record<string, string>> = {
29  workflow: 'CLAUDE_PLUGIN_OPTION_WORKFLOW',
30  research: 'CLAUDE_PLUGIN_OPTION_RESEARCH',
31  plan: 'CLAUDE_PLUGIN_OPTION_PLAN',
32  review: 'CLAUDE_PLUGIN_OPTION_REVIEW',
33  security_review: 'CLAUDE_PLUGIN_OPTION_SECURITY_REVIEW',
34  backend_memory: 'CLAUDE_PLUGIN_OPTION_BACKEND_MEMORY',
35  graphify_memory: 'CLAUDE_PLUGIN_OPTION_GRAPHIFY_MEMORY',
36}
37
38// Only these origins are the person's own prompt; every other origin may only read.
39const WRITE_ORIGINS: ReadonlySet<string> = new Set(['composer', 'sdk'])
40
41// Every other subcommand can change saved settings; `doctor` from these origins only checks.
42const READ_ONLY: ReadonlySet<string> = new Set(['status', 'help', 'list', 'doctor', 'update', 'graph'])
43
44type Stage = { key: string; label: string }
45
46const STAGES_FIRST: readonly Stage[] = [
47  { key: 'research', label: 'Research' },
48  { key: 'plan', label: 'Plan' },
49  { key: 'review', label: 'Review' },
50]
51const STAGES_SECOND: readonly Stage[] = [
52  { key: 'security-review', label: 'Security review' },
53  { key: 'backend-memory', label: 'Backend memory' },
54  { key: 'graphify-memory', label: 'Graphify memory' },
55]
56
57type Run = (tokens: readonly string[]) => Promise<string>
58
59/** A menu entry either opens a submenu, goes back, or runs one CLI command. */
60type Choice = { label: string; menu?: Menu; command?: readonly string[]; back?: true }
61type Menu = { question: string; header: string; choices: readonly Choice[] }
62
63type CliResult = { exitCode: number; text: string }
64type CliRunOptions = { timeoutMs?: number; extraEnv?: Record<string, string> }
65
66class CliUnavailable extends Error {}
67class DoctorTimedOut extends Error {}
68
69function optionEnvironment(options: PluginOptions): Record<string, string> {
70  const env: Record<string, string> = {}
71
72  for (const [field, name] of Object.entries(OPTION_ENV)) {
73    const value = options[field]
74
75    if (typeof value === 'boolean') {
76      env[name] = value ? 'true' : 'false'
77    }
78  }
79
80  return env
81}
82
83/** Runs the CLI and returns its output; a failure to start it is never described in detail. */
84async function runCli(
85  $: EngineInterface,
86  env: Record<string, string>,
87  tokens: readonly string[],
88  options: CliRunOptions = {},
89): Promise<CliResult> {
90  try {
91    const cwd = await $.session.cwd()
92    const result = await $.process.run(['node', `${$.plugin.root}/claude/command.js`, ...tokens], {
93      cwd,
94      env: { ...env, ...options.extraEnv },
95      timeoutMs: options.timeoutMs ?? COMMAND_TIMEOUT_MS,
96    })
97    const text = result.stdout.trim() || result.stderr.trim()
98
99    return { exitCode: result.exitCode, text }
100  } catch {
101    throw new CliUnavailable()
102  }
103}
104
105function sameTokens(tokens: readonly string[], expected: readonly string[]): boolean {
106  return tokens.length === expected.length && tokens.every((token, index) => token === expected[index])
107}
108
109/** A toast is only a hint: one that cannot be shown, now or later, never stops the command. */
110function showToast($: EngineInterface, text: string): void {
111  try {
112    const shown: unknown = $.ui.toast(text)
113
114    // Not awaited: a toast that fails later is dropped, never an unhandled rejection.
115    Promise.resolve(shown).catch(() => {})
116  } catch {
117    // No surface to show it on; the command still runs.
118  }
119}
120
121/** Full memory setup: may run for minutes, so it gets the longest timeout the host allows. */
122async function runDoctorSetup($: EngineInterface, env: Record<string, string>, extraEnv: Record<string, string>): Promise<CliResult> {
123  showToast($, DOCTOR_PREPARING)
124
125  const startedAt = await $.clock.now()
126  const hasTimedOut = async () => (await $.clock.now()) - startedAt >= DOCTOR_TIMEOUT_MS
127  let result: CliResult
128
129  try {
130    result = await runCli($, env, DOCTOR_SETUP, { timeoutMs: DOCTOR_TIMEOUT_MS, extraEnv })
131  } catch (error) {
132    if (await hasTimedOut()) {
133      throw new DoctorTimedOut()
134    }
135
136    throw error
137  }
138
139  // A host may report a run it killed at the limit as a silent failure (a signal reads as exit 1).
140  const isSilentFailure = result.exitCode !== 0 && !result.text
141
142  if (isSilentFailure && (await hasTimedOut())) {
143    throw new DoctorTimedOut()
144  }
145
146  return result
147}
148
149/** Runs one command; every `doctor` run is told the session's project root. */
150async function runTokens($: EngineInterface, env: Record<string, string>, tokens: readonly string[]): Promise<CliResult> {
151  if (tokens[0] !== 'doctor') {
152    return runCli($, env, tokens)
153  }
154
155  const extraEnv = { CLAUDE_PROJECT_DIR: await $.session.root() }
156
157  if (sameTokens(tokens, DOCTOR_SETUP)) {
158    return runDoctorSetup($, env, extraEnv)
159  }
160
161  return runCli($, env, tokens, { extraEnv })
162}
163
164/** Saved enabled/disabled values by key; empty when the CLI cannot report them. */
165async function readSaved($: EngineInterface, env: Record<string, string>): Promise<Record<string, string>> {
166  const saved: Record<string, string> = {}
167
168  try {
169    const { exitCode, text } = await runCli($, env, ['status', '--json'])
170
171    if (exitCode !== 0) {
172      return saved
173    }
174
175    const status = JSON.parse(text)
176    const entries: Record<string, unknown> = { workflow: status?.workflow, ...status?.stages }
177
178    for (const [key, entry] of Object.entries(entries)) {
179      const value = (entry as { saved?: unknown } | undefined)?.saved
180
181      if (value === 'enabled' || value === 'disabled' || value === 'unknown') {
182        saved[key] = value
183      }
184    }
185  } catch {
186    return {}
187  }
188
189  return saved
190}
191
192function modeMenu(title: string, header: string, saved: string, subject: readonly string[]): Menu {
193  return {
194    question: `Set ${title} (saved: ${saved})?`,
195    header,
196    choices: [
197      { label: 'Enabled', command: [...subject, 'enabled'] },
198      { label: 'Disabled', command: [...subject, 'disabled'] },
199      { label: 'Back', back: true },
200    ],
201  }
202}
203
204function stagesMenu(header: string, stages: readonly Stage[], saved: Record<string, string>): Menu {
205  const choices: Choice[] = stages.map(stage => {
206    const current = saved[stage.key] ?? 'unknown'
207
208    return {
209      label: `${stage.label} (${current})`,
210      menu: modeMenu(`${stage.label} stage`, 'Stage', current, ['stage', stage.key]),
211    }
212  })
213
214  return { question: 'Which stage?', header, choices: [...choices, { label: 'Back', back: true }] }
215}
216
217function buildMenu(saved: Record<string, string>): Menu {
218  const workflow = saved.workflow ?? 'unknown'
219
220  const settings: Menu = {
221    question: 'Which setting?',
222    header: 'Settings',
223    choices: [
224      {
225        label: `Repository workflow (${workflow})`,
226        menu: modeMenu('repository workflow', 'Workflow', workflow, ['workflow']),
227      },
228      { label: 'Stages 1/2', menu: stagesMenu('Stages 1/2', STAGES_FIRST, saved) },
229      { label: 'Stages 2/2', menu: stagesMenu('Stages 2/2', STAGES_SECOND, saved) },
230      { label: 'Back', back: true },
231    ],
232  }
233
234  // Setup installs and writes; checking only reports. The person picks which, never by default.
235  const doctor: Menu = {
236    question: 'Doctor: set up memory (may install and write files) or only check it?',
237    header: 'Doctor',
238    choices: [
239      { label: 'Set up memory', command: DOCTOR_SETUP },
240      { label: 'Check only', command: DOCTOR_CHECK },
241      { label: 'Back', back: true },
242    ],
243  }
244
245  const browse: Menu = {
246    question: 'What would you like to browse?',
247    header: 'Browse',
248    choices: [
249      { label: 'List', command: ['list'] },
250      { label: 'Doctor…', menu: doctor },
251      { label: 'Back', back: true },
252    ],
253  }
254
255  return {
256    question: 'Useful Skills: what would you like to do?',
257    header: 'Menu',
258    choices: [
259      { label: 'Settings', menu: settings },
260      { label: 'Status', command: ['status'] },
261      { label: 'Browse', menu: browse },
262      { label: 'Help', command: ['help'] },
263    ],
264  }
265}
266
267/** Walks the menu tree; only an exact option label ever reaches the CLI. */
268async function chooseAndRun($: EngineInterface, run: Run, saved: Record<string, string>): Promise<string> {
269  const path: Menu[] = [buildMenu(saved)]
270  let hasAsked = false
271
272  for (;;) {
273    const menu = path[path.length - 1]!
274    let answer: string
275
276    try {
277      answer = await $.ui.ask(menu.question, { options: menu.choices.map(choice => choice.label), header: menu.header })
278    } catch {
279      // Dismissed, or no UI to ask in (`-p`): help before any prompt, otherwise cancel.
280      return hasAsked ? CANCELLED : run(['help'])
281    }
282
283    hasAsked = true
284
285    const choice = menu.choices.find(entry => entry.label === answer)
286
287    if (!choice) {
288      return NO_CHANGE
289    }
290
291    if (choice.back) {
292      path.pop()
293    } else if (choice.menu) {
294      path.push(choice.menu)
295    } else if (choice.command) {
296      return run(choice.command)
297    }
298  }
299}
300
301export const register: Register = (on, options) => {
302  const env = optionEnvironment(options)
303
304  on('session.start', async ($, e, next) => {
305    const started = await next(e)
306
307    await $.command.register({
308      name: 'useful-skills',
309      description: 'Workflow settings, status, skills, and health for Useful Skills',
310      argumentHint: '[status|workflow|stage|list|doctor|help]',
311    })
312
313    return started
314  })
315
316  on('command.run', { command: 'useful-skills' }, async ($, e) => {
317    const run: Run = async tokens => {
318      const { exitCode, text } = await runTokens($, env, tokens)
319
320      if (text) {
321        return text
322      }
323
324      // Silence is never success: a CLI that did nothing must not be reported as done.
325      return exitCode === 0 ? NO_OUTPUT : `Useful Skills command failed (exit ${exitCode}).`
326    }
327
328    try {
329      const tokens = e.args.trim().split(/\s+/).filter(Boolean)
330
331      if (tokens.length > MAX_ARGUMENTS) {
332        return { text: TOO_MANY_ARGUMENTS }
333      }
334
335      // Fail closed: a missing or unrecognized origin is read-only, with no menu.
336      const kind: unknown = e.origin?.kind
337
338      if (typeof kind !== 'string' || !WRITE_ORIGINS.has(kind)) {
339        if (tokens.length === 0) {
340          return { text: await run(['help']) }
341        }
342
343        // Memory setup writes, so from here `doctor` only ever checks.
344        if (tokens[0] === 'doctor') {
345          const isCheckable = sameTokens(tokens, DOCTOR_SETUP) || sameTokens(tokens, DOCTOR_CHECK)
346
347          return { text: isCheckable ? await run(DOCTOR_CHECK) : USAGE }
348        }
349
350        if (!READ_ONLY.has(tokens[0]!)) {
351          return { text: WRITE_REFUSED }
352        }
353      }
354
355      if (tokens.length > 0) {
356        return { text: await run(tokens) }
357      }
358
359      const saved = await readSaved($, env)
360
361      return { text: await chooseAndRun($, run, saved) }
362    } catch (error) {
363      if (error instanceof DoctorTimedOut) {
364        return { text: DOCTOR_TIMED_OUT }
365      }
366
367      if (error instanceof CliUnavailable) {
368        return { text: NODE_REQUIRED }
369      }
370
371      return { text: FAILED }
372    }
373  })
374}
375