<objective> Build the Omp extension: detached autostart under the startup budget, single-label status with reason plus time beside it, the retry command, and the tool_call write/edit guard.
Purpose: Deliver OMP-01 to OMP-05 per P5-D4 to P5-D6, P5-D8, and P5-D14, coding only against the APIs plan 05-02 verified. Output: Plugin module, its test file, and the package.json omp field. </objective>
<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context>
<context> @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/05-omp-plugin-mcp-and-acp-surfaces/05-CONTEXT.md @docs/integration/omp.md </context>
<tasks>
<task type="tdd"> <name>Task 1: Detached autostart under startupBudgetMs with overrun settle (P5-D4, P5-D8)</name> <files>src/omp/plugin.ts, test/omp/plugin.test.ts</files> <read_first> - docs/integration/omp.md Q2 (from plan 05-02; the verified session_start registration call, timing budget, and timer create/clear calls to use) - src/realm/compose.ts (from plan 05-01; createRealmHandle signature and RealmHandle.close) - src/lifecycle/controller.ts lines 142-156 (create startup budget and STARTUP_BUDGET_EXCEEDED mapping to mirror) - src/core/policy.ts lines 27-37 (loadPolicyFile and the startupBudgetMs default 10000) - test/support/fake-execution.ts lines 46-63 (setDelay and failNext for the overrun drive) - test/support/fake-tools.ts lines 153-200 (makeHarness style for session-plus-dispatcher fixtures) </read_first> <behavior> - Test (OMP-01): on session start the plugin launches the realm without awaiting it on the UI path (the start handler returns while the realm is still connecting) - Test (OMP-01): when startup exceeds startupBudgetMs the state settles to disconnected with reason STARTUP_BUDGET_EXCEEDED and the session continues (no throw reaches the host) - Test (OMP-01): a start inside the budget reaches realm active - Test titles cite OMP-01 </behavior> <action>Write the failing tests first per behavior, then implement the autostart in src/omp/plugin.ts per P5-D8: register the verified session_start handler from 05-02 using only the verified registration call; inside it build the handle through createRealmHandle (the plugin never constructs adapters itself) and call create without awaiting it on the UI path; arm a timer for policy.startupBudgetMs using only the verified timer calls, and on expiry settle to the disconnected display with reason STARTUP_BUDGET_EXCEEDED while the session continues, clearing the timer on settle. Use only existing error codes. Drive the tests with a delayed fake execution service for both the inside-budget and overrun cases, with a fake ExtensionAPI host capturing registered handlers, timers, and thrown errors. (Implements OMP-01 per P5-D4 and P5-D8; OQ-P5-04 default: the policy value unchanged.)</action> <acceptance_criteria> - test/omp/plugin.test.ts asserts detached launch, overrun settle with session continuation, and inside-budget active - src/omp/plugin.ts uses only the 05-02-verified registration and timer calls </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/omp/plugin.test.js</automated> </verify> <done>Detached autostart green with budget settle and session continuation.</done> </task>
<task type="tdd"> <name>Task 2: Single-label status with reason plus time, and the retry command (P5-D5, P5-D14)</name> <files>src/omp/plugin.ts, test/omp/plugin.test.ts</files> <read_first> - src/omp/plugin.ts (from Task 1 of this plan; the handle and event wiring the status follows) - docs/integration/omp.md Q3 (from plan 05-02; the verified setStatus key-plus-text call and the no-tooltip limitation) - src/core/status.ts lines 7-27 (STATUS_LABELS and the StatusSnapshot fields reason and lastTransitionAt) - src/core/status.ts lines 38-54 (displayStatus: degraded keeps the active label with a flag) - src/tools/dispatch.ts lines 195-214 (retry by-state table the command reuses) - test/lifecycle/mutation-gate.test.ts lines 38-65 (CORE-05 title style to mirror for label assertions) </read_first> <behavior> - Test (OMP-02): at launch and on every transition the indicator shows exactly one label, realm active or realm disconnected, from displayStatus, with a text-plus-colour cue readable without colour - Test (OMP-03): the reason and last-transition time appear in the second status item or command output on every transition (deviation from literal tooltip per P5-D5) - Test (OMP-04): the retry command moves disconnected or error to connecting, then to active or error, by calling ToolDispatcher.call with retry - Test titles cite OMP-02, OMP-03, or OMP-04 </behavior> <action>Write the failing tests first per behavior, then implement the status and retry in src/omp/plugin.ts per P5-D5 and P5-D14: subscribe to the handle controller events and on every transition render displayStatus(snapshot).label through the verified setStatus call with a text marker plus colour that stays readable without colour (never invent a second label; degraded renders the active label with its flag); write snapshot.reason and snapshot.lastTransitionAt to the verified second status item or command output recorded by 05-02 (OQ-P5-05 default: whichever verified surface exists; the test asserts the content, not the widget); register the retry command with only the verified command-registration call, and implement it as ToolDispatcher.call with the retry map and nothing else. Record the P5-D5 tooltip deviation in the module doc comment. (Implements OMP-02 to OMP-04.)</action> <acceptance_criteria> - test/omp/plugin.test.ts asserts one shared label at launch and every transition, reason-plus-time content on each transition, and the retry state path through the dispatcher - The plugin renders no label outside STATUS_LABELS and adds no retry logic beside the dispatcher call </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/omp/plugin.test.js</automated> </verify> <done>Status and retry green with shared labels and dispatcher-owned retry.</done> </task>
<task type="tdd"> <name>Task 3: tool_call write/edit guard with manifest entry (P5-D4, P5-D6)</name> <files>src/omp/plugin.ts, test/omp/plugin.test.ts, package.json</files> <read_first> - docs/integration/omp.md Q4 (from plan 05-02; the verified tool_call hook behaviour, its block-on-error-or-timeout rule, and the carried OPEN status) - src/core/policy.ts lines 128-153 (decideMutation: required denies non-active, advisory warns; the guard mirrors this, never redefines it) - src/tools/dispatch.ts lines 91-107 (dispatcher constructor shape; the guard reads mode and state, never the dispatcher internals) - docs/integration/omp.md Q1 (from plan 05-02; the omp manifest field shape to copy into package.json) </read_first> <behavior> - Test (OMP-05): in required mode with a non-active realm, the tool_call handler refuses host write and edit (the block fires; the test proves the refusal, not the host's cooperation) - Test (OMP-05): in advisory mode the same call proceeds with a visible warning and no silent fallback - Test (OMP-05): read, list, status, and retry pass the guard untouched - Test titles cite OMP-05 </behavior> <action>Write the failing tests first per behavior, then implement the guard in src/omp/plugin.ts per P5-D6 using only the verified tool_call hook name and block semantics from 05-02: when policy mode is required and the controller snapshot is not active, refuse the built-in write and edit calls through the hook's documented block mechanism; in advisory mode attach the visible warning and let the call through; never silently fall back to an unsandboxed mutation. Record the OQ-P5-02 residual-risk statement both as a doc comment on the guard and in omp.md: the block holds inside the extension pipeline (proven by test); any host path that bypasses the extension pipeline is named as not covered. In the same task add the omp field to package.json pointing at the compiled entry for src/omp/plugin.ts under dist (per the Q1 shape), and assert in the test that the field exists and resolves to the built file. (Implements OMP-05 per P5-D4 and P5-D6.)</action> <acceptance_criteria> - test/omp/plugin.test.ts asserts required-mode refusal, advisory-mode warning, non-mutating pass-through, and the omp field resolving to the built entry - The residual-risk statement exists in both the module doc comment and omp.md </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/omp/plugin.test.js</automated> </verify> <done>Guard green with block proven, warning path, manifest entry, and residual risk recorded.</done> </task>
</tasks>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| session_start -> autostart | Host lifecycle crosses into realm startup; the UI path never awaits it and the budget timer always settles |
| tool_call -> host tools | Host tool invocations cross the guard; required mode refuses non-active writes, advisory warns, nothing is silent |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-05-09 | D (Denial of Service) | src/omp/plugin.ts | high | mitigate | Autostart never awaits on the UI path and the startupBudgetMs timer always settles to disconnected with STARTUP_BUDGET_EXCEEDED, so a hung startup cannot freeze the session (P5-D8); the detached-launch and overrun tests prove both paths |
| T-05-10 | T (Tampering) | src/omp/plugin.ts | high | mitigate | The tool_call guard refuses required-mode writes and edits while non-active and warns in advisory mode, mirroring decideMutation without redefining it; the block test proves the refusal fires and the residual risk names the unbypassable paths (P5-D6, OQ-P5-02) |
| T-05-11 | S (Spoofing) | src/omp/plugin.ts | medium | mitigate | Status renders only displayStatus labels with a text-plus-colour cue, so a degraded or error state can never present as a distinct trusted label (P5-D5); the one-label test pins this on every transition |
| </threat_model> |
<verification> npx tsc -p tsconfig.json && node --test dist/test/omp/plugin.test.js; grep test/omp/plugin.test.ts for OMP-01, OMP-02, OMP-03, OMP-04, and OMP-05 in test titles. </verification>
<success_criteria> Detached autostart with budget settle, single shared label with reason plus time, dispatcher-owned retry, and a proven tool_call guard with manifest entry and residual risk all green using only verified APIs. </success_criteria>
Artifacts this phase produces (contribution of this plan)
src/omp/plugin.ts(new source): session_start autostart, status rendering, retry command, tool_call guard with residual-risk comment (P5-D4 to P5-D6, P5-D8, P5-D14, P5-D5 deviation noted).test/omp/plugin.test.ts(new): OMP-01 to OMP-05 cases against fake host plus fake adapters.package.json(edited): the omp field pointing at the compiled entry under dist (P5-D4, Q1 shape).
<output> Create .planning/phases/05-omp-plugin-mcp-and-acp-surfaces/05-05-SUMMARY.md when done </output>