cclaw-cli 0.51.25 → 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/core-agents.js +21 -1
- package/dist/content/examples.js +9 -8
- package/dist/content/harness-doc.d.ts +1 -0
- package/dist/content/harness-doc.js +47 -0
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +369 -0
- package/dist/content/skills.d.ts +9 -0
- package/dist/content/skills.js +132 -5
- package/dist/content/stages/brainstorm.js +5 -5
- package/dist/content/status-command.js +8 -2
- package/dist/content/subagents.js +6 -2
- package/dist/content/templates.js +312 -20
- package/dist/content/tree-command.js +7 -1
- package/dist/delegation.d.ts +62 -4
- package/dist/delegation.js +218 -16
- package/dist/doctor-registry.js +9 -0
- package/dist/doctor.js +75 -2
- package/dist/harness-adapters.d.ts +48 -0
- package/dist/harness-adapters.js +123 -4
- package/dist/install.js +3 -1
- package/dist/internal/advance-stage.js +68 -18
- package/package.json +1 -1
package/dist/delegation.js
CHANGED
|
@@ -9,13 +9,48 @@ import { HARNESS_ADAPTERS } from "./harness-adapters.js";
|
|
|
9
9
|
import { readFlowState } from "./runs.js";
|
|
10
10
|
import { stageSchema } from "./content/stage-schema.js";
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
|
-
const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived"]);
|
|
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
|
}
|
|
16
45
|
function delegationLockPath(projectRoot) {
|
|
17
46
|
return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
|
|
18
47
|
}
|
|
48
|
+
function delegationEventsPath(projectRoot) {
|
|
49
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-events.jsonl");
|
|
50
|
+
}
|
|
51
|
+
function subagentsStatePath(projectRoot) {
|
|
52
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", "subagents.json");
|
|
53
|
+
}
|
|
19
54
|
function createSpanId() {
|
|
20
55
|
return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
21
56
|
}
|
|
@@ -131,13 +166,16 @@ function isDelegationEntry(value) {
|
|
|
131
166
|
const o = value;
|
|
132
167
|
const modeOk = o.mode === "mandatory" || o.mode === "proactive";
|
|
133
168
|
const statusOk = o.status === "scheduled" ||
|
|
169
|
+
o.status === "launched" ||
|
|
170
|
+
o.status === "acknowledged" ||
|
|
134
171
|
o.status === "completed" ||
|
|
135
172
|
o.status === "failed" ||
|
|
136
|
-
o.status === "waived"
|
|
173
|
+
o.status === "waived" ||
|
|
174
|
+
o.status === "stale";
|
|
137
175
|
const timestampOk = typeof o.ts === "string" ||
|
|
138
176
|
typeof o.startTs === "string";
|
|
139
|
-
const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived";
|
|
140
|
-
const lifecycleOk = o.status !== "scheduled" || o.endTs === undefined;
|
|
177
|
+
const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived" || o.status === "stale";
|
|
178
|
+
const lifecycleOk = (o.status !== "scheduled" && o.status !== "launched" && o.status !== "acknowledged") || o.endTs === undefined;
|
|
141
179
|
const terminalLifecycleOk = !terminalStatus ||
|
|
142
180
|
o.endTs === undefined ||
|
|
143
181
|
typeof o.endTs === "string";
|
|
@@ -166,46 +204,105 @@ function isDelegationEntry(value) {
|
|
|
166
204
|
o.fulfillmentMode === "isolated" ||
|
|
167
205
|
o.fulfillmentMode === "generic-dispatch" ||
|
|
168
206
|
o.fulfillmentMode === "role-switch" ||
|
|
169
|
-
o.fulfillmentMode === "harness-waiver"
|
|
207
|
+
o.fulfillmentMode === "harness-waiver" ||
|
|
208
|
+
o.fulfillmentMode === "legacy-inferred") &&
|
|
170
209
|
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
|
|
210
|
+
(o.dispatchId === undefined || typeof o.dispatchId === "string") &&
|
|
211
|
+
(o.workerRunId === undefined || typeof o.workerRunId === "string") &&
|
|
212
|
+
(o.dispatchSurface === undefined || isDelegationDispatchSurface(o.dispatchSurface)) &&
|
|
213
|
+
(o.agentDefinitionPath === undefined || typeof o.agentDefinitionPath === "string") &&
|
|
214
|
+
(o.ackTs === undefined || typeof o.ackTs === "string") &&
|
|
215
|
+
(o.launchedTs === undefined || typeof o.launchedTs === "string") &&
|
|
216
|
+
(o.completedTs === undefined || typeof o.completedTs === "string") &&
|
|
171
217
|
(o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
|
|
172
218
|
retryOk &&
|
|
173
219
|
(o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
|
|
174
220
|
(o.skill === undefined || typeof o.skill === "string") &&
|
|
175
|
-
(o.schemaVersion === undefined || o.schemaVersion === 1));
|
|
221
|
+
(o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3));
|
|
222
|
+
}
|
|
223
|
+
function isDelegationDispatchSurface(value) {
|
|
224
|
+
return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
|
|
225
|
+
}
|
|
226
|
+
function statusTimestampPatch(entry, ts) {
|
|
227
|
+
const patch = { ...entry };
|
|
228
|
+
if (patch.status === "launched")
|
|
229
|
+
patch.launchedTs = patch.launchedTs ?? ts;
|
|
230
|
+
if (patch.status === "acknowledged")
|
|
231
|
+
patch.ackTs = patch.ackTs ?? ts;
|
|
232
|
+
if (patch.status === "completed")
|
|
233
|
+
patch.completedTs = patch.completedTs ?? patch.endTs ?? ts;
|
|
234
|
+
return patch;
|
|
235
|
+
}
|
|
236
|
+
function eventFromEntry(entry) {
|
|
237
|
+
const eventTs = entry.completedTs ?? entry.ackTs ?? entry.launchedTs ?? entry.endTs ?? entry.startTs ?? entry.ts ?? new Date().toISOString();
|
|
238
|
+
return {
|
|
239
|
+
...entry,
|
|
240
|
+
event: entry.status,
|
|
241
|
+
eventTs,
|
|
242
|
+
schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function isDelegationEvent(value) {
|
|
246
|
+
if (!isDelegationEntry(value))
|
|
247
|
+
return false;
|
|
248
|
+
const o = value;
|
|
249
|
+
if (o.event !== o.status || typeof o.eventTs !== "string")
|
|
250
|
+
return false;
|
|
251
|
+
return true;
|
|
176
252
|
}
|
|
177
253
|
function parseLedger(raw, runId) {
|
|
178
254
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
179
|
-
return { runId, entries: [] };
|
|
255
|
+
return { runId, entries: [], schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION };
|
|
180
256
|
}
|
|
181
257
|
const o = raw;
|
|
258
|
+
const ledgerSchemaVersion = (o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3
|
|
259
|
+
? o.schemaVersion
|
|
260
|
+
: undefined);
|
|
182
261
|
const entriesRaw = o.entries;
|
|
183
262
|
const entries = [];
|
|
184
263
|
if (Array.isArray(entriesRaw)) {
|
|
185
264
|
for (const item of entriesRaw) {
|
|
186
265
|
if (isDelegationEntry(item)) {
|
|
187
266
|
const ts = item.startTs ?? item.ts ?? new Date().toISOString();
|
|
188
|
-
|
|
189
|
-
|
|
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 &&
|
|
190
283
|
item.status === "completed";
|
|
191
|
-
const inferredFulfillmentMode = item.fulfillmentMode
|
|
284
|
+
const inferredFulfillmentMode = item.fulfillmentMode
|
|
285
|
+
?? (looksLegacy ? "legacy-inferred" : (item.status === "completed" && item.schemaVersion === undefined ? "isolated" : undefined));
|
|
192
286
|
entries.push({
|
|
193
287
|
...item,
|
|
194
288
|
spanId: item.spanId ?? createSpanId(),
|
|
195
289
|
startTs: ts,
|
|
196
290
|
endTs: TERMINAL_DELEGATION_STATUSES.has(item.status) ? (item.endTs ?? ts) : undefined,
|
|
197
291
|
ts,
|
|
292
|
+
launchedTs: item.launchedTs ?? (item.status === "launched" ? ts : undefined),
|
|
293
|
+
ackTs: item.ackTs ?? (item.status === "acknowledged" ? ts : undefined),
|
|
294
|
+
completedTs: item.completedTs ?? (item.status === "completed" ? (item.endTs ?? ts) : undefined),
|
|
198
295
|
retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
|
|
199
296
|
? item.retryCount
|
|
200
297
|
: 0,
|
|
201
298
|
evidenceRefs: Array.isArray(item.evidenceRefs) ? item.evidenceRefs : [],
|
|
202
299
|
fulfillmentMode: inferredFulfillmentMode,
|
|
203
|
-
schemaVersion:
|
|
300
|
+
schemaVersion: item.schemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION
|
|
204
301
|
});
|
|
205
302
|
}
|
|
206
303
|
}
|
|
207
304
|
}
|
|
208
|
-
return { runId, entries };
|
|
305
|
+
return { runId, entries, schemaVersion: ledgerSchemaVersion ?? DELEGATION_LEDGER_SCHEMA_VERSION };
|
|
209
306
|
}
|
|
210
307
|
export async function readDelegationLedger(projectRoot) {
|
|
211
308
|
const { activeRunId } = await readFlowState(projectRoot);
|
|
@@ -222,6 +319,57 @@ export async function readDelegationLedger(projectRoot) {
|
|
|
222
319
|
return { runId: activeRunId, entries: [] };
|
|
223
320
|
}
|
|
224
321
|
}
|
|
322
|
+
export async function readDelegationEvents(projectRoot) {
|
|
323
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
324
|
+
if (!(await exists(filePath))) {
|
|
325
|
+
return { events: [], corruptLines: [] };
|
|
326
|
+
}
|
|
327
|
+
const events = [];
|
|
328
|
+
const corruptLines = [];
|
|
329
|
+
const text = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
330
|
+
const lines = text.split(/\r?\n/gu);
|
|
331
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
332
|
+
const line = lines[index]?.trim() ?? "";
|
|
333
|
+
if (line.length === 0)
|
|
334
|
+
continue;
|
|
335
|
+
try {
|
|
336
|
+
const parsed = JSON.parse(line);
|
|
337
|
+
if (isDelegationEvent(parsed)) {
|
|
338
|
+
events.push(parsed);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
corruptLines.push(index + 1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
corruptLines.push(index + 1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { events, corruptLines };
|
|
349
|
+
}
|
|
350
|
+
async function appendDelegationEvent(projectRoot, event) {
|
|
351
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
352
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
353
|
+
await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
354
|
+
}
|
|
355
|
+
async function writeSubagentTracker(projectRoot, entries) {
|
|
356
|
+
const active = entries
|
|
357
|
+
.filter((entry) => entry.status === "scheduled" || entry.status === "launched" || entry.status === "acknowledged")
|
|
358
|
+
.map((entry) => ({
|
|
359
|
+
spanId: entry.spanId,
|
|
360
|
+
dispatchId: entry.dispatchId,
|
|
361
|
+
workerRunId: entry.workerRunId,
|
|
362
|
+
stage: entry.stage,
|
|
363
|
+
agent: entry.agent,
|
|
364
|
+
status: entry.status,
|
|
365
|
+
dispatchSurface: entry.dispatchSurface,
|
|
366
|
+
agentDefinitionPath: entry.agentDefinitionPath,
|
|
367
|
+
startedAt: entry.startTs,
|
|
368
|
+
launchedAt: entry.launchedTs,
|
|
369
|
+
acknowledgedAt: entry.ackTs
|
|
370
|
+
}));
|
|
371
|
+
await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
372
|
+
}
|
|
225
373
|
export async function appendDelegation(projectRoot, entry) {
|
|
226
374
|
const { activeRunId } = await readFlowState(projectRoot);
|
|
227
375
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
@@ -231,17 +379,20 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
231
379
|
if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
|
|
232
380
|
throw new Error("waived delegation entries require a non-empty waiverReason");
|
|
233
381
|
}
|
|
234
|
-
const stamped = { ...entry, runId: entry.runId ?? activeRunId };
|
|
382
|
+
const stamped = statusTimestampPatch({ ...entry, runId: entry.runId ?? activeRunId }, startTs);
|
|
235
383
|
stamped.spanId = entry.spanId ?? createSpanId();
|
|
236
384
|
stamped.startTs = startTs;
|
|
237
385
|
stamped.ts = startTs;
|
|
238
386
|
if (TERMINAL_DELEGATION_STATUSES.has(stamped.status) && !stamped.endTs) {
|
|
239
387
|
stamped.endTs = new Date().toISOString();
|
|
240
388
|
}
|
|
389
|
+
if (stamped.status === "completed") {
|
|
390
|
+
stamped.completedTs = stamped.completedTs ?? stamped.endTs ?? new Date().toISOString();
|
|
391
|
+
}
|
|
241
392
|
if (stamped.status === "scheduled") {
|
|
242
393
|
delete stamped.endTs;
|
|
243
394
|
}
|
|
244
|
-
stamped.schemaVersion =
|
|
395
|
+
stamped.schemaVersion = DELEGATION_LEDGER_SCHEMA_VERSION;
|
|
245
396
|
if (stamped.retryCount === undefined ||
|
|
246
397
|
!Number.isInteger(stamped.retryCount) ||
|
|
247
398
|
stamped.retryCount < 0) {
|
|
@@ -268,11 +419,14 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
268
419
|
if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
|
|
269
420
|
return;
|
|
270
421
|
}
|
|
422
|
+
await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
|
|
271
423
|
const ledger = {
|
|
272
424
|
runId: activeRunId,
|
|
273
|
-
entries: [...prior.entries, stamped]
|
|
425
|
+
entries: [...prior.entries, stamped],
|
|
426
|
+
schemaVersion: DELEGATION_LEDGER_SCHEMA_VERSION
|
|
274
427
|
};
|
|
275
428
|
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
|
|
429
|
+
await writeSubagentTracker(projectRoot, ledger.entries);
|
|
276
430
|
});
|
|
277
431
|
}
|
|
278
432
|
/**
|
|
@@ -299,6 +453,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
299
453
|
const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
|
|
300
454
|
const { activeRunId } = flowState;
|
|
301
455
|
const ledger = await readDelegationLedger(projectRoot);
|
|
456
|
+
const events = await readDelegationEvents(projectRoot);
|
|
302
457
|
const forStage = ledger.entries.filter((e) => e.stage === stage);
|
|
303
458
|
const forRun = forStage.filter((e) => e.runId === activeRunId);
|
|
304
459
|
const staleIgnored = forStage
|
|
@@ -307,6 +462,9 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
307
462
|
const missing = [];
|
|
308
463
|
const waived = [];
|
|
309
464
|
const missingEvidence = [];
|
|
465
|
+
const missingDispatchProof = [];
|
|
466
|
+
const legacyInferredCompletions = [];
|
|
467
|
+
let legacyRequiresRerecord = false;
|
|
310
468
|
const terminalSpanIds = new Set(forRun
|
|
311
469
|
.filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
|
|
312
470
|
.map((entry) => entry.spanId));
|
|
@@ -342,13 +500,57 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
342
500
|
!completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
|
|
343
501
|
missingEvidence.push(agent);
|
|
344
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.
|
|
510
|
+
for (const row of completedRows) {
|
|
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
|
+
}
|
|
517
|
+
if (mode === "isolated") {
|
|
518
|
+
const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
|
|
519
|
+
event.stage === stage &&
|
|
520
|
+
event.agent === agent &&
|
|
521
|
+
event.spanId === row.spanId);
|
|
522
|
+
const dispatchId = row.dispatchId ?? row.workerRunId ?? spanEvents.find((event) => event.dispatchId || event.workerRunId)?.dispatchId ?? spanEvents.find((event) => event.workerRunId)?.workerRunId;
|
|
523
|
+
const dispatchSurface = row.dispatchSurface ?? spanEvents.find((event) => event.dispatchSurface)?.dispatchSurface;
|
|
524
|
+
const agentDefinitionPath = row.agentDefinitionPath ?? spanEvents.find((event) => event.agentDefinitionPath)?.agentDefinitionPath;
|
|
525
|
+
const hasAck = Boolean(row.ackTs || spanEvents.some((event) => event.event === "acknowledged" && event.ackTs));
|
|
526
|
+
const hasCompleted = Boolean(row.completedTs || spanEvents.some((event) => event.event === "completed" && event.completedTs));
|
|
527
|
+
const hasDispatchProof = Boolean(row.spanId && dispatchId && dispatchSurface && agentDefinitionPath && hasAck && hasCompleted);
|
|
528
|
+
if (!hasDispatchProof) {
|
|
529
|
+
const proofEraSignal = Boolean(row.dispatchId || row.workerRunId || row.dispatchSurface || row.agentDefinitionPath || spanEvents.some((event) => event.dispatchId || event.workerRunId || event.dispatchSurface || event.agentDefinitionPath || event.event === "acknowledged" || event.event === "launched"));
|
|
530
|
+
if (proofEraSignal) {
|
|
531
|
+
missingDispatchProof.push(agent);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
345
539
|
}
|
|
346
540
|
return {
|
|
347
|
-
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,
|
|
348
547
|
missing,
|
|
349
548
|
waived,
|
|
350
549
|
staleIgnored,
|
|
351
550
|
missingEvidence,
|
|
551
|
+
missingDispatchProof,
|
|
552
|
+
legacyInferredCompletions,
|
|
553
|
+
corruptEventLines: events.corruptLines,
|
|
352
554
|
staleWorkers,
|
|
353
555
|
expectedMode
|
|
354
556
|
};
|
package/dist/doctor-registry.js
CHANGED
|
@@ -107,6 +107,15 @@ const RULES = [
|
|
|
107
107
|
docRef: ref("harnesses.md")
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
test: /^harness:reality:/,
|
|
112
|
+
metadata: {
|
|
113
|
+
severity: "info",
|
|
114
|
+
summary: "Harness reality label for dispatch/proof support.",
|
|
115
|
+
fix: "No action required; use this label to interpret native/generic/role-switch proof requirements.",
|
|
116
|
+
docRef: ref("harnesses.md")
|
|
117
|
+
}
|
|
118
|
+
},
|
|
110
119
|
{
|
|
111
120
|
test: /^delegation:/,
|
|
112
121
|
metadata: {
|
package/dist/doctor.js
CHANGED
|
@@ -13,7 +13,7 @@ import { policyChecks } from "./policy.js";
|
|
|
13
13
|
import { CorruptFlowStateError, readFlowState } from "./runs.js";
|
|
14
14
|
import { createInitialFlowState, skippedStagesForTrack } from "./flow-state.js";
|
|
15
15
|
import { FLOW_STAGES, TRACK_STAGES } from "./types.js";
|
|
16
|
-
import { checkMandatoryDelegations } from "./delegation.js";
|
|
16
|
+
import { checkMandatoryDelegations, readDelegationEvents } from "./delegation.js";
|
|
17
17
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
18
18
|
import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
19
19
|
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
@@ -305,6 +305,20 @@ function normalizeOpenCodePluginEntry(entry) {
|
|
|
305
305
|
}
|
|
306
306
|
return null;
|
|
307
307
|
}
|
|
308
|
+
function generatedAgentShape(content, kind, agentName) {
|
|
309
|
+
if (kind === "opencode") {
|
|
310
|
+
return content.includes("mode: subagent") && content.includes(`# ${agentName}`) && content.includes("STRICT_RETURN_SCHEMA");
|
|
311
|
+
}
|
|
312
|
+
return content.includes(`name = "${agentName}"`) && content.includes("developer_instructions") && content.includes("STRICT_RETURN_SCHEMA");
|
|
313
|
+
}
|
|
314
|
+
function harnessRealityLabel(harness) {
|
|
315
|
+
const adapter = HARNESS_ADAPTERS[harness];
|
|
316
|
+
const declaredSupport = adapter.capabilities.nativeSubagentDispatch;
|
|
317
|
+
const runtimeLaunch = harness === "opencode" || harness === "codex" ? "prompt-level launch" : declaredSupport === "generic" ? "generic Task launch" : "native tool launch";
|
|
318
|
+
const proofRequired = adapter.capabilities.subagentFallback === "native" ? "dispatchId+spanId+ack" : "evidenceRefs";
|
|
319
|
+
const proofSource = harness === "opencode" ? ".opencode/agents + delegation-events.jsonl" : harness === "codex" ? ".codex/agents + delegation-events.jsonl" : ".cclaw/state/delegation-log.json";
|
|
320
|
+
return `declaredSupport=${declaredSupport}; runtimeLaunch=${runtimeLaunch}; proofRequired=${proofRequired}; proofSource=${proofSource}`;
|
|
321
|
+
}
|
|
308
322
|
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
309
323
|
function opencodeConfigCandidates(projectRoot) {
|
|
310
324
|
return [
|
|
@@ -780,6 +794,14 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
780
794
|
}
|
|
781
795
|
}
|
|
782
796
|
}
|
|
797
|
+
for (const harness of configuredHarnesses) {
|
|
798
|
+
checks.push({
|
|
799
|
+
name: `harness:reality:${harness}`,
|
|
800
|
+
ok: true,
|
|
801
|
+
severity: "info",
|
|
802
|
+
details: harnessRealityLabel(harness)
|
|
803
|
+
});
|
|
804
|
+
}
|
|
783
805
|
const agentsFile = path.join(projectRoot, "AGENTS.md");
|
|
784
806
|
let agentsBlockOk = false;
|
|
785
807
|
if (await exists(agentsFile)) {
|
|
@@ -884,12 +906,39 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
884
906
|
details: agentPath
|
|
885
907
|
});
|
|
886
908
|
}
|
|
909
|
+
for (const agent of CCLAW_AGENTS) {
|
|
910
|
+
if (configuredHarnesses.includes("opencode")) {
|
|
911
|
+
const agentPath = path.join(projectRoot, ".opencode", "agents", `${agent.name}.md`);
|
|
912
|
+
let ok = false;
|
|
913
|
+
if (await exists(agentPath)) {
|
|
914
|
+
ok = generatedAgentShape(await fs.readFile(agentPath, "utf8"), "opencode", agent.name);
|
|
915
|
+
}
|
|
916
|
+
checks.push({
|
|
917
|
+
name: `agent:opencode:${agent.name}:shape`,
|
|
918
|
+
ok,
|
|
919
|
+
details: `${agentPath} must be a generated OpenCode subagent with mode: subagent and strict return schema`
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
if (configuredHarnesses.includes("codex")) {
|
|
923
|
+
const agentPath = path.join(projectRoot, ".codex", "agents", `${agent.name}.toml`);
|
|
924
|
+
let ok = false;
|
|
925
|
+
if (await exists(agentPath)) {
|
|
926
|
+
ok = generatedAgentShape(await fs.readFile(agentPath, "utf8"), "codex", agent.name);
|
|
927
|
+
}
|
|
928
|
+
checks.push({
|
|
929
|
+
name: `agent:codex:${agent.name}:shape`,
|
|
930
|
+
ok,
|
|
931
|
+
details: `${agentPath} must be a generated Codex custom agent TOML with developer_instructions and strict return schema`
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
887
935
|
// Hook scripts
|
|
888
936
|
for (const script of [
|
|
889
937
|
"run-hook.mjs",
|
|
890
938
|
"run-hook.cmd",
|
|
891
939
|
"stage-complete.mjs",
|
|
892
940
|
"start-flow.mjs",
|
|
941
|
+
"delegation-record.mjs",
|
|
893
942
|
"opencode-plugin.mjs"
|
|
894
943
|
]) {
|
|
895
944
|
const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
|
|
@@ -1774,6 +1823,7 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1774
1823
|
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1775
1824
|
repairFeatureSystem: false
|
|
1776
1825
|
});
|
|
1826
|
+
const delegationEvents = await readDelegationEvents(projectRoot);
|
|
1777
1827
|
const delegationSatisfiedForDoctor = currentStageUntouched || delegation.satisfied;
|
|
1778
1828
|
const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
|
|
1779
1829
|
? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
|
|
@@ -1785,7 +1835,30 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1785
1835
|
? `mandatory delegation check deferred for untouched stage "${flowState.currentStage}"; stage-complete enforces it when work begins`
|
|
1786
1836
|
: delegation.satisfied
|
|
1787
1837
|
? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
|
|
1788
|
-
: `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
|
|
1838
|
+
: `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}; missingDispatchProof=${delegation.missingDispatchProof.join(", ")}; staleWorkers=${delegation.staleWorkers.join(", ")}; corruptEventLines=${delegation.corruptEventLines.join(", ")}`
|
|
1839
|
+
});
|
|
1840
|
+
checks.push({
|
|
1841
|
+
name: "delegation:events:parse",
|
|
1842
|
+
ok: delegationEvents.corruptLines.length === 0,
|
|
1843
|
+
details: delegationEvents.corruptLines.length === 0
|
|
1844
|
+
? `${RUNTIME_ROOT}/state/delegation-events.jsonl parsed successfully (${delegationEvents.events.length} event(s))`
|
|
1845
|
+
: `corrupt delegation event line(s): ${delegationEvents.corruptLines.join(", ")}`
|
|
1846
|
+
});
|
|
1847
|
+
checks.push({
|
|
1848
|
+
name: "delegation:proof:current_stage",
|
|
1849
|
+
ok: currentStageUntouched || delegation.missingDispatchProof.length === 0,
|
|
1850
|
+
details: currentStageUntouched
|
|
1851
|
+
? `dispatch proof check deferred for untouched stage "${flowState.currentStage}"`
|
|
1852
|
+
: delegation.missingDispatchProof.length === 0
|
|
1853
|
+
? `no dispatch proof gaps for current stage "${flowState.currentStage}"`
|
|
1854
|
+
: `isolated completions missing dispatchId/dispatchSurface/agentDefinitionPath/ackTs/completedTs: ${delegation.missingDispatchProof.join(", ")}`
|
|
1855
|
+
});
|
|
1856
|
+
checks.push({
|
|
1857
|
+
name: "warning:delegation:legacy_inferred_completions",
|
|
1858
|
+
ok: true,
|
|
1859
|
+
details: delegation.legacyInferredCompletions.length > 0
|
|
1860
|
+
? `warning: legacy inferred isolated completion rows lack event-log proof: ${delegation.legacyInferredCompletions.join(", ")}`
|
|
1861
|
+
: "no legacy inferred isolated completions for current stage"
|
|
1789
1862
|
});
|
|
1790
1863
|
checks.push({
|
|
1791
1864
|
name: "warning:delegation:waived",
|
|
@@ -37,6 +37,12 @@ export type SubagentFallback =
|
|
|
37
37
|
export type ShimKind = "command" | "skill";
|
|
38
38
|
export interface HarnessAdapter {
|
|
39
39
|
id: HarnessId;
|
|
40
|
+
reality: {
|
|
41
|
+
declaredSupport: "full" | "generic" | "partial" | "none";
|
|
42
|
+
runtimeLaunch: string;
|
|
43
|
+
proofRequired: string;
|
|
44
|
+
proofSource: string;
|
|
45
|
+
};
|
|
40
46
|
/**
|
|
41
47
|
* Root directory where cclaw writes `/cc*` entry points.
|
|
42
48
|
*
|
|
@@ -89,6 +95,48 @@ export declare function harnessShimFileNames(): string[];
|
|
|
89
95
|
export declare function harnessShimSkillNames(): string[];
|
|
90
96
|
export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
|
|
91
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[];
|
|
92
140
|
export declare function harnessDispatchFallback(harnessId: HarnessId): string;
|
|
93
141
|
export type HarnessTier = "tier1" | "tier2" | "tier3";
|
|
94
142
|
export declare function harnessTier(harnessId: HarnessId): HarnessTier;
|