cclaw-cli 0.51.25 → 0.51.26
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/content/core-agents.js +21 -1
- package/dist/content/harness-doc.d.ts +1 -0
- package/dist/content/harness-doc.js +3 -0
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +189 -0
- package/dist/content/skills.js +5 -4
- package/dist/content/status-command.js +8 -2
- package/dist/content/subagents.js +6 -2
- package/dist/content/tree-command.js +7 -1
- package/dist/delegation.d.ts +32 -1
- package/dist/delegation.js +145 -6
- package/dist/doctor-registry.js +9 -0
- package/dist/doctor.js +75 -2
- package/dist/harness-adapters.d.ts +6 -0
- package/dist/harness-adapters.js +28 -4
- package/dist/install.js +3 -1
- package/package.json +1 -1
|
@@ -30,6 +30,24 @@ const DOC_RETURN_SCHEMA = {
|
|
|
30
30
|
requiredFields: ["status", "filesUpdated", "summary", "evidenceRefs", "openQuestions"],
|
|
31
31
|
evidenceFields: ["filesUpdated", "evidenceRefs"]
|
|
32
32
|
};
|
|
33
|
+
function workerAckContract() {
|
|
34
|
+
return `## Worker ACK Contract
|
|
35
|
+
|
|
36
|
+
Before doing substantive work, return an ACK object that the parent can record:
|
|
37
|
+
|
|
38
|
+
\`\`\`json
|
|
39
|
+
{
|
|
40
|
+
"status": "ACK",
|
|
41
|
+
"spanId": "<parent spanId>",
|
|
42
|
+
"dispatchId": "<parent dispatchId or workerRunId>",
|
|
43
|
+
"dispatchSurface": "claude-task|cursor-task|opencode-agent|codex-agent|generic-task|role-switch",
|
|
44
|
+
"agentDefinitionPath": ".cclaw/agents/<agent>.md or harness generated path",
|
|
45
|
+
"ackTs": "<ISO timestamp>"
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
Finish with the required return schema plus the same \`spanId\` and \`dispatchId\`. The parent must not claim isolated completion unless ACK/result proof matches the ledger/event span.`;
|
|
50
|
+
}
|
|
33
51
|
function formatReturnSchema(schema) {
|
|
34
52
|
return [
|
|
35
53
|
`- Status field: \`${schema.statusField}\``,
|
|
@@ -446,9 +464,11 @@ ${agent.body}
|
|
|
446
464
|
- Mode: ${agent.activation}
|
|
447
465
|
- Related stages: ${relatedStages}
|
|
448
466
|
|
|
467
|
+
${workerAckContract()}
|
|
468
|
+
|
|
449
469
|
## Required Return Schema
|
|
450
470
|
|
|
451
|
-
STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated.
|
|
471
|
+
STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated. Include \`spanId\`, \`dispatchId\` or \`workerRunId\`, \`dispatchSurface\`, \`agentDefinitionPath\`, and lifecycle timestamps when provided by the parent.
|
|
452
472
|
|
|
453
473
|
${formatReturnSchema(agent.returnSchema)}
|
|
454
474
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function harnessIntegrationDocMarkdown(): string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function harnessIntegrationDocMarkdown() {
|
|
2
|
+
return "# Harness Integration Matrix\n\nGenerated from `src/harness-adapters.ts` capabilities and hook event mappings. For the end-to-end subagent dispatch model, proof sequence, controller/worker responsibilities, and future roadmap, see [`docs/subagent-flow.md`](./subagent-flow.md).\n\n## Capability tiers\n\n| Harness | ID | Tier | declaredSupport | runtimeLaunch | Fallback | proofRequired | proofSource | Hook surface | Structured ask |\n|---|---|---|---|---|---|---|---|---|---|\n| Claude Code | `claude` | `tier1` (full native automation) | full | native Task launch | native | spanId+dispatchId or workerRunId+ACK | `.cclaw/state/delegation-events.jsonl` + ledger | full | AskUserQuestion |\n| Cursor | `cursor` | `tier2` (supported with fallback paths) | generic | generic Task/Subagent role prompt | generic-dispatch | spanId+dispatchId/evidenceRefs | events + artifact evidenceRefs | full | AskQuestion |\n| OpenCode | `opencode` | `tier2` hooks, native dispatch declared | full | prompt-level launch via Task / `@agent` against `.opencode/agents` | native | spanId+dispatchId+ackTs+completedTs | `.opencode/agents/<agent>.md` + events | plugin | question |\n| OpenAI Codex | `codex` | `tier2` hooks, native dispatch declared | full | prompt-level request to spawn `.codex/agents` custom agents | native | spanId+dispatchId+ackTs+completedTs | `.codex/agents/<agent>.toml` + events | limited | request_user_input |\n\nFallback legend:\n\n- `native` \u2014 first-class named subagent dispatch (Claude).\n- `generic-dispatch` \u2014 generic Task dispatcher mapped to cclaw roles (Cursor).\n- `role-switch` \u2014 degraded fallback for a runtime where declared native/generic dispatch is unavailable; explicit role headers, artifact outputs, and non-empty delegation-log evidenceRefs are required.\n- `waiver` \u2014 no parity path; reserved for harnesses that cannot role-switch (none shipped).\n\n## Stage-Aware Native Dispatch Workflow\n\nOpenCode and Codex receive generated native isolated subagents. Use them before considering role-switch fallback:\n\n1. Use the active stage skill's generated dispatch table as the source of truth.\n2. OpenCode: invoke `.opencode/agents/<agent>.md` via Task or `@<agent>`; Codex: ask Codex to spawn `.codex/agents/<agent>.toml` by name, in parallel when lanes are independent.\n3. Load `.cclaw/agents/<agent>.md`, execute only that role's stage task, and write outputs into the active stage artifact.\n4. Append `.cclaw/state/delegation-events.jsonl` for scheduled/launched/acknowledged/completed/failed/waived/stale, then mirror current state in `.cclaw/state/delegation-log.json`. The ledger is current state; the event log is proof/audit.\n5. Treat completed role-switch rows without `evidenceRefs` as unresolved; treat native isolated completion without matching `spanId` + `dispatchId`/`workerRunId` + `ackTs` + `completedTs` as fake isolated completion. Native isolated rows are not a role-switch substitute and should reflect a real dispatched worker.\n\nThis is staged agent work backed by the harness-native subagent surfaces. Role-switch remains only a degraded fallback when that surface is unavailable in the active runtime.\n\n## Parallel research dispatch semantics\n\nDesign-stage research fleet uses the same parity model:\n\n- **Claude / Cursor**: dispatch all four research lenses in one turn\n (stack, features, architecture, pitfalls) and synthesize into\n `.cclaw/artifacts/02a-research.md`.\n- **OpenCode / Codex**: dispatch generated native subagents for the same\n four lenses and run independent lanes in parallel where the active runtime\n permits. Use role-switch with evidence only as a degraded fallback.\n\n## Semantic hook event coverage\n\n| Event | Claude | Cursor | OpenCode | Codex |\n|---|---|---|---|---|\n| `session_rehydrate` | SessionStart matcher startup|resume|clear|compact | sessionStart/sessionResume/sessionClear/sessionCompact | plugin event handlers + transform rehydration | SessionStart matcher startup|resume |\n| `pre_tool_prompt_guard` | PreToolUse -> prompt-guard | preToolUse -> prompt-guard | plugin tool.execute.before -> prompt-guard | PreToolUse matcher Bash -> prompt-guard (plus UserPromptSubmit for non-Bash prompts) |\n| `pre_tool_workflow_guard` | PreToolUse -> workflow-guard | preToolUse -> workflow-guard | plugin tool.execute.before -> workflow-guard | PreToolUse matcher Bash -> workflow-guard (Bash-only) |\n| `post_tool_context_monitor` | PostToolUse -> context-monitor | postToolUse -> context-monitor | plugin tool.execute.after -> context-monitor | PostToolUse matcher Bash -> context-monitor (Bash-only) |\n| `stop_handoff` | Stop -> stop-handoff | stop -> stop-handoff | plugin session.idle -> stop-handoff | Stop -> stop-handoff |\n| `precompact_compat` | PreCompact -> pre-compact | sessionCompact -> pre-compact | plugin session.compacted -> pre-compact | missing |\n| `strict_state_verify` | missing | missing | missing | UserPromptSubmit -> verify-current-state (blocks only in strict mode) |\n\n## Hook lifecycle aliases\n\nThe generated Node dispatcher accepts a small compatibility alias set for lifecycle names: `stop` and `stop-checkpoint` route to `stop-handoff`, `precompact` routes to `pre-compact`, and `session-rehydrate` routes to `session-start`. The `pre-compact` handler is intentionally a no-op compatibility marker; rehydration remains the `session-start` responsibility after compact events. Harness JSON should still emit the canonical handler names from `src/content/hook-manifest.ts`.\n\n## Hook event casing\n\nHook keys are intentionally harness-native and must not be normalized:\n\n| Harness | ID | Event key casing |\n|---|---|---|\n| Claude Code | `claude` | PascalCase (`SessionStart`, `PreToolUse`) |\n| Cursor | `cursor` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenCode | `opencode` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `PreToolUse`) |\n\nUse the exact event names from each harness schema. Treating all hooks as one\nshared casing silently breaks generated wiring.\n\n## Interpretation\n\n- `tier1`: full native delegation + structured asks + full hook surface.\n- `tier2`: usable flow with capability gaps; mandatory delegation can require waivers.\n- Codex-specific ceiling: `PreToolUse` can only intercept `Bash`. Direct\n `Write`/`Edit` to `.cclaw/state/flow-state.json` cannot be hard-blocked\n at hook level, so the canonical path is\n `node .cclaw/hooks/stage-complete.mjs <stage>` plus the non-blocking\n `UserPromptSubmit` state nudge.\n- In `strict` mode, Codex additionally runs the generated Node/runtime `verify-current-state` path on `UserPromptSubmit` as a fail-closed check. Advisory mode remains non-blocking, including when the generated local Node entrypoint is missing; doctor reports that install drift separately. This strict-only coverage is represented explicitly by the `strict_state_verify` semantic row above.\n\n## Shared command contract\n\nAll harnesses receive the same utility commands:\n\n- `/cc` - flow entry and resume\n- `/cc-next` - stage progression and post-ship closeout\n- `/cc-ideate` - ideate mode for ranked repo-improvement backlog\n- `/cc-view` - read-only router for status/tree/diff\n\nRead-only subcommands:\n- `/cc-view status` - visual flow snapshot\n- `/cc-view tree` - deep flow tree (stages, artifacts, stale markers)\n- `/cc-view diff` - before/after flow-state diff map\n\nOperational work is handled by `/cc`, `/cc-next`, `/cc-ideate`, `/cc-view`, and `node .cclaw/hooks/stage-complete.mjs <stage>` inside the installed harness runtime. `npx cclaw-cli` is the installer/support surface for init, sync, upgrade, doctor, and explicit/manual archive; the normal stage flow must not depend on a runtime `cclaw` binary in PATH.\n\nCritical-path stage order remains canonical:\n`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship`\n\nEvery track then closes out through:\n`retro -> compound -> archive`\n\n## Stage -> skill folder mapping\n\n| Stage | Skill folder |\n|---|---|\n| `brainstorm` | `brainstorming` |\n| `scope` | `scope-shaping` |\n| `design` | `engineering-design-lock` |\n| `spec` | `specification-authoring` |\n| `plan` | `planning-and-task-breakdown` |\n| `tdd` | `test-driven-development` |\n| `review` | `two-layer-review` |\n| `ship` | `shipping-and-handoff` |\n\nThis map is generated from `src/constants.ts::STAGE_TO_SKILL_FOLDER` so\nskill-path naming stays explicit and stable even when stage ids differ from\nfolder names.\n\n## Install surfaces\n\nAlways generated:\n\n- `.cclaw/commands/*.md`\n- `.cclaw/skills/*/SKILL.md`\n- `.cclaw/state/*.json|*.jsonl`\n- `AGENTS.md` managed block\n\nHarness-specific additions:\n\n- `claude`: `.claude/commands/cc*.md`, `.claude/hooks/hooks.json`\n- `cursor`: `.cursor/commands/cc*.md`, `.cursor/hooks.json`, `.cursor/rules/cclaw-workflow.mdc`\n- `opencode`: `.opencode/commands/cc*.md`, `.opencode/plugins/cclaw-plugin.mjs`, opencode plugin registration with `permission.question: \"allow\"`; set `OPENCODE_ENABLE_QUESTION_TOOL=1` for ACP clients so structured asks can route through question tooling. Doctor validates the config permission and warns when the environment hint is absent.\n- `codex`: `.agents/skills/cc/SKILL.md`, `.agents/skills/cc-next/SKILL.md`, `.agents/skills/cc-ideate/SKILL.md`, `.agents/skills/cc-view/SKILL.md`, `.codex/hooks.json` (Codex CLI reads `.agents/skills/` for custom skills and consumes `.codex/hooks.json` on v0.114+ when `[features] codex_hooks = true` is set in `~/.codex/config.toml`. `.codex/commands/` and the legacy `.agents/skills/cclaw-cc*/` layout from v0.39.x are auto-cleaned on sync.)\n\n## Runtime observability\n\n- `npx cclaw-cli doctor` validates shim, hook, and lifecycle surfaces against this capability model.\n- `/cc-view status` and `/cc-view tree` surface the same harness tier/fallback facts from the generated runtime metadata.\n\n## Delegation Proof Model\n\nRuntime state is split deliberately:\n\n- `.cclaw/state/delegation-log.json` is the compact current ledger used by stage gates and `/cc-view` summaries.\n- `.cclaw/state/delegation-events.jsonl` is append-only audit proof for `scheduled`, `launched`, `acknowledged`, `completed`, `failed`, `waived`, and `stale` lifecycle transitions.\n- `.cclaw/state/subagents.json` is a lightweight active-worker tracker for status/tree/doctor surfaces.\n- `.cclaw/hooks/delegation-record.mjs` is the generated helper for lifecycle rows/events. It validates required fields and emits JSON diagnostics with `--json`.\n\nIsolated completion requires `spanId`, `dispatchId` or `workerRunId`, `dispatchSurface`, `agentDefinitionPath`, `ackTs`, `launchedTs`, and `completedTs`. Cursor/generic dispatch and role-switch also require evidence refs when artifact evidence is the proof source. Legacy inferred completions remain readable, but doctor reports them as warnings because they predate event-log proof.\n\n## Reference Audit Appendix\n\nStatus meanings: `deep` = read for transferable implementation contract; `targeted` = inspected the relevant files only; `skimmed` = searched/read enough to classify; `not relevant` = intentionally excluded from implementation influence.\n\n| Reference path under `/Users/zuevrs/Downloads/references` | Status | Findings preserved |\n|---|---|---|\n| `evanklem-evanflow/skills/evanflow-coder-overseer/SKILL.md` | deep | Contract-first coder/overseer loop, reviewer reads code rather than worker narrative, and integration overseer pattern map cleanly onto cclaw subagent guidance. |\n| `evanklem-evanflow/agents/evanflow-coder.md` | targeted | Worker role is narrow: implement the pasted contract, avoid broad orchestration, and return evidence for overseer verification. |\n| `evanklem-evanflow/agents/evanflow-overseer.md` | targeted | Overseer validates actual code and acceptance evidence before controller marks work complete. |\n| `oh-my-codex/src/agents/native-config.ts` | deep | Native agent config shape supports explicit metadata/model/tool posture; cclaw should validate generated `.codex/agents/*.toml` shape instead of trusting file presence. |\n| `oh-my-codex/src/team/state/events.ts` and `src/team/state/workers.ts` | targeted | Append-only events plus worker state are useful as separate audit/current-state layers; cclaw mirrors that with `delegation-events.jsonl` and `subagents.json`. |\n| `oh-my-openagent/src/tools/delegate-task/tools.ts` | deep | Delegation should have an explicit dispatch surface and mode instead of relying on a prose claim that an agent was launched. |\n| `oh-my-openagent/src/tools/delegate-task/subagent-resolver.ts` | targeted | Agent discovery should be checked by doctor so missing/corrupt generated agent definitions are visible before dispatch. |\n| `oh-my-openagent/src/tools/delegate-task/prompt-builder.ts` | targeted | Prompt builders should include exact invocation/return contracts; cclaw generated worker prompts now carry ACK/result schemas. |\n| `giancarloerra-socraticode/**` | skimmed | Useful for workflow/e2e and graph-oriented contract testing, but not a subagent dispatch implementation reference; no runtime pattern imported. |\n| unrelated large reference trees not named above | not relevant | Searched/skipped because they did not contain flow/subagent/harness dispatch patterns relevant to this plan. |\n";
|
|
3
|
+
}
|
package/dist/content/hooks.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function startFlowScript(): string;
|
|
2
2
|
export declare function stageCompleteScript(): string;
|
|
3
|
+
export declare function delegationRecordScript(): string;
|
|
3
4
|
export declare function runHookCmdScript(): string;
|
|
4
5
|
export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
|
|
5
6
|
export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
|
package/dist/content/hooks.js
CHANGED
|
@@ -260,6 +260,195 @@ async function main() {
|
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
void main();
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
export function delegationRecordScript() {
|
|
267
|
+
return `#!/usr/bin/env node
|
|
268
|
+
import fs from "node:fs/promises";
|
|
269
|
+
import path from "node:path";
|
|
270
|
+
import process from "node:process";
|
|
271
|
+
|
|
272
|
+
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
273
|
+
const VALID_STATUSES = new Set(["scheduled", "launched", "acknowledged", "completed", "failed", "waived", "stale"]);
|
|
274
|
+
const TERMINAL = new Set(["completed", "failed", "waived", "stale"]);
|
|
275
|
+
|
|
276
|
+
function parseArgs(argv) {
|
|
277
|
+
const args = {};
|
|
278
|
+
for (const raw of argv) {
|
|
279
|
+
const valueMatch = /^--([^=]+)=(.*)$/u.exec(raw);
|
|
280
|
+
if (valueMatch) {
|
|
281
|
+
args[valueMatch[1]] = valueMatch[2];
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const flagMatch = /^--([^=]+)$/u.exec(raw);
|
|
285
|
+
if (flagMatch) args[flagMatch[1]] = true;
|
|
286
|
+
}
|
|
287
|
+
return args;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function exists(filePath) {
|
|
291
|
+
try {
|
|
292
|
+
await fs.access(filePath);
|
|
293
|
+
return true;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function detectRoot() {
|
|
300
|
+
const candidates = [
|
|
301
|
+
process.env.CCLAW_PROJECT_ROOT,
|
|
302
|
+
process.env.CLAUDE_PROJECT_DIR,
|
|
303
|
+
process.env.CURSOR_PROJECT_DIR,
|
|
304
|
+
process.env.CURSOR_PROJECT_ROOT,
|
|
305
|
+
process.env.OPENCODE_PROJECT_DIR,
|
|
306
|
+
process.env.OPENCODE_PROJECT_ROOT,
|
|
307
|
+
process.cwd()
|
|
308
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
309
|
+
for (const candidate of candidates) {
|
|
310
|
+
if (await exists(path.join(candidate, RUNTIME_ROOT))) return candidate;
|
|
311
|
+
}
|
|
312
|
+
return candidates[0] || process.cwd();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function readRunId(root) {
|
|
316
|
+
try {
|
|
317
|
+
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
|
|
318
|
+
const parsed = JSON.parse(raw);
|
|
319
|
+
return typeof parsed.activeRunId === "string" ? parsed.activeRunId : "unknown-run";
|
|
320
|
+
} catch {
|
|
321
|
+
return "unknown-run";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function readDelegationEvents(root) {
|
|
326
|
+
try {
|
|
327
|
+
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-events.jsonl"), "utf8");
|
|
328
|
+
return raw
|
|
329
|
+
.split(/\\r?\\n/u)
|
|
330
|
+
.filter((line) => line.trim().length > 0)
|
|
331
|
+
.map((line) => {
|
|
332
|
+
try {
|
|
333
|
+
return JSON.parse(line);
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
.filter((event) => event && typeof event === "object");
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasPriorAck(events, args, runId) {
|
|
345
|
+
return events.some((event) =>
|
|
346
|
+
event.runId === runId &&
|
|
347
|
+
event.stage === args.stage &&
|
|
348
|
+
event.agent === args.agent &&
|
|
349
|
+
event.spanId === args["span-id"] &&
|
|
350
|
+
event.event === "acknowledged" &&
|
|
351
|
+
typeof event.ackTs === "string" &&
|
|
352
|
+
event.ackTs.length > 0
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function usage() {
|
|
357
|
+
process.stderr.write("Usage: node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--json]\\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function main() {
|
|
361
|
+
const args = parseArgs(process.argv.slice(2));
|
|
362
|
+
const json = args.json !== undefined;
|
|
363
|
+
const problems = [];
|
|
364
|
+
if (!args.stage) problems.push("missing --stage");
|
|
365
|
+
if (!args.agent) problems.push("missing --agent");
|
|
366
|
+
if (args.mode !== "mandatory" && args.mode !== "proactive") problems.push("--mode must be mandatory or proactive");
|
|
367
|
+
if (!VALID_STATUSES.has(args.status)) problems.push("invalid --status");
|
|
368
|
+
if (!args["span-id"]) problems.push("missing --span-id");
|
|
369
|
+
if (args.status === "waived" && !args["waiver-reason"]) problems.push("waived status requires --waiver-reason");
|
|
370
|
+
if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
|
|
371
|
+
for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
372
|
+
if (!args[key]) problems.push("completed isolated/generic status requires --" + key);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (args.status === "completed" && args["dispatch-surface"] === "role-switch" && !args["evidence-ref"]) {
|
|
376
|
+
problems.push("completed role-switch status requires --evidence-ref");
|
|
377
|
+
}
|
|
378
|
+
if (problems.length > 0) {
|
|
379
|
+
if (json) process.stdout.write(JSON.stringify({ ok: false, problems }, null, 2) + "\\n");
|
|
380
|
+
else {
|
|
381
|
+
usage();
|
|
382
|
+
process.stderr.write("[cclaw] delegation-record: " + problems.join("; ") + "\\n");
|
|
383
|
+
}
|
|
384
|
+
process.exitCode = 1;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const root = await detectRoot();
|
|
389
|
+
const now = new Date().toISOString();
|
|
390
|
+
const runId = await readRunId(root);
|
|
391
|
+
if (args.status === "completed" && args["dispatch-surface"] !== "role-switch" && !args["ack-ts"]) {
|
|
392
|
+
const priorEvents = await readDelegationEvents(root);
|
|
393
|
+
if (!hasPriorAck(priorEvents, args, runId)) {
|
|
394
|
+
const ackProblem = "completed isolated/generic status requires prior acknowledged event for same span or --ack-ts";
|
|
395
|
+
if (json) process.stdout.write(JSON.stringify({ ok: false, problems: [ackProblem] }, null, 2) + "\\n");
|
|
396
|
+
else {
|
|
397
|
+
usage();
|
|
398
|
+
process.stderr.write("[cclaw] delegation-record: " + ackProblem + "\\n");
|
|
399
|
+
}
|
|
400
|
+
process.exitCode = 1;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const status = args.status;
|
|
405
|
+
const row = {
|
|
406
|
+
stage: args.stage,
|
|
407
|
+
agent: args.agent,
|
|
408
|
+
mode: args.mode,
|
|
409
|
+
status,
|
|
410
|
+
spanId: args["span-id"],
|
|
411
|
+
dispatchId: args["dispatch-id"],
|
|
412
|
+
workerRunId: args["worker-run-id"],
|
|
413
|
+
dispatchSurface: args["dispatch-surface"],
|
|
414
|
+
agentDefinitionPath: args["agent-definition-path"],
|
|
415
|
+
fulfillmentMode: args["dispatch-surface"] === "role-switch" ? "role-switch" : args["dispatch-surface"] === "cursor-task" || args["dispatch-surface"] === "generic-task" ? "generic-dispatch" : "isolated",
|
|
416
|
+
waiverReason: args["waiver-reason"],
|
|
417
|
+
evidenceRefs: args["evidence-ref"] ? [args["evidence-ref"]] : [],
|
|
418
|
+
runId,
|
|
419
|
+
startTs: now,
|
|
420
|
+
ts: now,
|
|
421
|
+
launchedTs: args["launched-ts"] || (status === "launched" ? now : undefined),
|
|
422
|
+
ackTs: args["ack-ts"] || (status === "acknowledged" ? now : undefined),
|
|
423
|
+
completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
|
|
424
|
+
endTs: TERMINAL.has(status) ? now : undefined,
|
|
425
|
+
schemaVersion: 1
|
|
426
|
+
};
|
|
427
|
+
const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
|
|
428
|
+
const event = { ...clean, event: status, eventTs: now };
|
|
429
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
430
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
431
|
+
await fs.appendFile(path.join(stateDir, "delegation-events.jsonl"), JSON.stringify(event) + "\\n", { encoding: "utf8", mode: 0o600 });
|
|
432
|
+
|
|
433
|
+
const ledgerPath = path.join(stateDir, "delegation-log.json");
|
|
434
|
+
let ledger = { runId, entries: [] };
|
|
435
|
+
try {
|
|
436
|
+
ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
|
|
437
|
+
if (!Array.isArray(ledger.entries)) ledger.entries = [];
|
|
438
|
+
} catch {
|
|
439
|
+
ledger = { runId, entries: [] };
|
|
440
|
+
}
|
|
441
|
+
if (!ledger.entries.some((entry) => entry.spanId === clean.spanId && entry.status === clean.status)) {
|
|
442
|
+
ledger.entries.push(clean);
|
|
443
|
+
ledger.runId = runId;
|
|
444
|
+
await fs.writeFile(ledgerPath, JSON.stringify(ledger, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const active = ledger.entries.filter((entry) => ["scheduled", "launched", "acknowledged"].includes(entry.status));
|
|
448
|
+
await fs.writeFile(path.join(stateDir, "subagents.json"), JSON.stringify({ active, updatedAt: now }, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
|
|
449
|
+
process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
|
|
450
|
+
}
|
|
451
|
+
|
|
263
452
|
void main();
|
|
264
453
|
`;
|
|
265
454
|
}
|
package/dist/content/skills.js
CHANGED
|
@@ -67,14 +67,14 @@ function autoSubagentDispatchBlock(stage, track) {
|
|
|
67
67
|
const mandatory = schema.mandatoryDelegations;
|
|
68
68
|
const mandatoryList = mandatory.length > 0 ? mandatory.map((a) => `\`${a}\``).join(", ") : "none";
|
|
69
69
|
const delegationLogRel = `${RUNTIME_ROOT}/state/delegation-log.json`;
|
|
70
|
+
const delegationEventsRel = `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
|
|
70
71
|
const artifactRef = `${RUNTIME_ROOT}/artifacts/${schema.artifactRules.artifactFile}`;
|
|
71
72
|
return `## Automatic Subagent Dispatch
|
|
72
73
|
| Agent | Mode | Class | Return Schema | User Gate | Trigger | Purpose |
|
|
73
74
|
|---|---|---|---|---|---|---|
|
|
74
75
|
${rows}
|
|
75
|
-
Mandatory: ${mandatoryList}. Record
|
|
76
|
-
### Harness Dispatch Contract
|
|
77
|
-
Use true harness dispatch: Claude native Task, Cursor generic dispatch, OpenCode \`.opencode/agents/<agent>.md\`, Codex \`.codex/agents/<agent>.toml\`. Run independent read-only/review agents in parallel where safe, write evidence into \`${artifactRef}\`, then append \`${delegationLogRel}\` rows with matching \`fulfillmentMode: "isolated"\` or \`"generic-dispatch"\`. Each dispatched worker should have a scheduled row and a terminal row sharing \`spanId\`; stale scheduled spans block completion. Do not collapse OpenCode or Codex to role-switch by default; role-switch is degraded fallback and must carry non-empty \`evidenceRefs\`. Missing evidence blocks completion.
|
|
76
|
+
Mandatory: ${mandatoryList}. Record lifecycle rows in \`${delegationLogRel}\` and append-only \`${delegationEventsRel}\` before completion.
|
|
77
|
+
### Harness Dispatch Contract — use true harness dispatch: Claude Task, Cursor generic dispatch, OpenCode \`.opencode/agents/<agent>.md\` via Task/@agent, Codex \`.codex/agents/<agent>.toml\`. Do not collapse OpenCode or Codex to role-switch by default. Worker ACK Contract: ACK must include \`spanId\`, \`dispatchId\`, \`dispatchSurface\`, \`agentDefinitionPath\`, and \`ackTs\`; never claim \`fulfillmentMode: "isolated"\` without matching lifecycle proof. Helper: \`.cclaw/hooks/delegation-record.mjs --status=<status> --span-id=<spanId> --dispatch-id=<dispatchId> --dispatch-surface=<surface> --agent-definition-path=<path> --json\`. Exact recipe: scheduled -> launched -> acknowledged -> completed with the same span; completed isolated/generic rows require a prior ACK event for that span or \`--ack-ts=<iso>\`.
|
|
78
78
|
`;
|
|
79
79
|
}
|
|
80
80
|
function researchPlaybooksBlock(playbooks) {
|
|
@@ -248,8 +248,9 @@ function completionParametersBlock(schema, track) {
|
|
|
248
248
|
- \`completion helper\`: \`node .cclaw/hooks/stage-complete.mjs ${schema.stage}\`
|
|
249
249
|
- \`completion helper with evidence\`: \`node .cclaw/hooks/stage-complete.mjs ${schema.stage} --evidence-json '{"<gate_id>":"<evidence note>"}' --passed=<gate_id>[,<gate_id>]\`
|
|
250
250
|
- \`completion helper JSON diagnostics\`: append \`--json\` to receive a machine-readable validation failure summary.
|
|
251
|
+
- \`delegation record helper\`: \`node .cclaw/hooks/delegation-record.mjs --stage=${schema.stage} --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<spanId> --dispatch-id=<dispatchId> --dispatch-surface=<surface> --agent-definition-path=<path> --json\`. \`delegation helper recipe\`: call \`--status=scheduled\`, then \`--status=launched\`, then \`--status=acknowledged\`, then \`--status=completed\` with the same \`--span-id\`, \`--dispatch-id\`, \`--dispatch-surface\`, and \`--agent-definition-path\`; completed isolated/generic rows fail unless that same span already has an acknowledged event or the completed call includes \`--ack-ts=<iso>\`. For role-switch fallback, use \`--dispatch-surface=role-switch --evidence-ref=<artifact#anchor>\` instead of pretending isolated completion.
|
|
251
252
|
- Fill \`## Learnings\` before closeout: either \`- None this stage.\` or JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (knowledge-schema compatible).
|
|
252
|
-
- Record mandatory delegation
|
|
253
|
+
- Record mandatory delegation lifecycle in \`${RUNTIME_ROOT}/state/delegation-log.json\` and append proof events to \`${RUNTIME_ROOT}/state/delegation-events.jsonl\`; the ledger is current state, the event log is audit proof.${mandatoryAgents.length > 0 ? ` If a mandatory delegation cannot run in this harness, use \`--waive-delegation=${mandatoryAgents.join(",")} --waiver-reason="<why safe>"\` on the completion helper.` : ""}
|
|
253
254
|
- Never edit raw \`flow-state.json\` to complete a stage, even in advisory mode; that bypasses validation, gate evidence, and Learnings harvest. If the helper fails, stop and report the exact command/output instead of applying a manual state workaround.
|
|
254
255
|
- Completion protocol: verify required gates, update the artifact, then use the completion helper with \`--evidence-json\` and \`--passed\` for every satisfied gate.
|
|
255
256
|
`;
|
|
@@ -5,6 +5,12 @@ function flowStatePath() {
|
|
|
5
5
|
function delegationLogPath() {
|
|
6
6
|
return `${RUNTIME_ROOT}/state/delegation-log.json`;
|
|
7
7
|
}
|
|
8
|
+
function delegationEventsPath() {
|
|
9
|
+
return `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
|
|
10
|
+
}
|
|
11
|
+
function subagentsPath() {
|
|
12
|
+
return `${RUNTIME_ROOT}/state/subagents.json`;
|
|
13
|
+
}
|
|
8
14
|
function knowledgePath() {
|
|
9
15
|
return `${RUNTIME_ROOT}/knowledge.jsonl`;
|
|
10
16
|
}
|
|
@@ -27,7 +33,7 @@ advancing or mutating anything. Safe to run at any point. The snapshot reflects:
|
|
|
27
33
|
- progress across stages with per-stage markers,
|
|
28
34
|
- gate coverage,
|
|
29
35
|
- mandatory delegations with **fulfillmentMode** (isolated / generic-dispatch /
|
|
30
|
-
role-switch)
|
|
36
|
+
role-switch), dispatch proof fields, explicit waived status, and evidence gate,
|
|
31
37
|
- **closeout substate** after ship (retro → compound → archive),
|
|
32
38
|
- **harness parity row** (tier + fallback) for the active harness set.
|
|
33
39
|
|
|
@@ -85,7 +91,7 @@ a read-only command.
|
|
|
85
91
|
\`Current\`, \`Stage\`, \`Gates\`, \`Delegations\`, \`Blocked by\`, \`Next\`, \`Evidence needed\`.
|
|
86
92
|
- When blocked, include a plain-English action block:
|
|
87
93
|
\`Current: <stage or closeout substate>\`; \`Blocked by: <gate/delegation/blocker code>\`; \`Next: <exact command or managed remediation>\`; \`Evidence needed: <artifact/test/review/delegation evidence>\`.
|
|
88
|
-
- Report counts, not full artifact contents.
|
|
94
|
+
- Report counts, not full artifact contents. Include active subagent count from \`${subagentsPath()}\` and proof gaps from \`${delegationEventsPath()}\` when present.
|
|
89
95
|
- If any data source is missing or corrupt, say so explicitly rather than guessing.
|
|
90
96
|
- Include \`/cc-view tree\` for deep structure and \`/cc-view diff\` for before/after map in the final line.
|
|
91
97
|
|
|
@@ -175,6 +175,10 @@ Borrow the good part of Team/Ruflo-style orchestration without adding a swarm ru
|
|
|
175
175
|
- **Checkpoint before synthesis.** Each agent returns status, files inspected/changed, evidence, and blockers before the parent acts.
|
|
176
176
|
- **Consensus is for hard calls only.** Use two reviewers when severity or architecture is disputed; otherwise one evidence-backed reviewer is enough.
|
|
177
177
|
|
|
178
|
+
## Parallelization Decision Gate
|
|
179
|
+
|
|
180
|
+
Before parallel dispatch, answer yes to all gates: tasks are independent, write sets do not overlap, outputs can be reconciled by evidence, and failure in one lane will not invalidate hidden assumptions in another. If any answer is no, serialize. Coder/overseer work is contract-first: the coder implements only the pasted contract, the overseer reads code and verifies acceptance evidence before the controller marks work complete.
|
|
181
|
+
|
|
178
182
|
## When to Use
|
|
179
183
|
|
|
180
184
|
- Mid/large plans with multiple discrete tasks, dependencies, or risky overlap.
|
|
@@ -1013,9 +1017,9 @@ Two patterns (skills under \`.cclaw/skills/\`):
|
|
|
1013
1017
|
- **SDD** (subagent-driven-development): sequential implementer→reviewer loops. Paste self-contained task text; never point subagents at plan files.
|
|
1014
1018
|
- **Parallel Agents** (dispatching-parallel-agents): parallel review/analysis lenses. Never parallelize implementers on same codebase.
|
|
1015
1019
|
|
|
1016
|
-
Status contract: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED. Worker returns must use the strict JSON schemas in \`subagent-driven-development
|
|
1020
|
+
Status contract: ACK first, then DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED. Worker returns must use the strict JSON schemas in \`subagent-driven-development\` and include matching spanId+dispatchId proof.
|
|
1017
1021
|
|
|
1018
|
-
- Controller sequentially dispatches **implementer → reviewer** loops per task.
|
|
1022
|
+
- Controller sequentially dispatches **implementer → reviewer** loops per task and records lifecycle events in \`.cclaw/state/delegation-events.jsonl\`.
|
|
1019
1023
|
- HARD-GATE: paste **self-contained task text**; never point subagents at plan files to “discover” scope.
|
|
1020
1024
|
- **Review fixers** are **fresh agents** after failed review passes — avoids parent-context pollution.
|
|
1021
1025
|
- **Machine-only flow checks auto-dispatch** by stage (design/plan/tdd/review/ship) without asking the user to trigger each specialist manually.
|
|
@@ -5,6 +5,12 @@ function flowStatePath() {
|
|
|
5
5
|
function delegationLogPath() {
|
|
6
6
|
return `${RUNTIME_ROOT}/state/delegation-log.json`;
|
|
7
7
|
}
|
|
8
|
+
function delegationEventsPath() {
|
|
9
|
+
return `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
|
|
10
|
+
}
|
|
11
|
+
function subagentsPath() {
|
|
12
|
+
return `${RUNTIME_ROOT}/state/subagents.json`;
|
|
13
|
+
}
|
|
8
14
|
function artifactsPath() {
|
|
9
15
|
return `${RUNTIME_ROOT}/artifacts`;
|
|
10
16
|
}
|
|
@@ -30,7 +36,7 @@ Do not modify state in this command. It is a pure read/render operation.
|
|
|
30
36
|
- stage marker: passed/current/pending/skipped/stale,
|
|
31
37
|
- gates summary,
|
|
32
38
|
- artifact summary,
|
|
33
|
-
- delegation branch for current stage with fulfillmentMode labels,
|
|
39
|
+
- delegation branch for current stage with fulfillmentMode, dispatchSurface, proof status, and active tracker labels,
|
|
34
40
|
6. When \`closeout.shipSubstate !== "idle"\` or \`currentStage === "ship"\`, add
|
|
35
41
|
a closeout sub-tree:
|
|
36
42
|
- \`retro:\` line derived from \`closeout.retroDraftedAt\` /
|
package/dist/delegation.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type SubagentFallback } from "./harness-adapters.js";
|
|
2
2
|
import type { FlowStage } from "./types.js";
|
|
3
3
|
export type DelegationMode = "mandatory" | "proactive";
|
|
4
|
-
export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
|
|
4
|
+
export type DelegationStatus = "scheduled" | "launched" | "acknowledged" | "completed" | "failed" | "waived" | "stale";
|
|
5
|
+
export type DelegationDispatchSurface = "claude-task" | "cursor-task" | "opencode-agent" | "codex-agent" | "generic-task" | "role-switch" | "manual";
|
|
6
|
+
export type DelegationEventType = DelegationStatus;
|
|
5
7
|
/**
|
|
6
8
|
* How a delegation was actually fulfilled. Advisory — mirrors the harness
|
|
7
9
|
* `subagentFallback` that was in effect when the entry was recorded.
|
|
@@ -53,6 +55,20 @@ export type DelegationEntry = {
|
|
|
53
55
|
retryCount?: number;
|
|
54
56
|
/** Optional references to evidence anchors in artifacts. */
|
|
55
57
|
evidenceRefs?: string[];
|
|
58
|
+
/** Dispatch proof id from the parent/controller side. */
|
|
59
|
+
dispatchId?: string;
|
|
60
|
+
/** Worker-reported run id or task id returned by the harness. */
|
|
61
|
+
workerRunId?: string;
|
|
62
|
+
/** Concrete runtime surface used to launch the worker. */
|
|
63
|
+
dispatchSurface?: DelegationDispatchSurface;
|
|
64
|
+
/** Path to the generated or canonical agent definition used for dispatch. */
|
|
65
|
+
agentDefinitionPath?: string;
|
|
66
|
+
/** ISO timestamp when the worker was acknowledged by the harness/worker. */
|
|
67
|
+
ackTs?: string;
|
|
68
|
+
/** ISO timestamp when the worker was launched. */
|
|
69
|
+
launchedTs?: string;
|
|
70
|
+
/** ISO timestamp when the worker completed. */
|
|
71
|
+
completedTs?: string;
|
|
56
72
|
/** Optional skill marker used for role-specific mandatory checks. */
|
|
57
73
|
skill?: string;
|
|
58
74
|
/**
|
|
@@ -68,6 +84,11 @@ export type DelegationLedger = {
|
|
|
68
84
|
runId: string;
|
|
69
85
|
entries: DelegationEntry[];
|
|
70
86
|
};
|
|
87
|
+
export type DelegationEvent = DelegationEntry & {
|
|
88
|
+
event: DelegationEventType;
|
|
89
|
+
eventTs: string;
|
|
90
|
+
schemaVersion: 1;
|
|
91
|
+
};
|
|
71
92
|
/**
|
|
72
93
|
* Heuristic: does a changed file path strongly imply a trust-boundary
|
|
73
94
|
* surface? Used by tests and prompt guidance for risk-triggered review.
|
|
@@ -79,6 +100,10 @@ export type DelegationLedger = {
|
|
|
79
100
|
*/
|
|
80
101
|
export declare function isTrustBoundaryPath(filePath: string): boolean;
|
|
81
102
|
export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
|
|
103
|
+
export declare function readDelegationEvents(projectRoot: string): Promise<{
|
|
104
|
+
events: DelegationEvent[];
|
|
105
|
+
corruptLines: number[];
|
|
106
|
+
}>;
|
|
82
107
|
export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
|
|
83
108
|
/**
|
|
84
109
|
* Aggregate the fulfillment mode cclaw expects for the active harness set.
|
|
@@ -96,6 +121,12 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
|
|
|
96
121
|
staleIgnored: string[];
|
|
97
122
|
/** Delegation rows missing required evidence under a role-switch fallback. */
|
|
98
123
|
missingEvidence: string[];
|
|
124
|
+
/** Native isolated completion rows that lack dispatch proof. */
|
|
125
|
+
missingDispatchProof: string[];
|
|
126
|
+
/** Legacy inferred isolated completions accepted only as migration warnings. */
|
|
127
|
+
legacyInferredCompletions: string[];
|
|
128
|
+
/** Current-run event log lines that could not be parsed. */
|
|
129
|
+
corruptEventLines: number[];
|
|
99
130
|
/** Current-run scheduled rows with no terminal row sharing the same spanId. */
|
|
100
131
|
staleWorkers: string[];
|
|
101
132
|
/** Expected fulfillment mode for the active harness set. */
|
package/dist/delegation.js
CHANGED
|
@@ -9,13 +9,19 @@ 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
13
|
function delegationLogPath(projectRoot) {
|
|
14
14
|
return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json");
|
|
15
15
|
}
|
|
16
16
|
function delegationLockPath(projectRoot) {
|
|
17
17
|
return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
|
|
18
18
|
}
|
|
19
|
+
function delegationEventsPath(projectRoot) {
|
|
20
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-events.jsonl");
|
|
21
|
+
}
|
|
22
|
+
function subagentsStatePath(projectRoot) {
|
|
23
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", "subagents.json");
|
|
24
|
+
}
|
|
19
25
|
function createSpanId() {
|
|
20
26
|
return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
21
27
|
}
|
|
@@ -131,13 +137,16 @@ function isDelegationEntry(value) {
|
|
|
131
137
|
const o = value;
|
|
132
138
|
const modeOk = o.mode === "mandatory" || o.mode === "proactive";
|
|
133
139
|
const statusOk = o.status === "scheduled" ||
|
|
140
|
+
o.status === "launched" ||
|
|
141
|
+
o.status === "acknowledged" ||
|
|
134
142
|
o.status === "completed" ||
|
|
135
143
|
o.status === "failed" ||
|
|
136
|
-
o.status === "waived"
|
|
144
|
+
o.status === "waived" ||
|
|
145
|
+
o.status === "stale";
|
|
137
146
|
const timestampOk = typeof o.ts === "string" ||
|
|
138
147
|
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;
|
|
148
|
+
const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived" || o.status === "stale";
|
|
149
|
+
const lifecycleOk = (o.status !== "scheduled" && o.status !== "launched" && o.status !== "acknowledged") || o.endTs === undefined;
|
|
141
150
|
const terminalLifecycleOk = !terminalStatus ||
|
|
142
151
|
o.endTs === undefined ||
|
|
143
152
|
typeof o.endTs === "string";
|
|
@@ -168,12 +177,53 @@ function isDelegationEntry(value) {
|
|
|
168
177
|
o.fulfillmentMode === "role-switch" ||
|
|
169
178
|
o.fulfillmentMode === "harness-waiver") &&
|
|
170
179
|
(o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
|
|
180
|
+
(o.dispatchId === undefined || typeof o.dispatchId === "string") &&
|
|
181
|
+
(o.workerRunId === undefined || typeof o.workerRunId === "string") &&
|
|
182
|
+
(o.dispatchSurface === undefined || isDelegationDispatchSurface(o.dispatchSurface)) &&
|
|
183
|
+
(o.agentDefinitionPath === undefined || typeof o.agentDefinitionPath === "string") &&
|
|
184
|
+
(o.ackTs === undefined || typeof o.ackTs === "string") &&
|
|
185
|
+
(o.launchedTs === undefined || typeof o.launchedTs === "string") &&
|
|
186
|
+
(o.completedTs === undefined || typeof o.completedTs === "string") &&
|
|
171
187
|
(o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
|
|
172
188
|
retryOk &&
|
|
173
189
|
(o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
|
|
174
190
|
(o.skill === undefined || typeof o.skill === "string") &&
|
|
175
191
|
(o.schemaVersion === undefined || o.schemaVersion === 1));
|
|
176
192
|
}
|
|
193
|
+
function isDelegationDispatchSurface(value) {
|
|
194
|
+
return (value === "claude-task" ||
|
|
195
|
+
value === "cursor-task" ||
|
|
196
|
+
value === "opencode-agent" ||
|
|
197
|
+
value === "codex-agent" ||
|
|
198
|
+
value === "generic-task" ||
|
|
199
|
+
value === "role-switch" ||
|
|
200
|
+
value === "manual");
|
|
201
|
+
}
|
|
202
|
+
function statusTimestampPatch(entry, ts) {
|
|
203
|
+
const patch = { ...entry };
|
|
204
|
+
if (patch.status === "launched")
|
|
205
|
+
patch.launchedTs = patch.launchedTs ?? ts;
|
|
206
|
+
if (patch.status === "acknowledged")
|
|
207
|
+
patch.ackTs = patch.ackTs ?? ts;
|
|
208
|
+
if (patch.status === "completed")
|
|
209
|
+
patch.completedTs = patch.completedTs ?? patch.endTs ?? ts;
|
|
210
|
+
return patch;
|
|
211
|
+
}
|
|
212
|
+
function eventFromEntry(entry) {
|
|
213
|
+
const eventTs = entry.completedTs ?? entry.ackTs ?? entry.launchedTs ?? entry.endTs ?? entry.startTs ?? entry.ts ?? new Date().toISOString();
|
|
214
|
+
return {
|
|
215
|
+
...entry,
|
|
216
|
+
event: entry.status,
|
|
217
|
+
eventTs,
|
|
218
|
+
schemaVersion: 1
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function isDelegationEvent(value) {
|
|
222
|
+
if (!isDelegationEntry(value))
|
|
223
|
+
return false;
|
|
224
|
+
const o = value;
|
|
225
|
+
return o.event === o.status && typeof o.eventTs === "string";
|
|
226
|
+
}
|
|
177
227
|
function parseLedger(raw, runId) {
|
|
178
228
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
179
229
|
return { runId, entries: [] };
|
|
@@ -195,6 +245,9 @@ function parseLedger(raw, runId) {
|
|
|
195
245
|
startTs: ts,
|
|
196
246
|
endTs: TERMINAL_DELEGATION_STATUSES.has(item.status) ? (item.endTs ?? ts) : undefined,
|
|
197
247
|
ts,
|
|
248
|
+
launchedTs: item.launchedTs ?? (item.status === "launched" ? ts : undefined),
|
|
249
|
+
ackTs: item.ackTs ?? (item.status === "acknowledged" ? ts : undefined),
|
|
250
|
+
completedTs: item.completedTs ?? (item.status === "completed" ? (item.endTs ?? ts) : undefined),
|
|
198
251
|
retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
|
|
199
252
|
? item.retryCount
|
|
200
253
|
: 0,
|
|
@@ -222,6 +275,57 @@ export async function readDelegationLedger(projectRoot) {
|
|
|
222
275
|
return { runId: activeRunId, entries: [] };
|
|
223
276
|
}
|
|
224
277
|
}
|
|
278
|
+
export async function readDelegationEvents(projectRoot) {
|
|
279
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
280
|
+
if (!(await exists(filePath))) {
|
|
281
|
+
return { events: [], corruptLines: [] };
|
|
282
|
+
}
|
|
283
|
+
const events = [];
|
|
284
|
+
const corruptLines = [];
|
|
285
|
+
const text = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
286
|
+
const lines = text.split(/\r?\n/gu);
|
|
287
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
288
|
+
const line = lines[index]?.trim() ?? "";
|
|
289
|
+
if (line.length === 0)
|
|
290
|
+
continue;
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(line);
|
|
293
|
+
if (isDelegationEvent(parsed)) {
|
|
294
|
+
events.push(parsed);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
corruptLines.push(index + 1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
corruptLines.push(index + 1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { events, corruptLines };
|
|
305
|
+
}
|
|
306
|
+
async function appendDelegationEvent(projectRoot, event) {
|
|
307
|
+
const filePath = delegationEventsPath(projectRoot);
|
|
308
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
309
|
+
await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
310
|
+
}
|
|
311
|
+
async function writeSubagentTracker(projectRoot, entries) {
|
|
312
|
+
const active = entries
|
|
313
|
+
.filter((entry) => entry.status === "scheduled" || entry.status === "launched" || entry.status === "acknowledged")
|
|
314
|
+
.map((entry) => ({
|
|
315
|
+
spanId: entry.spanId,
|
|
316
|
+
dispatchId: entry.dispatchId,
|
|
317
|
+
workerRunId: entry.workerRunId,
|
|
318
|
+
stage: entry.stage,
|
|
319
|
+
agent: entry.agent,
|
|
320
|
+
status: entry.status,
|
|
321
|
+
dispatchSurface: entry.dispatchSurface,
|
|
322
|
+
agentDefinitionPath: entry.agentDefinitionPath,
|
|
323
|
+
startedAt: entry.startTs,
|
|
324
|
+
launchedAt: entry.launchedTs,
|
|
325
|
+
acknowledgedAt: entry.ackTs
|
|
326
|
+
}));
|
|
327
|
+
await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
328
|
+
}
|
|
225
329
|
export async function appendDelegation(projectRoot, entry) {
|
|
226
330
|
const { activeRunId } = await readFlowState(projectRoot);
|
|
227
331
|
await withDirectoryLock(delegationLockPath(projectRoot), async () => {
|
|
@@ -231,13 +335,16 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
231
335
|
if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
|
|
232
336
|
throw new Error("waived delegation entries require a non-empty waiverReason");
|
|
233
337
|
}
|
|
234
|
-
const stamped = { ...entry, runId: entry.runId ?? activeRunId };
|
|
338
|
+
const stamped = statusTimestampPatch({ ...entry, runId: entry.runId ?? activeRunId }, startTs);
|
|
235
339
|
stamped.spanId = entry.spanId ?? createSpanId();
|
|
236
340
|
stamped.startTs = startTs;
|
|
237
341
|
stamped.ts = startTs;
|
|
238
342
|
if (TERMINAL_DELEGATION_STATUSES.has(stamped.status) && !stamped.endTs) {
|
|
239
343
|
stamped.endTs = new Date().toISOString();
|
|
240
344
|
}
|
|
345
|
+
if (stamped.status === "completed") {
|
|
346
|
+
stamped.completedTs = stamped.completedTs ?? stamped.endTs ?? new Date().toISOString();
|
|
347
|
+
}
|
|
241
348
|
if (stamped.status === "scheduled") {
|
|
242
349
|
delete stamped.endTs;
|
|
243
350
|
}
|
|
@@ -268,11 +375,13 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
268
375
|
if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
|
|
269
376
|
return;
|
|
270
377
|
}
|
|
378
|
+
await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
|
|
271
379
|
const ledger = {
|
|
272
380
|
runId: activeRunId,
|
|
273
381
|
entries: [...prior.entries, stamped]
|
|
274
382
|
};
|
|
275
383
|
await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
|
|
384
|
+
await writeSubagentTracker(projectRoot, ledger.entries);
|
|
276
385
|
});
|
|
277
386
|
}
|
|
278
387
|
/**
|
|
@@ -299,6 +408,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
299
408
|
const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
|
|
300
409
|
const { activeRunId } = flowState;
|
|
301
410
|
const ledger = await readDelegationLedger(projectRoot);
|
|
411
|
+
const events = await readDelegationEvents(projectRoot);
|
|
302
412
|
const forStage = ledger.entries.filter((e) => e.stage === stage);
|
|
303
413
|
const forRun = forStage.filter((e) => e.runId === activeRunId);
|
|
304
414
|
const staleIgnored = forStage
|
|
@@ -307,6 +417,8 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
307
417
|
const missing = [];
|
|
308
418
|
const waived = [];
|
|
309
419
|
const missingEvidence = [];
|
|
420
|
+
const missingDispatchProof = [];
|
|
421
|
+
const legacyInferredCompletions = [];
|
|
310
422
|
const terminalSpanIds = new Set(forRun
|
|
311
423
|
.filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
|
|
312
424
|
.map((entry) => entry.spanId));
|
|
@@ -342,13 +454,40 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
|
|
|
342
454
|
!completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
|
|
343
455
|
missingEvidence.push(agent);
|
|
344
456
|
}
|
|
457
|
+
for (const row of completedRows) {
|
|
458
|
+
const mode = row.fulfillmentMode ?? "isolated";
|
|
459
|
+
if (mode === "isolated") {
|
|
460
|
+
const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
|
|
461
|
+
event.stage === stage &&
|
|
462
|
+
event.agent === agent &&
|
|
463
|
+
event.spanId === row.spanId);
|
|
464
|
+
const dispatchId = row.dispatchId ?? row.workerRunId ?? spanEvents.find((event) => event.dispatchId || event.workerRunId)?.dispatchId ?? spanEvents.find((event) => event.workerRunId)?.workerRunId;
|
|
465
|
+
const dispatchSurface = row.dispatchSurface ?? spanEvents.find((event) => event.dispatchSurface)?.dispatchSurface;
|
|
466
|
+
const agentDefinitionPath = row.agentDefinitionPath ?? spanEvents.find((event) => event.agentDefinitionPath)?.agentDefinitionPath;
|
|
467
|
+
const hasAck = Boolean(row.ackTs || spanEvents.some((event) => event.event === "acknowledged" && event.ackTs));
|
|
468
|
+
const hasCompleted = Boolean(row.completedTs || spanEvents.some((event) => event.event === "completed" && event.completedTs));
|
|
469
|
+
const hasDispatchProof = Boolean(row.spanId && dispatchId && dispatchSurface && agentDefinitionPath && hasAck && hasCompleted);
|
|
470
|
+
if (!hasDispatchProof) {
|
|
471
|
+
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"));
|
|
472
|
+
if (proofEraSignal) {
|
|
473
|
+
missingDispatchProof.push(agent);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
345
481
|
}
|
|
346
482
|
return {
|
|
347
|
-
satisfied: missing.length === 0 && missingEvidence.length === 0 && staleWorkers.length === 0,
|
|
483
|
+
satisfied: missing.length === 0 && missingEvidence.length === 0 && missingDispatchProof.length === 0 && staleWorkers.length === 0 && events.corruptLines.length === 0,
|
|
348
484
|
missing,
|
|
349
485
|
waived,
|
|
350
486
|
staleIgnored,
|
|
351
487
|
missingEvidence,
|
|
488
|
+
missingDispatchProof,
|
|
489
|
+
legacyInferredCompletions,
|
|
490
|
+
corruptEventLines: events.corruptLines,
|
|
352
491
|
staleWorkers,
|
|
353
492
|
expectedMode
|
|
354
493
|
};
|
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
|
*
|
package/dist/harness-adapters.js
CHANGED
|
@@ -71,6 +71,12 @@ export function harnessShimSkillNames() {
|
|
|
71
71
|
export const HARNESS_ADAPTERS = {
|
|
72
72
|
claude: {
|
|
73
73
|
id: "claude",
|
|
74
|
+
reality: {
|
|
75
|
+
declaredSupport: "full",
|
|
76
|
+
runtimeLaunch: "native Task launch",
|
|
77
|
+
proofRequired: "spanId+dispatchId or workerRunId+ACK for isolated completion",
|
|
78
|
+
proofSource: ".cclaw/state/delegation-events.jsonl plus delegation-log.json"
|
|
79
|
+
},
|
|
74
80
|
commandDir: ".claude/commands",
|
|
75
81
|
shimKind: "command",
|
|
76
82
|
capabilities: {
|
|
@@ -82,6 +88,12 @@ export const HARNESS_ADAPTERS = {
|
|
|
82
88
|
},
|
|
83
89
|
cursor: {
|
|
84
90
|
id: "cursor",
|
|
91
|
+
reality: {
|
|
92
|
+
declaredSupport: "generic",
|
|
93
|
+
runtimeLaunch: "generic Task/Subagent launch with cclaw role prompt",
|
|
94
|
+
proofRequired: "spanId+dispatchId/evidenceRefs for generic-dispatch completion",
|
|
95
|
+
proofSource: ".cclaw/state/delegation-events.jsonl plus artifact evidenceRefs"
|
|
96
|
+
},
|
|
85
97
|
commandDir: ".cursor/commands",
|
|
86
98
|
shimKind: "command",
|
|
87
99
|
capabilities: {
|
|
@@ -97,6 +109,12 @@ export const HARNESS_ADAPTERS = {
|
|
|
97
109
|
},
|
|
98
110
|
opencode: {
|
|
99
111
|
id: "opencode",
|
|
112
|
+
reality: {
|
|
113
|
+
declaredSupport: "full",
|
|
114
|
+
runtimeLaunch: "prompt-level launch via Task or @agent against generated .opencode/agents",
|
|
115
|
+
proofRequired: "spanId+dispatchId+ackTs+completedTs before isolated completion",
|
|
116
|
+
proofSource: ".opencode/agents/<agent>.md and .cclaw/state/delegation-events.jsonl"
|
|
117
|
+
},
|
|
100
118
|
commandDir: ".opencode/commands",
|
|
101
119
|
shimKind: "command",
|
|
102
120
|
capabilities: {
|
|
@@ -119,6 +137,12 @@ export const HARNESS_ADAPTERS = {
|
|
|
119
137
|
},
|
|
120
138
|
codex: {
|
|
121
139
|
id: "codex",
|
|
140
|
+
reality: {
|
|
141
|
+
declaredSupport: "full",
|
|
142
|
+
runtimeLaunch: "prompt-level launch by asking Codex to spawn generated custom agents",
|
|
143
|
+
proofRequired: "spanId+dispatchId+ackTs+completedTs before isolated completion",
|
|
144
|
+
proofSource: ".codex/agents/<agent>.toml and .cclaw/state/delegation-events.jsonl"
|
|
145
|
+
},
|
|
122
146
|
// Codex CLI reads skills from the universal `.agents/skills/` path
|
|
123
147
|
// (OpenAI Codex 0.89, Jan 2026). It does NOT have a native
|
|
124
148
|
// `.codex/commands/*` slash-command discovery — cclaw installs
|
|
@@ -155,9 +179,9 @@ export function harnessDispatchSurface(harnessId) {
|
|
|
155
179
|
case "cursor":
|
|
156
180
|
return "Use Cursor Subagent/Task with a generic subagent_type (explore for read-only mapping, generalPurpose for broader work, shell/browser-use when specifically needed) and paste the cclaw role prompt; record fulfillmentMode: \"generic-dispatch\" with evidenceRefs.";
|
|
157
181
|
case "opencode":
|
|
158
|
-
return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent
|
|
182
|
+
return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent>; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
|
|
159
183
|
case "codex":
|
|
160
|
-
return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name
|
|
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\".";
|
|
161
185
|
}
|
|
162
186
|
}
|
|
163
187
|
export function harnessDispatchFallback(harnessId) {
|
|
@@ -597,7 +621,7 @@ async function cleanupLegacyCodexSurfaces(projectRoot) {
|
|
|
597
621
|
}
|
|
598
622
|
}
|
|
599
623
|
function codexAgentToml(agent) {
|
|
600
|
-
const instructions = `${agent
|
|
624
|
+
const instructions = `${agentMarkdown(agent)}\n\n${enhancedAgentInstruction(agent.name)}`.trim();
|
|
601
625
|
const sandboxMode = agent.tools.some((tool) => ["Write", "Edit", "Bash"].includes(tool))
|
|
602
626
|
? "workspace-write"
|
|
603
627
|
: "read-only";
|
|
@@ -625,7 +649,7 @@ permission:
|
|
|
625
649
|
${agentMarkdown(agent)}`;
|
|
626
650
|
}
|
|
627
651
|
function enhancedAgentInstruction(agentName) {
|
|
628
|
-
return
|
|
652
|
+
return `## Worker ACK Contract\n\nYou are the cclaw ${agentName} subagent. Follow the parent prompt as the task boundary. ACK first with JSON containing spanId, dispatchId or workerRunId, dispatchSurface, agentDefinitionPath, ackTs, and status: "ACK". Finish with the strict return schema plus the same spanId+dispatchId proof so the parent can append .cclaw/state/delegation-events.jsonl and .cclaw/state/delegation-log.json. Do not let the parent claim isolated completion without matching ACK/result proof. Do not recursively orchestrate other agents unless the parent explicitly asks.`;
|
|
629
653
|
}
|
|
630
654
|
async function syncAgentFiles(projectRoot, harnesses) {
|
|
631
655
|
const agentsDir = path.join(projectRoot, RUNTIME_ROOT, "agents");
|
package/dist/install.js
CHANGED
|
@@ -13,7 +13,7 @@ import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-co
|
|
|
13
13
|
import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
|
|
14
14
|
import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
|
|
15
15
|
import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
|
|
16
|
-
import { stageCompleteScript, startFlowScript, runHookCmdScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
16
|
+
import { stageCompleteScript, startFlowScript, runHookCmdScript, delegationRecordScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
|
|
17
17
|
import { nodeHookRuntimeScript } from "./content/node-hooks.js";
|
|
18
18
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
19
19
|
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
@@ -884,6 +884,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
884
884
|
compoundRecurrenceThreshold: config.compound?.recurrenceThreshold
|
|
885
885
|
}));
|
|
886
886
|
await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookCmdScript());
|
|
887
|
+
await writeFileSafe(path.join(hooksDir, "delegation-record.mjs"), delegationRecordScript());
|
|
887
888
|
const opencodePluginSource = opencodePluginJs();
|
|
888
889
|
await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
|
|
889
890
|
try {
|
|
@@ -892,6 +893,7 @@ async function writeHooks(projectRoot, config) {
|
|
|
892
893
|
"start-flow.mjs",
|
|
893
894
|
"run-hook.mjs",
|
|
894
895
|
"run-hook.cmd",
|
|
896
|
+
"delegation-record.mjs",
|
|
895
897
|
"opencode-plugin.mjs"
|
|
896
898
|
]) {
|
|
897
899
|
await fs.chmod(path.join(hooksDir, script), 0o755);
|