codex-workflow-v2 2.0.0-beta.13.7 → 2.0.0-beta.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +21 -13
  3. package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
  4. package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
  5. package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
  6. package/dist/src/alpha6/plan-risk.d.ts +2 -0
  7. package/dist/src/alpha6/plan-risk.js +10 -3
  8. package/dist/src/alpha6/plan-risk.js.map +1 -1
  9. package/dist/src/alpha7/autonomy.d.ts +3 -0
  10. package/dist/src/alpha7/autonomy.js +89 -42
  11. package/dist/src/alpha7/autonomy.js.map +1 -1
  12. package/dist/src/change-explanation.d.ts +64 -0
  13. package/dist/src/change-explanation.js +150 -0
  14. package/dist/src/change-explanation.js.map +1 -0
  15. package/dist/src/cli-actions.d.ts +1 -0
  16. package/dist/src/cli-actions.js +1 -0
  17. package/dist/src/cli-actions.js.map +1 -1
  18. package/dist/src/cli.js +4 -1
  19. package/dist/src/cli.js.map +1 -1
  20. package/dist/src/lifecycle/corrective-replan.js +3 -3
  21. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  22. package/dist/src/state/corrective-replan-public.js +5 -4
  23. package/dist/src/state/corrective-replan-public.js.map +1 -1
  24. package/dist/src/state/corrective-yield-executor.js +2 -2
  25. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  26. package/dist/src/state/store.d.ts +1 -1
  27. package/dist/src/state/store.js +17 -3
  28. package/dist/src/state/store.js.map +1 -1
  29. package/dist/src/version.d.ts +1 -1
  30. package/dist/src/version.js +1 -1
  31. package/dist/src/workflow.d.ts +49 -0
  32. package/dist/src/workflow.js +406 -238
  33. package/dist/src/workflow.js.map +1 -1
  34. package/docs/change-model.md +118 -0
  35. package/docs/delegated-approval.md +26 -0
  36. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  37. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  38. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  39. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
  40. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +3 -5
  41. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
  42. package/docs/release.md +72 -0
  43. package/package.json +1 -1
  44. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +76 -0
  45. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +116 -3
  46. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +6 -1
  47. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +52 -6
@@ -0,0 +1,118 @@
1
+ # Execution context and change explanation
2
+
3
+ A change is explained before it is attempted. The explanation is not an approval, a new grant,
4
+ or a replacement lifecycle. Existing Core `next` remains authoritative.
5
+
6
+ ## One execution context
7
+
8
+ App dispatch defaults to a worktree. An explicitly requested `executionMode: "local"` creates
9
+ in the saved project instead. Use local only when the user requested that execution environment.
10
+ The dispatch prompt separates repository identity from the child's actual write destination.
11
+ Correlated App readback records the real checkout root. A second readback cannot silently move
12
+ that bound Worker into another checkout, even when both share a Git common directory.
13
+
14
+ Before the first write and after a branch/cwd change, call the packaged registry action
15
+ `dispatch-context-check --reservation-id ... --file ... --project-id ...` with:
16
+
17
+ ```json
18
+ {
19
+ "observation": { "thread": {
20
+ "id": "actual-bound-thread-uuid", "kind": "codex", "hostId": "local",
21
+ "cwd": "/absolute/actual/worker/checkout"
22
+ } },
23
+ "writeRoot": "/absolute/actual/worker/checkout",
24
+ "expectedBranch": "exact-branch-from-fresh-Task"
25
+ }
26
+ ```
27
+
28
+ Use a fresh App `read_thread` observation, never a fabricated cwd. This read-only check detects
29
+ repository, checkout, write-root and branch mismatch. It does not replace Task revision, actor,
30
+ lease/credential or external permission checks. Do not send another create request, change the
31
+ target directory, or hand off an active writer merely to clear a mismatch. Reconcile the existing
32
+ Worker and Workflow context. Tracked dispatch without recorded executionContext needs a correlated `dispatch-observe` before
33
+ this check can match; it does not invent an existing checkout binding.
34
+
35
+ ## One change card
36
+
37
+ `change explain --task TASK-ID --file proposal.json --repo /actual/checkout --json` reads the
38
+ current Task and `next` from one observation. It changes no workflow state or product file.
39
+
40
+ ```json
41
+ {
42
+ "reason": "The current check requires one additional support file.",
43
+ "paths": ["tools/check-support.ts"],
44
+ "intent": "implementation",
45
+ "expectedRevision": 12,
46
+ "executionContext": {
47
+ "cwd": "/absolute/actual/worker/checkout",
48
+ "writeRoot": "/absolute/actual/worker/checkout",
49
+ "expectedBranch": "exact-task-branch"
50
+ },
51
+ "externalDecision": "unknown"
52
+ }
53
+ ```
54
+
55
+ `paths` are exact normalized relative file paths, not globs. `expectedPlanHash` is also accepted.
56
+ Inputs are closed; an agent cannot add a `supportProven` or `approved` flag. `externalDecision:
57
+ "denied"` preserves an external denial as a separate blocker. It does not call or emulate the
58
+ external permission service. `intent` may instead be `membership` or `semantic-scope`; declaring
59
+ intent never proves semantic safety. Omitted context is reported as not supplied, not verified.
60
+
61
+ The card binds Task revision/Plan, HEAD, request and bytes of proposed plus observed dirty paths.
62
+ Undeclared dirty files are included; escaping symlinks block byte inspection. The output contains
63
+ no confirmation/claim/writer values and no proposed file contents. A proposal hash is a diagnostic
64
+ binding, not an apply token; always refresh `next` and ordinary mutation preconditions.
65
+
66
+ ## Adaptation routes
67
+
68
+ - Implementation fixes continue within the same Task when the current Core route permits it.
69
+ Review count alone does not select corrective planning.
70
+ - `bounded-plan-amendment` appears only when Core advertises the exact eligible check-support
71
+ scope extension and every out-of-Step file is covered by that evidence. Execute the existing
72
+ `task plan-integrity-recover` route and fresh `next`; its existing atomic evidence and authority
73
+ rules apply. The card never edits `allowedWrites`, enlarges a grant, or supplies an arbitrary
74
+ new Plan. This release reuses the proven bounded amendment mechanism rather than adding a
75
+ competing mutation protocol.
76
+ - An unproved additional file requires Plan review. No broad glob or diff-size rule grants
77
+ automatic extension. Runtime changes and component-owner recovery retain their current audits.
78
+ - Corrective planning and downstream recovery retain existing Task/Worker identity and historical
79
+ evidence. A Knowledge context refresh does not resolve a Plan conflict.
80
+ - Membership is distinct from implementation. Existing audited remediation and full-contract
81
+ membership evolution remain the only corresponding machine routes. A reported membership
82
+ intent does not authorize deletion of a required Task or weakened acceptance.
83
+ - Semantic scope requires an explicit user decision. External denial remains independent of
84
+ Workflow grants and file scope.
85
+
86
+ `requiresUser: null` means the card has not established a specific human gate, not that all
87
+ checks passed. The card is advisory and cannot intercept arbitrary editor/tool writes. Gateway
88
+ agents must invoke the context check and fresh Core route before writing. No claim is made that
89
+ an external approval system will accept the resulting operation.
90
+
91
+ For a legacy Worker already bound before dispatch markers existed, the read-only context check
92
+ uses its existing thread/host plus a freshly supplied Task repository root. It explicitly reports
93
+ that historical checkout provenance is unavailable, preserves the legacy registry bytes and
94
+ never creates or adopts another Worker. A tracked Worker whose checkout changes still reports a
95
+ mismatch; no automatic App handoff or silent registry migration is performed.
96
+
97
+ ## Later independent corrective evidence after a fix review
98
+
99
+ A failed guarded Step can reveal a Plan obstruction after its immutable strict review recorded
100
+ `fix` without a Plan conflict. On a clean current Task checkout, after at least two real remediation
101
+ failures and with no decision already bound to the current ordinal, `next` can expose the optional
102
+ `correctiveDecisionOption`. The ordinary fix action remains the default. No failure, repeated run,
103
+ review rewrite or arbitrary scope extension is required to obtain an independent corrective audit.
104
+ The option is absent while a Step runs, a pending handoff exists, or an earlier navigation gate
105
+ blocks ordinary continuation. Existing writer/C1 actor and token requirements still apply; a
106
+ legitimate claimed Worker credential does not disable this independent audit route.
107
+
108
+ Only a genuine independent audit justifies using the existing `task corrective-decision` command.
109
+ Its resulting append-only decision preserves the original review and completed Steps. A decision
110
+ requiring replan leads to the existing corrective yield/prepare/confirmation route, including its
111
+ separate Human gate; the option supplies no replacement authority, grant or permission to write
112
+ additional files. A conflicting existing decision cannot be overwritten.
113
+
114
+ The option's revision, Brief/Plan/HEAD and remediation event hashes describe the observation used
115
+ for the audit. They are not a new apply token or additional executor-enforced hash contract: the
116
+ existing executor checks its revision, current decision ordinal, audit independence and actor/token
117
+ preconditions. Refresh `next` and audit evidence before recording, including after a HEAD change
118
+ that does not change the Task revision. Never treat stale option output as authorization.
@@ -66,6 +66,32 @@ half of an atomic content-only Task context refresh in that Milestone. Standalon
66
66
  approval remains project-scoped. The contract does not add a generic delegation transition:
67
67
  outcome, success signal, acceptance, checks, discovery, and base branch remain immutable.
68
68
 
69
+ ## Full contract during an active Milestone
70
+
71
+ The human may request `milestone autonomy-prepare` / `milestone autonomy-grant` while a
72
+ Milestone is `active`, as well as before its initial execution. This supports first full
73
+ issuance or replacement after expiry/revocation without resetting the Milestone, altering
74
+ its Plan or completed Task evidence, or recreating its Worker. Planning, blocked, validating,
75
+ final-acceptance and terminal Milestones are not eligible for this route.
76
+
77
+ Preparation is read-only. It requires a coherent registered repository, a complete current
78
+ Plan and active Knowledge Map. Its MAC binds the status, revision, Plan, semantic scope,
79
+ principal, delegate, exact expiry (at most 72 hours), policy and previous authority state.
80
+ Show this complete contract and obtain a separate exact human confirmation. Never renew a
81
+ contract automatically or treat general delegated execution as permission to issue authority.
82
+
83
+ A still-valid full contract cannot be replaced. An explicitly revoked or expired contract
84
+ may be replaced using a freshly prepared MAC. The previous confirmed code cannot undo a
85
+ revocation. Exact retries retain the same contract/grant; an interrupted event-first write
86
+ can be completed only with its original matching confirmation. Issuance and milestone grant
87
+ revocation share a Milestone lock. History remains append-only.
88
+
89
+ `next` exposes optional `activeMilestoneAutonomyOptions` even when a linked Task is selected;
90
+ this does not change the immediate Task action or waive its credential/review/recovery gates.
91
+ A coexisting generic grant remains unchanged and does not gain membership evolution or atomic
92
+ context-refresh authority. The new full contract carries those existing bounded capabilities;
93
+ it never grants standalone Project Knowledge approval or a semantic scope change.
94
+
69
95
  ## Starting a delegate chat
70
96
 
71
97
  A grant does not start an agent and is not attached to a Codex chat automatically. Open a new
@@ -463,7 +463,7 @@ Restart Docker Desktop требует отдельного разрешения
463
463
  # 10. Operational checklist
464
464
 
465
465
  - exact package version установлен, bundled gateway соответствует release и переустановлен;
466
- - handshake подтверждает `packageVersion=2.0.0-beta.13.5`, protocol 2, state schema 2 и beta.13
466
+ - handshake подтверждает `packageVersion=2.0.0-beta.13.9`, protocol 2, state schema 2 и beta.13
467
467
  capabilities;
468
468
  - routine observation начинается с компактного `status`; `status --full` запрашивается только для
469
469
  расследования, которому действительно нужна полная historical projection;
@@ -57,7 +57,7 @@ AGENTS.md
57
57
  -> doctor только как дополнительная диагностика
58
58
  ```
59
59
 
60
- Handshake beta.13.5 должен сообщать `packageVersion=2.0.0-beta.13.5`, `protocolVersion=2`,
60
+ Handshake beta.13.9 должен сообщать `packageVersion=2.0.0-beta.13.9`, `protocolVersion=2`,
61
61
  `stateSchemaVersion=2`, dependency DAG, initial Plan transaction, mechanical feasibility,
62
62
  Milestone autonomy, host-safe credential reference, failed-Step continuation transaction,
63
63
  cause-bound rolling refinement, structural production edge, compact status, reviewer runtime preflight,
@@ -168,8 +168,6 @@ base HEAD. Final acceptance выполняй только по exact eligible gr
168
168
  текущему MSA-code; иначе покажи полный requiredHumanGate и остановись.
169
169
  ```
170
170
 
171
- <!-- pagebreak -->
172
-
173
171
  ## 4.1. Что должно получиться до autonomy gate
174
172
 
175
173
  - Discovery без blocking unknowns;
@@ -481,8 +479,8 @@ Task/project DGR. `dependencyBinding` для standalone handoff равен null.
481
479
 
482
480
  # 14. Итоговый checklist пользователя
483
481
 
484
- - beta.13.1 exact package и новый bundled gateway установлены; handshake показывает
485
- `packageVersion=2.0.0-beta.13.5`, protocol/state schema `2/2` и beta.13 capabilities;
482
+ - beta.13.9 exact package и новый bundled gateway установлены; handshake показывает
483
+ `packageVersion=2.0.0-beta.13.9`, protocol/state schema `2/2` и beta.13 capabilities;
486
484
  - новый Coordinator chat не является fork старого Milestone;
487
485
  - bootstrap DGR, если нужен, выдан отдельным exact human confirmation;
488
486
  - Discovery не materialized при blocking unknowns;
@@ -15,7 +15,7 @@ authority сами: mutating permission определяется runtime state
15
15
 
16
16
  | Параметр beta.13 | Значение |
17
17
  |---|---|
18
- | npm package | `codex-workflow-v2@2.0.0-beta.13.5` |
18
+ | npm package | `codex-workflow-v2@2.0.0-beta.13.9` |
19
19
  | protocolVersion | 2 |
20
20
  | stateSchemaVersion | 2 |
21
21
  | lifecycle epoch | 2 |
@@ -767,7 +767,7 @@ dirty update.
767
767
  observed-route matrix/compact-status/reviewer-runtime tests, legacy compatibility regressions,
768
768
  `npm run test:signal-t10-check-support-e2e`, `npm run test:signal-remediation-e2e`,
769
769
  `npm run pack:check`, `npm run smoke:downstream`, `npm run plugin:check`, official plugin/skill validators,
770
- `npm run docs:pdf:check` и `npm run release:check` с exact `v2.0.0-beta.13.5` tag binding и append-only
770
+ `npm run docs:pdf:check` и `npm run release:check` с exact `v2.0.0-beta.13.9` tag binding и append-only
771
771
  real-agent lifecycle log. Candidate проверяется из clean source checkout; missing built module не должен
772
772
  превращаться в stack trace, а обязан быть детерминированно собран или диагностирован release gate.
773
773
 
package/docs/release.md CHANGED
@@ -4,6 +4,78 @@ The npm package is `codex-workflow-v2` with public visibility. Consumers must pi
4
4
  an exact version. Stable compatibility covers the latest minor release of the current
5
5
  major only; older minors are unsupported unless a release note explicitly says otherwise.
6
6
 
7
+ ## beta.13.9: failed-Step planning recovery after Knowledge rebind
8
+
9
+ A started Task can retain completed Steps and a failed guarded Step while a Knowledge
10
+ rebind supersedes its execution authorization. A truthful blocking Plan Risk Audit must
11
+ not require a false approval to reach corrective planning. This recovery uses the current
12
+ blocking audit as planning evidence and proves its unchanged safety baseline through
13
+ hash-checked Knowledge-only Plan history and the historical superseded authorization.
14
+ It never treats either record as current execution authority.
15
+
16
+ Fresh navigation can then advertise the existing independent corrective-decision route.
17
+ The ordinary corrective yield, independent replacement validation and risk audit, exact
18
+ human corrective-replan confirmation, and fresh execution authorization remain mandatory.
19
+ An ordinary `plan-set` cannot bypass that chain. `continue-fix` is not a valid decision
20
+ for this stopped planning posture; ordinary execution repair retains its existing choices.
21
+ The additive upstream-remediation route remains limited to genuinely unstarted Tasks.
22
+
23
+ Release evidence must exercise the complete callable recovery chain, including original
24
+ Worker credential replacement and yield, repeated Knowledge rebind, unchanged completed
25
+ Step and failed review evidence, and rejection of damaged Plan/audit/authorization history.
26
+
27
+ The exact beta.13.9 runner retains the consumed-carryover compatibility profile from
28
+ beta.13.6 with beta.13.9 as its pinned target. The source, ownership, lease, candidate
29
+ Plan, provenance and transaction checks are unchanged. Historical beta.13.7 and
30
+ beta.13.8 transcripts remain bound to their original targets.
31
+
32
+ ## beta.13.8: explain changes and continue active Milestones
33
+
34
+ Full autonomy can be issued during an active Milestone using an exact new human-confirmed
35
+ contract. Interrupted event-first issuance remains recoverable after expiry or revision change:
36
+ a fresh confirmation may supersede an inert missing grant, preserving its event and creating a
37
+ new grant identity. Known unpublished grant directories carry no authority; malformed state and
38
+ unknown incomplete delegations still fail closed.
39
+
40
+ The read-only `change explain` command separates execution context, external approval, semantic
41
+ scope, bounded check-support amendment, Plan correction, membership and context refresh. It
42
+ preserves the current Core route, including credential prerequisites and effective Step scope.
43
+ Its proposal hash is diagnostic evidence, not permission to apply a patch.
44
+
45
+ Chat dispatch records the observed child checkout and offers an advisory context check before
46
+ writes. Existing Workers are preserved; a parent repository path does not authorize writing
47
+ outside the actual child checkout. Model and reasoning selection remain task-specific.
48
+
49
+ The exact beta.13.8 runner retains the tested beta.13.6 consumed-carryover source
50
+ recovery profile with beta.13.8 as its pinned target. It does not admit a beta.13.7
51
+ source or a running Step; historical beta.13.7 transcript evidence remains separately
52
+ valid for its original target.
53
+
54
+ All ordinary release gates below apply to the exact versioned candidate. Historical failed
55
+ real-agent runs remain failed evidence; prior one-time release exceptions are not reused.
56
+ Signal deployment additionally requires a fresh safe update boundary, exact installed provenance
57
+ and verified continuation by the original Worker. This release does not waive external approval
58
+ or claim to prevent every future product failure.
59
+
60
+ ## Active Milestone autonomy contract issuance
61
+
62
+ The candidate extends full human-confirmed autonomy to an already active Milestone.
63
+ Release evidence must cover both initial active issuance and replacement after revocation
64
+ or expiry, without resetting Task state, changing membership/semantic scope, or replacing
65
+ an existing Worker. A generic delegation is neither upgraded nor revoked implicitly.
66
+
67
+ The MAC includes the previous contract/grant snapshot so a revoked confirmation cannot
68
+ restore authority. Exact successful and interrupted-write retries must preserve identity;
69
+ legacy event-first records retain their bounded pre-execution recovery. Shared Milestone
70
+ exclusion protects issuance, revocation and scope/status transitions.
71
+
72
+ Acceptance must continue through same-Worker execution and guarded atomic context refresh
73
+ using the new full contract, with the old grant rejected without Task/Knowledge mutations.
74
+ Include read-only preparation, stale bindings, valid-contract replacement refusal, malformed
75
+ actors/expiry, unavailable Knowledge and disallowed Milestone states. Optional Task-first
76
+ navigation must not replace the current execution/recovery action. Run the full test suite,
77
+ release checks, downstream package smoke and plugin validation on the final candidate.
78
+
7
79
  ## beta.13.7 candidate: retire completed carryover authority
8
80
 
9
81
  After a recovered Step passed review, its recorded dirty-work permission still
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-workflow-v2",
3
- "version": "2.0.0-beta.13.7",
3
+ "version": "2.0.0-beta.13.9",
4
4
  "description": "Requirements-first Codex development workflow with local state and explicit Git ownership.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -126,3 +126,79 @@ then mechanical lifecycle work Mini/low. Do not create another Task just to swit
126
126
  Keep the previous selection unless the phase/risk/scope changes materially. The journal records
127
127
  requested settings; tool acceptance/runtime readback, when available, is required to claim the
128
128
  actual model used. A model change never changes role, scope, credentials or approval authority.
129
+
130
+ ## Execution context before product writes
131
+
132
+ `dispatch-begin` accepts optional `executionMode: "local" | "worktree"` (default `worktree`).
133
+ Choose local only for an explicit user request to execute in the saved checkout. The observed
134
+ child checkout, not an absolute path copied from the parent's prompt, is the write destination.
135
+ `dispatch-observe` records it and refuses silent movement of a bound child.
136
+
137
+ Before the first write, and after any branch/cwd change, use read-only `dispatch-context-check`
138
+ with a fresh App observation, `writeRoot`, and `expectedBranch` from the current Core Task.
139
+ Require `matched=true`, then refresh Core next and writer authority. Mismatch means reconcile the
140
+ same Worker context; it never permits another create, an indirect write or an automatic handoff.
141
+ A matched context does not authorize external operations. See packaged `docs/change-model.md`.
142
+
143
+ ### Existing legacy bindings
144
+
145
+ `dispatch-context-check` also accepts an already bound legacy entry with no dispatch marker.
146
+ Supply the exact current Task `repositoryRoot` in addition to the fresh actual Worker observation,
147
+ `writeRoot`, and `expectedBranch`. The check verifies the bound thread/host, common Git repository,
148
+ actual checkout/write root and Task branch without changing the registry. It reports
149
+ `contextBinding=legacy-current-observation` and `historicalCheckoutVerified=false`: no historical
150
+ creation marker or checkout provenance is invented. It never enables another create permit.
151
+ This observation does not move a chat, authorize a write or reconcile a tracked dispatch whose
152
+ previous checkout changed; those remain explicit App/context operations followed by fresh checks.
153
+
154
+ ### Explicit App handoff reconciliation
155
+
156
+ An App handoff can return a new destination thread ID while retaining the task history.
157
+ `bind` still rejects replacing a bound child. After the App operation succeeds, use the
158
+ separate `dispatch-handoff-reconcile --reservation-id ... --file ... --project-id ...` route:
159
+
160
+ ```json
161
+ {
162
+ "sourceThreadId": "actual-source-thread-uuid-from-the-handoff-call",
163
+ "sourceHostId": "local",
164
+ "repositoryRoot": "/absolute/repository-from-fresh-Task",
165
+ "expectedBranch": "exact-task-branch",
166
+ "receipt": {
167
+ "operationId": "actual-operation-id",
168
+ "revision": 12,
169
+ "status": "success",
170
+ "destinationThreadId": "actual-destination-thread-uuid",
171
+ "destinationHostId": "local",
172
+ "destinationCwd": "/absolute/destination-checkout",
173
+ "threadTitle": "exact-current-bound-title"
174
+ },
175
+ "observation": { "thread": {
176
+ "id": "actual-destination-thread-uuid",
177
+ "hostId": "local",
178
+ "kind": "codex",
179
+ "cwd": "/absolute/destination-checkout",
180
+ "title": "exact-current-bound-title"
181
+ } }
182
+ }
183
+ ```
184
+
185
+ Copy the complete successful `get_handoff_status` receipt, including any extra returned fields,
186
+ and obtain a fresh destination `read_thread`. Preserve the source `handoff_thread` tool call:
187
+ its source ID is supplied separately because the result need not contain it. Pending, failed,
188
+ cancelled or inconsistent results cannot change the registry. The destination must match the
189
+ bound title, actual Git repository and current Task branch, and cannot belong to another entry.
190
+
191
+ This route changes only the existing reservation's current thread/host and, for tracked dispatch,
192
+ current execution context. An append-only `handoffHistory` retains the receipt, source and prior
193
+ dispatch snapshot; the original creation marker is never fabricated for legacy entries. Existing
194
+ observations and selection history are preserved. A tracked supervisor's old cursor is cleared
195
+ so it must observe the destination again. No create permit, grant, Task claim or lifecycle
196
+ transition is issued. Ordinary `dispatch-observe` still rejects an unexplained checkout change.
197
+ An exact retry of the latest handoff leaves registry bytes unchanged; changed receipts or replay
198
+ of a superseded handoff are rejected. Refresh Core status/next and writer authority after handoff.
199
+
200
+ These caller-supplied App results are coherence evidence, not cryptographic proof of origin or
201
+ external write authority. The coordinator must obtain genuine tool results and independently
202
+ check retained history and the safe writer boundary. The package does not execute a handoff,
203
+ verify arbitrary transcript continuity, or authorize writes merely because reconciliation passes.
204
+ Only destinations readable on the checking host with the same Git common directory are supported.
@@ -5,6 +5,8 @@ import path from 'node:path';
5
5
  import { selectChatModel } from './chat-model-policy.mjs';
6
6
 
7
7
  const hash = text => createHash('sha256').update(text).digest('hex');
8
+ const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
9
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
8
10
  const text = (value, name) => {
9
11
  if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} is required.`);
10
12
  return value.trim();
@@ -31,10 +33,12 @@ function promptMatches(value, digest) {
31
33
  }
32
34
  function view(entry) {
33
35
  const d = entry.dispatch;
36
+
34
37
  return {
35
38
  reservationId: entry.reservationId, state: d?.state ?? (entry.threadId ? 'legacy-bound' : 'untracked'),
36
39
  createAllowed: false, dispatchId: d?.id ?? null, clientThreadId: d?.clientThreadId ?? null,
37
40
  threadId: entry.threadId ?? d?.candidateThreadId ?? null, hostId: d?.hostId ?? entry.hostId ?? null,
41
+ executionContext: d?.executionContext ?? null,
38
42
  selection: d?.selection ?? null, supervision: d?.supervision ?? null,
39
43
  candidates: d?.candidates ?? [],
40
44
  nextAction: entry.threadId ? 'supervise-existing' : d?.state === 'resolved' ? 'title-readback-and-bind'
@@ -49,6 +53,8 @@ export function dispatchAction(action, entry, input = {}) {
49
53
  if (entry.status === 'abandoned' || entry.status === 'blocked') throw new Error('Reservation is not dispatchable.');
50
54
  const repositoryRoot = realpathSync(text(input.repositoryRoot, 'repositoryRoot'));
51
55
  const gitDir = commonGitDir(repositoryRoot);
56
+ const executionMode = input.executionMode ?? 'worktree';
57
+ if (!['local', 'worktree'].includes(executionMode)) throw new Error('executionMode must be local or worktree.');
52
58
  const appProjectId = text(input.appProjectId, 'appProjectId');
53
59
  const hostId = text(input.hostId, 'hostId');
54
60
  const promptPath = realpathSync(text(input.promptFile, 'promptFile'));
@@ -58,19 +64,116 @@ export function dispatchAction(action, entry, input = {}) {
58
64
  const selection = selectChatModel(entry.type, input.modelRequest);
59
65
  const id = randomUUID();
60
66
  const marker = `[workflow-dispatch:${id}:${hash(prompt)}]`;
61
- const dispatchedPrompt = `${marker}\n${prompt}`;
67
+ const dispatchedPrompt = `${marker}\n${prompt}\nExecution boundary: use the actual child checkout returned by App readback. Before any write, reconcile it with the exact Task branch and run dispatch-context-check. A repository path in this packet is identity context, not authorization to write outside that checkout.`;
62
68
  entry.dispatch = {
63
- version: 1, id, state: 'creating', appProjectId, hostId, repositoryRoot, gitDir,
69
+ version: 1, id, state: 'creating', appProjectId, hostId, repositoryRoot, gitDir, executionMode,
64
70
  promptPath, promptHash: hash(prompt), dispatchedPromptHash: hash(dispatchedPrompt), marker,
65
71
  selection, selectionHistory: [selection], clientThreadId: null, candidateThreadId: null,
66
72
  candidates: [], observations: [], createdAt: new Date().toISOString(),
67
73
  };
68
74
  return { ...view(entry), createAllowed: true, nextAction: 'create-thread-once', createArgs: {
69
- target: { type: 'project', projectId: appProjectId, environment: { type: 'worktree' } },
75
+ target: { type: 'project', projectId: appProjectId, environment: { type: executionMode } },
70
76
  title: entry.requestedTitle, prompt: dispatchedPrompt, model: selection.model, thinking: selection.thinking,
71
77
  } };
72
78
  }
73
79
  const d = entry.dispatch;
80
+ if (action === 'dispatch-handoff-reconcile') {
81
+ const receipt = input.receipt;
82
+ const t = input.observation?.thread;
83
+ const sourceThreadId = actualId(input.sourceThreadId);
84
+ const sourceHostId = text(input.sourceHostId, 'sourceHostId');
85
+ const operationId = text(receipt?.operationId, 'handoff operationId');
86
+ if (receipt?.status !== 'success' || !Number.isSafeInteger(receipt.revision) || receipt.revision < 1
87
+ || !Array.isArray(receipt.steps) || receipt.steps.length === 0
88
+ || receipt.steps.some(step => !step || typeof step.id !== 'string' || !step.id || step.status !== 'done')) {
89
+ throw new Error('Require a successful completed App handoff receipt.');
90
+ }
91
+ const destinationThreadId = actualId(receipt.destinationThreadId);
92
+ const destinationHostId = text(receipt.destinationHostId, 'destinationHostId');
93
+ if (!entry.titleVerified || entry.status !== 'bound' || !entry.threadId
94
+ || receipt.threadTitle !== entry.observedTitle || receipt.threadTitle !== entry.requestedTitle
95
+ || t?.kind !== 'codex' || t.id !== destinationThreadId || t.hostId !== destinationHostId
96
+ || t.title !== receipt.threadTitle) throw new Error('Handoff requires the exact bound title and destination readback.');
97
+ const cwd = realpathSync(text(t.cwd, 'destination cwd'));
98
+ if (cwd !== realpathSync(text(receipt.destinationCwd, 'receipt destinationCwd'))) {
99
+ throw new Error('Handoff destination cwd does not match its receipt.');
100
+ }
101
+ const root = realpathSync(execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', timeout: 5000 }).trim());
102
+ const repositoryRoot = realpathSync(text(input.repositoryRoot, 'repositoryRoot from fresh Task'));
103
+ const gitDir = commonGitDir(repositoryRoot);
104
+ const expectedBranch = text(input.expectedBranch, 'expectedBranch from fresh Task');
105
+ if (commonGitDir(root) !== gitDir || (d && d.gitDir !== gitDir)
106
+ || execFileSync('git', ['branch', '--show-current'], { cwd: root, encoding: 'utf8', timeout: 5000 }).trim() !== expectedBranch) {
107
+ throw new Error('Handoff repository or Task branch mismatch.');
108
+ }
109
+ const binding = { sourceThreadId, sourceHostId, receipt, repositoryRoot, expectedBranch,
110
+ destination: { threadId: destinationThreadId, hostId: destinationHostId, cwd, checkoutRoot: root, title: t.title } };
111
+ const bindingHash = hash(canonical(binding));
112
+ const history = entry.handoffHistory ?? [];
113
+ const previous = history.find(event => event.operationId === operationId);
114
+ if (previous) {
115
+ if (previous.bindingHash !== bindingHash || history.at(-1) !== previous
116
+ || entry.threadId !== destinationThreadId || entry.hostId !== destinationHostId) {
117
+ throw new Error('Handoff operation was already recorded with a different binding or superseded.');
118
+ }
119
+ return { ...view(entry), handoffOperationId: operationId, idempotentRetry: true };
120
+ }
121
+ if (entry.threadId !== sourceThreadId || entry.hostId !== sourceHostId
122
+ || (d && (d.hostId !== sourceHostId || d.candidateThreadId !== sourceThreadId))) {
123
+ throw new Error('Handoff source must match the currently bound Worker.');
124
+ }
125
+ const recordedAt = new Date().toISOString();
126
+ // Receipt input is coherence evidence, not a signed App capability. Preserve the
127
+ // complete prior dispatch instead of relabeling its original creation history.
128
+ entry.handoffHistory = [...history, { operationId, bindingHash, ...binding, recordedAt,
129
+ priorDispatch: d ? structuredClone(d) : null }];
130
+ entry.threadId = destinationThreadId;
131
+ entry.hostId = destinationHostId;
132
+ if (d) {
133
+ d.hostId = destinationHostId;
134
+ d.candidateThreadId = destinationThreadId;
135
+ d.executionContext = { checkoutRoot: root, gitDir, threadId: destinationThreadId, hostId: destinationHostId };
136
+ d.executionMode = root === d.repositoryRoot ? 'local' : 'worktree';
137
+ d.candidates = [];
138
+ d.duplicateDisposition = null;
139
+ d.state = 'resolved';
140
+ d.observations.push({ threadId: destinationThreadId, hostId: destinationHostId, cwd, title: t.title,
141
+ observedAt: recordedAt, handoffOperationId: operationId });
142
+ if (d.supervision) d.supervision = { ...d.supervision, cursor: null,
143
+ state: 'attention', nextAction: 'refresh-destination-observation-and-core-next', updatedAt: recordedAt };
144
+ }
145
+ return { ...view(entry), handoffOperationId: operationId, idempotentRetry: false };
146
+ }
147
+ if (action === 'dispatch-context-check') {
148
+ // A fresh observation is mandatory; prior readback is not a filesystem capability.
149
+ const t = input.observation?.thread;
150
+ if (!entry.threadId || t?.id !== entry.threadId || t.kind !== 'codex' || t.hostId !== (d?.hostId ?? entry.hostId)) {
151
+ throw new Error('Require a fresh observation of the bound Worker.');
152
+ }
153
+ const cwd = realpathSync(text(t.cwd, 'thread cwd'));
154
+ const writeRoot = realpathSync(text(input.writeRoot, 'writeRoot'));
155
+ const root = realpathSync(execFileSync('git', ['rev-parse', '--show-toplevel'], {
156
+ cwd, encoding: 'utf8', timeout: 5000,
157
+ }).trim());
158
+ const branch = execFileSync('git', ['branch', '--show-current'], {
159
+ cwd: root, encoding: 'utf8', timeout: 5000,
160
+ }).trim();
161
+ const expectedBranch = text(input.expectedBranch, 'expectedBranch from fresh Task');
162
+ const reasons = [];
163
+ const expectedGitDir = d?.gitDir ?? commonGitDir(realpathSync(text(input.repositoryRoot, 'repositoryRoot from fresh Task')));
164
+ if (commonGitDir(root) !== expectedGitDir) reasons.push('REPOSITORY_MISMATCH');
165
+ if (d && d.executionContext?.checkoutRoot !== root) reasons.push('WORKER_CHECKOUT_CHANGED');
166
+ if (writeRoot !== root) reasons.push('WRITE_ROOT_MISMATCH');
167
+ if (branch !== expectedBranch) reasons.push('TASK_BRANCH_MISMATCH');
168
+ return { kind: 'execution-context-check', readOnly: true, authority: 'none',
169
+ matched: reasons.length === 0, reasons, threadId: entry.threadId,
170
+ contextBinding: d ? 'tracked-dispatch' : 'legacy-current-observation',
171
+ historicalCheckoutVerified: Boolean(d?.executionContext),
172
+ checkoutRoot: root, writeRoot, branch, expectedBranch,
173
+ nextAction: reasons.length ? 'reconcile-existing-worker-context' : 'refresh-core-next-and-writer-authority',
174
+ limitations: ['This check does not grant filesystem access or override external approval.',
175
+ 'Recheck before a write after cwd or branch changes. No automatic handoff or duplicate dispatch.'] };
176
+ }
74
177
  if (!d) throw new Error('No tracked dispatch; reconcile legacy chats manually, never assume creation failed.');
75
178
  if (action === 'dispatch-result') {
76
179
  const result = input.result;
@@ -157,6 +260,16 @@ export function dispatchAction(action, entry, input = {}) {
157
260
  || (candidate && realpathSync(t.cwd) !== candidate.cwd)) throw new Error('Readback repository/cwd mismatch.');
158
261
  if (t.projectId !== undefined && t.projectId !== d.appProjectId) throw new Error('Readback App project mismatch.');
159
262
  if (d.candidateThreadId && d.candidateThreadId !== id) throw new Error('Readback differs from create result.');
263
+ const checkoutRoot = realpathSync(execFileSync('git', ['rev-parse', '--show-toplevel'], {
264
+ cwd: t.cwd, encoding: 'utf8', timeout: 5000,
265
+ }).trim());
266
+ if (d.executionMode === 'local' && checkoutRoot !== d.repositoryRoot) {
267
+ throw new Error('Local dispatch readback checkout mismatch.');
268
+ }
269
+ if (d.executionContext && d.executionContext.checkoutRoot !== checkoutRoot) {
270
+ throw new Error('Bound Worker checkout changed; reconcile context without replacing the child.');
271
+ }
272
+ d.executionContext = { checkoutRoot, gitDir: d.gitDir, threadId: id, hostId: d.hostId };
160
273
  d.candidateThreadId = id;
161
274
  d.state = 'resolved';
162
275
  d.observations.push({ threadId: id, hostId: t.hostId, cwd: realpathSync(t.cwd),
@@ -170,8 +170,13 @@ export function dispatchChat(options) {
170
170
  return withRegistryLock(options.projectId, options.registryHome, file => {
171
171
  const registry = readRegistry(file, options.projectId);
172
172
  const entry = requireEntry(registry, options.reservationId);
173
+ if (options.action === 'dispatch-handoff-reconcile') {
174
+ const destination = options.input?.observation?.thread;
175
+ if (registry.entries.some(other => other !== entry && other.threadId === destination?.id
176
+ && other.hostId === destination?.hostId)) throw new Error('Handoff destination already belongs to another reservation.');
177
+ }
173
178
  const result = dispatchAction(options.action, entry, options.input);
174
- if (options.action !== 'dispatch-status') {
179
+ if (!['dispatch-status', 'dispatch-context-check'].includes(options.action) && result.idempotentRetry !== true) {
175
180
  entry.updatedAt = new Date().toISOString();
176
181
  writeRegistry(file, registry);
177
182
  }