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.
- package/README.md +1 -1
- package/dist/reviewer-runtime-build.json +13 -9
- package/dist/src/alpha7/autonomy.d.ts +3 -0
- package/dist/src/alpha7/autonomy.js +89 -42
- package/dist/src/alpha7/autonomy.js.map +1 -1
- package/dist/src/change-explanation.d.ts +64 -0
- package/dist/src/change-explanation.js +150 -0
- package/dist/src/change-explanation.js.map +1 -0
- package/dist/src/cli-actions.d.ts +1 -0
- package/dist/src/cli-actions.js +1 -0
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +4 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/state/store.d.ts +1 -1
- package/dist/src/state/store.js +17 -3
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +49 -0
- package/dist/src/workflow.js +376 -230
- package/dist/src/workflow.js.map +1 -1
- package/docs/change-model.md +118 -0
- package/docs/delegated-approval.md +26 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/release.md +47 -0
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +76 -0
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +116 -3
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +6 -1
- 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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
@@ -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:
|
|
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 !==
|
|
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.
|
|
128
|
-
published external beta.13.
|
|
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.
|
|
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.
|
|
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.
|
|
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"`
|
|
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.
|