cclaw-cli 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/artifact-linter/brainstorm.js +15 -1
  2. package/dist/artifact-linter/design.js +14 -0
  3. package/dist/artifact-linter/scope.js +14 -0
  4. package/dist/artifact-linter/shared.d.ts +1 -0
  5. package/dist/artifact-linter/shared.js +32 -0
  6. package/dist/artifact-linter.js +11 -1
  7. package/dist/content/hook-events.js +1 -2
  8. package/dist/content/hook-manifest.d.ts +3 -4
  9. package/dist/content/hook-manifest.js +22 -25
  10. package/dist/content/hooks.js +54 -14
  11. package/dist/content/meta-skill.js +4 -3
  12. package/dist/content/node-hooks.js +259 -89
  13. package/dist/content/observe.js +3 -3
  14. package/dist/content/opencode-plugin.js +0 -6
  15. package/dist/content/skills-elicitation.d.ts +1 -0
  16. package/dist/content/skills-elicitation.js +123 -0
  17. package/dist/content/skills.js +6 -4
  18. package/dist/content/stages/brainstorm.js +7 -3
  19. package/dist/content/stages/design.js +4 -0
  20. package/dist/content/stages/scope.js +6 -2
  21. package/dist/content/start-command.js +4 -4
  22. package/dist/content/templates.js +21 -0
  23. package/dist/flow-state.d.ts +7 -0
  24. package/dist/flow-state.js +1 -0
  25. package/dist/hook-schemas/claude-hooks.v1.json +2 -3
  26. package/dist/hook-schemas/codex-hooks.v1.json +1 -1
  27. package/dist/hook-schemas/cursor-hooks.v1.json +1 -1
  28. package/dist/install.js +12 -3
  29. package/dist/internal/advance-stage/advance.js +22 -1
  30. package/dist/internal/advance-stage/parsers.d.ts +1 -0
  31. package/dist/internal/advance-stage/parsers.js +6 -0
  32. package/dist/run-persistence.d.ts +1 -1
  33. package/dist/run-persistence.js +29 -2
  34. package/dist/runtime/run-hook.mjs +259 -89
  35. package/dist/track-heuristics.d.ts +7 -1
  36. package/dist/track-heuristics.js +12 -0
  37. package/package.json +1 -1
@@ -1,8 +1,22 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { checkCriticPredictionsContract, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
3
+ import { checkCriticPredictionsContract, sectionBodyByName, validateApproachesTaxonomy, headingLineIndex, meaningfulLineCount, getMarkdownTableRows, parseShortCircuitStatus, validateCalibratedSelfReview, markdownFieldRegex } from "./shared.js";
4
4
  export async function lintBrainstormStage(ctx) {
5
5
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
6
+ const qaLogBody = sectionBodyByName(sections, "Q&A Log");
7
+ const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
8
+ const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
9
+ findings.push({
10
+ section: "qa_log_missing",
11
+ required: false,
12
+ rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
13
+ found: qaLogOk,
14
+ details: qaLogOk
15
+ ? `Q&A Log contains ${qaLogRows.length} data row(s).`
16
+ : qaLogBody === null
17
+ ? "Missing `## Q&A Log` section."
18
+ : "Q&A Log is present but has zero data rows."
19
+ });
6
20
  // Brainstorm Iron Law: "NO ARTIFACT IS COMPLETE WITHOUT AN EXPLICITLY
7
21
  // APPROVED DIRECTION — SILENCE IS NOT APPROVAL." Previously this was
8
22
  // prose-only — nothing failed when the Selected Direction section
@@ -204,6 +204,20 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
204
204
  }
205
205
  export async function lintDesignStage(ctx) {
206
206
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
207
+ const qaLogBody = sectionBodyByName(sections, "Q&A Log");
208
+ const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
209
+ const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
210
+ findings.push({
211
+ section: "qa_log_missing",
212
+ required: false,
213
+ rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
214
+ found: qaLogOk,
215
+ details: qaLogOk
216
+ ? `Q&A Log contains ${qaLogRows.length} data row(s).`
217
+ : qaLogBody === null
218
+ ? "Missing `## Q&A Log` section."
219
+ : "Q&A Log is present but has zero data rows."
220
+ });
207
221
  const criticPredictions = checkCriticPredictionsContract(sections);
208
222
  if (criticPredictions !== null) {
209
223
  findings.push({
@@ -12,6 +12,20 @@ export async function lintScopeStage(ctx) {
12
12
  sectionBodyByName(sections, "Scope Summary") ?? "",
13
13
  lockedDecisionsBody
14
14
  ].join("\n");
15
+ const qaLogBody = sectionBodyByName(sections, "Q&A Log");
16
+ const qaLogRows = qaLogBody ? getMarkdownTableRows(qaLogBody) : [];
17
+ const qaLogOk = qaLogBody !== null && qaLogRows.length > 0;
18
+ findings.push({
19
+ section: "qa_log_missing",
20
+ required: false,
21
+ rule: "[P3] qa_log_missing — Q&A Log empty — confirm you actually had a dialogue with the user, not a draft from memory.",
22
+ found: qaLogOk,
23
+ details: qaLogOk
24
+ ? `Q&A Log contains ${qaLogRows.length} data row(s).`
25
+ : qaLogBody === null
26
+ ? "Missing `## Q&A Log` section."
27
+ : "Q&A Log is present but has zero data rows."
28
+ });
15
29
  const strategistRequired = selectedScopeMode === "SCOPE EXPANSION" || selectedScopeMode === "SELECTIVE EXPANSION";
16
30
  if (strategistRequired) {
17
31
  const delegationLedger = await readDelegationLedger(projectRoot);
@@ -26,6 +26,7 @@ export type H2SectionMap = Map<string, string>;
26
26
  * into multiple passes.
27
27
  */
28
28
  export declare function extractH2Sections(markdown: string): H2SectionMap;
29
+ export declare function duplicateH2Headings(markdown: string): string[];
29
30
  export declare function headingPresent(sections: H2SectionMap, section: string): boolean;
30
31
  export declare function sectionBodyByName(sections: H2SectionMap, section: string): string | null;
31
32
  export declare function sectionBodyByAnyName(sections: H2SectionMap, sectionNames: string[]): string | null;
@@ -57,6 +57,38 @@ export function extractH2Sections(markdown) {
57
57
  flush();
58
58
  return sections;
59
59
  }
60
+ export function duplicateH2Headings(markdown) {
61
+ const lines = markdown.split(/\r?\n/);
62
+ let fenced = null;
63
+ const counts = new Map();
64
+ const displayHeading = new Map();
65
+ for (const line of lines) {
66
+ const fenceMatch = /^(```|~~~)/u.exec(line);
67
+ if (fenceMatch) {
68
+ if (fenced === null) {
69
+ fenced = fenceMatch[1] ?? null;
70
+ }
71
+ else if (line.startsWith(fenced)) {
72
+ fenced = null;
73
+ }
74
+ continue;
75
+ }
76
+ if (fenced !== null)
77
+ continue;
78
+ const match = /^##\s+(.+)$/u.exec(line);
79
+ if (!match)
80
+ continue;
81
+ const heading = normalizeHeadingTitle(match[1] ?? "");
82
+ const key = heading.toLowerCase();
83
+ counts.set(key, (counts.get(key) ?? 0) + 1);
84
+ if (!displayHeading.has(key)) {
85
+ displayHeading.set(key, heading);
86
+ }
87
+ }
88
+ return [...counts.entries()]
89
+ .filter(([, count]) => count > 1)
90
+ .map(([key]) => displayHeading.get(key) ?? key);
91
+ }
60
92
  export function headingPresent(sections, section) {
61
93
  const want = normalizeHeadingTitle(section).toLowerCase();
62
94
  for (const h of sections.keys()) {
@@ -3,7 +3,7 @@ import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-path
3
3
  import { readConfig } from "./config.js";
4
4
  import { exists } from "./fs-utils.js";
5
5
  import { stageSchema } from "./content/stage-schema.js";
6
- import { extractH2Sections, extractLockedDecisionAnchors, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody } from "./artifact-linter/shared.js";
6
+ import { duplicateH2Headings, extractH2Sections, extractLockedDecisionAnchors, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody } from "./artifact-linter/shared.js";
7
7
  import { lintBrainstormStage } from "./artifact-linter/brainstorm.js";
8
8
  import { lintDesignStage } from "./artifact-linter/design.js";
9
9
  import { lintPlanStage } from "./artifact-linter/plan.js";
@@ -48,6 +48,16 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
48
48
  }
49
49
  const raw = await fs.readFile(absFile, "utf8");
50
50
  const sections = extractH2Sections(raw);
51
+ const duplicateHeadings = duplicateH2Headings(raw);
52
+ if (duplicateHeadings.length > 0) {
53
+ findings.push({
54
+ section: "duplicate_h2_heading",
55
+ required: false,
56
+ rule: "[P3] keep each `##` heading unique within an artifact; append updates to the existing section instead of cloning headings.",
57
+ found: false,
58
+ details: `Duplicate H2 heading(s): ${duplicateHeadings.join(", ")}. Merge edits into the existing heading to avoid split contracts.`
59
+ });
60
+ }
51
61
  const projectConfig = await readConfig(projectRoot);
52
62
  const parsedFrontmatter = parseFrontmatter(raw);
53
63
  const frontmatterMissingKeys = FRONTMATTER_REQUIRED_KEYS.filter((key) => {
@@ -11,8 +11,7 @@ const OPENCODE_SEMANTIC_COVERAGE = {
11
11
  pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard",
12
12
  pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard",
13
13
  post_tool_context_monitor: "plugin tool.execute.after -> context-monitor",
14
- stop_handoff: "plugin session.idle -> stop-handoff",
15
- precompact_compat: "plugin session.compacted -> pre-compact"
14
+ stop_handoff: "plugin session.idle -> stop-handoff"
16
15
  };
17
16
  /**
18
17
  * Public semantic coverage map derived from `HOOK_MANIFEST` for
@@ -21,7 +21,7 @@
21
21
  */
22
22
  export declare const HOOK_MANIFEST_HARNESSES: readonly ["claude", "cursor", "codex"];
23
23
  export type HookManifestHarness = (typeof HOOK_MANIFEST_HARNESSES)[number];
24
- export declare const HOOK_HANDLERS: readonly ["session-start", "prompt-guard", "workflow-guard", "context-monitor", "stop-handoff", "pre-compact", "verify-current-state"];
24
+ export declare const HOOK_HANDLERS: readonly ["session-start", "prompt-guard", "workflow-guard", "pre-tool-pipeline", "prompt-pipeline", "context-monitor", "stop-handoff", "verify-current-state"];
25
25
  export type HookHandlerId = (typeof HOOK_HANDLERS)[number];
26
26
  export interface HookBinding {
27
27
  /**
@@ -35,8 +35,7 @@ export interface HookBinding {
35
35
  * Within a single (harness, event) group, entries are sorted by
36
36
  * `priority` ASC, ties broken by manifest-declaration order. Use
37
37
  * this to express "this handler must run BEFORE/AFTER that handler
38
- * on the same event" (e.g. pre-compact must run before session-start
39
- * on cursor `sessionCompact`). Default `0`.
38
+ * on the same event". Default `0`.
40
39
  */
41
40
  priority?: number;
42
41
  }
@@ -50,7 +49,7 @@ export interface HookHandlerSpec {
50
49
  semantic: HookSemanticEvent | null;
51
50
  bindings: Partial<Record<HookManifestHarness, HookBinding[]>>;
52
51
  }
53
- export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_handoff", "precompact_compat", "strict_state_verify"];
52
+ export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_handoff", "strict_state_verify"];
54
53
  export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
55
54
  export declare const HOOK_MANIFEST: readonly HookHandlerSpec[];
56
55
  export interface EventGroup {
@@ -24,9 +24,10 @@ export const HOOK_HANDLERS = [
24
24
  "session-start",
25
25
  "prompt-guard",
26
26
  "workflow-guard",
27
+ "pre-tool-pipeline",
28
+ "prompt-pipeline",
27
29
  "context-monitor",
28
30
  "stop-handoff",
29
- "pre-compact",
30
31
  "verify-current-state"
31
32
  ];
32
33
  export const HOOK_SEMANTIC_EVENTS = [
@@ -35,7 +36,6 @@ export const HOOK_SEMANTIC_EVENTS = [
35
36
  "pre_tool_workflow_guard",
36
37
  "post_tool_context_monitor",
37
38
  "stop_handoff",
38
- "precompact_compat",
39
39
  "strict_state_verify"
40
40
  ];
41
41
  export const HOOK_MANIFEST = [
@@ -59,12 +59,7 @@ export const HOOK_MANIFEST = [
59
59
  description: "Stage-aware prompt gate (iron-laws + strictness).",
60
60
  semantic: "pre_tool_prompt_guard",
61
61
  bindings: {
62
- claude: [{ event: "PreToolUse", matcher: "*" }],
63
- cursor: [{ event: "preToolUse", matcher: "*" }],
64
- codex: [
65
- { event: "UserPromptSubmit" },
66
- { event: "PreToolUse", matcher: "Bash|bash" }
67
- ]
62
+ claude: [{ event: "PreToolUse", matcher: "*" }]
68
63
  }
69
64
  },
70
65
  {
@@ -72,11 +67,26 @@ export const HOOK_MANIFEST = [
72
67
  description: "TDD and workflow gate on Write/Edit/Bash style tool invocations.",
73
68
  semantic: "pre_tool_workflow_guard",
74
69
  bindings: {
75
- claude: [{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash" }],
70
+ claude: [{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash" }]
71
+ }
72
+ },
73
+ {
74
+ handler: "pre-tool-pipeline",
75
+ description: "In-process pre-tool pipeline for harnesses that would otherwise spawn prompt/workflow guards separately.",
76
+ semantic: null,
77
+ bindings: {
76
78
  cursor: [{ event: "preToolUse", matcher: "*" }],
77
79
  codex: [{ event: "PreToolUse", matcher: "Bash|bash" }]
78
80
  }
79
81
  },
82
+ {
83
+ handler: "prompt-pipeline",
84
+ description: "In-process prompt pipeline for Codex UserPromptSubmit (prompt-guard + verify-current-state).",
85
+ semantic: "strict_state_verify",
86
+ bindings: {
87
+ codex: [{ event: "UserPromptSubmit" }]
88
+ }
89
+ },
80
90
  {
81
91
  handler: "context-monitor",
82
92
  description: "Post-tool context usage + stage signal monitor.",
@@ -97,24 +107,11 @@ export const HOOK_MANIFEST = [
97
107
  codex: [{ event: "Stop", timeout: 10 }]
98
108
  }
99
109
  },
100
- {
101
- handler: "pre-compact",
102
- description: "No-op compatibility hook for harness pre-compact events; session-start rehydrates from flow-state, artifacts, and knowledge.",
103
- semantic: "precompact_compat",
104
- bindings: {
105
- claude: [{ event: "PreCompact", matcher: "manual|auto", timeout: 10 }],
106
- // Keep this before session-start on cursor `sessionCompact` so the
107
- // compatibility handler runs before rehydration.
108
- cursor: [{ event: "sessionCompact", priority: -10 }]
109
- }
110
- },
111
110
  {
112
111
  handler: "verify-current-state",
113
- description: "Supplementary Codex strict-mode guard that runs on UserPromptSubmit to assert the live state matches the flow.",
114
- semantic: "strict_state_verify",
115
- bindings: {
116
- codex: [{ event: "UserPromptSubmit" }]
117
- }
112
+ description: "Supplementary strict-mode guard callable from in-process pipelines to assert live state matches flow.",
113
+ semantic: null,
114
+ bindings: {}
118
115
  }
119
116
  ];
120
117
  /** Sanity: every harness in HOOK_MANIFEST_HARNESSES must be a HarnessId. */
@@ -5,6 +5,15 @@ import { RUNTIME_ROOT } from "../constants.js";
5
5
  import { DELEGATION_DISPATCH_SURFACES, DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES } from "../delegation.js";
6
6
  function resolveCliRuntimeForGeneratedHook() {
7
7
  const here = fileURLToPath(import.meta.url);
8
+ // Vitest runs init/sync from src/ and expects helpers to execute the same
9
+ // source runtime, even when a stale dist/ exists in the repository.
10
+ if (process.env.VITEST === "true") {
11
+ const sourceCli = path.resolve(path.dirname(here), "..", "cli.ts");
12
+ const viteNode = path.resolve(path.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
13
+ if (existsSync(sourceCli) && existsSync(viteNode)) {
14
+ return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
15
+ }
16
+ }
8
17
  const candidates = [
9
18
  path.resolve(path.dirname(here), "..", "cli.js"),
10
19
  path.resolve(path.dirname(here), "..", "..", "dist", "cli.js")
@@ -15,15 +24,6 @@ function resolveCliRuntimeForGeneratedHook() {
15
24
  if (existsSync(candidate))
16
25
  return { entrypoint: candidate, argsPrefix: [] };
17
26
  }
18
- // Vitest exercises init/sync directly from src/ without a compiled dist/.
19
- // Route that dev-only shape through vite-node so hooks still prove a local runtime.
20
- if (process.env.VITEST === "true") {
21
- const sourceCli = path.resolve(path.dirname(here), "..", "cli.ts");
22
- const viteNode = path.resolve(path.dirname(here), "..", "..", "node_modules", "vite-node", "vite-node.mjs");
23
- if (existsSync(sourceCli) && existsSync(viteNode)) {
24
- return { entrypoint: viteNode, argsPrefix: ["--script", sourceCli] };
25
- }
26
- }
27
27
  return { entrypoint: null, argsPrefix: [] };
28
28
  }
29
29
  function internalHelperScript(helperName, internalSubcommand, usage, options) {
@@ -42,6 +42,7 @@ const INTERNAL_SUBCOMMAND = ${JSON.stringify(internalSubcommand)};
42
42
  const USAGE = ${JSON.stringify(usage)};
43
43
  const POSITIONAL_ARG_NAME = ${JSON.stringify(options?.positionalArgName ?? null)};
44
44
  const POSITIONAL_ARG_REQUIRED = ${JSON.stringify(options?.positionalArgRequired === true)};
45
+ const DEFAULT_QUIET_ENV_VAR = ${JSON.stringify(options?.defaultQuietEnvVar ?? null)};
45
46
 
46
47
  async function detectRoot() {
47
48
  const candidates = [
@@ -88,6 +89,19 @@ async function main() {
88
89
  }
89
90
  }
90
91
 
92
+ if (DEFAULT_QUIET_ENV_VAR !== null) {
93
+ const envRaw = process.env[DEFAULT_QUIET_ENV_VAR];
94
+ if (typeof envRaw !== "string" || envRaw.trim().length === 0) {
95
+ process.env[DEFAULT_QUIET_ENV_VAR] = "1";
96
+ }
97
+ const quietRaw = (process.env[DEFAULT_QUIET_ENV_VAR] ?? "").trim().toLowerCase();
98
+ const quietEnabled = !/^(0|false|no|off)$/u.test(quietRaw);
99
+ const alreadyQuiet = flags.includes("--quiet");
100
+ if (quietEnabled && !alreadyQuiet) {
101
+ flags = [...flags, "--quiet"];
102
+ }
103
+ }
104
+
91
105
  const root = await detectRoot();
92
106
  const runtimePath = path.join(root, RUNTIME_ROOT);
93
107
  try {
@@ -171,13 +185,13 @@ void main();
171
185
  `;
172
186
  }
173
187
  export function startFlowScript() {
174
- return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]");
188
+ return internalHelperScript("start-flow", "start-flow", "Usage: node " + RUNTIME_ROOT + "/hooks/start-flow.mjs --track=<standard|medium|quick> [--class=...] [--prompt=...] [--stack=...] [--reason=...] [--reclassify] [--force-reset]", { defaultQuietEnvVar: "CCLAW_START_FLOW_QUIET" });
175
189
  }
176
190
  export function cancelRunScript() {
177
191
  return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
178
192
  }
179
193
  export function stageCompleteScript() {
180
- return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason=...] [--json]", {
194
+ return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--accept-proactive-waiver] [--accept-proactive-waiver-reason=...] [--skip-questions] [--json]", {
181
195
  positionalArgName: "stage",
182
196
  positionalArgRequired: true
183
197
  });
@@ -280,7 +294,7 @@ function usage() {
280
294
  process.stderr.write([
281
295
  "Usage:",
282
296
  " 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]",
283
- " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--json]",
297
+ " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
284
298
  "",
285
299
  "Allowed --dispatch-surface values:",
286
300
  " " + VALID_DISPATCH_SURFACES.join(", "),
@@ -322,6 +336,18 @@ async function pathExists(filePath) {
322
336
  }
323
337
  }
324
338
 
339
+ function normalizeEvidenceRefs(args) {
340
+ if (Array.isArray(args["evidence-refs"])) {
341
+ return args["evidence-refs"]
342
+ .filter((ref) => typeof ref === "string" && ref.trim().length > 0)
343
+ .map((ref) => ref.trim());
344
+ }
345
+ if (typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0) {
346
+ return [args["evidence-ref"].trim()];
347
+ }
348
+ return [];
349
+ }
350
+
325
351
  function buildRow(args, status, runId, now) {
326
352
  const fulfillmentMode = args["dispatch-surface"] === "role-switch"
327
353
  ? "role-switch"
@@ -340,7 +366,7 @@ function buildRow(args, status, runId, now) {
340
366
  agentDefinitionPath: args["agent-definition-path"],
341
367
  fulfillmentMode,
342
368
  waiverReason: args["waiver-reason"],
343
- evidenceRefs: args["evidence-ref"] ? [args["evidence-ref"]] : [],
369
+ evidenceRefs: normalizeEvidenceRefs(args),
344
370
  runId,
345
371
  startTs: now,
346
372
  ts: now,
@@ -420,6 +446,18 @@ async function runRerecord(args, json) {
420
446
  emitProblems(["no legacy ledger entry found for --span-id=" + args["span-id"]], json, 1);
421
447
  return;
422
448
  }
449
+ const explicitEvidenceRef =
450
+ typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0
451
+ ? args["evidence-ref"].trim()
452
+ : "";
453
+ const legacyEvidenceRefs = Array.isArray(legacyEntry.evidenceRefs)
454
+ ? legacyEntry.evidenceRefs
455
+ .filter((ref) => typeof ref === "string" && ref.trim().length > 0)
456
+ .map((ref) => ref.trim())
457
+ : [];
458
+ const mergedEvidenceRefs = explicitEvidenceRef.length > 0
459
+ ? [explicitEvidenceRef]
460
+ : legacyEvidenceRefs;
423
461
  if (args["dispatch-surface"] !== "role-switch") {
424
462
  if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
425
463
  const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
@@ -445,7 +483,9 @@ async function runRerecord(args, json) {
445
483
  "agent-definition-path": args["agent-definition-path"],
446
484
  "ack-ts": args["ack-ts"] || legacyEntry.ackTs || now,
447
485
  "completed-ts": args["completed-ts"] || legacyEntry.completedTs || now,
448
- "launched-ts": args["launched-ts"] || legacyEntry.launchedTs || now
486
+ "launched-ts": args["launched-ts"] || legacyEntry.launchedTs || now,
487
+ "evidence-ref": explicitEvidenceRef.length > 0 ? explicitEvidenceRef : undefined,
488
+ "evidence-refs": mergedEvidenceRefs
449
489
  };
450
490
  const status = "completed";
451
491
  const clean = Object.fromEntries(Object.entries(buildRow(merged, status, runId, now)).filter(([, value]) => value !== undefined));
@@ -44,9 +44,10 @@ Substantive vs. non-substantive:
44
44
  - **Non-substantive** (skill load optional): one-line acknowledgement,
45
45
  clarifying a typo, confirming a prior answer, pure conversation.
46
46
 
47
- If the current stage is ambiguous because \`flow-state.json\` is missing
48
- or corrupt, stop and route through \`/cc\` before any substantive
49
- response.
47
+ If \`.cclaw/state/flow-state.json\` is missing, treat it as a normal fresh-init
48
+ state and route through \`/cc <idea>\` to start the first tracked run.
49
+ If the file exists but is corrupt/unreadable, stop and route through \`/cc\`
50
+ before any substantive response.
50
51
 
51
52
  ## Red Flags (stop and re-route)
52
53