cclaw-cli 0.46.15 → 0.48.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/README.md +3 -1
  2. package/dist/artifact-linter.d.ts +7 -0
  3. package/dist/artifact-linter.js +169 -8
  4. package/dist/config.d.ts +6 -6
  5. package/dist/config.js +22 -0
  6. package/dist/constants.d.ts +10 -1
  7. package/dist/constants.js +19 -10
  8. package/dist/content/contracts.d.ts +1 -1
  9. package/dist/content/contracts.js +1 -1
  10. package/dist/content/{harnesses-doc.js → harness-doc.js} +32 -1
  11. package/dist/content/harness-playbooks.js +4 -4
  12. package/dist/content/ideate-command.js +19 -19
  13. package/dist/content/skills.js +2 -2
  14. package/dist/content/stage-schema.js +54 -15
  15. package/dist/content/stages/design.js +2 -2
  16. package/dist/content/stages/review.js +1 -1
  17. package/dist/content/stages/ship.js +2 -0
  18. package/dist/content/stages/tdd.js +8 -4
  19. package/dist/content/templates.js +4 -3
  20. package/dist/delegation.js +107 -26
  21. package/dist/doctor.js +77 -9
  22. package/dist/flow-state.d.ts +8 -0
  23. package/dist/flow-state.js +11 -8
  24. package/dist/gate-evidence.js +26 -2
  25. package/dist/harness-adapters.d.ts +2 -2
  26. package/dist/harness-adapters.js +2 -2
  27. package/dist/install.js +28 -6
  28. package/dist/internal/advance-stage.js +53 -16
  29. package/dist/internal/detect-public-api-changes.d.ts +5 -0
  30. package/dist/internal/detect-public-api-changes.js +45 -0
  31. package/dist/policy.js +3 -2
  32. package/dist/retro-gate.js +30 -3
  33. package/dist/run-persistence.js +16 -5
  34. package/dist/tdd-cycle.js +19 -1
  35. package/dist/types.d.ts +6 -1
  36. package/package.json +4 -1
  37. /package/dist/content/{harnesses-doc.d.ts → harness-doc.d.ts} +0 -0
package/README.md CHANGED
@@ -174,7 +174,7 @@ inside `/cc-ops` subcommands.
174
174
  |---|---|
175
175
  | **`/cc <idea>`** | Classify the task, discover origin docs (`docs/prd/**`, ADRs, root `PRD.md`, …), sniff the stack, recommend a track, then start the first stage of that track. `/cc` without arguments resumes the current flow. |
176
176
  | **`/cc-next`** | The one progression primitive. Reads `flow-state.json`, checks gates + mandatory subagent delegations, and either resumes the current stage or advances to the next. `/cc-next` in a new session is how you **resume**. |
177
- | **`/cc-ideate`** | Repository improvement discovery. Scans for TODOs, flaky tests, oversized modules, docs drift, and recurring knowledge-store lessons, **persists the ranked backlog** to `.cclaw/artifacts/ideation-<date>-<slug>.md`, and ends with a concrete handoff: launch `/cc` on the selected candidate in the same session, save-and-close, or discard. Resume check on next run reuses any ideation artifact younger than 30 days. Never mutates `flow-state.json`. |
177
+ | **`/cc-ideate`** | Repository improvement ideate mode. Scans for TODOs, flaky tests, oversized modules, docs drift, and recurring knowledge-store lessons, **persists the ranked backlog** to `.cclaw/artifacts/ideate-<date>-<slug>.md`, and ends with a concrete handoff: launch `/cc` on the selected candidate in the same session, save-and-close, or discard. Resume check on next run reuses any ideate artifact younger than 30 days. Never mutates `flow-state.json`. |
178
178
  | **`/cc-view`** | Read-only flow visibility. `/cc-view status` (default) shows stage progress, mandatory delegations with their fulfillment mode (isolated / generic-dispatch / role-switch), the ship closeout substate (retro → compound → archive), and the active harness parity row. `/cc-view tree` renders the same picture as a tree with a closeout sub-branch under ship and a per-harness playbook summary. `/cc-view diff` shows stage/gate/closeout/delegation deltas since the last run. Never mutates state (except diff's snapshot baseline). |
179
179
 
180
180
  > Power-user surface: `/cc-ops` is an operational router for manual
@@ -458,6 +458,8 @@ CCLAW_EVAL_MODEL=glm-5.1 # default
458
458
 
459
459
  Full details, corpus format, and the eval contract live in
460
460
  [`docs/evals.md`](./docs/evals.md).
461
+ Mutation-testing setup lives in `stryker.config.mjs` and
462
+ `.github/workflows/mutation.yml` (manual + weekly run).
461
463
 
462
464
  ---
463
465
 
@@ -58,9 +58,16 @@ export interface ReviewVerdictConsistencyResult {
58
58
  openCriticalCount: number;
59
59
  shipBlockerCount: number;
60
60
  }
61
+ export interface ReviewSecurityNoChangeAttestationResult {
62
+ ok: boolean;
63
+ errors: string[];
64
+ hasSecurityFinding: boolean;
65
+ hasNoChangeAttestation: boolean;
66
+ }
61
67
  /**
62
68
  * Ensure the narrative verdict in 07-review.md is consistent with the
63
69
  * structured review-army reconciliation. A review cannot declare
64
70
  * APPROVED while open Critical findings or shipBlockers remain.
65
71
  */
66
72
  export declare function checkReviewVerdictConsistency(projectRoot: string): Promise<ReviewVerdictConsistencyResult>;
73
+ export declare function checkReviewSecurityNoChangeAttestation(projectRoot: string): Promise<ReviewSecurityNoChangeAttestationResult>;
@@ -12,25 +12,52 @@ async function resolveArtifactPath(projectRoot, fileName) {
12
12
  function normalizeHeadingTitle(title) {
13
13
  return title.trim().replace(/\s+/g, " ");
14
14
  }
15
- /** Collect H2 sections and body content (`## Section Name`). */
15
+ /**
16
+ * Collect H2 sections and body content (`## Section Name`).
17
+ *
18
+ * - Ignores lines that live inside fenced code blocks (``` / ~~~) so a
19
+ * commented `## Approaches` inside an example doesn't open a phantom
20
+ * section and swallow real content.
21
+ * - When the same heading appears more than once at the top level we
22
+ * concatenate the bodies rather than silently overwriting the earlier
23
+ * occurrence. This keeps lint rules honest when authors split a section
24
+ * into multiple passes.
25
+ */
16
26
  function extractH2Sections(markdown) {
17
27
  const sections = new Map();
18
28
  const lines = markdown.split(/\r?\n/);
19
29
  let currentHeading = null;
20
30
  let buffer = [];
31
+ let fenced = null;
21
32
  const flush = () => {
22
33
  if (currentHeading === null)
23
34
  return;
24
- sections.set(currentHeading, buffer.join("\n"));
35
+ const existing = sections.get(currentHeading);
36
+ const body = buffer.join("\n");
37
+ sections.set(currentHeading, existing === undefined ? body : `${existing}\n${body}`);
25
38
  };
26
39
  for (const line of lines) {
27
- const match = /^##\s+(.+)$/u.exec(line);
28
- if (match) {
29
- flush();
30
- currentHeading = normalizeHeadingTitle(match[1] ?? "");
31
- buffer = [];
40
+ const fenceMatch = /^(```|~~~)/u.exec(line);
41
+ if (fenceMatch) {
42
+ if (fenced === null) {
43
+ fenced = fenceMatch[1] ?? null;
44
+ }
45
+ else if (line.startsWith(fenced)) {
46
+ fenced = null;
47
+ }
48
+ if (currentHeading !== null)
49
+ buffer.push(line);
32
50
  continue;
33
51
  }
52
+ if (fenced === null) {
53
+ const match = /^##\s+(.+)$/u.exec(line);
54
+ if (match) {
55
+ flush();
56
+ currentHeading = normalizeHeadingTitle(match[1] ?? "");
57
+ buffer = [];
58
+ continue;
59
+ }
60
+ }
34
61
  if (currentHeading !== null) {
35
62
  buffer.push(line);
36
63
  }
@@ -869,6 +896,49 @@ export async function lintArtifact(projectRoot, stage) {
869
896
  details: learnings.details
870
897
  });
871
898
  }
899
+ if (stage === "brainstorm") {
900
+ // Brainstorm Iron Law: "NO ARTIFACT IS COMPLETE WITHOUT AN EXPLICITLY
901
+ // APPROVED DIRECTION — SILENCE IS NOT APPROVAL." Previously this was
902
+ // prose-only — nothing failed when the Selected Direction section
903
+ // omitted an approval marker, or when the Approaches table collapsed
904
+ // to a single row (defeating the "2-3 distinct approaches" gate).
905
+ const approachesBody = sectionBodyByName(sections, "Approaches");
906
+ if (approachesBody !== null) {
907
+ const tableRows = approachesBody
908
+ .split(/\r?\n/u)
909
+ .map((line) => line.trim())
910
+ .filter((line) => line.startsWith("|"))
911
+ .filter((line) => !/^\|\s*[-: |]+\|\s*$/u.test(line))
912
+ .filter((line) => !/^\|\s*approach\b/iu.test(line));
913
+ const bulletRows = approachesBody
914
+ .split(/\r?\n/u)
915
+ .map((line) => line.trim())
916
+ .filter((line) => /^(?:[-*]|\d+\.)\s+\S/u.test(line));
917
+ const rowCount = Math.max(tableRows.length, bulletRows.length);
918
+ findings.push({
919
+ section: "Distinct Approaches Enforcement",
920
+ required: true,
921
+ rule: "Approaches section must document at least 2 distinct approaches so the Iron Law comparison is meaningful.",
922
+ found: rowCount >= 2,
923
+ details: rowCount >= 2
924
+ ? `Detected ${rowCount} approach row(s).`
925
+ : `Detected ${rowCount} approach row(s); at least 2 required.`
926
+ });
927
+ }
928
+ const directionBody = sectionBodyByName(sections, "Selected Direction");
929
+ if (directionBody !== null) {
930
+ const approvalMarker = /\bapprov(?:ed|al)\b/iu.test(directionBody);
931
+ findings.push({
932
+ section: "Direction Approval Marker",
933
+ required: true,
934
+ rule: "Selected Direction section must state an explicit approval marker (for example `Approval: approved` or `Approved by: user`).",
935
+ found: approvalMarker,
936
+ details: approvalMarker
937
+ ? "Approval marker present in Selected Direction."
938
+ : "No explicit `approved`/`approval` marker found in Selected Direction."
939
+ });
940
+ }
941
+ }
872
942
  if (stage === "plan") {
873
943
  const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
874
944
  headingPresent(sections, "No-Placeholder Scan") ||
@@ -914,12 +984,13 @@ export async function lintArtifact(projectRoot, stage) {
914
984
  });
915
985
  }
916
986
  if (stage === "scope") {
987
+ const lockedDecisionsBody = sectionBodyByName(sections, "Locked Decisions (D-XX)") ?? "";
917
988
  const strictScopeGuards = parsedFrontmatter.hasFrontmatter ||
918
989
  headingPresent(sections, "Locked Decisions (D-XX)");
919
990
  const scopeSections = [
920
991
  sectionBodyByName(sections, "In Scope / Out of Scope") ?? "",
921
992
  sectionBodyByName(sections, "Scope Summary") ?? "",
922
- sectionBodyByName(sections, "Locked Decisions (D-XX)") ?? ""
993
+ lockedDecisionsBody
923
994
  ].join("\n");
924
995
  const reductionHits = collectPatternHits(scopeSections, SCOPE_REDUCTION_PATTERNS);
925
996
  findings.push({
@@ -931,6 +1002,45 @@ export async function lintArtifact(projectRoot, stage) {
931
1002
  ? "No scope-reduction phrases detected in scope boundary sections."
932
1003
  : `Detected scope-reduction phrase(s): ${reductionHits.join(", ")}.`
933
1004
  });
1005
+ // When the Locked Decisions section is present we must enforce the
1006
+ // D-XX ID contract at runtime (previously this was prose-only in the
1007
+ // artifactValidation rule). Empty body, missing IDs, and duplicate
1008
+ // IDs all fail the lint; absence of the section remains advisory so
1009
+ // scope stays optional for small/quick tracks.
1010
+ if (headingPresent(sections, "Locked Decisions (D-XX)")) {
1011
+ const decisionIds = extractDecisionIds(lockedDecisionsBody);
1012
+ const bulletLines = lockedDecisionsBody
1013
+ .split(/\r?\n/u)
1014
+ .map((line) => line.trim())
1015
+ .filter((line) => /^(?:[-*]|\|)\s+\S/u.test(line));
1016
+ const orphanBullets = bulletLines.filter((line) => !/\bD-\d+\b/u.test(line));
1017
+ const duplicateIds = (() => {
1018
+ const all = lockedDecisionsBody.match(/\bD-\d+\b/gu) ?? [];
1019
+ const counts = new Map();
1020
+ for (const id of all)
1021
+ counts.set(id, (counts.get(id) ?? 0) + 1);
1022
+ return [...counts.entries()].filter(([, n]) => n > 1).map(([id]) => id);
1023
+ })();
1024
+ const issues = [];
1025
+ if (decisionIds.length === 0 && bulletLines.length === 0) {
1026
+ issues.push("section is empty");
1027
+ }
1028
+ if (orphanBullets.length > 0) {
1029
+ issues.push(`${orphanBullets.length} bullet(s) missing a D-XX ID`);
1030
+ }
1031
+ if (duplicateIds.length > 0) {
1032
+ issues.push(`duplicate IDs: ${duplicateIds.join(", ")}`);
1033
+ }
1034
+ findings.push({
1035
+ section: "Locked Decisions ID Integrity",
1036
+ required: true,
1037
+ rule: "Locked Decisions section must list each decision with a unique stable D-XX ID.",
1038
+ found: issues.length === 0,
1039
+ details: issues.length === 0
1040
+ ? `${decisionIds.length} decision ID(s) recorded with no duplicates.`
1041
+ : issues.join("; ")
1042
+ });
1043
+ }
934
1044
  }
935
1045
  const passed = findings.every((f) => !f.required || f.found);
936
1046
  return { stage, file: relFile, passed, findings };
@@ -1198,6 +1308,14 @@ export async function checkReviewVerdictConsistency(projectRoot) {
1198
1308
  if (finalVerdict === "APPROVED" && (openCriticalCount > 0 || shipBlockerCount > 0)) {
1199
1309
  errors.push(`Final Verdict is APPROVED but review-army has ${openCriticalCount} open Critical finding(s) and ${shipBlockerCount} shipBlocker(s). Use BLOCKED or APPROVED_WITH_CONCERNS.`);
1200
1310
  }
1311
+ // APPROVED_WITH_CONCERNS is intended for Important/Suggestion findings
1312
+ // the author has accepted. An *open* Critical finding or an active
1313
+ // shipBlocker must route through BLOCKED (review_verdict_blocked gate)
1314
+ // rather than pass as a concession — previously this slipped through.
1315
+ if (finalVerdict === "APPROVED_WITH_CONCERNS" &&
1316
+ (openCriticalCount > 0 || shipBlockerCount > 0)) {
1317
+ errors.push(`Final Verdict is APPROVED_WITH_CONCERNS but review-army has ${openCriticalCount} open Critical finding(s) and ${shipBlockerCount} shipBlocker(s). Resolve them or use BLOCKED.`);
1318
+ }
1201
1319
  return {
1202
1320
  ok: errors.length === 0,
1203
1321
  errors,
@@ -1206,3 +1324,46 @@ export async function checkReviewVerdictConsistency(projectRoot) {
1206
1324
  shipBlockerCount
1207
1325
  };
1208
1326
  }
1327
+ export async function checkReviewSecurityNoChangeAttestation(projectRoot) {
1328
+ const reviewMdPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "07-review.md");
1329
+ if (!(await exists(reviewMdPath))) {
1330
+ return {
1331
+ ok: true,
1332
+ errors: [],
1333
+ hasSecurityFinding: false,
1334
+ hasNoChangeAttestation: false
1335
+ };
1336
+ }
1337
+ const errors = [];
1338
+ const raw = await fs.readFile(reviewMdPath, "utf8");
1339
+ const sections = extractH2Sections(raw);
1340
+ const securityBody = sectionBodyByName(sections, "Layer 2 Security")
1341
+ ?? sectionBodyByName(sections, "Layer 2b: Security")
1342
+ ?? sectionBodyByName(sections, "Layer 2 Findings");
1343
+ if (!securityBody) {
1344
+ errors.push('07-review.md is missing a Layer 2 security section.');
1345
+ return {
1346
+ ok: false,
1347
+ errors,
1348
+ hasSecurityFinding: false,
1349
+ hasNoChangeAttestation: false
1350
+ };
1351
+ }
1352
+ const securityTableRowPattern = /^\|\s*[^|\n]+\|\s*[^|\n]+\|\s*security\s*\|\s*[^|\n]+\|\s*[^|\n]+\|/imu;
1353
+ const securityBulletPattern = /^[*-]\s+.*\b(?:security|auth|injection|secret|credential|permission)\b/imu;
1354
+ const hasSecurityFinding = securityTableRowPattern.test(securityBody) || securityBulletPattern.test(securityBody);
1355
+ const attestationMatch = /NO_CHANGE_ATTESTATION\s*:\s*(.*)/iu.exec(securityBody);
1356
+ const hasNoChangeAttestation = Boolean(attestationMatch && attestationMatch[1]?.trim().length > 0);
1357
+ if (attestationMatch && attestationMatch[1]?.trim().length === 0) {
1358
+ errors.push("NO_CHANGE_ATTESTATION must include a non-empty rationale.");
1359
+ }
1360
+ if (!hasSecurityFinding && !hasNoChangeAttestation) {
1361
+ errors.push("Layer 2 security evidence missing: include at least one security finding or `NO_CHANGE_ATTESTATION: <reason>`.");
1362
+ }
1363
+ return {
1364
+ ok: errors.length === 0,
1365
+ errors,
1366
+ hasSecurityFinding,
1367
+ hasNoChangeAttestation
1368
+ };
1369
+ }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FlowTrack, HarnessId, LanguageRulePack, VibyConfig } from "./types.js";
1
+ import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack } from "./types.js";
2
2
  export declare function configPath(projectRoot: string): string;
3
3
  /**
4
4
  * Default test-path patterns used by workflow-guard.sh to classify TDD writes.
@@ -20,7 +20,7 @@ export declare const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
20
20
  * regardless of whether the user wrote `strictness`, the legacy keys, both,
21
21
  * or neither.
22
22
  */
23
- export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrack?: FlowTrack): VibyConfig;
23
+ export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrack?: FlowTrack): CclawConfig;
24
24
  /**
25
25
  * Probe common project-root manifests to infer which language rule packs the
26
26
  * user would reasonably want. Pure-functional best-effort: any filesystem
@@ -31,9 +31,9 @@ export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrac
31
31
  * never surprise a user who intentionally cleared the list.
32
32
  */
33
33
  export declare function detectLanguageRulePacks(projectRoot: string): Promise<LanguageRulePack[]>;
34
- export declare function readConfig(projectRoot: string): Promise<VibyConfig>;
34
+ export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
35
35
  /**
36
- * Fields that live on the populated runtime `VibyConfig` but are considered
36
+ * Fields that live on the populated runtime `CclawConfig` but are considered
37
37
  * "advanced" — we keep them in the in-memory object so downstream callers
38
38
  * don't have to branch, but we do **not** write them to `config.yaml` unless
39
39
  * the user set them explicitly. Keeps the default template small and honest:
@@ -43,7 +43,7 @@ type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" |
43
43
  /**
44
44
  * Options controlling the serialisation shape of `config.yaml`.
45
45
  *
46
- * - `"full"` (default): write every field on the `VibyConfig` object that
46
+ * - `"full"` (default): write every field on the `CclawConfig` object that
47
47
  * isn't `undefined`. Preserves existing shapes and keeps legacy callers
48
48
  * working without migration.
49
49
  * - `"minimal"`: write only the user-facing knobs (`MINIMAL_CONFIG_KEYS`)
@@ -60,7 +60,7 @@ export interface WriteConfigOptions {
60
60
  mode?: "full" | "minimal";
61
61
  advancedKeysPresent?: ReadonlySet<AdvancedConfigKey>;
62
62
  }
63
- export declare function writeConfig(projectRoot: string, config: VibyConfig, options?: WriteConfigOptions): Promise<void>;
63
+ export declare function writeConfig(projectRoot: string, config: CclawConfig, options?: WriteConfigOptions): Promise<void>;
64
64
  /**
65
65
  * Enumerate which advanced keys are currently set in the on-disk config.
66
66
  * Used by `cclaw upgrade` to preserve the user's existing shape — if they
package/dist/config.js CHANGED
@@ -44,6 +44,22 @@ const MINIMAL_CONFIG_KEYS = [
44
44
  ];
45
45
  const DEFAULT_SLICE_REVIEW_THRESHOLD = 5;
46
46
  const DEFAULT_SLICE_REVIEW_TRACKS = ["standard"];
47
+ const emittedConfigWarnings = new Set();
48
+ function emitConfigWarningOnce(code, message) {
49
+ const key = `${code}:${message}`;
50
+ if (emittedConfigWarnings.has(key)) {
51
+ return;
52
+ }
53
+ emittedConfigWarnings.add(key);
54
+ process.emitWarning(message, { code });
55
+ }
56
+ function sameStringArray(a, b) {
57
+ if (!a || !b)
58
+ return false;
59
+ if (a.length !== b.length)
60
+ return false;
61
+ return a.every((value, index) => value === b[index]);
62
+ }
47
63
  function configFixExample() {
48
64
  return `harnesses:
49
65
  - claude
@@ -244,6 +260,12 @@ export async function readConfig(projectRoot) {
244
260
  explicitTddTestPathPatterns = validateStringArray(tddRaw.testPathPatterns, "tdd.testPathPatterns", fullPath);
245
261
  explicitTddProductionPathPatterns = validateStringArray(tddRaw.productionPathPatterns, "tdd.productionPathPatterns", fullPath);
246
262
  }
263
+ if (tddTestGlobsRaw !== undefined &&
264
+ explicitTddTestPathPatterns !== undefined &&
265
+ !sameStringArray(tddTestGlobs, explicitTddTestPathPatterns)) {
266
+ emitConfigWarningOnce("CCLAW_CONFIG_DEPRECATED_TDD_TEST_GLOBS", `[cclaw] Both "tddTestGlobs" (deprecated) and "tdd.testPathPatterns" are set in ${fullPath}. ` +
267
+ `Using "tdd.testPathPatterns".`);
268
+ }
247
269
  const resolvedTddTestPathPatterns = [
248
270
  ...(explicitTddTestPathPatterns ?? tddTestGlobs ?? DEFAULT_TDD_TEST_PATH_PATTERNS)
249
271
  ];
@@ -15,7 +15,16 @@ export declare const EVALS_CONFIG_PATH = ".cclaw/evals/config.yaml";
15
15
  export declare const EVALS_DIRS: readonly [".cclaw/evals", ".cclaw/evals/corpus", ".cclaw/evals/rubrics", ".cclaw/evals/baselines", ".cclaw/evals/reports"];
16
16
  export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/worktrees", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks", ".cclaw/custom-skills", ".cclaw/evals", ".cclaw/evals/corpus", ".cclaw/evals/rubrics", ".cclaw/evals/baselines", ".cclaw/evals/reports"];
17
17
  export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", "# cclaw evals: user-owned, track in git", "!.cclaw/evals/", "!.cclaw/evals/config.yaml", "!.cclaw/evals/corpus/", "!.cclaw/evals/corpus/**", "!.cclaw/evals/rubrics/", "!.cclaw/evals/rubrics/**", "!.cclaw/evals/baselines/", "!.cclaw/evals/baselines/**", ".claude/commands/cc-*.md", ".claude/commands/cc.md", ".cursor/commands/cc-*.md", ".cursor/commands/cc.md", ".opencode/commands/cc-*.md", ".opencode/commands/cc.md", ".agents/skills/cc/SKILL.md", ".agents/skills/cc-*/SKILL.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json", ".opencode/plugins/cclaw-plugin.mjs", ".cursor/rules/cclaw-workflow.mdc"];
18
- export declare const COMMAND_FILE_ORDER: FlowStage[];
18
+ /**
19
+ * Canonical stage -> skill folder mapping.
20
+ *
21
+ * Intentional divergence from stage ids:
22
+ * - stage ids stay short and flow-oriented (`spec`, `tdd`, `ship`)
23
+ * - skill folders stay descriptive and user-facing for `.cclaw/skills/*`.
24
+ *
25
+ * Keep this map as the single source of truth for generated skill paths.
26
+ */
27
+ export declare const STAGE_TO_SKILL_FOLDER: Record<FlowStage, string>;
19
28
  export declare const UTILITY_COMMANDS: readonly ["learn", "next", "ideate", "view", "status", "tree", "diff", "ops", "feature", "tdd-log", "retro", "compound", "archive", "rewind"];
20
29
  export declare const SUBAGENT_SKILL_FOLDERS: readonly ["subagent-dev", "parallel-dispatch"];
21
30
  export type UtilityCommand = (typeof UTILITY_COMMANDS)[number];
package/dist/constants.js CHANGED
@@ -103,16 +103,25 @@ export const REQUIRED_GITIGNORE_PATTERNS = [
103
103
  ".opencode/plugins/cclaw-plugin.mjs",
104
104
  ".cursor/rules/cclaw-workflow.mdc"
105
105
  ];
106
- export const COMMAND_FILE_ORDER = [
107
- "brainstorm",
108
- "scope",
109
- "design",
110
- "spec",
111
- "plan",
112
- "tdd",
113
- "review",
114
- "ship"
115
- ];
106
+ /**
107
+ * Canonical stage -> skill folder mapping.
108
+ *
109
+ * Intentional divergence from stage ids:
110
+ * - stage ids stay short and flow-oriented (`spec`, `tdd`, `ship`)
111
+ * - skill folders stay descriptive and user-facing for `.cclaw/skills/*`.
112
+ *
113
+ * Keep this map as the single source of truth for generated skill paths.
114
+ */
115
+ export const STAGE_TO_SKILL_FOLDER = {
116
+ brainstorm: "brainstorming",
117
+ scope: "scope-shaping",
118
+ design: "engineering-design-lock",
119
+ spec: "specification-authoring",
120
+ plan: "planning-and-task-breakdown",
121
+ tdd: "test-driven-development",
122
+ review: "two-layer-review",
123
+ ship: "shipping-and-handoff"
124
+ };
116
125
  export const UTILITY_COMMANDS = [
117
126
  "learn",
118
127
  "next",
@@ -1,2 +1,2 @@
1
1
  import type { FlowStage } from "../types.js";
2
- export declare function commandContract(stage: FlowStage): string;
2
+ export declare function stageCommandContract(stage: FlowStage): string;
@@ -1,6 +1,6 @@
1
1
  import { stageSchema } from "./stage-schema.js";
2
2
  import { stageSkillFolder } from "./skills.js";
3
- export function commandContract(stage) {
3
+ export function stageCommandContract(stage) {
4
4
  const schema = stageSchema(stage);
5
5
  const skillPath = `.cclaw/skills/${stageSkillFolder(stage)}/SKILL.md`;
6
6
  const reads = schema.crossStageTrace.readsFrom;
@@ -1,4 +1,5 @@
1
1
  import { HARNESS_ADAPTERS, harnessTier } from "../harness-adapters.js";
2
+ import { STAGE_TO_SKILL_FOLDER } from "../constants.js";
2
3
  import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./hook-events.js";
3
4
  import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName } from "./harness-playbooks.js";
4
5
  import { HARNESS_TOOL_REFS_DIR } from "./harness-tool-refs.js";
@@ -23,6 +24,15 @@ function tierDescription(tier) {
23
24
  }
24
25
  export function harnessIntegrationDocMarkdown() {
25
26
  const harnesses = Object.keys(HARNESS_ADAPTERS);
27
+ const stageSkillRows = Object.entries(STAGE_TO_SKILL_FOLDER)
28
+ .map(([stage, skillFolder]) => `| \`${stage}\` | \`${skillFolder}\` |`)
29
+ .join("\n");
30
+ const hookCasingRows = [
31
+ "| Claude Code | `claude` | PascalCase (`SessionStart`, `PreToolUse`) |",
32
+ "| Cursor | `cursor` | camelCase (`sessionStart`, `preToolUse`) |",
33
+ "| OpenCode | `opencode` | camelCase (`sessionStart`, `preToolUse`) |",
34
+ "| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `PreToolUse`) |"
35
+ ].join("\n");
26
36
  const capabilityRows = harnesses
27
37
  .map((harness) => {
28
38
  const adapter = HARNESS_ADAPTERS[harness];
@@ -75,6 +85,17 @@ Design-stage research fleet uses the same parity model:
75
85
  |---|---|---|---|---|
76
86
  ${hookRows}
77
87
 
88
+ ## Hook event casing
89
+
90
+ Hook keys are intentionally harness-native and must not be normalized:
91
+
92
+ | Harness | ID | Event key casing |
93
+ |---|---|---|
94
+ ${hookCasingRows}
95
+
96
+ Use the exact event names from each harness schema. Treating all hooks as one
97
+ shared casing silently breaks generated wiring.
98
+
78
99
  ## Interpretation
79
100
 
80
101
  - \`tier1\`: full native delegation + structured asks + full hook surface.
@@ -91,7 +112,7 @@ All harnesses receive the same utility commands:
91
112
 
92
113
  - \`/cc\` - flow entry and resume
93
114
  - \`/cc-next\` - stage progression
94
- - \`/cc-ideate\` - discovery mode for ranked repo-improvement backlog
115
+ - \`/cc-ideate\` - ideate mode for ranked repo-improvement backlog
95
116
  - \`/cc-view\` - read-only router for status/tree/diff
96
117
  - \`/cc-ops\` - operations router for feature/tdd-log/retro/compound/archive/rewind
97
118
 
@@ -112,6 +133,16 @@ Operations subcommands:
112
133
  Stage order remains canonical:
113
134
  \`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship\`
114
135
 
136
+ ## Stage -> skill folder mapping
137
+
138
+ | Stage | Skill folder |
139
+ |---|---|
140
+ ${stageSkillRows}
141
+
142
+ This map is generated from \`src/constants.ts::STAGE_TO_SKILL_FOLDER\` so
143
+ skill-path naming stays explicit and stable even when stage ids differ from
144
+ folder names.
145
+
115
146
  ## Install surfaces
116
147
 
117
148
  Always generated:
@@ -95,9 +95,9 @@ generic dispatcher with a strict role prompt.
95
95
  ## Dispatch pattern
96
96
 
97
97
  1. Pick the mapped \`subagent_type\` from the table above.
98
- 2. Build the \`prompt\` from the cclaw agent contract in
98
+ 2. Build the \`prompt\` from the cclaw agent role brief in
99
99
  \`.cclaw/agents/<agent>.md\`, prefaced with a single line naming the
100
- cclaw role (\`You are the cclaw <agent>. Follow the contract below.\`).
100
+ cclaw role (\`You are the cclaw <agent>. Follow the role brief below.\`).
101
101
  3. Set \`readonly: true\` when the table says yes — Cursor enforces it.
102
102
  4. Before dispatch, append a delegation row:
103
103
 
@@ -142,7 +142,7 @@ description: "OpenCode has plugin-based dispatch hooks and a native structured-a
142
142
  **Fallback: role-switch.** OpenCode exposes tool/session event hooks via a
143
143
  plugin but does not provide an isolated subagent worker. cclaw closes the
144
144
  delegation gate by role-switching inside the same session: the agent
145
- announces the role, performs the work against the contract, and records
145
+ announces the role, performs the work against the role brief, and records
146
146
  evidence.
147
147
 
148
148
  **Structured ask: native \`question\` tool.** OpenCode ships a first-class
@@ -168,7 +168,7 @@ artifact decision log. Full mapping:
168
168
  > Acting as cclaw **<agent>** per \`.cclaw/agents/<agent>.md\`. No other
169
169
  > role may be assumed until the delegation row is closed.
170
170
 
171
- 2. Execute the role's contract. Do NOT interleave other roles' work.
171
+ 2. Execute the role's brief. Do NOT interleave other roles' work.
172
172
  3. Write the result into the stage artifact (e.g. TDD work lands in
173
173
  \`.cclaw/artifacts/06-tdd.md\`).
174
174
  4. Append a delegation row:
@@ -2,13 +2,13 @@ import { RUNTIME_ROOT } from "../constants.js";
2
2
  const IDEATE_SKILL_FOLDER = "flow-ideate";
3
3
  const IDEATE_SKILL_NAME = "flow-ideate";
4
4
  /**
5
- * Directory + filename convention for ideation artifacts. These are separate
5
+ * Directory + filename convention for ideate artifacts. These are separate
6
6
  * from stage artifacts (00-..08-*.md) because `/cc-ideate` runs outside the
7
7
  * critical-path flow state machine and must not collide with stage numbering.
8
8
  */
9
- const IDEATION_ARTIFACT_GLOB = ".cclaw/artifacts/ideation-*.md";
10
- const IDEATION_ARTIFACT_PATTERN = ".cclaw/artifacts/ideation-<YYYY-MM-DD-slug>.md";
11
- const IDEATION_RESUME_WINDOW_DAYS = 30;
9
+ const IDEATE_ARTIFACT_GLOB = ".cclaw/artifacts/ideate-*.md";
10
+ const IDEATE_ARTIFACT_PATTERN = ".cclaw/artifacts/ideate-<YYYY-MM-DD-slug>.md";
11
+ const IDEATE_RESUME_WINDOW_DAYS = 30;
12
12
  /**
13
13
  * Structured-ask tool list reused across cclaw skills. Kept inline here (small
14
14
  * enough) to avoid cross-module coupling; larger stage skills cite the shared
@@ -23,25 +23,25 @@ export function ideateCommandContract() {
23
23
 
24
24
  ## Purpose
25
25
 
26
- Repository-improvement discovery mode. Generate a ranked backlog of
26
+ Repository-improvement ideate mode. Generate a ranked backlog of
27
27
  high-value improvements, persist it as an artifact on disk, and end with
28
28
  an explicit handoff — either launch \`/cc\` on a chosen candidate in the
29
29
  same session, or save/discard the backlog.
30
30
 
31
31
  ## HARD-GATE
32
32
 
33
- - Discovery mode only. Never mutate \`.cclaw/state/flow-state.json\`.
33
+ - Ideate mode only. Never mutate \`.cclaw/state/flow-state.json\`.
34
34
  - Every recommendation cites evidence from the current repository
35
35
  (file path, command output, or knowledge-store entry id).
36
36
  - Always write a persisted artifact to
37
- \`${IDEATION_ARTIFACT_PATTERN}\`. Chat-only output is not acceptable —
37
+ \`${IDEATE_ARTIFACT_PATTERN}\`. Chat-only output is not acceptable —
38
38
  the next session must be able to resume.
39
39
  - Always end with a structured handoff prompt, not an open question.
40
40
 
41
41
  ## Algorithm
42
42
 
43
- 1. **Resume check.** Glob \`${IDEATION_ARTIFACT_GLOB}\`. If any artifact
44
- has been modified within the last ${IDEATION_RESUME_WINDOW_DAYS} days,
43
+ 1. **Resume check.** Glob \`${IDEATE_ARTIFACT_GLOB}\`. If any artifact
44
+ has been modified within the last ${IDEATE_RESUME_WINDOW_DAYS} days,
45
45
  offer the user: continue that backlog, start fresh, or cancel.
46
46
  2. **Scan repo signals:**
47
47
  - open TODO/FIXME/XXX/HACK notes,
@@ -54,7 +54,7 @@ same session, or save/discard the backlog.
54
54
  per candidate.
55
55
  4. **Rank by impact/effort**, recommend the top item.
56
56
  5. **Write the artifact** at
57
- \`${IDEATION_ARTIFACT_PATTERN}\` using the schema in the skill.
57
+ \`${IDEATE_ARTIFACT_PATTERN}\` using the schema in the skill.
58
58
  6. **Present the handoff prompt** with four concrete options — not A/B/C
59
59
  letters. Default = "Start /cc on the top recommendation".
60
60
 
@@ -66,7 +66,7 @@ same session, or save/discard the backlog.
66
66
  export function ideateCommandSkillMarkdown() {
67
67
  return `---
68
68
  name: ${IDEATE_SKILL_NAME}
69
- description: "Repository ideation mode: detect and rank high-leverage improvements, persist a backlog artifact, and hand off to /cc or save/discard."
69
+ description: "Repository ideate mode: detect and rank high-leverage improvements, persist a backlog artifact, and hand off to /cc or save/discard."
70
70
  ---
71
71
 
72
72
  # /cc-ideate
@@ -75,12 +75,12 @@ description: "Repository ideation mode: detect and rank high-leverage improvemen
75
75
 
76
76
  "Using flow-ideate to identify highest-leverage improvements in this
77
77
  repository. Will persist a ranked backlog to
78
- \`${IDEATION_ARTIFACT_PATTERN}\` and end with an explicit handoff."
78
+ \`${IDEATE_ARTIFACT_PATTERN}\` and end with an explicit handoff."
79
79
 
80
80
  ## HARD-GATE
81
81
 
82
82
  - Do not start coding in ideate mode.
83
- - Do not mutate \`.cclaw/state/flow-state.json\` — ideation sits outside
83
+ - Do not mutate \`.cclaw/state/flow-state.json\` — ideate mode sits outside
84
84
  the critical-path flow.
85
85
  - Always produce the artifact file on disk before presenting the handoff.
86
86
  - Always end with a structured handoff that names the concrete follow-up
@@ -91,12 +91,12 @@ repository. Will persist a ranked backlog to
91
91
  ### Phase 0 — Resume check
92
92
 
93
93
  1. Use the harness's file-glob tool (\`Glob\` pattern
94
- \`${IDEATION_ARTIFACT_GLOB}\` or equivalent \`ls\`/\`find\`).
95
- 2. Filter to files modified within the last ${IDEATION_RESUME_WINDOW_DAYS} days.
94
+ \`${IDEATE_ARTIFACT_GLOB}\` or equivalent \`ls\`/\`find\`).
95
+ 2. Filter to files modified within the last ${IDEATE_RESUME_WINDOW_DAYS} days.
96
96
  3. If one or more match, present **one** structured ask using the
97
97
  harness's native tool (${STRUCTURED_ASK_TOOLS}) with options:
98
98
  - **Continue the existing backlog** — read the most-recent
99
- ideation-*.md and work from its candidate list; skip re-scanning.
99
+ ideate-*.md and work from its candidate list; skip re-scanning.
100
100
  - **Start a fresh scan** — proceed to Phase 1; the old artifact stays
101
101
  on disk for history.
102
102
  - **Cancel** — stop; do not scan or write anything.
@@ -137,10 +137,10 @@ Aim for 5–10 candidates. Do not invent candidates without evidence.
137
137
  1. Sort by impact/effort ratio; break ties with confidence.
138
138
  2. Compute the artifact filename:
139
139
  - \`slug\` = first 3–5 words of the top recommendation, lowercase,
140
- non-alphanumeric collapsed to \`-\`, trimmed. When ideation is
140
+ non-alphanumeric collapsed to \`-\`, trimmed. When ideate mode is
141
141
  focus-hinted (user passed an argument), use the focus hint instead.
142
142
  - \`date\` = today in \`YYYY-MM-DD\` (local time).
143
- - Path = \`.cclaw/artifacts/ideation-<date>-<slug>.md\`.
143
+ - Path = \`.cclaw/artifacts/ideate-<date>-<slug>.md\`.
144
144
  3. Use the harness's write-file tool (\`Write\`, \`apply_patch\`, or shell
145
145
  \`cat <<EOF > path\`) to create the artifact with this schema:
146
146
 
@@ -195,7 +195,7 @@ lettered list with the same four labels. Do not invent extra options.
195
195
  - **Start /cc on I-1** or **different candidate:** announce
196
196
  "Handing off to /cc <phrase>" and load the \`using-cclaw\` router
197
197
  skill. From there, the normal \`/cc\` classification and stage flow
198
- takes over. Do not produce a second artifact; the ideation file is
198
+ takes over. Do not produce a second artifact; the ideate file is
199
199
  preserved as the origin document for this run.
200
200
  - **Save and close:** reply with the artifact path and stop.
201
201
  - **Discard:** delete the artifact file, confirm deletion, stop.
@@ -1,4 +1,4 @@
1
- import { RUNTIME_ROOT } from "../constants.js";
1
+ import { RUNTIME_ROOT, STAGE_TO_SKILL_FOLDER } from "../constants.js";
2
2
  import { STAGE_EXAMPLES_REFERENCE_DIR, stageDomainExamples, stageExamples, stageGoodBadExamples } from "./examples.js";
3
3
  import { STAGE_COMMON_GUIDANCE_REL_PATH } from "./stage-common-guidance.js";
4
4
  import { stageAutoSubagentDispatch, stageSchema } from "./stage-schema.js";
@@ -295,7 +295,7 @@ After T-3 REFACTOR, before declaring Batch 1 done:
295
295
  - The same RED failure reappears after a GREEN change → **escalate** per the 3-attempts rule; do not keep patching.
296
296
  `;
297
297
  export function stageSkillFolder(stage) {
298
- return stageSchema(stage).skillFolder;
298
+ return STAGE_TO_SKILL_FOLDER[stage];
299
299
  }
300
300
  export function stageSkillMarkdown(stage, track = "standard") {
301
301
  const schema = stageSchema(stage, track);