codex-workflow-v2 2.0.0-beta.13.2 → 2.0.0-beta.13.5

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +18 -10
  3. package/dist/src/alpha6/plan-risk.d.ts +1 -0
  4. package/dist/src/alpha6/plan-risk.js +9 -6
  5. package/dist/src/alpha6/plan-risk.js.map +1 -1
  6. package/dist/src/alpha6/preexecution-replan.d.ts +38 -0
  7. package/dist/src/alpha6/preexecution-replan.js +130 -0
  8. package/dist/src/alpha6/preexecution-replan.js.map +1 -0
  9. package/dist/src/alpha6/remediation.d.ts +4 -2
  10. package/dist/src/alpha6/remediation.js +155 -20
  11. package/dist/src/alpha6/remediation.js.map +1 -1
  12. package/dist/src/cli-actions.d.ts +1 -1
  13. package/dist/src/cli-actions.js +1 -0
  14. package/dist/src/cli-actions.js.map +1 -1
  15. package/dist/src/cli.js +44 -0
  16. package/dist/src/cli.js.map +1 -1
  17. package/dist/src/domain/plan-semantics.d.ts +11 -0
  18. package/dist/src/domain/plan-semantics.js +49 -0
  19. package/dist/src/domain/plan-semantics.js.map +1 -0
  20. package/dist/src/state/corrective-yield-executor.js +10 -1
  21. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  22. package/dist/src/version.d.ts +1 -1
  23. package/dist/src/version.js +1 -1
  24. package/dist/src/workflow.d.ts +19 -1
  25. package/dist/src/workflow.js +518 -84
  26. package/dist/src/workflow.js.map +1 -1
  27. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  28. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  29. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  30. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
  31. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
  32. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +3 -3
  33. package/docs/release.md +59 -2
  34. package/docs/updating-existing-project.md +10 -2
  35. package/package.json +1 -1
  36. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +128 -0
  37. package/plugins/codex-workflow-gateway/references/protocol.md +4 -0
  38. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +185 -0
  39. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
  40. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +50 -3
  41. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +63 -6
  42. package/schemas/preexecution-replan-event.schema.json +45 -0
@@ -12,6 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import process from 'node:process';
14
14
  import { randomUUID } from 'node:crypto';
15
+ import { dispatchAction, dispatchInput } from './chat-dispatch.mjs';
15
16
 
16
17
  const TYPES = new Set(['coordinator', 'task', 'step-review', 'final-review', 'corrective-audit', 'plan-audit']);
17
18
  const LOCK_STALE_MS = 30_000;
@@ -25,6 +26,14 @@ export function allocateChat(options) {
25
26
  const sequence = registry.nextSequence;
26
27
  const reservationId = randomUUID();
27
28
  const candidates = buildTitleCandidates({ ...options, sequence });
29
+ const existing = registry.entries.find(entry => entry.status !== 'abandoned'
30
+ && entry.type === options.type && entry.entityId === options.entityId
31
+ && entry.milestoneOrdinal === requiredOrdinal(options.milestoneOrdinal, 'M')
32
+ && entry.taskOrdinal === optionalOrdinal(options.taskOrdinal, 'T')
33
+ && entry.stepOrdinal === optionalOrdinal(options.stepOrdinal, 'S')
34
+ && (['task', 'coordinator'].includes(options.type)
35
+ || entry.attempt === positiveInteger(options.attempt ?? 1, 'attempt')));
36
+ if (existing) return { ...publicEntry(existing), reused: true };
28
37
  const entry = {
29
38
  sequence,
30
39
  reservationId,
@@ -92,6 +101,17 @@ export function bindChat(options) {
92
101
  const registry = readRegistry(file, options.projectId);
93
102
  const entry = requireEntry(registry, options.reservationId);
94
103
  if (!entry.titleVerified) throw new Error('Chat title must pass exact readback before binding.');
104
+ if (options.threadId?.startsWith('client-')) throw new Error('Cannot bind a pending clientThreadId.');
105
+ if (entry.threadId && (entry.threadId !== options.threadId || entry.hostId !== options.hostId)) {
106
+ throw new Error('Reservation already bound to another thread.');
107
+ }
108
+ if (registry.entries.some(other => other !== entry && other.threadId === options.threadId
109
+ && other.hostId === options.hostId)) throw new Error('Thread already belongs to another reservation.');
110
+ if (entry.dispatch && (entry.dispatch.state !== 'resolved' || entry.dispatch.candidateThreadId !== options.threadId
111
+ || entry.dispatch.hostId !== options.hostId
112
+ || entry.dispatch.observations.at(-1)?.title !== entry.observedTitle)) {
113
+ throw new Error('Tracked creation must pass correlated read_thread observation before binding.');
114
+ }
95
115
  entry.threadId = requiredText(options.threadId, 'threadId');
96
116
  entry.hostId = requiredText(options.hostId, 'hostId');
97
117
  entry.status = 'bound';
@@ -106,6 +126,9 @@ export function retitleChat(options) {
106
126
  return withRegistryLock(options.projectId, options.registryHome, (file) => {
107
127
  const registry = readRegistry(file, options.projectId);
108
128
  const entry = requireEntry(registry, options.reservationId);
129
+ if (entry.dispatch && options.entityId !== entry.entityId) {
130
+ throw new Error('Tracked dispatch entity is immutable; retitle cannot replace its identity.');
131
+ }
109
132
  entry.entityId = requiredText(options.entityId, 'entityId');
110
133
  entry.semanticTitle = requiredText(options.semanticTitle, 'semanticTitle');
111
134
  entry.candidates = buildTitleCandidates({
@@ -134,6 +157,7 @@ export function abandonChat(options) {
134
157
  return withRegistryLock(options.projectId, options.registryHome, (file) => {
135
158
  const registry = readRegistry(file, options.projectId);
136
159
  const entry = requireEntry(registry, options.reservationId);
160
+ if (entry.dispatch || entry.threadId) throw new Error('Cannot abandon a started creation or bound chat to enable a duplicate.');
137
161
  entry.status = 'abandoned';
138
162
  entry.updatedAt = new Date().toISOString();
139
163
  writeRegistry(file, registry);
@@ -141,6 +165,20 @@ export function abandonChat(options) {
141
165
  });
142
166
  }
143
167
 
168
+ export function dispatchChat(options) {
169
+ validateProjectId(options.projectId);
170
+ return withRegistryLock(options.projectId, options.registryHome, file => {
171
+ const registry = readRegistry(file, options.projectId);
172
+ const entry = requireEntry(registry, options.reservationId);
173
+ const result = dispatchAction(options.action, entry, options.input);
174
+ if (options.action !== 'dispatch-status') {
175
+ entry.updatedAt = new Date().toISOString();
176
+ writeRegistry(file, registry);
177
+ }
178
+ return result;
179
+ });
180
+ }
181
+
144
182
  export function showRegistry(options) {
145
183
  validateProjectId(options.projectId);
146
184
  const file = registryFile(options.projectId, options.registryHome);
@@ -217,7 +255,10 @@ function withRegistryLock(projectId, registryHome, operation) {
217
255
 
218
256
  function lockIsStale(lock) {
219
257
  try {
220
- return Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS;
258
+ if (Date.now() - statSync(lock).mtimeMs <= LOCK_STALE_MS) return false;
259
+ const owner = JSON.parse(readFileSync(path.join(lock, 'owner.json'), 'utf8'));
260
+ if (!Number.isSafeInteger(owner.pid) || owner.pid < 1) return false;
261
+ try { process.kill(owner.pid, 0); return false; } catch (error) { return error?.code === 'ESRCH'; }
221
262
  } catch {
222
263
  return false;
223
264
  }
@@ -253,7 +294,7 @@ function readRegistry(file, projectId) {
253
294
 
254
295
  function writeRegistry(file, registry) {
255
296
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
256
- writeFileSync(temporary, `${JSON.stringify(registry, null, 2)}\n`, { flag: 'wx' });
297
+ writeFileSync(temporary, `${JSON.stringify(registry, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
257
298
  renameSync(temporary, file);
258
299
  }
259
300
 
@@ -275,6 +316,7 @@ function publicEntry(entry) {
275
316
  threadId: entry.threadId,
276
317
  hostId: entry.hostId,
277
318
  status: entry.status,
319
+ dispatchState: entry.dispatch?.state ?? null,
278
320
  };
279
321
  }
280
322
 
@@ -334,6 +376,11 @@ function cli() {
334
376
  projectId: option(args, 'project-id'),
335
377
  registryHome: option(args, 'registry-home'),
336
378
  };
379
+ if (action?.startsWith('dispatch-') || action === 'supervision-record' || action === 'model-select') {
380
+ const file = option(args, 'file');
381
+ return dispatchChat({ ...common, action, reservationId: option(args, 'reservation-id'),
382
+ input: file ? dispatchInput(file) : {} });
383
+ }
337
384
  if (action === 'allocate') return allocateChat({
338
385
  ...common,
339
386
  type: option(args, 'type'),
@@ -363,7 +410,7 @@ function cli() {
363
410
  });
364
411
  if (action === 'abandon') return abandonChat({ ...common, reservationId: option(args, 'reservation-id') });
365
412
  if (action === 'show') return showRegistry(common);
366
- throw new Error('Use allocate, retitle, readback, bind, abandon, or show.');
413
+ throw new Error('Use allocate, retitle, readback, bind, abandon, show, dispatch-begin/result/discover/resolve-duplicates/observe/status, supervision-record, or model-select.');
367
414
  }
368
415
 
369
416
  if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
@@ -92,6 +92,29 @@ active writer token when the C1 contract requires it. The recovery records only
92
92
  `task run` merely to manufacture the second failure, and never invoke this recovery for a generic
93
93
  test failure, a writable manifest, or unconfined changes.
94
94
 
95
+ When fresh `next` includes `preExecutionReplanOption.state=eligible`, an independent Plan auditor
96
+ may stop a `ready` Task before its next Step because the current implementation Plan is obstructed.
97
+ Use only `task preexecution-replan` with the exact advertised Task revision, Brief hash, Plan hash,
98
+ current Plan Risk Audit event hash, and HEAD. The audit file must be a genuine independent
99
+ `stop-escalate` replacement audit: its planner remains the original planner, its auditor is the
100
+ transition actor, and it retains current guarded categories, review requirements, failure modes,
101
+ evidence commands, and proof bindings while recording the obstruction. This transition requires a
102
+ clean checkout with no Step in progress, C1 posture, or writer lease. It appends immutable audit and
103
+ provenance records, supersedes only current execution authorization, preserves completed Step
104
+ authority and existing delegation scope, and does not require a new approval. After success, run
105
+ sequential `status` then `next`; Core must advertise ordinary `task plan-set`, not audited-remediation
106
+ topology. Submit a semantically changed Plan with the exact Task objective, requirements, and
107
+ acceptance, preserve completed Steps and prior safety boundaries, attach a fresh Plan Risk Audit,
108
+ then obtain fresh execution authorization. Never append an approved audit to the unchanged Plan,
109
+ rebind context around the marker, invent a failure/corrective cause, or invoke the command when the
110
+ option is absent or blocked.
111
+
112
+ For this route, Core treats each `(Step ID, failureMode, evidence)` tuple as the immutable proof
113
+ binding. It does not claim to understand whether revised `counterexample` or `expectedResult` prose
114
+ is semantically stronger. Changing that prose therefore requires an independent
115
+ `approved-with-rationale` audit, a changed substantive summary, and an explicit rationale; the
116
+ earlier audit remains immutable history for human review.
117
+
95
118
  When fresh `next` advertises `task downstream-proof-recover`, the active proof Step has produced
96
119
  uncommitted files inside its own `allowedWrites` and has also exposed required changes owned by one
97
120
  or more completed transitive predecessor Steps. Do not invoke `task step-complete`: its scope is
@@ -329,9 +352,11 @@ A Milestone chat and its Task chats are separate user-visible execution contexts
329
352
  conversation. Create every Task chat as a brand-new Codex task with `create_thread`, an explicit
330
353
  Task-only prompt, and an explicit title. Never use `fork_thread`, thread handoff, or a continuation
331
354
  of the Milestone chat to create a Task chat: those paths carry parent history into the child. A
332
- failure to create a new task is a routing blocker. Check the thread list for an already-created
333
- exact Task ID, retry `create_thread` once sequentially when none exists, and then stop; never fall
334
- back to a fork.
355
+ confirmed failure to create a new task is a routing blocker. A pending `clientThreadId`, timeout,
356
+ lost response, or absence from `list_threads` does not establish failure and never permits a
357
+ second creation. Persist the attempt before calling the App, resolve its actual ID and confirm
358
+ it with `read_thread`; never fall back to a fork. Read and follow
359
+ `../../references/chat-dispatch.md` for the executable dispatch/recovery and model-selection contract.
335
360
 
336
361
  Codex App may wrap the supplied prompt in a host-generated `codex_delegation` envelope containing
337
362
  only routing provenance such as `source_thread_id`. That envelope is permitted and is not parent
@@ -376,6 +401,22 @@ chat type, Milestone/Task/Step membership ordinals, semantic title, authoritativ
376
401
  review attempt. Use returned `requestedTitle` verbatim. Never derive its prefix from
377
402
  `list_threads`, sidebar order, a count, or `count + 1`: allocation atomically reserves the
378
403
  project-wide monotonically increasing `#NNN`, and an abandoned number is never reused.
404
+ Allocation reuses an existing matching reservation. For a new dispatch, call `dispatch-begin`
405
+ with the closed packet and explicit model request, then call the App exactly once only when
406
+ `createAllowed=true`, using returned `createArgs` unchanged. Record the full result via
407
+ `dispatch-result`, resolve/read back via `dispatch-discover` and `dispatch-observe`, and bind.
408
+ After restart use `dispatch-status`; an unresolved creation cannot be abandoned to create another.
409
+ For legacy reservations without a dispatch marker, first reconcile existing App/source evidence;
410
+ absence of the new marker does not prove no earlier creation occurred.
411
+
412
+ When the user authorizes model selection, use the versioned role/phase/complexity matrix rather
413
+ than coordinator inheritance. Both `model` and App `thinking` must be explicit in create arguments.
414
+ Use the destination host's current supported model/effort catalog; no guessed aliases or silent
415
+ fallback. Before a materially different phase, `model-select` returns explicit follow-up settings
416
+ for the same bound thread. Routine lifecycle work should not retain planning/review reasoning by
417
+ accident; security-sensitive and complex work must not be classified as routine to save tokens.
418
+ Without user authorization for model selection, do not manufacture it from a registry field;
419
+ resolve that policy choice before starting a new dispatch.
379
420
 
380
421
  The builder owns these compact title families; callers must not hand-compose variants:
381
422
 
@@ -397,7 +438,7 @@ Put `#NNN` and membership ordinals first so clipped sidebar titles remain distin
397
438
  inherit the parent title, use a generic title such as `Milestone recovery` / `Task execution`, or
398
439
  reuse one title for different entity IDs. Maintain the registry-backed
399
440
  `Task ID -> thread ID -> title` mapping in
400
- the Milestone chat. Before dispatch, verify from the thread list that the title contains the exact
441
+ the Milestone chat. Before supervision, verify from actual `read_thread` evidence that the title contains the exact
401
442
  ordinal and entity ID, is unique, and the new chat input contains only its TaskContextPacket plus
402
443
  the permitted host routing envelope. Never trust the `create_thread` title argument without a
403
444
  readback: pass the observed title to registry `readback`. If it returns `renameRequired=true`,
@@ -406,6 +447,12 @@ compact the role while preserving `#NNN`, membership ordinals, attempt, and the
406
447
  Never invent another fallback or abbreviate the ID. Bind the verified entry to `threadId` and
407
448
  `hostId`; if all candidates fail, mark it blocked and stop routing. Visual sidebar clipping is
408
449
  harmless only when exact API readback still matches. Do not dispatch while title/context is wrong.
450
+ Persist the single supervisor, cursor and next action using `supervision-record`. Large tool/CLI
451
+ JSON must be saved completely to a private local file and parsed there, with only bounded non-secret
452
+ summaries printed. For an uncertain mutation outcome, run fresh `status -> next` before recovery;
453
+ never replay a mutation merely because displayed output was truncated. Review the exact serialized
454
+ external-review envelope passed to Core, not a separately reconstructed object. Public CREF expiry
455
+ is independent of lease expiry and must be checked before every credential-consuming transition.
409
456
 
410
457
  After every significant Task Step boundary—completion, failure, block, skip, sealed-review result,
411
458
  or corrective decision—the Task chat prints a compact Task progress Markdown table from fresh
@@ -562,7 +609,13 @@ When `next.strictStepReview.recommendedMode` is `external-sealed`, do not launch
562
609
  reviewer from inside a Codex App sandbox. Run the advertised read-only `task step-review-packet`,
563
610
  create a separate user-visible reviewer chat for that exact packet, and require one closed
564
611
  `ReviewInput` JSON result bound to its reviewed commit. The reviewer must not modify repository or
565
- Workflow state. Then run only the advertised `task step-review-record` with the unchanged packet
612
+ Workflow state. The `--file` passed to `task step-review-record` must be the full
613
+ `ExternalStrictStepReviewInput` envelope: top-level `protocol`, `packetHash`,
614
+ `repositorySealHash`, `reviewerThreadId`, and `reviewMode` (`ordinary` or `security`), plus the
615
+ nested `review` object containing `status`, `reviewer`, `summary`, and `findings`. Copy both hashes
616
+ and the mode from the packet; do not flatten `review`, and do not pass `--review-mode` to the record
617
+ command because it reads the mode from the JSON file. Run `task step-review-record --help` for a
618
+ complete minimal example. Then run only the advertised `task step-review-record` with the unchanged packet
566
619
  and repository-seal hashes, the separate reviewer thread ID, the lifecycle actor from `next`, and
567
620
  the active writer token when required. Never author the independent review in the Worker or
568
621
  Delivery Coordinator chat, never alter the packet, and never treat an unverified local reviewer
@@ -573,7 +626,11 @@ When `next.strictTaskReview.recommendedMode` is `external-sealed`, apply the sam
573
626
  the final submitted-Task review. Run the advertised read-only `task review-packet`, send the exact
574
627
  packet to a separate user-visible Independent Reviewer chat, and accept only a closed `ReviewInput`
575
628
  bound to the unchanged packet and repository-seal hashes. Record it only through the advertised
576
- `task review-sealed-record`, using the lifecycle actor from `next` and the active writer token when
629
+ `task review-sealed-record`. Its `--file` must be the analogous full
630
+ `ExternalStrictTaskReviewInput` envelope using protocol
631
+ `codex-workflow-external-strict-task-review-v1`, the packet and repository-seal hashes,
632
+ `reviewerThreadId`, top-level `reviewMode`, and nested `review`; its `--help` contains a minimal
633
+ example. Use the lifecycle actor from `next` and the active writer token when
577
634
  required. Do not call `task review-launch` from inside a Codex App sandbox, do not use the Worker or
578
635
  Delivery Coordinator as reviewer, and discard the result if a fresh packet changes either hash.
579
636
 
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://codex-workflow.local/schemas/preexecution-replan-event.schema.json",
4
+ "title": "Codex Workflow V2 Pre-execution Replan Event",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "eventId", "taskId", "sourceTaskRevision", "resultTaskRevision", "briefHash",
9
+ "sourcePlanHash", "knowledgeMapRevision", "knowledgeMapHash",
10
+ "sourcePlanRiskAuditEventId", "sourcePlanRiskAuditEventHash",
11
+ "obstructionPlanRiskAuditEventId", "obstructionPlanRiskAuditEventHash",
12
+ "sourceExecutionAuthorizationHash", "sourceHeadCommit", "completedStepIds",
13
+ "completedStepsHash", "actor", "reason", "recordedAt", "previousEventHash", "eventHash"
14
+ ],
15
+ "properties": {
16
+ "eventId": { "type": "string", "pattern": "^PER-[0-9A-HJKMNP-TV-Z]{26}$" },
17
+ "taskId": { "type": "string", "pattern": "^TASK-[0-9A-HJKMNP-TV-Z]{26}$" },
18
+ "sourceTaskRevision": { "type": "integer", "minimum": 1 },
19
+ "resultTaskRevision": { "type": "integer", "minimum": 2 },
20
+ "briefHash": { "$ref": "#/$defs/hash64" },
21
+ "sourcePlanHash": { "$ref": "#/$defs/hash64" },
22
+ "knowledgeMapRevision": { "type": "integer", "minimum": 1 },
23
+ "knowledgeMapHash": { "$ref": "#/$defs/hash64" },
24
+ "sourcePlanRiskAuditEventId": { "type": "string", "pattern": "^PRA-[0-9A-HJKMNP-TV-Z]{26}$" },
25
+ "sourcePlanRiskAuditEventHash": { "$ref": "#/$defs/hash64" },
26
+ "obstructionPlanRiskAuditEventId": { "type": "string", "pattern": "^PRA-[0-9A-HJKMNP-TV-Z]{26}$" },
27
+ "obstructionPlanRiskAuditEventHash": { "$ref": "#/$defs/hash64" },
28
+ "sourceExecutionAuthorizationHash": { "$ref": "#/$defs/hash64" },
29
+ "sourceHeadCommit": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
30
+ "completedStepIds": {
31
+ "type": "array",
32
+ "uniqueItems": true,
33
+ "items": { "type": "string", "pattern": "^STEP-[0-9]{3}(?:-[A-Z0-9-]+)?$" }
34
+ },
35
+ "completedStepsHash": { "$ref": "#/$defs/hash64" },
36
+ "actor": { "type": "string", "minLength": 1 },
37
+ "reason": { "type": "string", "minLength": 1, "maxLength": 2048 },
38
+ "recordedAt": { "type": "string", "format": "date-time" },
39
+ "previousEventHash": { "anyOf": [{ "$ref": "#/$defs/hash64" }, { "type": "null" }] },
40
+ "eventHash": { "$ref": "#/$defs/hash64" }
41
+ },
42
+ "$defs": {
43
+ "hash64": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
44
+ }
45
+ }