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.
- package/README.md +1 -1
- package/dist/reviewer-runtime-build.json +21 -13
- package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +2 -0
- package/dist/src/alpha6/plan-risk.js +10 -3
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- 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/lifecycle/corrective-replan.js +3 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +5 -4
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +2 -2
- package/dist/src/state/corrective-yield-executor.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 +406 -238
- 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/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +3 -5
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
- package/docs/release.md +72 -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 +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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
485
|
-
`packageVersion=2.0.0-beta.13.
|
|
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.
|
|
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.
|
|
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
|
@@ -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
|
}
|