cclaw-cli 6.2.0 → 6.3.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.
@@ -191,7 +191,7 @@ export function cancelRunScript() {
191
191
  return internalHelperScript("cancel-run", "cancel-run", "Usage: node " + RUNTIME_ROOT + "/hooks/cancel-run.mjs --reason=<text> [--disposition=<cancelled|abandoned>] [--name=<slug>]");
192
192
  }
193
193
  export function stageCompleteScript() {
194
- return internalHelperScript("stage-complete", "advance-stage", "Usage: node " + RUNTIME_ROOT + "/hooks/stage-complete.mjs <stage> [--passed=...] [--evidence-json=...] [--waive-delegation=...] [--waiver-reason=...] [--skip-questions] [--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=\"<why safe>\"] [--skip-questions] [--json]", {
195
195
  positionalArgName: "stage",
196
196
  positionalArgRequired: true,
197
197
  defaultQuietEnvVar: "CCLAW_STAGE_COMPLETE_QUIET"
@@ -296,6 +296,7 @@ function usage() {
296
296
  "Usage:",
297
297
  " 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]",
298
298
  " 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]",
299
+ " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\"<why>\" [--json]",
299
300
  "",
300
301
  "Allowed --dispatch-surface values:",
301
302
  " " + VALID_DISPATCH_SURFACES.join(", "),
@@ -496,10 +497,161 @@ async function runRerecord(args, json) {
496
497
  process.stdout.write(JSON.stringify({ ok: true, event, rerecord: true }, null, 2) + "\\n");
497
498
  }
498
499
 
500
+ const LIFECYCLE_PHASES = ["scheduled", "launched", "acknowledged", "completed"];
501
+
502
+ function mergeSpanTemplate(spanEvents) {
503
+ const base = {};
504
+ const keys = [
505
+ "stage",
506
+ "agent",
507
+ "mode",
508
+ "runId",
509
+ "dispatchId",
510
+ "dispatchSurface",
511
+ "agentDefinitionPath",
512
+ "workerRunId",
513
+ "fulfillmentMode",
514
+ "schemaVersion",
515
+ "parentSpanId",
516
+ "evidenceRefs",
517
+ "waiverReason"
518
+ ];
519
+ for (const e of spanEvents) {
520
+ if (!e || typeof e !== "object") continue;
521
+ for (const k of keys) {
522
+ if (base[k] === undefined && e[k] !== undefined) {
523
+ base[k] = e[k];
524
+ }
525
+ }
526
+ }
527
+ return base;
528
+ }
529
+
530
+ function repairFulfillmentMode(base) {
531
+ if (base.fulfillmentMode) return base.fulfillmentMode;
532
+ if (base.dispatchSurface === "role-switch") return "role-switch";
533
+ if (base.dispatchSurface === "cursor-task" || base.dispatchSurface === "generic-task") {
534
+ return "generic-dispatch";
535
+ }
536
+ return "isolated";
537
+ }
538
+
539
+ async function runRepair(args, json) {
540
+ const problems = [];
541
+ if (!args["span-id"]) problems.push("repair mode requires --span-id");
542
+ if (!args["repair-reason"] || String(args["repair-reason"]).trim().length === 0) {
543
+ problems.push("repair mode requires --repair-reason=<text>");
544
+ }
545
+ if (problems.length > 0) {
546
+ emitProblems(problems, json, 2);
547
+ return;
548
+ }
549
+ const spanId = args["span-id"];
550
+ const repairedReason = String(args["repair-reason"]).trim();
551
+ const root = await detectRoot();
552
+ const events = await readDelegationEvents(root);
553
+ const spanEvents = events.filter(
554
+ (e) => e && e.spanId === spanId && typeof e.event === "string" && LIFECYCLE_PHASES.includes(e.event)
555
+ );
556
+ if (spanEvents.length === 0) {
557
+ emitProblems(
558
+ ["repair refused: no lifecycle delegation-events.jsonl rows found for --span-id=" + spanId],
559
+ json,
560
+ 2
561
+ );
562
+ return;
563
+ }
564
+ const present = new Set(spanEvents.map((e) => e.event));
565
+ const base = mergeSpanTemplate(spanEvents);
566
+ if (!base.stage || !base.agent || !base.mode) {
567
+ emitProblems(["repair refused: span events missing stage/agent/mode to clone"], json, 2);
568
+ return;
569
+ }
570
+ const runId =
571
+ typeof base.runId === "string" && base.runId.length > 0 ? base.runId : await readRunId(root);
572
+ const fulfillmentMode = repairFulfillmentMode(base);
573
+ const schemaVersion =
574
+ typeof base.schemaVersion === "number" && base.schemaVersion > 0
575
+ ? base.schemaVersion
576
+ : LEDGER_SCHEMA_VERSION;
577
+ const evidenceRefs = Array.isArray(base.evidenceRefs)
578
+ ? base.evidenceRefs.filter((r) => typeof r === "string" && r.trim().length > 0)
579
+ : [];
580
+ const now = new Date().toISOString();
581
+ const appended = [];
582
+
583
+ for (const status of LIFECYCLE_PHASES) {
584
+ if (present.has(status)) continue;
585
+ if (status === "completed" && base.dispatchSurface !== "role-switch") {
586
+ if (!base.dispatchId || !base.dispatchSurface || !base.agentDefinitionPath) {
587
+ emitProblems(
588
+ [
589
+ "repair refused: cannot synthesize completed row without dispatchId, dispatchSurface, and agentDefinitionPath on span " +
590
+ spanId
591
+ ],
592
+ json,
593
+ 2
594
+ );
595
+ return;
596
+ }
597
+ }
598
+ if (status === "completed" && base.dispatchSurface === "role-switch" && evidenceRefs.length === 0) {
599
+ emitProblems(
600
+ ["repair refused: role-switch completed synthesis requires evidenceRefs on span " + spanId],
601
+ json,
602
+ 2
603
+ );
604
+ return;
605
+ }
606
+ const launchedTs =
607
+ status === "launched" || status === "acknowledged" || status === "completed" ? now : undefined;
608
+ const ackTs = status === "acknowledged" || status === "completed" ? now : undefined;
609
+ const completedTs = status === "completed" ? now : undefined;
610
+ const endTs = status === "completed" ? now : undefined;
611
+ const row = {
612
+ stage: base.stage,
613
+ agent: base.agent,
614
+ mode: base.mode,
615
+ status,
616
+ spanId,
617
+ dispatchId: base.dispatchId,
618
+ workerRunId: base.workerRunId,
619
+ dispatchSurface: base.dispatchSurface,
620
+ agentDefinitionPath: base.agentDefinitionPath,
621
+ fulfillmentMode,
622
+ evidenceRefs,
623
+ runId,
624
+ startTs: now,
625
+ ts: now,
626
+ launchedTs,
627
+ ackTs,
628
+ completedTs,
629
+ endTs,
630
+ schemaVersion
631
+ };
632
+ const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
633
+ const event = { ...clean, event: status, eventTs: now, repairedAt: now, repairedReason };
634
+ await persistEntry(root, runId, clean, event);
635
+ present.add(status);
636
+ appended.push(status);
637
+ }
638
+
639
+ if (json) {
640
+ process.stdout.write(
641
+ JSON.stringify({ ok: true, repair: true, spanId, appended, repairedAt: now, repairedReason }, null, 2) + "\\n"
642
+ );
643
+ }
644
+ }
645
+
499
646
  async function main() {
500
647
  const args = parseArgs(process.argv.slice(2));
501
648
  const json = args.json !== undefined;
502
649
 
650
+ if (args.repair) {
651
+ await runRepair(args, json);
652
+ return;
653
+ }
654
+
503
655
  if (args.rerecord) {
504
656
  await runRerecord(args, json);
505
657
  return;
@@ -389,6 +389,7 @@ function completionParametersBlock(schema, track) {
389
389
  - Fill \`## Learnings\` before closeout: either \`- None this stage.\` or JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (knowledge-schema compatible).
390
390
  - 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.` : ""} If proactive delegations were intentionally skipped, rerun only with \`--accept-proactive-waiver\` (optionally \`--accept-proactive-waiver-reason="<why safe>"\`) after explicit user approval.
391
391
  - Never edit raw \`flow-state.json\` to complete a stage, even in advisory mode; that bypasses validation, gate evidence, and Learnings harvest. If a helper fails, report a one-line human-readable failure plus fenced JSON diagnostics; never echo the invoking command line or apply a manual state workaround.
392
+ - Stage completion claim requires \`stage-complete\` exit 0 in the current turn. Quote the success line; do not paraphrase, do not infer success from skipped retries.
392
393
  - Completion protocol: verify required gates, update the artifact, then use the completion helper with \`--evidence-json\` and \`--passed\` for every satisfied gate.
393
394
  `;
394
395
  }
@@ -498,7 +498,8 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
498
498
  runPhase: "post-elicitation",
499
499
  when: "When repository, market, docs, or prior-art context changes the approach set. Runs only after the adaptive elicitation Q&A loop converges.",
500
500
  purpose: "Provide search-before-read summaries and context-readiness evidence before large reads or decisions.",
501
- requiresUserGate: false
501
+ requiresUserGate: false,
502
+ dependsOnInternalRepoSignals: true
502
503
  }
503
504
  ],
504
505
  scope: [
@@ -535,7 +536,8 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
535
536
  runPhase: "post-elicitation",
536
537
  when: "When churn, prior attempts, reference patterns, or external constraints may change scope boundaries. Runs only after the adaptive elicitation Q&A loop converges.",
537
538
  purpose: "Summarize search/context findings before the scope contract locks accepted/rejected/deferred ideas.",
538
- requiresUserGate: false
539
+ requiresUserGate: false,
540
+ dependsOnInternalRepoSignals: true
539
541
  },
540
542
  {
541
543
  agent: "product-discovery",
@@ -53,6 +53,12 @@ export interface StageAutoSubagentDispatch {
53
53
  returnSchema?: StageSubagentReturnSchema;
54
54
  /** Optional skill folder the dispatched agent should load as additional context. */
55
55
  skill?: string;
56
+ /**
57
+ * When true, proactive trace requirements for this row may be skipped on an
58
+ * empty/sparse repo (see `ensureProactiveDelegationTrace`). Used for
59
+ * `researcher` on early elicitation stages in deep discovery mode.
60
+ */
61
+ dependsOnInternalRepoSignals?: boolean;
56
62
  }
57
63
  export type StageComplexityTier = "lightweight" | "standard" | "deep";
58
64
  export interface StagePhilosophy {
@@ -87,6 +87,8 @@ ${conversationLanguagePolicyMarkdown()}
87
87
  > \`Choose discovery mode: Lean / Guided / Deep\`.
88
88
  > \`Lean\` = compact shaping, \`Guided\` = recommended default with enough questions and key specialists before drafting, \`Deep\` = stronger probing and broader specialist/research passes.
89
89
  > Mention the internal track recommendation only as context, not as the primary decision. Offer track override only when reset, contradiction, or reclassification evidence makes the internal recommendation suspect.
90
+ Normalize the user's answer before calling the start helper: \`trim\`, lower-case, map UI labels to \`lean\` / \`guided\` / \`deep\` only; if the answer is not one of those three, re-ask the same question once with the compact definitions. In \`flow-state.json\`, persist only the canonical lowercase token (never \`Deep\`/\`Guided\` casing).
91
+ If the user prompt is one short line (at most 12 words) and the workspace matches an empty-repo signal set — either \`flow-state.repoSignals\` from the last successful \`start-flow\` shows \`fileCount < 5\` with \`hasReadme\` and \`hasPackageManifest\` both false, OR (before any \`start-flow\` yet) a shallow scan finds no root \`README.md\`, no root \`package.json\`/\`pyproject.toml\`/\`Cargo.toml\`, and fewer than five relevant files excluding \`node_modules\`/\`.git\` — recommend \`guided\` and ask for explicit confirmation before defaulting to \`deep\`.
90
92
  If the harness's native ask tool is available (\`AskUserQuestion\` / \`AskQuestion\` / \`question\` / \`request_user_input\`), send exactly ONE question; on schema error, fall back to a plain-text lettered list.
91
93
  10. Start the tracked flow only through the managed helper:
92
94
  \`node .cclaw/hooks/start-flow.mjs --track=<quick|medium|standard> --discovery-mode=<lean|guided|deep> --class=<class> --prompt=<prompt> --stack=<stack> --reason=<matched heuristic>\`
@@ -188,8 +190,10 @@ ${conversationLanguagePolicyMarkdown()}
188
190
  - On conflict, prefer \`standard\` over \`medium\`, and \`medium\` over \`quick\`.
189
191
  - Always state the recommendation as a one-line reason citing matched triggers and a high/medium/low track selection confidence. Clarify that the heuristic is advisory until the managed helper writes state; after that, \`/cc\` follows the selected track. Include override guidance: switch to standard when architecture, schema, migration, security, or unclear scope appears; switch to medium when product framing is needed but architecture is known.
190
192
  8. Ask for the single explicit start choice: \`Lean / Guided / Deep\`. Use \`Guided\` as the recommended default unless the user clearly wants compact shaping or unusually deep probing. Keep track internal unless contradiction/reset/reclassification requires surfacing an override.
193
+ Normalize the answer (\`trim\`, lower-case) to exactly \`lean\` / \`guided\` / \`deep\` before invoking the start helper; re-ask once if the reply is not one of those. Pass only canonical lowercase tokens to \`--discovery-mode\`.
194
+ If the prompt is one short line (at most 12 words) and the workspace matches an empty-repo signal set — either persisted \`repoSignals\` has \`fileCount < 5\` with \`hasReadme\` and \`hasPackageManifest\` false, OR a shallow scan before the first \`start-flow\` shows the same — recommend \`guided\` and confirm before defaulting to \`deep\`.
191
195
  9. Run the managed start helper: \`node .cclaw/hooks/start-flow.mjs --track=<quick|medium|standard> --discovery-mode=<lean|guided|deep> --class=<class> --prompt=<prompt> --stack=<stack> --reason=<matched heuristic>\`. The helper writes \`${flowPath}\`, including \`discoveryMode\`, computes \`skippedStages\`, resets the gate catalog, and writes \`${RUNTIME_ROOT}/artifacts/00-idea.md\`. If it fails, STOP, report one human-readable failure line from the JSON \`error\` field, and include the helper JSON payload in a fenced \`json\` block; do not echo the invoking command line, and do not manually edit flow state.
192
- 9. Load and execute the **first stage skill of the chosen track** (\`brainstorm\` for medium/standard, \`spec\` for quick) plus its matching command file.
196
+ 10. Load and execute the **first stage skill of the chosen track** (\`brainstorm\` for medium/standard, \`spec\` for quick) plus its matching command file.
193
197
 
194
198
  ### Reclassification on discovery
195
199
 
@@ -7,5 +7,5 @@ export declare const RULEBOOK_MARKDOWN = "# Cclaw Rulebook\n\n## MUST_ALWAYS\n-
7
7
  * (premature draft, premature subagent dispatch, command-line echo to chat).
8
8
  */
9
9
  export declare const CURSOR_GUIDELINES_RULE_MDC = "---\ndescription: cclaw zero-install behavior baseline (always-on)\nglobs:\n - \"**/*\"\nalwaysApply: true\n---\n\n<!-- cclaw-managed-cursor-guidelines-rule -->\n\n# Cclaw Baseline Guidelines\n\nThese three rules apply to every Cursor agent session in this project,\nregardless of whether stage skills loaded.\n\n## 1. Q&A floor before drafting (brainstorm/scope/design)\n\nBefore drafting any `.cclaw/artifacts/01-brainstorm-*.md`,\n`02-scope-*.md`, or `03-design-*.md`, verify that the artifact's\n`## Q&A Log` table demonstrates Ralph-Loop convergence: every\nforcing-question topic id is tagged `[topic:<id>]` on at least one row\n(see the stage's forcing-questions checklist for the id list), the last\n2 turns produce no new decision-changing impact, OR an explicit user\nstop-signal row is recorded. Walk the stage forcing questions one at a\ntime via the `AskQuestion` tool. If you find yourself proposing a\ndraft after 1-2 questions while forcing topic ids remain untagged, STOP\nand continue the loop.\n\nThe `qa_log_unconverged` linter rule will block `stage-complete` when\nconvergence has not been reached. Wave 24 (v6.0.0) made `[topic:<id>]`\ntagging mandatory; the English keyword fallback was removed because it\nmis-reported convergence on RU/UA Q&A logs.\n\n## 2. Mandatory subagents run after Q&A approval\n\nFor brainstorm / scope / design, mandatory subagents (\n`product-discovery`, `critic`, `planner`, `architect`,\n`test-author`) run **only AFTER the user approves the elicitation\noutcome**, never before the Q&A loop converges. Dispatching them early\npreempts the user dialogue and violates the elicitation contract \u2014 the\nlinter will block stage-complete.\n\nSee each stage's \"Run Phase: post-elicitation\" rows in the materialized\nAutomatic Subagent Dispatch table.\n\n## 3. Never echo cclaw command lines to chat\n\nThe user does not run cclaw helpers (`node .cclaw/hooks/...`) manually.\nNEVER paste full command lines, `--evidence-json '{...}'` payloads,\n`--waive-delegation=...`, or shell hash commands (`shasum`,\n`sha256sum`, `Get-FileHash`, `certutil`, etc.) into chat. Run the\nhelper via the tool layer and report only the resulting summary. On\nfailure, report a compact human-readable summary plus the helper JSON in\na single fenced `json` block.\n";
10
- export declare const CURSOR_WORKFLOW_RULE_MDC = "---\ndescription: cclaw workflow guardrails for Cursor agent sessions\nglobs:\n - \"**/*\"\nalwaysApply: true\n---\n\n<!-- cclaw-managed-cursor-workflow-rule -->\n\n# Cclaw Workflow Guardrails\n\n## Activation Rule\n\nBefore responding to coding work:\n1. Read `.cclaw/state/flow-state.json`.\n2. Start with `/cc` or continue with `/cc`.\n3. If no software-stage flow applies, respond normally.\n\n## Stage Order\n\n`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship`\n\nTrack-specific skips are allowed only when `flow-state.track` + `skippedStages` explicitly say so.\n\n## Task Classification\n\n| Class | Route |\n|---|---|\n| non-trivial software work | `/cc <idea>` |\n| trivial software fix | `/cc <idea>` (quick track) |\n| bugfix with repro | `/cc <idea>` and enforce RED-first in tdd |\n| pure question / non-software | direct answer (no stage flow) |\n\n## Command Surface\n\n- `/cc` = entry and resume.\n- `/cc` = only progression path.\n- Knowledge capture and recall use the `learnings` skill when requested.\n\n## Verification Discipline\n\n- No completion claim without fresh command evidence in this turn.\n- Do not mark gates passed from memory.\n- Keep evidence in `.cclaw/artifacts/`; archive through closeout via `/cc` or cancel early via `node .cclaw/hooks/cancel-run.mjs`.\n\n## Delegation And Approvals\n\n- Machine-only checks in design/plan/tdd/review/ship should auto-dispatch when tooling supports it.\n- **For brainstorm / scope / design stages**: ask user input continuously via adaptive elicitation (one question per turn through the harness-native question tool \u2014 `AskQuestion` in Cursor). Walk the stage forcing-questions list one-by-one. **Tag each Q&A Log row's `Decision impact` cell with `[topic:<id>]`** (the id is given in the stage's forcing-questions checklist) so the linter can verify coverage in any natural language. Do NOT batch and do NOT defer to a single approval gate at the end. The `qa_log_unconverged` linter rule will block `stage-complete` when convergence is not reached (forcing topic ids untagged AND last 2 turns still produce decision-changing rows AND no stop-signal).\n- **For other stages** (spec/plan/tdd/build/review/ship): ask user input only at explicit approval gates (scope mode, plan approval, challenge resolution, ship finalization), not for routine progress updates.\n- If you find yourself proposing a draft after 1-2 questions in brainstorm/scope/design, STOP \u2014 go back to the forcing-questions list and continue.\n- Mandatory subagents in brainstorm/scope/design run only AFTER the user approves the elicitation outcome (see each stage's \"Run Phase: post-elicitation\" rows). Dispatching them before the Q&A loop converges violates the contract.\n- Never echo cclaw command lines (`node .cclaw/hooks/...`, `--evidence-json '{...}'`) to chat \u2014 the user does not run cclaw manually. Run helpers via the tool layer; report only the resulting summary.\n- If harness capabilities are partial, record waiver reasons in delegation logs.\n\n## Routing Source Of Truth\n\n- Primary router: `.cclaw/skills/using-cclaw/SKILL.md`.\n- Stage behavior: current stage skill plus `.cclaw/state/flow-state.json`.\n- Preamble budget: keep role/status announcements brief and avoid repeating\n them unless the stage or role changes.\n";
10
+ export declare const CURSOR_WORKFLOW_RULE_MDC = "---\ndescription: cclaw workflow guardrails for Cursor agent sessions\nglobs:\n - \"**/*\"\nalwaysApply: true\n---\n\n<!-- cclaw-managed-cursor-workflow-rule -->\n\n# Cclaw Workflow Guardrails\n\n## Activation Rule\n\nBefore responding to coding work:\n1. Read `.cclaw/state/flow-state.json`.\n2. Start with `/cc` or continue with `/cc`.\n3. If no software-stage flow applies, respond normally.\n\n## Stage Order\n\n`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship`\n\nTrack-specific skips are allowed only when `flow-state.track` + `skippedStages` explicitly say so.\n\n## Task Classification\n\n| Class | Route |\n|---|---|\n| non-trivial software work | `/cc <idea>` |\n| trivial software fix | `/cc <idea>` (quick track) |\n| bugfix with repro | `/cc <idea>` and enforce RED-first in tdd |\n| pure question / non-software | direct answer (no stage flow) |\n\n## Command Surface\n\n- `/cc` = entry and resume.\n- `/cc` = only progression path.\n- Knowledge capture and recall use the `learnings` skill when requested.\n\n## Verification Discipline\n\n- No completion claim without fresh command evidence in this turn.\n- Stage completion claim requires `stage-complete` exit 0 in the current turn. Quote the success line; do not paraphrase, do not infer success from skipped retries.\n- Do not mark gates passed from memory.\n- Keep evidence in `.cclaw/artifacts/`; archive through closeout via `/cc` or cancel early via `node .cclaw/hooks/cancel-run.mjs`.\n\n## Delegation And Approvals\n\n- Machine-only checks in design/plan/tdd/review/ship should auto-dispatch when tooling supports it.\n- **For brainstorm / scope / design stages**: ask user input continuously via adaptive elicitation (one question per turn through the harness-native question tool \u2014 `AskQuestion` in Cursor). Walk the stage forcing-questions list one-by-one. **Tag each Q&A Log row's `Decision impact` cell with `[topic:<id>]`** (the id is given in the stage's forcing-questions checklist) so the linter can verify coverage in any natural language. Do NOT batch and do NOT defer to a single approval gate at the end. The `qa_log_unconverged` linter rule will block `stage-complete` when convergence is not reached (forcing topic ids untagged AND last 2 turns still produce decision-changing rows AND no stop-signal).\n- **For other stages** (spec/plan/tdd/build/review/ship): ask user input only at explicit approval gates (scope mode, plan approval, challenge resolution, ship finalization), not for routine progress updates.\n- If you find yourself proposing a draft after 1-2 questions in brainstorm/scope/design, STOP \u2014 go back to the forcing-questions list and continue.\n- Mandatory subagents in brainstorm/scope/design run only AFTER the user approves the elicitation outcome (see each stage's \"Run Phase: post-elicitation\" rows). Dispatching them before the Q&A loop converges violates the contract.\n- Never echo cclaw command lines (`node .cclaw/hooks/...`, `--evidence-json '{...}'`) to chat \u2014 the user does not run cclaw manually. Run helpers via the tool layer; report only the resulting summary.\n- If harness capabilities are partial, record waiver reasons in delegation logs.\n\n## Routing Source Of Truth\n\n- Primary router: `.cclaw/skills/using-cclaw/SKILL.md`.\n- Stage behavior: current stage skill plus `.cclaw/state/flow-state.json`.\n- Preamble budget: keep role/status announcements brief and avoid repeating\n them unless the stage or role changes.\n";
11
11
  export declare function buildRulesJson(): Record<string, unknown>;
@@ -1565,6 +1565,7 @@ Track-specific skips are allowed only when \`flow-state.track\` + \`skippedStage
1565
1565
  ## Verification Discipline
1566
1566
 
1567
1567
  - No completion claim without fresh command evidence in this turn.
1568
+ - Stage completion claim requires \`stage-complete\` exit 0 in the current turn. Quote the success line; do not paraphrase, do not infer success from skipped retries.
1568
1569
  - Do not mark gates passed from memory.
1569
1570
  - Keep evidence in \`.cclaw/artifacts/\`; archive through closeout via \`/cc\` or cancel early via \`node .cclaw/hooks/cancel-run.mjs\`.
1570
1571
 
@@ -1,6 +1,13 @@
1
1
  import type { DiscoveryMode, FlowStage, FlowTrack, TransitionRule } from "./types.js";
2
2
  export declare const TRANSITION_RULES: TransitionRule[];
3
3
  export declare const FLOW_STATE_SCHEMA_VERSION = 1;
4
+ /** Snapshot from `collectRepoSignals` at last successful `start-flow` (optional on older states). */
5
+ export interface RepoSignals {
6
+ fileCount: number;
7
+ hasReadme: boolean;
8
+ hasPackageManifest: boolean;
9
+ capturedAt: string;
10
+ }
4
11
  export interface StageGateState {
5
12
  required: string[];
6
13
  recommended: string[];
@@ -104,6 +111,8 @@ export interface FlowState {
104
111
  retro: RetroState;
105
112
  /** Ship → post_ship_review → archive substate for resumable closeout. */
106
113
  closeout: CloseoutState;
114
+ /** Repo shape signals captured at last successful start-flow (omit on legacy files). */
115
+ repoSignals?: RepoSignals;
107
116
  }
108
117
  export interface StageInteractionHint {
109
118
  skipQuestions?: boolean;
@@ -434,6 +434,19 @@ export async function runAdvanceStage(projectRoot, args, io) {
434
434
  extraStageFlags: args.skipQuestions ? ["--skip-questions"] : undefined
435
435
  });
436
436
  if (!validation.ok) {
437
+ const delegationFailureCount = validation.delegation.missing.length +
438
+ validation.delegation.missingEvidence.length +
439
+ validation.delegation.missingDispatchProof.length +
440
+ validation.delegation.legacyInferredCompletions.length +
441
+ validation.delegation.corruptEventLines.length +
442
+ validation.delegation.staleWorkers.length;
443
+ const gatesFailureCount = validation.gates.issues.length;
444
+ const closureFailureCount = validation.completedStages.issues.length;
445
+ const failureCounts = {
446
+ delegation: delegationFailureCount,
447
+ gates: gatesFailureCount,
448
+ closure: closureFailureCount
449
+ };
437
450
  const ledgerForDiag = await readDelegationLedger(projectRoot).catch(() => ({ entries: [] }));
438
451
  const eventsForDiag = await readDelegationEvents(projectRoot).catch(() => ({ events: [], corruptLines: [] }));
439
452
  const ledgerEntriesText = await fs.readFile(path.join(projectRoot, ".cclaw/state/delegation-events.jsonl"), "utf8").catch(() => "");
@@ -482,6 +495,7 @@ export async function runAdvanceStage(projectRoot, args, io) {
482
495
  command: "advance-stage",
483
496
  stage: args.stage,
484
497
  kind: "validation-failed",
498
+ failureCounts,
485
499
  delegation: validation.delegation,
486
500
  gates: validation.gates,
487
501
  completedStages: validation.completedStages,
@@ -493,7 +507,7 @@ export async function runAdvanceStage(projectRoot, args, io) {
493
507
  nextActions
494
508
  })}\n`);
495
509
  }
496
- io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}".\n`);
510
+ io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}" (delegation=${failureCounts.delegation}, gates=${failureCounts.gates}, closure=${failureCounts.closure}).\n`);
497
511
  if (validation.delegation.missing.length > 0) {
498
512
  io.stderr.write(`- missing delegations: ${validation.delegation.missing.join(", ")}\n`);
499
513
  io.stderr.write(` next action: run the named agent(s) for this stage, or rerun with --waive-delegation=${validation.delegation.missing.join(",")} --waiver-reason="<why safe>" only when the user accepts the safety trade-off.\n`);
@@ -531,7 +545,8 @@ export async function runAdvanceStage(projectRoot, args, io) {
531
545
  const proactiveTrace = await ensureProactiveDelegationTrace(projectRoot, args.stage, {
532
546
  acceptWaiver: args.acceptProactiveWaiver,
533
547
  waiverReason: args.acceptProactiveWaiverReason,
534
- discoveryMode: flowState.discoveryMode
548
+ discoveryMode: flowState.discoveryMode,
549
+ repoSignals: flowState.repoSignals
535
550
  });
536
551
  if (proactiveTrace.missingRules.length > 0) {
537
552
  const missingSummary = proactiveTrace.missingRules
@@ -248,10 +248,11 @@ export function parseStartFlowArgs(tokens) {
248
248
  }
249
249
  if (token === "--discovery-mode" || token.startsWith("--discovery-mode=")) {
250
250
  const raw = readValue("--discovery-mode").trim();
251
- if (!isDiscoveryMode(raw)) {
251
+ const normalized = raw.toLowerCase();
252
+ if (!isDiscoveryMode(normalized)) {
252
253
  throw new Error(`--discovery-mode must be one of: lean, guided, deep.`);
253
254
  }
254
- discoveryMode = raw;
255
+ discoveryMode = normalized;
255
256
  continue;
256
257
  }
257
258
  if (token === "--class" || token.startsWith("--class=")) {
@@ -1,5 +1,6 @@
1
1
  import { type StageAutoSubagentDispatch } from "../../content/stage-schema.js";
2
2
  import type { DiscoveryMode, FlowStage } from "../../types.js";
3
+ import type { RepoSignals } from "../../flow-state.js";
3
4
  export interface ProactiveDelegationTraceResult {
4
5
  missingRules: StageAutoSubagentDispatch[];
5
6
  }
@@ -16,4 +17,5 @@ export declare function ensureProactiveDelegationTrace(projectRoot: string, stag
16
17
  acceptWaiver: boolean;
17
18
  waiverReason?: string;
18
19
  discoveryMode: DiscoveryMode;
20
+ repoSignals?: RepoSignals;
19
21
  }): Promise<ProactiveDelegationTraceResult>;
@@ -3,6 +3,20 @@ import { stageAutoSubagentDispatch } from "../../content/stage-schema.js";
3
3
  function isEarlyElicitationStage(stage) {
4
4
  return stage === "brainstorm" || stage === "scope" || stage === "design";
5
5
  }
6
+ function isSparseRepoForResearcherSkip(repoSignals) {
7
+ if (!repoSignals)
8
+ return false;
9
+ return repoSignals.fileCount < 5 && !repoSignals.hasReadme && !repoSignals.hasPackageManifest;
10
+ }
11
+ function skipRepoDependentProactiveRule(rule, stage, discoveryMode, repoSignals) {
12
+ if (discoveryMode !== "deep")
13
+ return false;
14
+ if (stage !== "brainstorm" && stage !== "scope")
15
+ return false;
16
+ if (!rule.dependsOnInternalRepoSignals)
17
+ return false;
18
+ return isSparseRepoForResearcherSkip(repoSignals);
19
+ }
6
20
  /**
7
21
  * Ensure every proactive dispatch rule for the stage has a ledger row for the
8
22
  * active run, or an explicit user-flag waiver.
@@ -16,7 +30,9 @@ export async function ensureProactiveDelegationTrace(projectRoot, stage, options
16
30
  if (isEarlyElicitationStage(stage) && (options.discoveryMode === "lean" || options.discoveryMode === "guided")) {
17
31
  return { missingRules: [] };
18
32
  }
19
- const proactiveRules = stageAutoSubagentDispatch(stage).filter((rule) => rule.mode === "proactive");
33
+ const proactiveRules = stageAutoSubagentDispatch(stage)
34
+ .filter((rule) => rule.mode === "proactive")
35
+ .filter((rule) => !skipRepoDependentProactiveRule(rule, stage, options.discoveryMode, options.repoSignals));
20
36
  if (proactiveRules.length === 0)
21
37
  return { missingRules: [] };
22
38
  const ledger = await readDelegationLedger(projectRoot);
@@ -1,10 +1,12 @@
1
- import { type FlowState } from "../../flow-state.js";
1
+ import { type FlowState, type RepoSignals } from "../../flow-state.js";
2
2
  import type { StartFlowArgs } from "./parsers.js";
3
3
  import type { Writable } from "node:stream";
4
4
  interface InternalIo {
5
5
  stdout: Writable;
6
6
  stderr: Writable;
7
7
  }
8
+ /** One-pass repo snapshot (max ~200 files, skips `node_modules`/`.git`). */
9
+ export declare function collectRepoSignals(projectRoot: string): Promise<RepoSignals>;
8
10
  export declare function discoverStartFlowContext(projectRoot: string): Promise<string[]>;
9
11
  export declare function appendIdeaArtifact(projectRoot: string, args: StartFlowArgs, previous?: FlowState): Promise<void>;
10
12
  export declare function runStartFlow(projectRoot: string, args: StartFlowArgs, io: InternalIo): Promise<number>;
@@ -13,6 +13,68 @@ function resolveTaskClass(className, fallback) {
13
13
  }
14
14
  return fallback;
15
15
  }
16
+ const REPO_SIGNAL_SKIP_DIRS = new Set(["node_modules", ".git"]);
17
+ /** One-pass repo snapshot (max ~200 files, skips `node_modules`/`.git`). */
18
+ export async function collectRepoSignals(projectRoot) {
19
+ const capturedAt = new Date().toISOString();
20
+ const cap = 200;
21
+ let fileCount = 0;
22
+ async function visit(absDir, depth) {
23
+ if (fileCount >= cap)
24
+ return;
25
+ let entries;
26
+ try {
27
+ entries = await fs.readdir(absDir, { withFileTypes: true });
28
+ }
29
+ catch {
30
+ return;
31
+ }
32
+ for (const ent of entries) {
33
+ if (fileCount >= cap)
34
+ return;
35
+ const name = ent.name;
36
+ if (REPO_SIGNAL_SKIP_DIRS.has(name))
37
+ continue;
38
+ const abs = path.join(absDir, name);
39
+ if (ent.isFile()) {
40
+ fileCount += 1;
41
+ continue;
42
+ }
43
+ if (ent.isDirectory() && depth < 1) {
44
+ await visit(abs, depth + 1);
45
+ }
46
+ }
47
+ }
48
+ let hasReadme = false;
49
+ let hasPackageManifest = false;
50
+ for (const fname of ["README.md", "readme.md", "Readme.md"]) {
51
+ try {
52
+ const st = await fs.stat(path.join(projectRoot, fname));
53
+ if (st.isFile())
54
+ hasReadme = true;
55
+ }
56
+ catch {
57
+ // ignore
58
+ }
59
+ }
60
+ for (const manifest of ["package.json", "pyproject.toml", "Cargo.toml"]) {
61
+ try {
62
+ const st = await fs.stat(path.join(projectRoot, manifest));
63
+ if (st.isFile())
64
+ hasPackageManifest = true;
65
+ }
66
+ catch {
67
+ // ignore
68
+ }
69
+ }
70
+ try {
71
+ await visit(projectRoot, 0);
72
+ }
73
+ catch {
74
+ fileCount = Math.min(fileCount, cap);
75
+ }
76
+ return { fileCount, hasReadme, hasPackageManifest, capturedAt };
77
+ }
16
78
  export async function discoverStartFlowContext(projectRoot) {
17
79
  const lines = [];
18
80
  const seedFiles = (await listFilesUnder(projectRoot, path.join(RUNTIME_ROOT, "seeds"), 10))
@@ -145,6 +207,8 @@ export async function runStartFlow(projectRoot, args, io) {
145
207
  }
146
208
  };
147
209
  }
210
+ const repoSignals = await collectRepoSignals(projectRoot);
211
+ nextState = { ...nextState, repoSignals };
148
212
  await writeFlowState(projectRoot, nextState, { allowReset: true });
149
213
  await appendIdeaArtifact(projectRoot, args, current);
150
214
  if (!args.quiet) {
@@ -163,7 +163,32 @@ function coerceTrack(value) {
163
163
  return isFlowTrack(value) ? value : "standard";
164
164
  }
165
165
  function coerceDiscoveryMode(value) {
166
- return isDiscoveryMode(value) ? value : "guided";
166
+ if (typeof value === "string") {
167
+ const normalized = value.trim().toLowerCase();
168
+ if (isDiscoveryMode(normalized))
169
+ return normalized;
170
+ }
171
+ return "guided";
172
+ }
173
+ function coerceRepoSignals(value) {
174
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
175
+ return undefined;
176
+ }
177
+ const typed = value;
178
+ const fileCountRaw = typed.fileCount;
179
+ const fileCount = typeof fileCountRaw === "number" && Number.isFinite(fileCountRaw) && fileCountRaw >= 0
180
+ ? Math.min(Math.floor(fileCountRaw), 1_000_000)
181
+ : undefined;
182
+ const capturedAt = typeof typed.capturedAt === "string" ? typed.capturedAt.trim() : "";
183
+ if (fileCount === undefined || !capturedAt) {
184
+ return undefined;
185
+ }
186
+ return {
187
+ fileCount,
188
+ hasReadme: typed.hasReadme === true,
189
+ hasPackageManifest: typed.hasPackageManifest === true,
190
+ capturedAt
191
+ };
167
192
  }
168
193
  /**
169
194
  * Wave 24 follow-up (v6.1.1) — preserve `flow-state.json#taskClass`
@@ -385,6 +410,7 @@ function coerceFlowState(parsed) {
385
410
  ? activeRunIdRaw.trim()
386
411
  : next.activeRunId;
387
412
  const taskClass = coerceTaskClass(parsed.taskClass);
413
+ const repoSignals = coerceRepoSignals(parsed.repoSignals);
388
414
  const state = {
389
415
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
390
416
  activeRunId,
@@ -395,6 +421,7 @@ function coerceFlowState(parsed) {
395
421
  track,
396
422
  discoveryMode,
397
423
  ...(taskClass !== undefined ? { taskClass } : {}),
424
+ ...(repoSignals ? { repoSignals } : {}),
398
425
  skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
399
426
  staleStages: sanitizeStaleStages(parsed.staleStages),
400
427
  rewinds: sanitizeRewinds(parsed.rewinds),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {