cclaw-cli 0.51.26 → 0.51.27
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/dist/artifact-linter.js +574 -0
- package/dist/content/examples.js +9 -8
- package/dist/content/harness-doc.js +45 -1
- package/dist/content/hooks.js +232 -52
- package/dist/content/skills.d.ts +9 -0
- package/dist/content/skills.js +127 -1
- package/dist/content/stages/brainstorm.js +5 -5
- package/dist/content/templates.js +312 -20
- package/dist/delegation.d.ts +32 -5
- package/dist/delegation.js +83 -20
- package/dist/harness-adapters.d.ts +42 -0
- package/dist/harness-adapters.js +95 -0
- package/dist/internal/advance-stage.js +68 -18
- package/package.json +1 -1
package/dist/delegation.js
CHANGED
|
@@ -10,6 +10,35 @@ import { readFlowState } from "./runs.js";
|
|
|
10
10
|
import { stageSchema } from "./content/stage-schema.js";
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
|
|
13
|
+
export const DELEGATION_DISPATCH_SURFACES = [
|
|
14
|
+
"claude-task",
|
|
15
|
+
"cursor-task",
|
|
16
|
+
"opencode-agent",
|
|
17
|
+
"codex-agent",
|
|
18
|
+
"generic-task",
|
|
19
|
+
"role-switch",
|
|
20
|
+
"manual"
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Per-surface allowed agent-definition path prefixes. Used by the generated
|
|
24
|
+
* `.cclaw/hooks/delegation-record.mjs` helper to reject mismatched
|
|
25
|
+
* `--agent-definition-path` values without inspecting any harness state.
|
|
26
|
+
*
|
|
27
|
+
* The list is intentionally structural: each surface maps to one or more
|
|
28
|
+
* repo-relative path prefixes that must be a parent of the supplied path.
|
|
29
|
+
* `role-switch` and `manual` accept any path because the agent-definition
|
|
30
|
+
* is intentionally not a generated artifact for those surfaces.
|
|
31
|
+
*/
|
|
32
|
+
export const DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES = {
|
|
33
|
+
"claude-task": [".claude/agents/", ".cclaw/agents/"],
|
|
34
|
+
"cursor-task": [".cursor/agents/", ".cclaw/agents/"],
|
|
35
|
+
"opencode-agent": [".opencode/agents/", ".cclaw/agents/"],
|
|
36
|
+
"codex-agent": [".codex/agents/", ".cclaw/agents/"],
|
|
37
|
+
"generic-task": [".cclaw/agents/"],
|
|
38
|
+
"role-switch": [],
|
|
39
|
+
"manual": []
|
|
40
|
+
};
|
|
41
|
+
export const DELEGATION_LEDGER_SCHEMA_VERSION = 3;
|
|
13
42
|
function delegationLogPath(projectRoot) {
|
|
14
43
|
return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json");
|
|
15
44
|
}
|
|
@@ -175,7 +204,8 @@ function isDelegationEntry(value) {
|
|
|
175
204
|
o.fulfillmentMode === "isolated" ||
|
|
176
205
|
o.fulfillmentMode === "generic-dispatch" ||
|
|
177
206
|
o.fulfillmentMode === "role-switch" ||
|
|
178
|
-
o.fulfillmentMode === "harness-waiver"
|
|
207
|
+
o.fulfillmentMode === "harness-waiver" ||
|
|
208
|
+
o.fulfillmentMode === "legacy-inferred") &&
|
|
179
209
|
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
|
|
180
210
|
(o.dispatchId === undefined || typeof o.dispatchId === "string") &&
|
|
181
211
|
(o.workerRunId === undefined || typeof o.workerRunId === "string") &&
|
|
@@ -188,16 +218,10 @@ function isDelegationEntry(value) {
|
|
|
188
218
|
retryOk &&
|
|
189
219
|
(o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
|
|
190
220
|
(o.skill === undefined || typeof o.skill === "string") &&
|
|
191
|
-
(o.schemaVersion === undefined || o.schemaVersion === 1));
|
|
221
|
+
(o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3));
|
|
192
222
|
}
|
|
193
223
|
function isDelegationDispatchSurface(value) {
|
|
194
|
-
return
|
|
195
|
-
value === "cursor-task" ||
|
|
196
|
-
value === "opencode-agent" ||
|
|
197
|
-
value === "codex-agent" ||
|
|
198
|
-
value === "generic-task" ||
|
|
199
|
-
value === "role-switch" ||
|
|
200
|
-
value === "manual");
|
|
224
|
+
return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
|
|
201
225
|
}
|
|
202
226
|
function statusTimestampPatch(entry, ts) {
|
|
203
227
|
const patch = { ...entry };
|
|
@@ -215,30 +239,50 @@ function eventFromEntry(entry) {
|
|
|
215
239
|
...entry,
|
|
216
240
|
event: entry.status,
|
|
217
241
|
eventTs,
|
|
218
|
-
schemaVersion:
|
|
242
|
+
schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
|
|
219
243
|
};
|
|
220
244
|
}
|
|
221
245
|
function isDelegationEvent(value) {
|
|
222
246
|
if (!isDelegationEntry(value))
|
|
223
247
|
return false;
|
|
224
248
|
const o = value;
|
|
225
|
-
|
|
249
|
+
if (o.event !== o.status || typeof o.eventTs !== "string")
|
|
250
|
+
return false;
|
|
251
|
+
return true;
|
|
226
252
|
}
|
|
227
253
|
function parseLedger(raw, runId) {
|
|
228
254
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
229
|
-
return { runId, entries: [] };
|
|
255
|
+
return { runId, entries: [], schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION };
|
|
230
256
|
}
|
|
231
257
|
const o = raw;
|
|
258
|
+
const ledgerSchemaVersion = (o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3
|
|
259
|
+
? o.schemaVersion
|
|
260
|
+
: undefined);
|
|
232
261
|
const entriesRaw = o.entries;
|
|
233
262
|
const entries = [];
|
|
234
263
|
if (Array.isArray(entriesRaw)) {
|
|
235
264
|
for (const item of entriesRaw) {
|
|
236
265
|
if (isDelegationEntry(item)) {
|
|
237
266
|
const ts = item.startTs ?? item.ts ?? new Date().toISOString();
|
|
238
|
-
|
|
239
|
-
|
|
267
|
+
// A row is "pre-v3 legacy" when the file format predates the
|
|
268
|
+
// dispatch-proof contract: schemaVersion is missing on both ledger
|
|
269
|
+
// and entry, the entry has no fulfillmentMode, and there is no
|
|
270
|
+
// dispatch-surface or dispatch-id evidence on the row. We honor
|
|
271
|
+
// that by tagging fulfillmentMode = "legacy-inferred" so callers
|
|
272
|
+
// (stage-complete, doctor) can require an explicit `--rerecord`
|
|
273
|
+
// before the row counts as proof-era.
|
|
274
|
+
const ledgerHasNoVersion = ledgerSchemaVersion === undefined || ledgerSchemaVersion === 1;
|
|
275
|
+
const entryHasNoVersion = item.schemaVersion === undefined || item.schemaVersion === 1;
|
|
276
|
+
const looksLegacy = ledgerHasNoVersion &&
|
|
277
|
+
entryHasNoVersion &&
|
|
278
|
+
item.fulfillmentMode === undefined &&
|
|
279
|
+
item.dispatchSurface === undefined &&
|
|
280
|
+
item.dispatchId === undefined &&
|
|
281
|
+
item.workerRunId === undefined &&
|
|
282
|
+
item.agentDefinitionPath === undefined &&
|
|
240
283
|
item.status === "completed";
|
|
241
|
-
const inferredFulfillmentMode = item.fulfillmentMode
|
|
284
|
+
const inferredFulfillmentMode = item.fulfillmentMode
|
|
285
|
+
?? (looksLegacy ? "legacy-inferred" : (item.status === "completed" && item.schemaVersion === undefined ? "isolated" : undefined));
|
|
242
286
|
entries.push({
|
|
243
287
|
...item,
|
|
244
288
|
spanId: item.spanId ?? createSpanId(),
|
|
@@ -253,12 +297,12 @@ function parseLedger(raw, runId) {
|
|
|
253
297
|
: 0,
|
|
254
298
|
evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs : [],
|
|
255
299
|
fulfillmentMode: inferredFulfillmentMode,
|
|
256
|
-
schemaVersion:
|
|
300
|
+
schemaVersion: item.schemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION
|
|
257
301
|
});
|
|
258
302
|
}
|
|
259
303
|
}
|
|
260
304
|
}
|
|
261
|
-
return { runId, entries };
|
|
305
|
+
return { runId, entries, schemaVersion: ledgerSchemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION };
|
|
262
306
|
}
|
|
263
307
|
export async function readDelegationLedger(projectRoot) {
|
|
264
308
|
const { activeRunId } = await readFlowState(projectRoot);
|
|
@@ -348,7 +392,7 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
348
392
|
if (stamped.status === "scheduled") {
|
|
349
393
|
delete stamped.endTs;
|
|
350
394
|
}
|
|
351
|
-
stamped.schemaVersion =
|
|
395
|
+
stamped.schemaVersion = DELEGATION_LEDGER_SCHEMA_VERSION;
|
|
352
396
|
if (stamped.retryCount === undefined ||
|
|
353
397
|
!Number.isInteger(stamped.retryCount) ||
|
|
354
398
|
stamped.retryCount < 0) {
|
|
@@ -378,7 +422,8 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
378
422
|
await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
|
|
379
423
|
const ledger = {
|
|
380
424
|
runId: activeRunId,
|
|
381
|
-
entries: [...prior.entries, stamped]
|
|
425
|
+
entries: [...prior.entries, stamped],
|
|
426
|
+
schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
|
|
382
427
|
};
|
|
383
428
|
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
|
|
384
429
|
await writeSubagentTracker(projectRoot, ledger.entries);
|
|
@@ -419,6 +464,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
419
464
|
const missingEvidence = [];
|
|
420
465
|
const missingDispatchProof = [];
|
|
421
466
|
const legacyInferredCompletions = [];
|
|
467
|
+
let legacyRequiresRerecord = false;
|
|
422
468
|
const terminalSpanIds = new Set(forRun
|
|
423
469
|
.filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
|
|
424
470
|
.map((entry) => entry.spanId));
|
|
@@ -454,8 +500,20 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
454
500
|
!completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
|
|
455
501
|
missingEvidence.push(agent);
|
|
456
502
|
}
|
|
503
|
+
// legacyInferredCompletions has two sources, split by `legacyTagged`:
|
|
504
|
+
// - legacyTagged === true : the row was *parsed* as legacy-inferred
|
|
505
|
+
// from a pre-v3 ledger file. Requires `delegation-record.mjs
|
|
506
|
+
// --rerecord` and BLOCKS satisfied.
|
|
507
|
+
// - legacyTagged === false: in-check inference for minimally-spec'd
|
|
508
|
+
// isolated rows that lack proof-era signals. Advisory only —
|
|
509
|
+
// preserves backward-compatible behavior for existing API callers.
|
|
457
510
|
for (const row of completedRows) {
|
|
458
511
|
const mode = row.fulfillmentMode ?? "isolated";
|
|
512
|
+
if (mode === "legacy-inferred") {
|
|
513
|
+
legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
|
|
514
|
+
legacyRequiresRerecord = true;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
459
517
|
if (mode === "isolated") {
|
|
460
518
|
const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
|
|
461
519
|
event.stage === stage &&
|
|
@@ -480,7 +538,12 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
480
538
|
}
|
|
481
539
|
}
|
|
482
540
|
return {
|
|
483
|
-
satisfied: missing.length === 0 &&
|
|
541
|
+
satisfied: missing.length === 0 &&
|
|
542
|
+
missingEvidence.length === 0 &&
|
|
543
|
+
missingDispatchProof.length === 0 &&
|
|
544
|
+
!legacyRequiresRerecord &&
|
|
545
|
+
staleWorkers.length === 0 &&
|
|
546
|
+
events.corruptLines.length === 0,
|
|
484
547
|
missing,
|
|
485
548
|
waived,
|
|
486
549
|
staleIgnored,
|
|
@@ -95,6 +95,48 @@ export declare function harnessShimFileNames(): string[];
|
|
|
95
95
|
export declare function harnessShimSkillNames(): string[];
|
|
96
96
|
export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
|
|
97
97
|
export declare function harnessDispatchSurface(harnessId: HarnessId): string;
|
|
98
|
+
export interface HarnessDelegationRecipe {
|
|
99
|
+
harnessId: HarnessId;
|
|
100
|
+
dispatchSurface: "claude-task" | "cursor-task" | "opencode-agent" | "codex-agent";
|
|
101
|
+
agentDefinitionDirectory: string;
|
|
102
|
+
agentDefinitionExample: string;
|
|
103
|
+
invocationLine: string;
|
|
104
|
+
fulfillmentMode: "isolated" | "generic-dispatch";
|
|
105
|
+
/**
|
|
106
|
+
* Step-by-step lifecycle commands rendered with structural placeholders only:
|
|
107
|
+
* `<agent-name>`, `<stage>`, `<run-id>`, `<span-id>`, `<dispatch-id>`,
|
|
108
|
+
* `<agent-def-path>`, `<iso-ts>`. No domain/example values.
|
|
109
|
+
*/
|
|
110
|
+
lifecycleCommands: string[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Per-harness lifecycle recipe used by skills and harness docs to render the
|
|
114
|
+
* canonical scheduled -> launched -> acknowledged -> completed sequence in
|
|
115
|
+
* structural form. The recipe never embeds task-specific or domain-specific
|
|
116
|
+
* placeholders — only neutral angle-bracket tokens (`<agent-name>`, `<stage>`,
|
|
117
|
+
* `<span-id>`, `<dispatch-id>`, `<agent-def-path>`, `<iso-ts>`).
|
|
118
|
+
*
|
|
119
|
+
* This function returns the **canonical primary recipe** for each shipped
|
|
120
|
+
* harness — the dispatch surface that maps 1:1 onto the harness's vendor-
|
|
121
|
+
* native subagent surface:
|
|
122
|
+
*
|
|
123
|
+
* - `claude` -> `claude-task` (isolated)
|
|
124
|
+
* - `cursor` -> `cursor-task` (generic-dispatch)
|
|
125
|
+
* - `opencode` -> `opencode-agent` (isolated)
|
|
126
|
+
* - `codex` -> `codex-agent` (isolated)
|
|
127
|
+
*
|
|
128
|
+
* The remaining `--dispatch-surface` enum values (`generic-task`,
|
|
129
|
+
* `role-switch`, `manual`) are universal fallback paths available to any
|
|
130
|
+
* harness when the canonical surface is unavailable; they are documented in
|
|
131
|
+
* the dispatch-surface table in `docs/harnesses.md` rather than per-harness
|
|
132
|
+
* here, because their lifecycle commands are structurally identical except
|
|
133
|
+
* for the surface token. No shipped harness has a non-canonical *primary*
|
|
134
|
+
* surface, so this function only needs to enumerate the four canonical
|
|
135
|
+
* recipes above.
|
|
136
|
+
*/
|
|
137
|
+
export declare function harnessDelegationRecipe(harnessId: HarnessId): HarnessDelegationRecipe;
|
|
138
|
+
/** All four harness recipes in tier-stable order. */
|
|
139
|
+
export declare function harnessDelegationRecipes(): HarnessDelegationRecipe[];
|
|
98
140
|
export declare function harnessDispatchFallback(harnessId: HarnessId): string;
|
|
99
141
|
export type HarnessTier = "tier1" | "tier2" | "tier3";
|
|
100
142
|
export declare function harnessTier(harnessId: HarnessId): HarnessTier;
|
package/dist/harness-adapters.js
CHANGED
|
@@ -184,6 +184,101 @@ export function harnessDispatchSurface(harnessId) {
|
|
|
184
184
|
return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Per-harness lifecycle recipe used by skills and harness docs to render the
|
|
189
|
+
* canonical scheduled -> launched -> acknowledged -> completed sequence in
|
|
190
|
+
* structural form. The recipe never embeds task-specific or domain-specific
|
|
191
|
+
* placeholders — only neutral angle-bracket tokens (`<agent-name>`, `<stage>`,
|
|
192
|
+
* `<span-id>`, `<dispatch-id>`, `<agent-def-path>`, `<iso-ts>`).
|
|
193
|
+
*
|
|
194
|
+
* This function returns the **canonical primary recipe** for each shipped
|
|
195
|
+
* harness — the dispatch surface that maps 1:1 onto the harness's vendor-
|
|
196
|
+
* native subagent surface:
|
|
197
|
+
*
|
|
198
|
+
* - `claude` -> `claude-task` (isolated)
|
|
199
|
+
* - `cursor` -> `cursor-task` (generic-dispatch)
|
|
200
|
+
* - `opencode` -> `opencode-agent` (isolated)
|
|
201
|
+
* - `codex` -> `codex-agent` (isolated)
|
|
202
|
+
*
|
|
203
|
+
* The remaining `--dispatch-surface` enum values (`generic-task`,
|
|
204
|
+
* `role-switch`, `manual`) are universal fallback paths available to any
|
|
205
|
+
* harness when the canonical surface is unavailable; they are documented in
|
|
206
|
+
* the dispatch-surface table in `docs/harnesses.md` rather than per-harness
|
|
207
|
+
* here, because their lifecycle commands are structurally identical except
|
|
208
|
+
* for the surface token. No shipped harness has a non-canonical *primary*
|
|
209
|
+
* surface, so this function only needs to enumerate the four canonical
|
|
210
|
+
* recipes above.
|
|
211
|
+
*/
|
|
212
|
+
export function harnessDelegationRecipe(harnessId) {
|
|
213
|
+
const helper = "node .cclaw/hooks/delegation-record.mjs";
|
|
214
|
+
const common = "--stage=<stage> --agent=<agent-name> --mode=mandatory --span-id=<span-id> --dispatch-id=<dispatch-id>";
|
|
215
|
+
switch (harnessId) {
|
|
216
|
+
case "claude":
|
|
217
|
+
return {
|
|
218
|
+
harnessId,
|
|
219
|
+
dispatchSurface: "claude-task",
|
|
220
|
+
agentDefinitionDirectory: ".claude/agents/",
|
|
221
|
+
agentDefinitionExample: ".claude/agents/<agent-name>.md",
|
|
222
|
+
invocationLine: "Call Task with subagent_type=<agent-name> and prompt body that paraphrases the stage skill role.",
|
|
223
|
+
fulfillmentMode: "isolated",
|
|
224
|
+
lifecycleCommands: [
|
|
225
|
+
`${helper} ${common} --status=scheduled --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --json`,
|
|
226
|
+
`${helper} ${common} --status=launched --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --launched-ts=<iso-ts> --json`,
|
|
227
|
+
`${helper} ${common} --status=acknowledged --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --ack-ts=<iso-ts> --json`,
|
|
228
|
+
`${helper} ${common} --status=completed --dispatch-surface=claude-task --agent-definition-path=.claude/agents/<agent-name>.md --completed-ts=<iso-ts> --json`
|
|
229
|
+
]
|
|
230
|
+
};
|
|
231
|
+
case "cursor":
|
|
232
|
+
return {
|
|
233
|
+
harnessId,
|
|
234
|
+
dispatchSurface: "cursor-task",
|
|
235
|
+
agentDefinitionDirectory: ".cclaw/agents/",
|
|
236
|
+
agentDefinitionExample: ".cclaw/agents/<agent-name>.md",
|
|
237
|
+
invocationLine: "Call Task with a generic subagent_type and paste the cclaw role prompt; capture worker output as evidenceRefs in the artifact.",
|
|
238
|
+
fulfillmentMode: "generic-dispatch",
|
|
239
|
+
lifecycleCommands: [
|
|
240
|
+
`${helper} ${common} --status=scheduled --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --json`,
|
|
241
|
+
`${helper} ${common} --status=launched --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --launched-ts=<iso-ts> --json`,
|
|
242
|
+
`${helper} ${common} --status=acknowledged --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --ack-ts=<iso-ts> --json`,
|
|
243
|
+
`${helper} ${common} --status=completed --dispatch-surface=cursor-task --agent-definition-path=.cclaw/agents/<agent-name>.md --completed-ts=<iso-ts> --evidence-ref=<artifact-anchor> --json`
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
case "opencode":
|
|
247
|
+
return {
|
|
248
|
+
harnessId,
|
|
249
|
+
dispatchSurface: "opencode-agent",
|
|
250
|
+
agentDefinitionDirectory: ".opencode/agents/",
|
|
251
|
+
agentDefinitionExample: ".opencode/agents/<agent-name>.md",
|
|
252
|
+
invocationLine: "Invoke the generated agent via Task or `@<agent-name>`; the agent body lives in `.opencode/agents/<agent-name>.md`.",
|
|
253
|
+
fulfillmentMode: "isolated",
|
|
254
|
+
lifecycleCommands: [
|
|
255
|
+
`${helper} ${common} --status=scheduled --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --json`,
|
|
256
|
+
`${helper} ${common} --status=launched --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --launched-ts=<iso-ts> --json`,
|
|
257
|
+
`${helper} ${common} --status=acknowledged --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --ack-ts=<iso-ts> --json`,
|
|
258
|
+
`${helper} ${common} --status=completed --dispatch-surface=opencode-agent --agent-definition-path=.opencode/agents/<agent-name>.md --completed-ts=<iso-ts> --json`
|
|
259
|
+
]
|
|
260
|
+
};
|
|
261
|
+
case "codex":
|
|
262
|
+
return {
|
|
263
|
+
harnessId,
|
|
264
|
+
dispatchSurface: "codex-agent",
|
|
265
|
+
agentDefinitionDirectory: ".codex/agents/",
|
|
266
|
+
agentDefinitionExample: ".codex/agents/<agent-name>.toml",
|
|
267
|
+
invocationLine: "Ask Codex to spawn the named custom agent; the agent definition lives in `.codex/agents/<agent-name>.toml`.",
|
|
268
|
+
fulfillmentMode: "isolated",
|
|
269
|
+
lifecycleCommands: [
|
|
270
|
+
`${helper} ${common} --status=scheduled --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --json`,
|
|
271
|
+
`${helper} ${common} --status=launched --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --launched-ts=<iso-ts> --json`,
|
|
272
|
+
`${helper} ${common} --status=acknowledged --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --ack-ts=<iso-ts> --json`,
|
|
273
|
+
`${helper} ${common} --status=completed --dispatch-surface=codex-agent --agent-definition-path=.codex/agents/<agent-name>.toml --completed-ts=<iso-ts> --json`
|
|
274
|
+
]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** All four harness recipes in tier-stable order. */
|
|
279
|
+
export function harnessDelegationRecipes() {
|
|
280
|
+
return harnessesByTier().map((id) => harnessDelegationRecipe(id));
|
|
281
|
+
}
|
|
187
282
|
export function harnessDispatchFallback(harnessId) {
|
|
188
283
|
const adapter = HARNESS_ADAPTERS[harnessId];
|
|
189
284
|
if (adapter.capabilities.subagentFallback !== "role-switch") {
|
|
@@ -6,7 +6,7 @@ import { resolveArtifactPath } from "../artifact-paths.js";
|
|
|
6
6
|
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
7
7
|
import { ensureDir } from "../fs-utils.js";
|
|
8
8
|
import { stageAutoSubagentDispatch, stageSchema } from "../content/stage-schema.js";
|
|
9
|
-
import { appendDelegation, checkMandatoryDelegations, readDelegationLedger } from "../delegation.js";
|
|
9
|
+
import { appendDelegation, checkMandatoryDelegations, readDelegationEvents, readDelegationLedger } from "../delegation.js";
|
|
10
10
|
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
11
11
|
import { extractMarkdownSectionBody, parseLearningsSection } from "../artifact-linter.js";
|
|
12
12
|
import { getAvailableTransitions, getTransitionGuards, isFlowTrack, createInitialFlowState } from "../flow-state.js";
|
|
@@ -669,6 +669,9 @@ async function buildValidationReport(projectRoot, flowState, options = {}) {
|
|
|
669
669
|
missing: delegation.missing,
|
|
670
670
|
waived: delegation.waived,
|
|
671
671
|
missingEvidence: delegation.missingEvidence,
|
|
672
|
+
missingDispatchProof: delegation.missingDispatchProof,
|
|
673
|
+
legacyInferredCompletions: delegation.legacyInferredCompletions,
|
|
674
|
+
corruptEventLines: delegation.corruptEventLines,
|
|
672
675
|
staleWorkers: delegation.staleWorkers,
|
|
673
676
|
expectedMode: delegation.expectedMode
|
|
674
677
|
},
|
|
@@ -897,6 +900,48 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
897
900
|
allowBlockedReviewRoute: blockedReviewRoute
|
|
898
901
|
});
|
|
899
902
|
if (!validation.ok) {
|
|
903
|
+
const ledgerForDiag = await readDelegationLedger(projectRoot).catch(() => ({ entries: [] }));
|
|
904
|
+
const eventsForDiag = await readDelegationEvents(projectRoot).catch(() => ({ events: [], corruptLines: [] }));
|
|
905
|
+
const ledgerEntriesText = await fs.readFile(path.join(projectRoot, ".cclaw/state/delegation-events.jsonl"), "utf8").catch(() => "");
|
|
906
|
+
const corruptSnippets = (() => {
|
|
907
|
+
if (validation.delegation.corruptEventLines.length === 0)
|
|
908
|
+
return [];
|
|
909
|
+
const lines = ledgerEntriesText.split(/\r?\n/u);
|
|
910
|
+
return validation.delegation.corruptEventLines.slice(0, 3).map((lineNo) => {
|
|
911
|
+
const line = lines[lineNo - 1] ?? "";
|
|
912
|
+
const sample = line.length > 120 ? `${line.slice(0, 117)}...` : line;
|
|
913
|
+
return `line ${lineNo}: ${sample}`;
|
|
914
|
+
});
|
|
915
|
+
})();
|
|
916
|
+
const dispatchProofDetails = validation.delegation.missingDispatchProof.flatMap((agent) => {
|
|
917
|
+
const rows = ledgerForDiag.entries.filter((entry) => entry.agent === agent && entry.status === "completed");
|
|
918
|
+
return rows.map((row) => `${agent}(spanId=${row.spanId ?? "unknown"})`);
|
|
919
|
+
});
|
|
920
|
+
const nextActions = [];
|
|
921
|
+
if (validation.delegation.missing.length > 0) {
|
|
922
|
+
nextActions.push(`Complete or waive mandatory delegation(s): ${validation.delegation.missing.join(", ")}. Helper: \`node .cclaw/hooks/stage-complete.mjs ${args.stage} --waive-delegation=${validation.delegation.missing.join(",")} --waiver-reason="<why safe>"\`.`);
|
|
923
|
+
}
|
|
924
|
+
if (validation.delegation.missingEvidence.length > 0) {
|
|
925
|
+
nextActions.push(`Role-switch fallback completion needs --evidence-ref or escalate to a real isolated dispatch surface.`);
|
|
926
|
+
}
|
|
927
|
+
if (validation.delegation.missingDispatchProof.length > 0) {
|
|
928
|
+
nextActions.push(`Isolated completion(s) ${dispatchProofDetails.join(", ") || validation.delegation.missingDispatchProof.join(", ")} lack matching dispatch proof; run the helper lifecycle scheduled -> launched -> acknowledged -> completed with --span-id, --dispatch-id, --dispatch-surface and --agent-definition-path before advancing.`);
|
|
929
|
+
}
|
|
930
|
+
if (validation.delegation.legacyInferredCompletions.length > 0) {
|
|
931
|
+
nextActions.push(`Pre-v3 ledger entries found: ${validation.delegation.legacyInferredCompletions.join(", ")}. Run \`node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path>\` to upgrade the row to dispatch-proof shape.`);
|
|
932
|
+
}
|
|
933
|
+
if (validation.delegation.corruptEventLines.length > 0) {
|
|
934
|
+
nextActions.push(`delegation-events.jsonl has ${validation.delegation.corruptEventLines.length} corrupt line(s) at ${validation.delegation.corruptEventLines.slice(0, 3).join(", ")}${validation.delegation.corruptEventLines.length > 3 ? ", ..." : ""}; remove or fix them before advancing.`);
|
|
935
|
+
}
|
|
936
|
+
if (validation.delegation.staleWorkers.length > 0) {
|
|
937
|
+
nextActions.push(`Stale scheduled delegations ${validation.delegation.staleWorkers.join(", ")} have no terminal row sharing the same spanId; emit launched/acknowledged/completed (or failed/stale) before advancing.`);
|
|
938
|
+
}
|
|
939
|
+
if (validation.gates.issues.length > 0) {
|
|
940
|
+
nextActions.push("Fix the artifact/gate issue shown in gates.issues, then rerun stage-complete.");
|
|
941
|
+
}
|
|
942
|
+
if (validation.completedStages.issues.length > 0) {
|
|
943
|
+
nextActions.push("Repair previously completed stage gate closure before advancing.");
|
|
944
|
+
}
|
|
900
945
|
if (args.json) {
|
|
901
946
|
io.stdout.write(`${JSON.stringify({
|
|
902
947
|
ok: false,
|
|
@@ -906,23 +951,12 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
906
951
|
delegation: validation.delegation,
|
|
907
952
|
gates: validation.gates,
|
|
908
953
|
completedStages: validation.completedStages,
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
: []),
|
|
916
|
-
...(validation.delegation.staleWorkers.length > 0
|
|
917
|
-
? ["Resolve scheduled delegation span(s) without terminal lifecycle evidence before advancing."]
|
|
918
|
-
: []),
|
|
919
|
-
...(validation.gates.issues.length > 0
|
|
920
|
-
? ["Fix the artifact/gate issue shown in gates.issues, then rerun stage-complete."]
|
|
921
|
-
: []),
|
|
922
|
-
...(validation.completedStages.issues.length > 0
|
|
923
|
-
? ["Repair previously completed stage gate closure before advancing."]
|
|
924
|
-
: [])
|
|
925
|
-
]
|
|
954
|
+
diagnostics: {
|
|
955
|
+
dispatchProofRows: dispatchProofDetails,
|
|
956
|
+
corruptEventSamples: corruptSnippets,
|
|
957
|
+
unawareEvents: eventsForDiag.corruptLines.length
|
|
958
|
+
},
|
|
959
|
+
nextActions
|
|
926
960
|
})}\n`);
|
|
927
961
|
}
|
|
928
962
|
io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}".\n`);
|
|
@@ -932,9 +966,25 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
932
966
|
}
|
|
933
967
|
if (validation.delegation.missingEvidence.length > 0) {
|
|
934
968
|
io.stderr.write(`- role-switch evidence missing: ${validation.delegation.missingEvidence.join(", ")}\n`);
|
|
969
|
+
io.stderr.write(` next action: include --evidence-ref=<artifact#anchor> when emitting the completed event, or escalate to a true isolated dispatch surface.\n`);
|
|
970
|
+
}
|
|
971
|
+
if (validation.delegation.missingDispatchProof.length > 0) {
|
|
972
|
+
io.stderr.write(`- isolated completion lacks dispatch proof: ${dispatchProofDetails.join(", ") || validation.delegation.missingDispatchProof.join(", ")}\n`);
|
|
973
|
+
io.stderr.write(` next action: emit scheduled -> launched -> acknowledged -> completed with --span-id, --dispatch-id, --dispatch-surface, --agent-definition-path before advancing.\n`);
|
|
974
|
+
}
|
|
975
|
+
if (validation.delegation.legacyInferredCompletions.length > 0) {
|
|
976
|
+
io.stderr.write(`- legacy-inferred completions need rerecord: ${validation.delegation.legacyInferredCompletions.join(", ")}\n`);
|
|
977
|
+
io.stderr.write(` next action: \`node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path>\`.\n`);
|
|
978
|
+
}
|
|
979
|
+
if (validation.delegation.corruptEventLines.length > 0) {
|
|
980
|
+
io.stderr.write(`- corrupt delegation-events.jsonl line(s): ${validation.delegation.corruptEventLines.slice(0, 3).join(", ")}${validation.delegation.corruptEventLines.length > 3 ? `, ... (+${validation.delegation.corruptEventLines.length - 3})` : ""}\n`);
|
|
981
|
+
for (const snippet of corruptSnippets) {
|
|
982
|
+
io.stderr.write(` sample: ${snippet}\n`);
|
|
983
|
+
}
|
|
935
984
|
}
|
|
936
985
|
if (validation.delegation.staleWorkers.length > 0) {
|
|
937
986
|
io.stderr.write(`- stale scheduled delegations: ${validation.delegation.staleWorkers.join(", ")}\n`);
|
|
987
|
+
io.stderr.write(` next action: emit a terminal row (completed/failed/stale) for the same span before advancing.\n`);
|
|
938
988
|
}
|
|
939
989
|
if (validation.gates.issues.length > 0) {
|
|
940
990
|
io.stderr.write(`- gate issues: ${validation.gates.issues.join(" | ")}\n`);
|