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

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.
| Develop | Inspect | Deliver | Remember |
|---|---|---|---|
| Clarify, research, approve, implement, review | Focused security, performance, and quality audits | Explicitly authorized feature-branch PR delivery | Durable 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.
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.
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.
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.
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 name | When disabled |
|---|---|
research | Skip the package-mandatory research stage; still inspect essential source code, callers, and behavior needed for the request. |
plan | Skip the package-mandatory planner draft and plan approval; independently required authorization still applies. |
review | Skip the automatic correctness review; security review remains independently controlled. |
security-review | Skip the automatic security review; correctness review remains independently controlled. |
backend-memory | Skip automatic native-memory fact search/save guidance, not explicit memory actions or host retention. |
graphify-memory | Skip 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.
| Skill | Selection and responsibility |
|---|---|
us-workflow | Natural development requests; pre-composition risk triage and applicable effective stages |
us-ignore-workflow | Explicit one-request fast lane; changes no repository/profile settings |
us-grill-me | Consequential unresolved product/engineering choices; inspect before asking |
us-research | Current source/callers, conventions, documentation, and applicable memory when selected |
us-plan | Applicable planner draft, concrete acceptance/ownership, one approval |
us-implement | Authorized implementation/repairs, inline focused or required classified workers |
us-review | Fresh required/selected evidence-based correctness review |
us-memory | Explicit or applicable automatic recall and verified graph/fact updates |
us-concise | Caveman-lite context/prose guidance; preserves technical meaning and exact text |
us-check-security | Read-only trust-boundary audit or post-correctness security review |
us-check-performance | Measured representative performance audit, not speculative optimization |
us-check-code-quality | Evidence-backed cohesion/duplication audit and explicit guidance merge |
us-improve-codebase-architecture | Read-only architectural opportunities, then approved focused refactoring |
us-improve-code-coverage | Measured coverage gaps, then approved behavior-focused test improvements |
us-ship-backlog-item | One item, isolated feature branch, reviewed linked PR, and Done only after confirmed merge |
us-open-pr | Explicitly authorized reviewed feature-branch PR delivery |
us-approve-work | Explicitly 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>.
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.
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.
| Role | Native task agent | Example model |
|---|---|---|
research | scout | openai-codex/gpt-5.6-luna:xhigh |
frontend | frontend | openai-codex/gpt-5.6-terra:high |
backend | backend | openai-codex/gpt-5.6-terra:high |
implementation | task | openai-codex/gpt-5.6-terra:high |
review | reviewer | openai-codex/gpt-6-astra:medium |
security | security-reviewer | openai-codex/gpt-5.6-sol:high |
plan | planner | openai-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:hooks/register.ts 375 lines1import 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