cclaw-cli 6.3.0 → 6.4.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.
@@ -385,6 +385,9 @@ export interface LearningsParseResult {
385
385
  errors: string[];
386
386
  details: string;
387
387
  }
388
+ /** Multiline block used by linter + learnings harvest stderr (identical text). */
389
+ export declare function formatLearningsErrorsBullets(errors: string[]): string;
390
+ export declare function learningsParseFailureHumanSummary(artifactRelPath: string, errors: string[]): string;
388
391
  export declare const LEARNING_TYPE_SET: Set<LearningEntryType>;
389
392
  export declare const LEARNING_CONFIDENCE_SET: Set<LearningConfidence>;
390
393
  export declare const LEARNING_SEVERITY_SET: Set<LearningSeverity>;
@@ -1501,6 +1501,16 @@ export function hasVerificationLadderTableRow(sectionBody) {
1501
1501
  }
1502
1502
  return false;
1503
1503
  }
1504
+ /** Multiline block used by linter + learnings harvest stderr (identical text). */
1505
+ export function formatLearningsErrorsBullets(errors) {
1506
+ if (errors.length === 0) {
1507
+ return "Errors:\n - Learnings section could not be parsed.";
1508
+ }
1509
+ return `Errors:\n${errors.map((error) => ` - ${error}`).join("\n")}`;
1510
+ }
1511
+ export function learningsParseFailureHumanSummary(artifactRelPath, errors) {
1512
+ return `learnings harvest failed for \`${artifactRelPath}\`.\n${formatLearningsErrorsBullets(errors)}`;
1513
+ }
1504
1514
  export const LEARNING_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
1505
1515
  export const LEARNING_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
1506
1516
  export const LEARNING_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
@@ -1,7 +1,7 @@
1
1
  import type { FlowStage, FlowTrack } from "./types.js";
2
2
  import { type LintResult } from "./artifact-linter/shared.js";
3
3
  export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication, type ReviewVerdictConsistencyResult, type ReviewSecurityNoChangeAttestationResult, type ReviewTddDuplicationConflict, type ReviewTddDuplicationResult } from "./artifact-linter/review-army.js";
4
- export { type LintFinding, type LintResult, type LearningEntryType, type LearningConfidence, type LearningSeverity, type LearningSource, type LearningSeedEntry, type LearningsParseResult, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
4
+ export { type LintFinding, type LintResult, type LearningEntryType, type LearningConfidence, type LearningSeverity, type LearningSource, type LearningSeedEntry, type LearningsParseResult, formatLearningsErrorsBullets, learningsParseFailureHumanSummary, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
5
5
  export interface LintArtifactOptions {
6
6
  /**
7
7
  * Stage-level flags supplied by the caller (typically `advance-stage`)
@@ -3,7 +3,7 @@ import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-path
3
3
  import { exists } from "./fs-utils.js";
4
4
  import { stageSchema } from "./content/stage-schema.js";
5
5
  import { readFlowState } from "./run-persistence.js";
6
- import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody } from "./artifact-linter/shared.js";
6
+ import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
7
7
  import { shouldDemoteArtifactValidationByTrack } from "./content/stage-schema.js";
8
8
  import { recordArtifactValidationDemotedByTrack } from "./delegation.js";
9
9
  import { lintBrainstormStage } from "./artifact-linter/brainstorm.js";
@@ -15,7 +15,7 @@ import { lintTddStage } from "./artifact-linter/tdd.js";
15
15
  import { lintReviewStage } from "./artifact-linter/review.js";
16
16
  import { lintShipStage } from "./artifact-linter/ship.js";
17
17
  export { validateReviewArmy, checkReviewVerdictConsistency, checkReviewSecurityNoChangeAttestation, checkReviewTddNoCrossArtifactDuplication } from "./artifact-linter/review-army.js";
18
- export { extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
18
+ export { formatLearningsErrorsBullets, learningsParseFailureHumanSummary, extractMarkdownSectionBody, parseLearningsSection } from "./artifact-linter/shared.js";
19
19
  const FRONTMATTER_REQUIRED_KEYS = [
20
20
  "stage",
21
21
  "schema_version",
@@ -117,6 +117,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
117
117
  let discoveryMode = "guided";
118
118
  let taskClass = null;
119
119
  let activeRunId = null;
120
+ let completedStagesForAudit = [];
121
+ let completedStageMetaForAudit;
120
122
  try {
121
123
  const flowState = await readFlowState(projectRoot);
122
124
  const hint = flowState.interactionHints?.[stage];
@@ -125,12 +127,16 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
125
127
  discoveryMode = flowState.discoveryMode ?? "guided";
126
128
  taskClass = flowState.taskClass ?? null;
127
129
  activeRunId = flowState.activeRunId ?? null;
130
+ completedStagesForAudit = flowState.completedStages;
131
+ completedStageMetaForAudit = flowState.completedStageMeta;
128
132
  }
129
133
  catch {
130
134
  activeStageFlags = [];
131
135
  discoveryMode = "guided";
132
136
  taskClass = null;
133
137
  activeRunId = null;
138
+ completedStagesForAudit = [];
139
+ completedStageMetaForAudit = undefined;
134
140
  }
135
141
  for (const extra of options.extraStageFlags ?? []) {
136
142
  if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
@@ -186,14 +192,56 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
186
192
  const meaningfulStageNoneWarning = learnings.ok && learnings.none && ["design", "tdd", "review"].includes(stage)
187
193
  ? " Warning: design/tdd/review usually produce reusable decisions, test patterns, or review lessons; keep `None this stage` only for truly mechanical work."
188
194
  : "";
195
+ const learningsErrorBlock = !learnings.ok && learnings.errors.length > 0
196
+ ? `\n${formatLearningsErrorsBullets(learnings.errors)}`
197
+ : "";
189
198
  findings.push({
190
199
  section: "Learnings",
191
200
  required: requireLearnings,
192
201
  rule: "`## Learnings` must contain either a single `- None this stage.` bullet or JSON bullets compatible with knowledge.jsonl fields (type/trigger/action/confidence required).",
193
202
  found: learnings.ok,
194
- details: `${learnings.details}${meaningfulStageNoneWarning}`
203
+ details: `${learnings.details}${learningsErrorBlock}${meaningfulStageNoneWarning}`
195
204
  });
196
205
  }
206
+ for (const doneStage of completedStagesForAudit) {
207
+ const completionIso = completedStageMetaForAudit?.[doneStage]?.completedAt;
208
+ if (!completionIso)
209
+ continue;
210
+ const completedMs = Date.parse(completionIso);
211
+ if (!Number.isFinite(completedMs))
212
+ continue;
213
+ try {
214
+ const resolvedDone = await resolveStageArtifactPath(doneStage, {
215
+ projectRoot,
216
+ track,
217
+ intent: "read"
218
+ });
219
+ if (!(await exists(resolvedDone.absPath)))
220
+ continue;
221
+ const artifactStat = await fs.stat(resolvedDone.absPath);
222
+ if (artifactStat.mtimeMs <= completedMs)
223
+ continue;
224
+ const priorRaw = await fs.readFile(resolvedDone.absPath, "utf8");
225
+ const priorSections = extractH2Sections(priorRaw);
226
+ const amendBody = sectionBodyByName(priorSections, "Amendments");
227
+ const trimmedAmend = amendBody === null
228
+ ? ""
229
+ : amendBody.replace(/<!--[\s\S]*?-->/gu, "").replace(/\s+/gu, " ").trim();
230
+ if (trimmedAmend.length > 0)
231
+ continue;
232
+ findings.push({
233
+ section: "stage_artifact_post_closure_mutation",
234
+ required: false,
235
+ rule: "stage_artifact_post_closure_mutation — substantive post-closure edit without `## Amendments` (advisory)",
236
+ found: false,
237
+ details: `Completed stage "${doneStage}" snapshot closed at ${completionIso}, but ${resolvedDone.relPath} has a newer mtime without nonempty \`## Amendments\`. ` +
238
+ "Append dated bullets describing each drift fix, or restore the archived copy."
239
+ });
240
+ }
241
+ catch {
242
+ continue;
243
+ }
244
+ }
197
245
  const stageContext = {
198
246
  projectRoot,
199
247
  stage,
@@ -19,9 +19,9 @@ export function closeoutSubstateProtocolBullets() {
19
19
  return `When \`currentStage === "ship"\`, route by **${closeoutSubstateInline()}**:
20
20
  - \`"idle"\` or missing -> outcome: initialize closeout by setting
21
21
  ${closeoutSubstateInline()} = \`"post_ship_review"\`, then continue \`/cc\`
22
- into the in-stage retro protocol (draft + one structured accept/edit/skip ask).
22
+ into the in-stage retro protocol (draft + one structured accept/edit/no changes ask).
23
23
  - \`"post_ship_review"\` -> outcome: execute the unified post-ship closeout leg
24
- (retro acceptance/edit/skip + in-stage compound scan, not \`ce:compound\`)
24
+ (retro acceptance/edit/no changes + in-stage compound scan, not \`ce:compound\`)
25
25
  and advance toward archive readiness:
26
26
  read \`.cclaw/state/compound-readiness.json\` plus the relevant tail of
27
27
  \`.cclaw/knowledge.jsonl\`, assess overlap before adding duplicate knowledge,
@@ -30,8 +30,8 @@ export function closeoutSubstateProtocolBullets() {
30
30
  guidance in place instead of introducing extra lineage metadata. Optionally ask
31
31
  whether to scan Cursor/Claude/Codex
32
32
  session transcripts for matching historical learnings; only do it after opt-in.
33
- Ask **one** structured question (apply / skip) per candidate cluster or a
34
- single accept-all / skip choice, then advance substate.
33
+ Ask **one** structured question (apply / no changes) per candidate cluster or a
34
+ single accept-all / no-changes choice, then advance substate.
35
35
  - \`"ready_to_archive"\` -> outcome: continue \`/cc\` so the runtime archive step
36
36
  executes, snapshots, and resets active state.
37
37
  - \`"archived"\` (transient) -> outcome: report "run archived" and stop (flow complete).`;
@@ -46,6 +46,7 @@ These behaviors are the exact reason this skill exists. The linter will block yo
46
46
  - Ask exactly one question per turn and wait for the answer before asking the next one.
47
47
  - Use harness-native question tools first; prose fallback is allowed only when the tool is unavailable.
48
48
  - Keep a running Q&A trace in the active artifact under \`## Q&A Log\` in \`${RUNTIME_ROOT}/artifacts/\` as append-only rows.
49
+ - **Early-loop ledger discipline**: Never append \`.cclaw/state/early-loop-log.jsonl\` rows whose \`iteration\` exceeds the active \`maxIterations\`. If the cap fired, escalate or accept convergence outcomes—do not bump the iteration counter afterward. \`deriveEarlyLoopStatus\` clamps persistence, but the log source should stay honest too.
49
50
  - **Convergence floor**: do NOT advance the stage (do NOT call \`stage-complete.mjs\`) until Q&A converges. The machine contract matches \`evaluateQaLogFloor\` in \`src/artifact-linter/shared.ts\` (rule \`qa_log_unconverged\`). Pass when ANY holds: (a) every forcing-question topic id is tagged \`[topic:<id>]\` on at least one \`## Q&A Log\` row; (b) the Ralph-Loop detector fires (last 2 substantive rows are non-decision-changing: \`skip\`/\`continue\`/\`no-change\`/\`done\`/etc.) **and** the log has at least \`max(2, questionBudgetHint(discoveryMode, stage).min)\` substantive rows — **unless** \`discoveryMode\` is \`guided\` or \`deep\` with pending forcing-topic ids (then Ralph-Loop alone cannot pass until topics are tagged, a stop-signal is recorded, or \`--skip-questions\` downgrades the finding to advisory); (c) an explicit user stop-signal row; or (d) \`--skip-questions\` was persisted (unconverged is advisory only). Wave 24 (v6.0.0) made \`[topic:<id>]\` mandatory (no English keyword fallback).
50
51
  - **NEVER run shell hash commands** (\`shasum\`, \`sha256sum\`, \`md5sum\`, \`Get-FileHash\`, \`certutil\`, etc.) to compute artifact hashes. If a linter ever asks you for a hash, that is a linter bug — report failure and stop, do not auto-fix in bash.
51
52
  - **NEVER paste cclaw command lines into chat** (e.g. \`node .cclaw/hooks/stage-complete.mjs ... --evidence-json '{...}'\`). Run them via the tool layer; report only the resulting summary. The user does not run cclaw manually and seeing the command line is noise.
@@ -85,6 +86,8 @@ Treat these as stop-and-draft signals:
85
86
  - EN: "enough", "skip", "just draft it", "stop asking", "move on", "no more questions"
86
87
  - UA: "досить", "вистачить", "давай драфт", "рухаємось далі"
87
88
 
89
+ Label disambiguation: The word **skip** is a valid stop phrase during brainstorm/scope/design Q&A. In ship closeout retros, compound clustering, or any structured retro ask, expose **no changes** / **accept as-is** for the passive option instead of wording it as "skip" so agents do not mix elicitation stop-signals with closeout choreography.
90
+
88
91
  When detected:
89
92
  - Append a Q&A Log row exactly like: \`Turn N | (stop-signal) | <user quote> | stop-and-draft\` — this row satisfies the linter floor escape hatch.
90
93
  - Do not ask another question in this stage loop.
@@ -387,6 +387,7 @@ function completionParametersBlock(schema, track) {
387
387
  - \`completion helper JSON diagnostics\`: append \`--json\` to receive a machine-readable validation failure summary.
388
388
  - \`delegation lifecycle proof\`: use the delegation helper recipe in this section with explicit lifecycle rows: \`--status=scheduled\` -> \`--status=launched\` -> \`--status=acknowledged\` -> \`--status=completed\` (completed isolated/generic requires prior ACK for the same span or \`--ack-ts=<iso>\`).
389
389
  - Fill \`## Learnings\` before closeout: either \`- None this stage.\` or JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (knowledge-schema compatible).
390
+ - If you edit any completed-stage artifact after it shipped (\`completedStageMeta\` timestamps exist), append a short \`## Amendments\` section with dated bullets (timestamp + reason) instead of overwriting the archived narrative silently — advisory linter rule \`stage_artifact_post_closure_mutation\` enforces visibility when this trail is missing.
390
391
  - 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
392
  - 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
393
  - 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.
@@ -10,6 +10,14 @@ import { trackRenderContext } from "./track-render-context.js";
10
10
  // Protocol). They are no longer re-exported from here to avoid duplication
11
11
  // and drift. Stage skills cite the meta-skill by path instead.
12
12
  // ---------------------------------------------------------------------------
13
+ // ---------------------------------------------------------------------------
14
+ // Optional artifact appendix (documentation-only — not a tiered gate)
15
+ //
16
+ // `## Amendments` — after a stage is closed, substantive edits SHOULD append dated
17
+ // bullets (ISO timestamp + reason) here instead of silently rewriting history. The
18
+ // linter surfaces advisory `stage_artifact_post_closure_mutation` findings when mtimes
19
+ // move without this trail (`completedStageMeta` must exist).
20
+ // ---------------------------------------------------------------------------
13
21
  export const SKILL_ENVELOPE_KINDS = [
14
22
  "stage-output",
15
23
  "gate-result",
@@ -499,7 +507,7 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
499
507
  when: "When repository, market, docs, or prior-art context changes the approach set. Runs only after the adaptive elicitation Q&A loop converges.",
500
508
  purpose: "Provide search-before-read summaries and context-readiness evidence before large reads or decisions.",
501
509
  requiresUserGate: false,
502
- dependsOnInternalRepoSignals: true
510
+ essentialAcrossModes: true
503
511
  }
504
512
  ],
505
513
  scope: [
@@ -537,7 +545,7 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
537
545
  when: "When churn, prior attempts, reference patterns, or external constraints may change scope boundaries. Runs only after the adaptive elicitation Q&A loop converges.",
538
546
  purpose: "Summarize search/context findings before the scope contract locks accepted/rejected/deferred ideas.",
539
547
  requiresUserGate: false,
540
- dependsOnInternalRepoSignals: true
548
+ essentialAcrossModes: true
541
549
  },
542
550
  {
543
551
  agent: "product-discovery",
@@ -600,7 +608,8 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
600
608
  runPhase: "post-elicitation",
601
609
  when: "When framework/library docs, repo graph context, or reference contracts may change the design. Runs only after the adaptive elicitation Q&A loop converges.",
602
610
  purpose: "Run search-before-read context synthesis before architecture locks.",
603
- requiresUserGate: false
611
+ requiresUserGate: false,
612
+ essentialAcrossModes: true
604
613
  },
605
614
  {
606
615
  agent: "security-reviewer",
@@ -54,11 +54,10 @@ export interface StageAutoSubagentDispatch {
54
54
  /** Optional skill folder the dispatched agent should load as additional context. */
55
55
  skill?: string;
56
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.
57
+ * When true on a proactive dispatch row for brainstorm/scope/design, the trace
58
+ * gate keeps this rule even when `discoveryMode` is `lean` or `guided`.
60
59
  */
61
- dependsOnInternalRepoSignals?: boolean;
60
+ essentialAcrossModes?: boolean;
62
61
  }
63
62
  export type StageComplexityTier = "lightweight" | "standard" | "deep";
64
63
  export interface StagePhilosophy {
@@ -578,6 +578,8 @@ Use this payload when a stage needs context-readiness or search-before-read evid
578
578
  ${MARKDOWN_CODE_FENCE}
579
579
  You are a researcher subagent.
580
580
 
581
+ SCOPE — External plus internal research: search official docs/libraries/prior art and current best practices (use web or package-index tooling whenever it is live; cite URLs or authoritative references). Internally scan the repo for commits, manifests, conventions, migrations, configuration, and latent decisions—even when the workspace is sparse, external benchmarking stays mandatory.
582
+
581
583
  QUESTION: {one falsifiable research question}
582
584
  SCOPE: {repo paths, docs, references, providers to inspect}
583
585
  CONTEXT READINESS: {graph/search/provider status if known}
@@ -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- 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";
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\n## Protocol label hygiene\n\n`skip` wording means different things depending on phase: brainstorm/scope/design Q&A stop-signals may still literal **skip**/enough/move-on wording; structured ship closeout retros and compound clustering prompts should expose **no changes** (or accept-as-is language) rather than labeling the passive path as skip. Keep the verbs aligned with the harness question copy you present to the human.\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>;
@@ -1566,6 +1566,10 @@ Track-specific skips are allowed only when \`flow-state.track\` + \`skippedStage
1566
1566
 
1567
1567
  - No completion claim without fresh command evidence in this turn.
1568
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.
1569
+
1570
+ ## Protocol label hygiene
1571
+
1572
+ \`skip\` wording means different things depending on phase: brainstorm/scope/design Q&A stop-signals may still literal **skip**/enough/move-on wording; structured ship closeout retros and compound clustering prompts should expose **no changes** (or accept-as-is language) rather than labeling the passive path as skip. Keep the verbs aligned with the harness question copy you present to the human.
1569
1573
  - Do not mark gates passed from memory.
1570
1574
  - Keep evidence in \`.cclaw/artifacts/\`; archive through closeout via \`/cc\` or cancel early via \`node .cclaw/hooks/cancel-run.mjs\`.
1571
1575
 
@@ -23,6 +23,10 @@ export interface EarlyLoopStatus {
23
23
  escalationReason?: string;
24
24
  lastUpdatedAt: string;
25
25
  }
26
+ export declare function clampEarlyLoopStatusForWrite(status: EarlyLoopStatus): {
27
+ status: EarlyLoopStatus;
28
+ clampedFrom: number | null;
29
+ };
26
30
  export interface EarlyLoopLogConcern {
27
31
  id: string;
28
32
  severity: EarlyLoopConcernSeverity;
@@ -1,6 +1,18 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { DEFAULT_EARLY_LOOP_MAX_ITERATIONS } from "./config.js";
3
3
  export const EARLY_LOOP_STAGES = ["brainstorm", "scope", "design"];
4
+ export function clampEarlyLoopStatusForWrite(status) {
5
+ if (status.iteration <= status.maxIterations) {
6
+ return { status, clampedFrom: null };
7
+ }
8
+ return {
9
+ status: {
10
+ ...status,
11
+ iteration: status.maxIterations
12
+ },
13
+ clampedFrom: status.iteration
14
+ };
15
+ }
4
16
  const CONCERN_ID_PREFIX = "C-";
5
17
  function severityWeight(severity) {
6
18
  if (severity === "critical")
@@ -234,11 +246,12 @@ export function deriveEarlyLoopStatus(entries, options) {
234
246
  convergenceTripped = true;
235
247
  escalationReason = `max iterations ${maxIterations} reached with ${openConcerns.length} open concern(s)`;
236
248
  }
249
+ const iteration = Math.min(currentIteration, maxIterations);
237
250
  return {
238
251
  schemaVersion: 1,
239
252
  stage: options.stage,
240
253
  runId: options.runId,
241
- iteration: currentIteration,
254
+ iteration,
242
255
  maxIterations,
243
256
  openConcerns,
244
257
  resolvedConcerns,
@@ -44,8 +44,8 @@ export interface RetroState {
44
44
  * from the correct step even across sessions.
45
45
  *
46
46
  * - `idle` — ship not complete, or closeout not yet started.
47
- * - `post_ship_review` — unified closeout leg: retro acceptance/edit/skip
48
- * plus compound pass execution (or explicit skip).
47
+ * - `post_ship_review` — unified closeout leg: retro acceptance/edit/no changes
48
+ * plus compound pass execution (or explicit no-additional-changes path).
49
49
  * - `ready_to_archive` — retro + compound done; archive is the next
50
50
  * automatic step.
51
51
  * - `archived` — archive completed in this session (transient — archive
@@ -113,6 +113,14 @@ export interface FlowState {
113
113
  closeout: CloseoutState;
114
114
  /** Repo shape signals captured at last successful start-flow (omit on legacy files). */
115
115
  repoSignals?: RepoSignals;
116
+ /**
117
+ * Best-effort stage completion timestamps (ISO strings) captured as stages
118
+ * enter `completedStages`. Missing keys behave like legacy flows with no audit
119
+ * clock for post-closure mutation hints.
120
+ */
121
+ completedStageMeta?: Partial<Record<FlowStage, {
122
+ completedAt: string;
123
+ }>>;
116
124
  }
117
125
  export interface StageInteractionHint {
118
126
  skipQuestions?: boolean;
@@ -10,8 +10,8 @@ export const FLOW_STATE_SCHEMA_VERSION = 1;
10
10
  * from the correct step even across sessions.
11
11
  *
12
12
  * - `idle` — ship not complete, or closeout not yet started.
13
- * - `post_ship_review` — unified closeout leg: retro acceptance/edit/skip
14
- * plus compound pass execution (or explicit skip).
13
+ * - `post_ship_review` — unified closeout leg: retro acceptance/edit/no changes
14
+ * plus compound pass execution (or explicit no-additional-changes path).
15
15
  * - `ready_to_archive` — retro + compound done; archive is the next
16
16
  * automatic step.
17
17
  * - `archived` — archive completed in this session (transient — archive
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { resolveArtifactPath } from "../../artifact-paths.js";
4
4
  import { appendDelegation, checkMandatoryDelegations, readDelegationEvents, readDelegationLedger } from "../../delegation.js";
5
5
  import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../../gate-evidence.js";
6
- import { extractMarkdownSectionBody, parseLearningsSection } from "../../artifact-linter.js";
6
+ import { extractMarkdownSectionBody, learningsParseFailureHumanSummary, parseLearningsSection } from "../../artifact-linter.js";
7
7
  import { getAvailableTransitions, getTransitionGuards } from "../../flow-state.js";
8
8
  import { appendKnowledge } from "../../knowledge-store.js";
9
9
  import { readFlowState, writeFlowState } from "../../runs.js";
@@ -262,7 +262,7 @@ export async function harvestStageLearnings(projectRoot, stage, track) {
262
262
  parsedEntries: 0,
263
263
  appendedEntries: 0,
264
264
  skippedDuplicates: 0,
265
- details: parsed.details
265
+ details: learningsParseFailureHumanSummary(resolvedArtifact.relPath, parsed.errors)
266
266
  };
267
267
  }
268
268
  const appendResult = await appendKnowledge(projectRoot, parsed.entries, {
@@ -575,20 +575,28 @@ export async function runAdvanceStage(projectRoot, args, io) {
575
575
  }
576
576
  const learningsHarvest = await harvestStageLearnings(projectRoot, args.stage, flowState.track);
577
577
  if (!learningsHarvest.ok) {
578
- io.stderr.write(`cclaw internal advance-stage: learnings harvest failed for "${schema.artifactFile}". ${learningsHarvest.details}\n`);
578
+ io.stderr.write(`cclaw internal advance-stage: ${learningsHarvest.details}\n`);
579
579
  return 1;
580
580
  }
581
581
  const satisfiedGuards = new Set([...nextPassed, ...selectedTransitionGuards]);
582
582
  const successor = resolveSuccessorTransition(args.stage, flowState.track, transitionTargets, satisfiedGuards, new Set(selectedTransitionGuards));
583
583
  const completedStages = blockedReviewRoute
584
- ? flowState.completedStages.filter((stage) => stage !== args.stage)
584
+ ? flowState.completedStages.filter((finished) => finished !== args.stage)
585
585
  : flowState.completedStages.includes(args.stage)
586
586
  ? [...flowState.completedStages]
587
587
  : [...flowState.completedStages, args.stage];
588
+ let completedStageMeta = flowState.completedStageMeta ?? {};
589
+ if (!blockedReviewRoute && !flowState.completedStages.includes(args.stage)) {
590
+ completedStageMeta = {
591
+ ...completedStageMeta,
592
+ [args.stage]: { completedAt: new Date().toISOString() }
593
+ };
594
+ }
588
595
  const interactionHints = nextInteractionHints(flowState, args, successor);
589
596
  const finalState = {
590
597
  ...candidateState,
591
598
  completedStages,
599
+ completedStageMeta,
592
600
  currentStage: successor ?? args.stage,
593
601
  interactionHints
594
602
  };
@@ -8,10 +8,10 @@ export interface ProactiveDelegationTraceResult {
8
8
  * Ensure every proactive dispatch rule for the stage has a ledger row for the
9
9
  * active run, or an explicit user-flag waiver.
10
10
  *
11
- * Lean/guided discovery on early elicitation stages intentionally does not
12
- * require a full proactive trace: specialists run only when triggers warrant
13
- * them. Deep discovery keeps the blanket trace so mandatory + proactive
14
- * coverage stays auditably complete before advance.
11
+ * Lean/guided discovery on brainstorm/scope/design keeps only proactive rules
12
+ * marked `essentialAcrossModes` (researcher today) so external research stays
13
+ * auditable without requiring every discretionary proactive lens. Deep
14
+ * discovery evaluates the full proactive matrix.
15
15
  */
16
16
  export declare function ensureProactiveDelegationTrace(projectRoot: string, stage: FlowStage, options: {
17
17
  acceptWaiver: boolean;
@@ -3,36 +3,25 @@ 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);
6
+ function proactiveRulesForDiscoveryMode(stage, discoveryMode) {
7
+ const proactiveRules = stageAutoSubagentDispatch(stage).filter((rule) => rule.mode === "proactive");
8
+ if (isEarlyElicitationStage(stage) && (discoveryMode === "lean" || discoveryMode === "guided")) {
9
+ return proactiveRules.filter((rule) => rule.essentialAcrossModes === true);
10
+ }
11
+ return proactiveRules;
19
12
  }
20
13
  /**
21
14
  * Ensure every proactive dispatch rule for the stage has a ledger row for the
22
15
  * active run, or an explicit user-flag waiver.
23
16
  *
24
- * Lean/guided discovery on early elicitation stages intentionally does not
25
- * require a full proactive trace: specialists run only when triggers warrant
26
- * them. Deep discovery keeps the blanket trace so mandatory + proactive
27
- * coverage stays auditably complete before advance.
17
+ * Lean/guided discovery on brainstorm/scope/design keeps only proactive rules
18
+ * marked `essentialAcrossModes` (researcher today) so external research stays
19
+ * auditable without requiring every discretionary proactive lens. Deep
20
+ * discovery evaluates the full proactive matrix.
28
21
  */
29
22
  export async function ensureProactiveDelegationTrace(projectRoot, stage, options) {
30
- if (isEarlyElicitationStage(stage) && (options.discoveryMode === "lean" || options.discoveryMode === "guided")) {
31
- return { missingRules: [] };
32
- }
33
- const proactiveRules = stageAutoSubagentDispatch(stage)
34
- .filter((rule) => rule.mode === "proactive")
35
- .filter((rule) => !skipRepoDependentProactiveRule(rule, stage, options.discoveryMode, options.repoSignals));
23
+ void options.repoSignals;
24
+ const proactiveRules = proactiveRulesForDiscoveryMode(stage, options.discoveryMode);
36
25
  if (proactiveRules.length === 0)
37
26
  return { missingRules: [] };
38
27
  const ledger = await readDelegationLedger(projectRoot);
@@ -221,7 +221,8 @@ export async function runStartFlow(projectRoot, args, io) {
221
221
  taskClass: nextState.taskClass ?? null,
222
222
  currentStage: nextState.currentStage,
223
223
  skippedStages: nextState.skippedStages,
224
- activeRunId: nextState.activeRunId
224
+ activeRunId: nextState.activeRunId,
225
+ repoSignals
225
226
  }, null, 2)}\n`);
226
227
  }
227
228
  return 0;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { RUNTIME_ROOT } from "../constants.js";
3
- import { computeEarlyLoopStatus, formatEarlyLoopStatusLine, isEarlyLoopStage } from "../early-loop.js";
3
+ import { clampEarlyLoopStatusForWrite, computeEarlyLoopStatus, formatEarlyLoopStatusLine, isEarlyLoopStage } from "../early-loop.js";
4
4
  import { writeFileSafe } from "../fs-utils.js";
5
5
  import { readFlowState } from "../runs.js";
6
6
  function parseArgs(tokens) {
@@ -71,7 +71,12 @@ export async function runEarlyLoopStatusCommand(projectRoot, argv, io) {
71
71
  return 1;
72
72
  }
73
73
  const runId = (args.runId ?? flow?.activeRunId ?? "active").trim() || "active";
74
- const status = await computeEarlyLoopStatus(stage, runId, path.join(stateDir(projectRoot), "early-loop-log.jsonl"));
74
+ let status = await computeEarlyLoopStatus(stage, runId, path.join(stateDir(projectRoot), "early-loop-log.jsonl"));
75
+ const persisted = clampEarlyLoopStatusForWrite(status);
76
+ if (persisted.clampedFrom !== null) {
77
+ io.stderr.write(`cclaw internal early-loop-status: early-loop iteration ${persisted.clampedFrom} exceeds maxIterations ${status.maxIterations}; clamping before write.\n`);
78
+ status = persisted.status;
79
+ }
75
80
  if (args.write) {
76
81
  const target = path.join(stateDir(projectRoot), "early-loop.json");
77
82
  await writeFileSafe(target, `${JSON.stringify(status, null, 2)}\n`);
@@ -255,6 +255,24 @@ function sanitizeStaleStages(value) {
255
255
  }
256
256
  return out;
257
257
  }
258
+ function sanitizeCompletedStageMeta(value) {
259
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
260
+ return undefined;
261
+ }
262
+ const out = {};
263
+ for (const [key, raw] of Object.entries(value)) {
264
+ if (!isFlowStage(key))
265
+ continue;
266
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
267
+ continue;
268
+ const record = raw;
269
+ const ca = typeof record.completedAt === "string" ? record.completedAt.trim() : "";
270
+ if (ca.length > 0) {
271
+ out[key] = { completedAt: ca };
272
+ }
273
+ }
274
+ return Object.keys(out).length > 0 ? out : undefined;
275
+ }
258
276
  function sanitizeRewinds(value) {
259
277
  if (!Array.isArray(value)) {
260
278
  return [];
@@ -411,6 +429,7 @@ function coerceFlowState(parsed) {
411
429
  : next.activeRunId;
412
430
  const taskClass = coerceTaskClass(parsed.taskClass);
413
431
  const repoSignals = coerceRepoSignals(parsed.repoSignals);
432
+ const completedStageMeta = sanitizeCompletedStageMeta(parsed.completedStageMeta);
414
433
  const state = {
415
434
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
416
435
  activeRunId,
@@ -422,6 +441,7 @@ function coerceFlowState(parsed) {
422
441
  discoveryMode,
423
442
  ...(taskClass !== undefined ? { taskClass } : {}),
424
443
  ...(repoSignals ? { repoSignals } : {}),
444
+ ...(completedStageMeta ? { completedStageMeta } : {}),
425
445
  skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
426
446
  staleStages: sanitizeStaleStages(parsed.staleStages),
427
447
  rewinds: sanitizeRewinds(parsed.rewinds),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {