|
| 1 | +# ZCode 0.16.5 Permission Turn Lifecycle Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Preserve the active protocol turn across an admission-time legacy completion wake so later ZCode permission requests succeed, then release the local turn at the executor's authoritative cleanup boundary. |
| 6 | + |
| 7 | +**Architecture:** Add a non-destructive completion observer alongside the existing destructive waiter in the protocol/client layer. Migrate only `executeJob` to that observer and explicitly release its local turn during unconditional teardown; retain compatibility fallbacks for injected test clients that expose only the historical interface. |
| 8 | + |
| 9 | +**Tech Stack:** Node.js 22.13, ECMAScript modules, `node:test`, the existing JSON-RPC protocol client and job executor. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## File map |
| 14 | + |
| 15 | +- Modify `scripts/lib/zcode-protocol.mjs`: share completion validation/wait registration while distinguishing destructive consumption from observation; add local turn release behavior. |
| 16 | +- Modify `scripts/lib/zcode-client.mjs`: expose `observeCompletion()` and `releaseTurn()` without changing `waitForCompletion()`. |
| 17 | +- Modify `scripts/lib/review.mjs`: use non-destructive observation for `legacyWake` and release the local turn during teardown. |
| 18 | +- Modify `tests/process-zcode.test.mjs`: cover low-level observer, permission, timeout, release, and destructive-wait invariants. |
| 19 | +- Modify `tests/job-control.test.mjs`: cover captured 0.16.5 executor ordering and success/error cleanup. |
| 20 | + |
| 21 | +### Task 1: Add non-destructive completion observation |
| 22 | + |
| 23 | +**Files:** |
| 24 | +- Modify: `scripts/lib/zcode-protocol.mjs:95-145` |
| 25 | +- Modify: `scripts/lib/zcode-client.mjs:125-135` |
| 26 | +- Test: `tests/process-zcode.test.mjs` |
| 27 | + |
| 28 | +- [ ] **Step 1: Write failing protocol tests** |
| 29 | + |
| 30 | +Add tests that construct `ZCodeProtocolClient` with `PassThrough` streams, arm a turn, start `observeCompletion(sessionId)`, emit a matching `prompt_completed`, and assert: |
| 31 | + |
| 32 | +```js |
| 33 | +const completion = protocol.observeCompletion(sessionId); |
| 34 | +protocol.handleLine(JSON.stringify({ method: 'state.updated', params: matchingCompletion })); |
| 35 | +assert.equal((await completion).reason, 'prompt_completed'); |
| 36 | +assert.equal(protocol.turnState(sessionId), 'armed'); |
| 37 | +``` |
| 38 | + |
| 39 | +Then emit `interaction/requestPermission` for the same session, return an offered allow response from the handler, and verify the protocol writes the allow result instead of `ZCODE_PERMISSION_SESSION_INVALID`. Add companion assertions that `waitForCompletion()` still clears the turn, observer timeout leaves it armed, and `releaseTurn()` clears it and rejects any still-pending observer. |
| 40 | + |
| 41 | +- [ ] **Step 2: Run the focused tests and verify RED** |
| 42 | + |
| 43 | +Run: |
| 44 | + |
| 45 | +```bash |
| 46 | +node --test --test-name-pattern='non-destructive completion|completion observer|destructive completion' tests/process-zcode.test.mjs |
| 47 | +``` |
| 48 | + |
| 49 | +Expected: FAIL because `observeCompletion` and `releaseTurn` do not exist. |
| 50 | + |
| 51 | +- [ ] **Step 3: Implement the minimal protocol behavior** |
| 52 | + |
| 53 | +Refactor the current waiter registration into one internal path with an explicit consumption mode. Preserve the public destructive path exactly, and add: |
| 54 | + |
| 55 | +```js |
| 56 | +observeCompletion(sessionId, timeoutMs) { |
| 57 | + return this.waitForCompletionMode(sessionId, timeoutMs, false); |
| 58 | +} |
| 59 | + |
| 60 | +releaseTurn(sessionId) { |
| 61 | + if (!nonEmpty(sessionId)) throw protocolInputError(); |
| 62 | + this.cancelTurn(sessionId, localTurnReleasedError(sessionId)); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +For observation mode, peek at an already queued completion rather than shifting it, do not call `abortTurn()` on resolution or timeout, and always unregister the observer. For destructive mode, keep the current shift, timeout cleanup, and `abortTurn()` behavior. Continue enforcing one waiter/observer per session with `waiterSessions`. |
| 67 | + |
| 68 | +Expose the two operations from `ZCodeClient` with normal session-ID validation/documentation: |
| 69 | + |
| 70 | +```js |
| 71 | +observeCompletion(sessionId, timeoutMs) { |
| 72 | + requireSessionId(sessionId); |
| 73 | + return this.protocol.observeCompletion(sessionId, timeoutMs); |
| 74 | +} |
| 75 | + |
| 76 | +releaseTurn(sessionId) { |
| 77 | + requireSessionId(sessionId); |
| 78 | + this.protocol.releaseTurn(sessionId); |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +- [ ] **Step 4: Run focused and adjacent tests and verify GREEN** |
| 83 | + |
| 84 | +Run: |
| 85 | + |
| 86 | +```bash |
| 87 | +node --test tests/process-zcode.test.mjs tests/zcode-client.test.mjs |
| 88 | +``` |
| 89 | + |
| 90 | +Expected: PASS, including all existing destructive completion and permission tests. |
| 91 | + |
| 92 | +- [ ] **Step 5: Self-review and commit** |
| 93 | + |
| 94 | +Check that no existing call site changed and that observer cleanup cannot retain a timer, subscriber, or waiter-session entry. Then commit: |
| 95 | + |
| 96 | +```bash |
| 97 | +git add scripts/lib/zcode-protocol.mjs scripts/lib/zcode-client.mjs tests/process-zcode.test.mjs |
| 98 | +git commit -m "fix: observe legacy completion without ending turn" |
| 99 | +``` |
| 100 | + |
| 101 | +### Task 2: Migrate executor wake and own local cleanup |
| 102 | + |
| 103 | +**Files:** |
| 104 | +- Modify: `scripts/lib/review.mjs:220-360` |
| 105 | +- Test: `tests/job-control.test.mjs` |
| 106 | + |
| 107 | +- [ ] **Step 1: Write the captured-ordering regression test** |
| 108 | + |
| 109 | +Extend the existing `0.16.5 foreground execution treats legacy completion as admission` fixture client with `observeCompletion`, `releaseTurn`, and a permission handler capture. Make `observeCompletion` publish the legacy wake first, then invoke the captured permission handler with a medium-risk Write request offering allow/deny. Assert the handler returns allow while the executor remains running, then publish the v4 authoritative terminal and assert success plus one local release: |
| 110 | + |
| 111 | +```js |
| 112 | +assert.deepEqual(permissionDecision, { decision: 'allow' }); |
| 113 | +assert.equal(releaseTurnCalls, 1); |
| 114 | +assert.equal(releasedSessionId, sessionId); |
| 115 | +``` |
| 116 | + |
| 117 | +Add an error-path test where observation or authoritative reconciliation fails after admission; assert `releaseTurn(sessionId)` still runs once before `close()`. |
| 118 | + |
| 119 | +- [ ] **Step 2: Run the executor regressions and verify RED** |
| 120 | + |
| 121 | +Run: |
| 122 | + |
| 123 | +```bash |
| 124 | +node --test --test-name-pattern='0.16.5 foreground execution|releases local turn' tests/job-control.test.mjs |
| 125 | +``` |
| 126 | + |
| 127 | +Expected: FAIL because `executeJob` still calls destructive `waitForCompletion()` and never releases the local turn explicitly. |
| 128 | + |
| 129 | +- [ ] **Step 3: Implement the executor migration** |
| 130 | + |
| 131 | +Construct `legacyWake` from `client.observeCompletion(activeSessionId)` when available. Keep a fallback to `client.waitForCompletion(activeSessionId)` only for existing injected test doubles that predate the internal interface: |
| 132 | + |
| 133 | +```js |
| 134 | +const observeLegacyCompletion = typeof client.observeCompletion === 'function' |
| 135 | + ? client.observeCompletion.bind(client) |
| 136 | + : client.waitForCompletion.bind(client); |
| 137 | +const legacyWake = waitForCompletionOrAbort(observeLegacyCompletion(activeSessionId), input.signal); |
| 138 | +``` |
| 139 | + |
| 140 | +In unconditional teardown, after all terminal/cancellation/error reconciliation and progress cleanup but before `client.close()`, release the known local session exactly once when supported: |
| 141 | + |
| 142 | +```js |
| 143 | +try { |
| 144 | + if (sessionId && typeof client.releaseTurn === 'function') client.releaseTurn(sessionId); |
| 145 | +} catch (cleanupError) { |
| 146 | + if (!primaryError) primaryError = cleanupError; |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +Preserve the primary-error and cleanup-error conventions already used by the executor. Do not send an upstream stop from this release path and do not modify `decidePermission`. |
| 151 | + |
| 152 | +- [ ] **Step 4: Run focused and executor-adjacent tests and verify GREEN** |
| 153 | + |
| 154 | +Run: |
| 155 | + |
| 156 | +```bash |
| 157 | +node --test tests/job-control.test.mjs tests/integration/companion.test.mjs |
| 158 | +``` |
| 159 | + |
| 160 | +Expected: PASS with the captured ordering, cleanup tests, and existing cancellation behavior unchanged. |
| 161 | + |
| 162 | +- [ ] **Step 5: Self-review and commit** |
| 163 | + |
| 164 | +Inspect the diff for exactly one production caller migration, one unconditional local cleanup, and no permission-policy changes. Then commit: |
| 165 | + |
| 166 | +```bash |
| 167 | +git add scripts/lib/review.mjs tests/job-control.test.mjs |
| 168 | +git commit -m "fix: retain active turn through legacy wake" |
| 169 | +``` |
| 170 | + |
| 171 | +### Task 3: Verify contracts and release readiness |
| 172 | + |
| 173 | +**Files:** |
| 174 | +- Modify if required by generated parity checks: checked-in marketplace mirrors only through the repository's existing builder |
| 175 | +- Test: repository-wide verification |
| 176 | + |
| 177 | +- [ ] **Step 1: Run permission-policy and protocol contract tests** |
| 178 | + |
| 179 | +Run: |
| 180 | + |
| 181 | +```bash |
| 182 | +node --test --test-name-pattern='permission|completion' tests/process-zcode.test.mjs tests/zcode-client.test.mjs tests/job-control.test.mjs |
| 183 | +``` |
| 184 | + |
| 185 | +Expected: PASS; Rescue permission decisions remain unchanged and destructive completion callers retain their contract. |
| 186 | + |
| 187 | +- [ ] **Step 2: Run the full repository check** |
| 188 | + |
| 189 | +Run: |
| 190 | + |
| 191 | +```bash |
| 192 | +npm run check |
| 193 | +``` |
| 194 | + |
| 195 | +Expected: PASS for line endings, lint, typecheck, all tests, qualification tests, and marketplace parity/build checks. |
| 196 | + |
| 197 | +- [ ] **Step 3: Inspect final scope** |
| 198 | + |
| 199 | +Run: |
| 200 | + |
| 201 | +```bash |
| 202 | +git diff --check origin/main...HEAD |
| 203 | +git diff --stat origin/main...HEAD |
| 204 | +git diff origin/main...HEAD -- scripts/lib/review.mjs scripts/lib/zcode-client.mjs scripts/lib/zcode-protocol.mjs |
| 205 | +``` |
| 206 | + |
| 207 | +Expected: no whitespace errors; changes remain limited to the completion lifecycle, tests, and approved docs. |
| 208 | + |
| 209 | +- [ ] **Step 4: Commit any verification-only generated parity update** |
| 210 | + |
| 211 | +If and only if the repository's official check regenerates tracked marketplace parity files, review and commit those exact generated changes: |
| 212 | + |
| 213 | +```bash |
| 214 | +git add marketplace |
| 215 | +git commit -m "build: refresh marketplace snapshot" |
| 216 | +``` |
| 217 | + |
| 218 | +If there are no generated tracked changes, skip this commit. |
0 commit comments