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.
- package/README.md +1 -1
- package/dist/reviewer-runtime-build.json +17 -13
- package/dist/src/alpha6/remediation.d.ts +1 -0
- package/dist/src/alpha6/remediation.js +12 -3
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/cli-actions.d.ts +1 -1
- package/dist/src/cli-actions.js +3 -0
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +15 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/gateway-handshake.js +1 -0
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/observed-routes.js +1 -0
- package/dist/src/observed-routes.js.map +1 -1
- package/dist/src/pending-review-update.d.ts +49 -0
- package/dist/src/pending-review-update.js +132 -0
- package/dist/src/pending-review-update.js.map +1 -0
- package/dist/src/repository.js +16 -4
- package/dist/src/repository.js.map +1 -1
- package/dist/src/reviewer.js +4 -1
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +10 -1
- package/dist/src/state/corrective-yield-executor.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 +10 -0
- package/dist/src/workflow.js +161 -8
- package/dist/src/workflow.js.map +1 -1
- package/docs/beta13.2-signal-review-recovery.md +38 -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 +2 -2
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +3 -3
- package/docs/pending-review-update.md +15 -0
- package/docs/release.md +27 -2
- package/docs/updating-existing-project.md +10 -2
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +128 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +11 -0
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +185 -0
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +50 -3
- 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
|
-
|
|
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
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
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.
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
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
|
|