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

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 (32) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +13 -9
  3. package/dist/src/alpha7/autonomy.d.ts +3 -0
  4. package/dist/src/alpha7/autonomy.js +89 -42
  5. package/dist/src/alpha7/autonomy.js.map +1 -1
  6. package/dist/src/change-explanation.d.ts +64 -0
  7. package/dist/src/change-explanation.js +150 -0
  8. package/dist/src/change-explanation.js.map +1 -0
  9. package/dist/src/cli-actions.d.ts +1 -0
  10. package/dist/src/cli-actions.js +1 -0
  11. package/dist/src/cli-actions.js.map +1 -1
  12. package/dist/src/cli.js +4 -1
  13. package/dist/src/cli.js.map +1 -1
  14. package/dist/src/state/store.d.ts +1 -1
  15. package/dist/src/state/store.js +17 -3
  16. package/dist/src/state/store.js.map +1 -1
  17. package/dist/src/version.d.ts +1 -1
  18. package/dist/src/version.js +1 -1
  19. package/dist/src/workflow.d.ts +49 -0
  20. package/dist/src/workflow.js +376 -230
  21. package/dist/src/workflow.js.map +1 -1
  22. package/docs/change-model.md +118 -0
  23. package/docs/delegated-approval.md +26 -0
  24. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  25. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  26. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  27. package/docs/release.md +47 -0
  28. package/package.json +1 -1
  29. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +76 -0
  30. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +116 -3
  31. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +6 -1
  32. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +35 -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
package/docs/release.md CHANGED
@@ -4,6 +4,53 @@ 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.8: explain changes and continue active Milestones
8
+
9
+ Full autonomy can be issued during an active Milestone using an exact new human-confirmed
10
+ contract. Interrupted event-first issuance remains recoverable after expiry or revision change:
11
+ a fresh confirmation may supersede an inert missing grant, preserving its event and creating a
12
+ new grant identity. Known unpublished grant directories carry no authority; malformed state and
13
+ unknown incomplete delegations still fail closed.
14
+
15
+ The read-only `change explain` command separates execution context, external approval, semantic
16
+ scope, bounded check-support amendment, Plan correction, membership and context refresh. It
17
+ preserves the current Core route, including credential prerequisites and effective Step scope.
18
+ Its proposal hash is diagnostic evidence, not permission to apply a patch.
19
+
20
+ Chat dispatch records the observed child checkout and offers an advisory context check before
21
+ writes. Existing Workers are preserved; a parent repository path does not authorize writing
22
+ outside the actual child checkout. Model and reasoning selection remain task-specific.
23
+
24
+ The exact beta.13.8 runner retains the tested beta.13.6 consumed-carryover source
25
+ recovery profile with beta.13.8 as its pinned target. It does not admit a beta.13.7
26
+ source or a running Step; historical beta.13.7 transcript evidence remains separately
27
+ valid for its original target.
28
+
29
+ All ordinary release gates below apply to the exact versioned candidate. Historical failed
30
+ real-agent runs remain failed evidence; prior one-time release exceptions are not reused.
31
+ Signal deployment additionally requires a fresh safe update boundary, exact installed provenance
32
+ and verified continuation by the original Worker. This release does not waive external approval
33
+ or claim to prevent every future product failure.
34
+
35
+ ## Active Milestone autonomy contract issuance
36
+
37
+ The candidate extends full human-confirmed autonomy to an already active Milestone.
38
+ Release evidence must cover both initial active issuance and replacement after revocation
39
+ or expiry, without resetting Task state, changing membership/semantic scope, or replacing
40
+ an existing Worker. A generic delegation is neither upgraded nor revoked implicitly.
41
+
42
+ The MAC includes the previous contract/grant snapshot so a revoked confirmation cannot
43
+ restore authority. Exact successful and interrupted-write retries must preserve identity;
44
+ legacy event-first records retain their bounded pre-execution recovery. Shared Milestone
45
+ exclusion protects issuance, revocation and scope/status transitions.
46
+
47
+ Acceptance must continue through same-Worker execution and guarded atomic context refresh
48
+ using the new full contract, with the old grant rejected without Task/Knowledge mutations.
49
+ Include read-only preparation, stale bindings, valid-contract replacement refusal, malformed
50
+ actors/expiry, unavailable Knowledge and disallowed Milestone states. Optional Task-first
51
+ navigation must not replace the current execution/recovery action. Run the full test suite,
52
+ release checks, downstream package smoke and plugin validation on the final candidate.
53
+
7
54
  ## beta.13.7 candidate: retire completed carryover authority
8
55
 
9
56
  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.8",
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
  }
@@ -124,8 +124,8 @@ paths and content hash. Every retained path must already be allowed by the new S
124
124
  and remain outside forbidden scope. A changed binding or blocked assessment is a hard
125
125
  stop; never clean, stash, reset, manually commit, or edit state to make it eligible.
126
126
 
127
- The beta.13.7 consumed-carryover compatibility profile also permits the exact
128
- published external beta.13.7 runner to execute only the read-only
127
+ The beta.13.8 consumed-carryover compatibility profile also permits the exact
128
+ published external beta.13.8 runner to execute only the read-only
129
129
  `update corrective-carryover-preflight --id <TASK-ID>` against a coherent beta.13.6
130
130
  repository. A completed, verified Step's carryover remains historical evidence;
131
131
  it must not override a later Step's repeated-cause recovery route.
@@ -151,13 +151,17 @@ another compatibility action. `sourceRecovery` never authorizes transport or
151
151
  Once the replacement Plan is authorized and that same Worker has claimed it,
152
152
  the preflight may expose the exact expired-lease repair described below. Never
153
153
  force-release a live lease. Require a fresh top-level `eligible=true` before
154
- dependency transport. beta.13.7 accepts one dependency-only commit on each of
154
+ dependency transport. beta.13.8 accepts one dependency-only commit on each of
155
155
  the Task and Milestone base, with the same exact source beta.13.6 and target
156
- beta.13.7. After installing the target, use its ordinary advertised
156
+ beta.13.8. After installing the target, use its ordinary advertised
157
157
  dependency provenance recovery, atomic context refresh and original-Worker
158
158
  credential recovery. This profile permits no external state mutation, installed
159
159
  code patch, wider product commit, changed dirty bytes, or approval substitution.
160
160
 
161
+ The historical beta.13.7 runner retains its own exact beta.13.6-to-beta.13.7 profile.
162
+ The current beta.13.8 profile does not admit a beta.13.7 source or an in-progress
163
+ Step and cannot be used to bypass the ordinary safe update boundary.
164
+
161
165
  The historical beta.13.6 corrective-carryover update compatibility profile permits its exact
162
166
  external runner to execute only the read-only
163
167
  `update corrective-carryover-preflight --id <TASK-ID>` against a repository coherently
@@ -653,10 +657,20 @@ delegable only through an explicit project-scoped `project_memory.approve` permi
653
657
  changes, grant issuance, and grant expansion are never delegated by `delegated-approval-v1`.
654
658
 
655
659
  For an entire Milestone, prefer one bounded `milestone autonomy-prepare` gate after the complete
656
- initial membership Plan exists. Show principal, delegate, expiry, semantic-scope hash, policy
660
+ initial membership Plan exists. An explicitly human-requested full contract may also be prepared
661
+ for an `active` Milestone, including after a previous contract expired or was revoked. This is
662
+ new issuance, never automatic renewal. Fresh Task-first or repository `next` may expose
663
+ `activeMilestoneAutonomyOptions`; these are optional preparation routes and do not replace the
664
+ current Task action. Preparation still requires coherent state and active Project Knowledge.
665
+ A generic grant does not become a full contract and is not automatically revoked. A still-valid
666
+ full contract must first be explicitly revoked by its principal before a different one is issued.
667
+ The new confirmation binds the current Milestone status, revision, Plan, semantic scope, policy,
668
+ and previous contract/grant state; any change requires preparation and confirmation again.
669
+ An exact successful retry returns the same grant; an old code cannot revive a revoked grant. Show principal, delegate, expiry, semantic-scope hash, policy
657
670
  hash, and `MAC-*` code, then stop. A later exact approval permits `milestone autonomy-grant`.
658
671
  Use the delegated path only when the same `next` response exposes
659
- `milestoneAuthorizationOptions[].action = "milestone autonomy-prepare"` and the user explicitly
672
+ `milestoneAuthorizationOptions[].action = "milestone autonomy-prepare"` or an
673
+ `activeMilestoneAutonomyOptions` entry for that exact Milestone, and the user explicitly
660
674
  requested delegated Milestone operation; otherwise follow the ordinary `milestone authorize`
661
675
  human gate.
662
676
  The resulting grant covers the existing Task/Milestone approval transitions and the Project
@@ -736,3 +750,18 @@ returns `safe=true` with a clean checkout, no running Step, and no active writer
736
750
  - A lifecycle `status` or `next` failed; do not continue with a different lifecycle mutation.
737
751
  - A human gate was emitted but the user has not approved its exact confirmation code in a
738
752
  later message and no exact eligible delegated approval option exists.
753
+
754
+ ## Explain an implementation change before escalating
755
+
756
+ Before a product write by a tracked Worker, run the packaged registry `dispatch-context-check`
757
+ using fresh App readback, the actual write root and the branch from the current Task. Resolve
758
+ checkout mismatch without duplicate dispatch or silent canonical-checkout writes. A matching
759
+ context is not a filesystem permission or writer credential.
760
+
761
+ When a fix needs additional files, the route is unclear, or an external operation was denied,
762
+ use read-only `change explain --task <exact Task ID> --file <closed proposal JSON>` as documented
763
+ in `docs/change-model.md`. Present all known blocking layers together. Never relabel an external
764
+ permission refusal as a missing grant. `requiresUser: null` is unknown, not blanket approval.
765
+ For an exact Core-derived check-support amendment, follow the existing advertised recovery and
766
+ fresh next; do not manually widen allowedWrites or issue new authority. Membership, semantic
767
+ scope and Knowledge refresh retain their separate existing routes and evidence requirements.