codex-workflow-v2 2.0.0-beta.13.1 → 2.0.0-beta.13.3

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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +17 -13
  3. package/dist/src/alpha6/remediation.d.ts +1 -0
  4. package/dist/src/alpha6/remediation.js +12 -3
  5. package/dist/src/alpha6/remediation.js.map +1 -1
  6. package/dist/src/cli-actions.d.ts +1 -1
  7. package/dist/src/cli-actions.js +3 -0
  8. package/dist/src/cli-actions.js.map +1 -1
  9. package/dist/src/cli.js +15 -0
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/gateway-handshake.js +1 -0
  12. package/dist/src/gateway-handshake.js.map +1 -1
  13. package/dist/src/observed-routes.js +1 -0
  14. package/dist/src/observed-routes.js.map +1 -1
  15. package/dist/src/pending-review-update.d.ts +49 -0
  16. package/dist/src/pending-review-update.js +132 -0
  17. package/dist/src/pending-review-update.js.map +1 -0
  18. package/dist/src/repository.js +16 -4
  19. package/dist/src/repository.js.map +1 -1
  20. package/dist/src/reviewer.js +4 -1
  21. package/dist/src/reviewer.js.map +1 -1
  22. package/dist/src/state/corrective-yield-executor.js +10 -1
  23. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  24. package/dist/src/version.d.ts +1 -1
  25. package/dist/src/version.js +1 -1
  26. package/dist/src/workflow.d.ts +10 -0
  27. package/dist/src/workflow.js +161 -8
  28. package/dist/src/workflow.js.map +1 -1
  29. package/docs/beta13.2-signal-review-recovery.md +38 -0
  30. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  31. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  32. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  33. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
  34. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
  35. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +3 -3
  36. package/docs/pending-review-update.md +15 -0
  37. package/docs/release.md +27 -2
  38. package/docs/updating-existing-project.md +10 -2
  39. package/package.json +1 -1
  40. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +128 -0
  41. package/plugins/codex-workflow-gateway/references/protocol.md +11 -0
  42. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +185 -0
  43. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
  44. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +50 -3
  45. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +64 -17
@@ -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)) {
@@ -278,22 +278,32 @@ because the sandboxed call returned a permission error; those actions require th
278
278
  authorization and evidence.
279
279
 
280
280
  When `next` includes `writerTokenContract`, treat it as an exact first-call precondition. Retain
281
- the active writer lease from the first-field `writerLeaseReceipt` returned by `task run` and pass it using the advertised `option` for that
282
- transition. The token is sensitive: never print, summarize, persist, or substitute a redacted
283
- fingerprint. Stop before mutation if the active lease is unavailable. This input contract is
284
- independent from `gitMutationPolicy`; satisfy both when both are present.
285
-
286
- On successful `task handoff-prepare`, consume the first-field `credentialHandoff` before reading
287
- the Task/event/bundle payload. Retain its one-time token only in working memory and use it exactly
288
- once with the declared `action` and `option` for the declared `targetActor`. Successful `task claim`
289
- returns a first-field `writerLeaseReceipt`; retain that lease and pass it to claimed `task run` when
290
- the fresh `next.writerTokenContract` requires it. Successful `task run` returns the refreshed
291
- `writerLeaseReceipt`; retain it only until Core releases that lease.
292
- Never search later payload fields for either credential and never copy credentials into evidence.
281
+ the `writerCredentialReference` returned by `task run`, `task claim`, or
282
+ `task writer-credential-replace`, including its public `credentialRef`, actor, allowed actions and
283
+ `expiresAt`. Pass the reference using the advertised `--writer-credential-ref` option. The secret
284
+ stays in the local vault: never read, print, copy, or edit vault records or substitute a redacted
285
+ token. This input contract is independent from `gitMutationPolicy`; satisfy both when present.
286
+
287
+ Before every credential-consuming mutation, especially after an independent review or a pause,
288
+ compare the held reference's `expiresAt` with the current time and verify its actor/action binding.
289
+ A live or renewed lease does not extend an existing reference's expiry; `reuse-reference` is not
290
+ permission to reuse an expired reference. If the reference is expired, missing, consumed, or its
291
+ validity cannot be established, use only the simultaneously advertised
292
+ `next.writerCredentialRecovery` action with its exact Task, revision and `requiredActor` before
293
+ attempting the blocked mutation. Follow successful replacement with `status` then `next`, retain
294
+ the newly returned reference and expiry, and discard the prior reference. If recovery is not
295
+ advertised, stop before mutation. Never extend expiry manually or retry with the old reference.
296
+
297
+ Successful `task handoff-prepare` returns a one-time `claimCredentialReference`; use its
298
+ `credentialRef` with the declared `--claim-credential-ref` option for `task claim` and the bound
299
+ actor. Successful claim consumes that reference. A renewed claim/run may return
300
+ `writerCredential.disposition=renewed` without a new reference; the held reference must still
301
+ satisfy its own expiry and binding checks. Never search later payload fields for raw tokens and
302
+ never copy credentials into evidence.
293
303
 
294
304
  When a passed final Task review routes to `task result-set`, pass the active writer credential and,
295
305
  for claimed C1 posture, the exact claimant as `--actor`. A successful `result-set` consumes that
296
- execution lease before entering `awaiting_final_acceptance`; never reuse the old token and never
306
+ execution lease before entering `awaiting_final_acceptance`; never reuse the old reference and never
297
307
  leave or manufacture a lease while waiting for the Human decision. After `task accept`, follow fresh
298
308
  `next`: local merge or external merge confirmation normally advertises
299
309
  `task writer-credential-replace`, which acquires a new bounded lease for the exact recovery actor.
@@ -319,9 +329,11 @@ A Milestone chat and its Task chats are separate user-visible execution contexts
319
329
  conversation. Create every Task chat as a brand-new Codex task with `create_thread`, an explicit
320
330
  Task-only prompt, and an explicit title. Never use `fork_thread`, thread handoff, or a continuation
321
331
  of the Milestone chat to create a Task chat: those paths carry parent history into the child. A
322
- failure to create a new task is a routing blocker. Check the thread list for an already-created
323
- exact Task ID, retry `create_thread` once sequentially when none exists, and then stop; never fall
324
- back to a fork.
332
+ confirmed failure to create a new task is a routing blocker. A pending `clientThreadId`, timeout,
333
+ lost response, or absence from `list_threads` does not establish failure and never permits a
334
+ second creation. Persist the attempt before calling the App, resolve its actual ID and confirm
335
+ it with `read_thread`; never fall back to a fork. Read and follow
336
+ `../../references/chat-dispatch.md` for the executable dispatch/recovery and model-selection contract.
325
337
 
326
338
  Codex App may wrap the supplied prompt in a host-generated `codex_delegation` envelope containing
327
339
  only routing provenance such as `source_thread_id`. That envelope is permitted and is not parent
@@ -366,6 +378,22 @@ chat type, Milestone/Task/Step membership ordinals, semantic title, authoritativ
366
378
  review attempt. Use returned `requestedTitle` verbatim. Never derive its prefix from
367
379
  `list_threads`, sidebar order, a count, or `count + 1`: allocation atomically reserves the
368
380
  project-wide monotonically increasing `#NNN`, and an abandoned number is never reused.
381
+ Allocation reuses an existing matching reservation. For a new dispatch, call `dispatch-begin`
382
+ with the closed packet and explicit model request, then call the App exactly once only when
383
+ `createAllowed=true`, using returned `createArgs` unchanged. Record the full result via
384
+ `dispatch-result`, resolve/read back via `dispatch-discover` and `dispatch-observe`, and bind.
385
+ After restart use `dispatch-status`; an unresolved creation cannot be abandoned to create another.
386
+ For legacy reservations without a dispatch marker, first reconcile existing App/source evidence;
387
+ absence of the new marker does not prove no earlier creation occurred.
388
+
389
+ When the user authorizes model selection, use the versioned role/phase/complexity matrix rather
390
+ than coordinator inheritance. Both `model` and App `thinking` must be explicit in create arguments.
391
+ Use the destination host's current supported model/effort catalog; no guessed aliases or silent
392
+ fallback. Before a materially different phase, `model-select` returns explicit follow-up settings
393
+ for the same bound thread. Routine lifecycle work should not retain planning/review reasoning by
394
+ accident; security-sensitive and complex work must not be classified as routine to save tokens.
395
+ Without user authorization for model selection, do not manufacture it from a registry field;
396
+ resolve that policy choice before starting a new dispatch.
369
397
 
370
398
  The builder owns these compact title families; callers must not hand-compose variants:
371
399
 
@@ -387,7 +415,7 @@ Put `#NNN` and membership ordinals first so clipped sidebar titles remain distin
387
415
  inherit the parent title, use a generic title such as `Milestone recovery` / `Task execution`, or
388
416
  reuse one title for different entity IDs. Maintain the registry-backed
389
417
  `Task ID -> thread ID -> title` mapping in
390
- the Milestone chat. Before dispatch, verify from the thread list that the title contains the exact
418
+ the Milestone chat. Before supervision, verify from actual `read_thread` evidence that the title contains the exact
391
419
  ordinal and entity ID, is unique, and the new chat input contains only its TaskContextPacket plus
392
420
  the permitted host routing envelope. Never trust the `create_thread` title argument without a
393
421
  readback: pass the observed title to registry `readback`. If it returns `renameRequired=true`,
@@ -396,6 +424,12 @@ compact the role while preserving `#NNN`, membership ordinals, attempt, and the
396
424
  Never invent another fallback or abbreviate the ID. Bind the verified entry to `threadId` and
397
425
  `hostId`; if all candidates fail, mark it blocked and stop routing. Visual sidebar clipping is
398
426
  harmless only when exact API readback still matches. Do not dispatch while title/context is wrong.
427
+ Persist the single supervisor, cursor and next action using `supervision-record`. Large tool/CLI
428
+ JSON must be saved completely to a private local file and parsed there, with only bounded non-secret
429
+ summaries printed. For an uncertain mutation outcome, run fresh `status -> next` before recovery;
430
+ never replay a mutation merely because displayed output was truncated. Review the exact serialized
431
+ external-review envelope passed to Core, not a separately reconstructed object. Public CREF expiry
432
+ is independent of lease expiry and must be checked before every credential-consuming transition.
399
433
 
400
434
  After every significant Task Step boundary—completion, failure, block, skip, sealed-review result,
401
435
  or corrective decision—the Task chat prints a compact Task progress Markdown table from fresh
@@ -571,6 +605,19 @@ When `next` returns a derived C1 Worker or corrective Auditor actor, use that ex
571
605
  asking the user to name one. Derivation removes a routing pause; handoff claim, writer lease,
572
606
  strict review, and auditor-independence checks remain mandatory.
573
607
 
608
+ The beta.13.2 pending strict-review update exception is documented in
609
+ `docs/pending-review-update.md` in the exact package. An exact external target runner may run
610
+ only `update pending-review-source-preflight` against the known beta.13.1 source. Require its
611
+ verified runtime build, exact Task/revision/Plan/HEAD/pending binding and no blockers. If it
612
+ returns an exact stale Task lease repair, run that `locks repair` with the installed source and
613
+ repeat preflight. Preserve the eligible receipt outside the checkout; transport only the two
614
+ bound dependency commits on base and Task, then install the target locally. Fresh local `next`
615
+ must advertise `update pending-review-dependency-recover`; run its matching read-only preflight
616
+ with `--file <source-receipt>` before recovery. Recovery does not approve Knowledge or review.
617
+ Use the original pending product SHA through the newly sealed external packet and record,
618
+ then follow fresh credential/context-refresh/ordinary lifecycle actions. Never treat this as
619
+ permission for other external-runner mutations, product transport, or manual state repair.
620
+
574
621
  Before an npm package update, run the project-local `update preflight` and stop unless it
575
622
  returns `safe=true` with a clean checkout, no running Step, and no active writer lease.
576
623