cclaw-cli 0.51.18 → 0.51.21

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.
@@ -485,15 +485,39 @@ const SCOPE_MODE_SHORT_TOKEN_REGEX = /\b(?:hold(?:[\s_-]?scope)?|selective(?:[\s
485
485
  // not the wording of the rationale.
486
486
  const NEXT_STAGE_HANDOFF_REGEX = /(?:`(?:design|spec)`|\bdesign\b|\bspec\b|next[-\s_]stage|next stage|handoff|hand[-\s]off)/iu;
487
487
  function hasCanonicalScopeMode(body) {
488
- if (SCOPE_MODE_FULL_REGEX.test(body))
489
- return true;
488
+ // Strict: a Mode: / Selected mode: line that picks exactly ONE canonical mode
489
+ // is the strongest signal. The template scaffolding contains all four mode
490
+ // tokens inside an instructional `(one of ...)` placeholder; we ignore that
491
+ // line so authors who never replace the scaffolding still fail validation.
490
492
  for (const match of body.matchAll(new RegExp(SCOPE_MODE_LINE_REGEX, "giu"))) {
491
- const value = match[1] ?? "";
492
- if (SCOPE_MODE_SHORT_TOKEN_REGEX.test(value))
493
+ const raw = (match[1] ?? "").trim();
494
+ const sanitized = raw.replace(/\(.*?\)/gu, "").trim();
495
+ if (sanitized.length === 0)
496
+ continue;
497
+ if (countCanonicalModeMentions(sanitized) === 1)
498
+ return true;
499
+ if (countCanonicalModeMentions(sanitized) === 0 && SCOPE_MODE_SHORT_TOKEN_REGEX.test(sanitized))
500
+ return true;
501
+ }
502
+ // Fallback: any line outside an instructional `(one of ...)` placeholder
503
+ // names exactly one mode. Block lines that list multiple modes (the
504
+ // unfilled template) or are wrapped in an instructional parenthetical.
505
+ for (const rawLine of body.split(/\r?\n/u)) {
506
+ const line = rawLine.trim();
507
+ if (line.length === 0)
508
+ continue;
509
+ if (/\(\s*one\s+of\b/iu.test(line))
510
+ continue;
511
+ const sanitized = line.replace(/\(.*?\)/gu, "");
512
+ if (countCanonicalModeMentions(sanitized) === 1)
493
513
  return true;
494
514
  }
495
515
  return false;
496
516
  }
517
+ function countCanonicalModeMentions(text) {
518
+ const matches = text.match(new RegExp(SCOPE_MODE_FULL_REGEX, "giu"));
519
+ return matches ? matches.length : 0;
520
+ }
497
521
  function validatePremiseChallenge(sectionBody) {
498
522
  // gstack-style premise challenge requires a real Q/A structure (table or
499
523
  // list), not free-form prose. The validation is *structural* only — we do
@@ -1051,10 +1075,12 @@ function validateTddGreenEvidence(sectionBody) {
1051
1075
  };
1052
1076
  }
1053
1077
  function validateVerificationLadder(sectionBody) {
1054
- if (!/highest tier reached/iu.test(sectionBody)) {
1078
+ const hasTextLine = /highest tier reached/iu.test(sectionBody);
1079
+ const hasCanonicalTable = hasVerificationLadderTableRow(sectionBody);
1080
+ if (!hasTextLine && !hasCanonicalTable) {
1055
1081
  return {
1056
1082
  ok: false,
1057
- details: "Verification Ladder must include a 'Highest tier reached' line."
1083
+ details: "Verification Ladder must include either a 'Highest tier reached' line or a canonical table row (Slice | Tier reached | Evidence) with non-empty tier and evidence."
1058
1084
  };
1059
1085
  }
1060
1086
  if (!/\b(static|command|behavioral|human)\b/iu.test(sectionBody)) {
@@ -1074,6 +1100,49 @@ function validateVerificationLadder(sectionBody) {
1074
1100
  details: "Verification Ladder includes tier + evidence fields."
1075
1101
  };
1076
1102
  }
1103
+ function hasVerificationLadderTableRow(sectionBody) {
1104
+ const lines = sectionBody.split(/\r?\n/u);
1105
+ let sawHeader = false;
1106
+ let sawSeparator = false;
1107
+ for (const line of lines) {
1108
+ const trimmed = line.trim();
1109
+ if (!trimmed.startsWith("|")) {
1110
+ sawHeader = false;
1111
+ sawSeparator = false;
1112
+ continue;
1113
+ }
1114
+ const cells = trimmed
1115
+ .replace(/^\|/u, "")
1116
+ .replace(/\|$/u, "")
1117
+ .split("|")
1118
+ .map((cell) => cell.trim());
1119
+ if (!sawHeader) {
1120
+ const lowered = cells.map((cell) => cell.toLowerCase());
1121
+ const hasTierColumn = lowered.some((cell) => /tier(?:\s+reached)?/u.test(cell));
1122
+ const hasEvidenceColumn = lowered.some((cell) => cell.includes("evidence"));
1123
+ if (hasTierColumn && hasEvidenceColumn) {
1124
+ sawHeader = true;
1125
+ continue;
1126
+ }
1127
+ continue;
1128
+ }
1129
+ if (!sawSeparator) {
1130
+ if (cells.every((cell) => /^[:\-\s]+$/u.test(cell))) {
1131
+ sawSeparator = true;
1132
+ continue;
1133
+ }
1134
+ sawHeader = false;
1135
+ continue;
1136
+ }
1137
+ if (cells.length >= 2 && cells.some((cell) => /\b(static|command|behavioral|human)\b/iu.test(cell))) {
1138
+ const evidenceCellHasContent = cells.some((cell) => cell.length > 0 && !/^\s*$/u.test(cell) && !/^[:\-\s]+$/u.test(cell));
1139
+ if (evidenceCellHasContent) {
1140
+ return true;
1141
+ }
1142
+ }
1143
+ }
1144
+ return false;
1145
+ }
1077
1146
  const LEARNING_TYPE_SET = new Set(["rule", "pattern", "lesson", "compound"]);
1078
1147
  const LEARNING_CONFIDENCE_SET = new Set(["high", "medium", "low"]);
1079
1148
  const LEARNING_SEVERITY_SET = new Set(["critical", "important", "suggestion"]);
@@ -1786,6 +1855,20 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1786
1855
  ? "Selected Direction is traceable to prior user reaction."
1787
1856
  : "Selected Direction is not traceable to user reaction. Add `## Approach Reaction` before it, or mention the user's reaction/concerns in the rationale."
1788
1857
  });
1858
+ // Track-aware handoff: standard track goes to `scope`; medium track
1859
+ // goes directly to `spec`; the quick track skips brainstorm entirely.
1860
+ // We accept either canonical successor token plus a generic
1861
+ // `next-stage` / `handoff` phrase to preserve i18n flexibility.
1862
+ const handoffTrace = /(?:`(?:scope|spec)`|\bscope\b|\bspec\b|next[-\s_]stage|next stage|\bhandoff\b|hand[-\s]off)/iu.test(directionBody);
1863
+ findings.push({
1864
+ section: "Direction Next-Stage Handoff",
1865
+ required: true,
1866
+ rule: "Selected Direction must record the track-aware next-stage handoff (mention `scope` for standard, `spec` for medium, or include a `Next-stage handoff:` line).",
1867
+ found: handoffTrace,
1868
+ details: handoffTrace
1869
+ ? "Selected Direction names the next-stage handoff."
1870
+ : "Selected Direction is missing a next-stage handoff token. Mention `scope` (standard) or `spec` (medium), or add a `Next-stage handoff:` line so downstream stages can trace the contract."
1871
+ });
1789
1872
  }
1790
1873
  }
1791
1874
  const shortCircuitBody = brainstormShortCircuitBody;
@@ -48,6 +48,7 @@ const STAGE_EXAMPLES = {
48
48
  - **Approach:** A — Reusable validation module
49
49
  - **Rationale:** based on user reaction favoring fast delivery and lower complexity, shared TS module gives consistent behavior in CI/local, avoids script duplication, and stays within the no-new-dependency constraint.
50
50
  - **Approval:** approved
51
+ - **Next-stage handoff:** \`scope\` — carry the locked stack constraints and the validator module boundary forward.
51
52
 
52
53
  ## Design
53
54
 
@@ -1,8 +1,5 @@
1
- import { HOOK_MANIFEST_HARNESSES, semanticEventCoverage } from "./hook-manifest.js";
1
+ import { semanticEventCoverage } from "./hook-manifest.js";
2
2
  export { HOOK_SEMANTIC_EVENTS } from "./hook-manifest.js";
3
- function isManifestHarness(value) {
4
- return HOOK_MANIFEST_HARNESSES.includes(value);
5
- }
6
3
  /**
7
4
  * OpenCode is covered by the inline plugin (`opencode-plugin.ts`), not
8
5
  * by the generated `run-hook.mjs` dispatcher. We keep its semantic
@@ -28,4 +25,3 @@ export const HOOK_EVENTS_BY_HARNESS = Object.freeze({
28
25
  codex: semanticEventCoverage("codex"),
29
26
  opencode: OPENCODE_SEMANTIC_COVERAGE
30
27
  });
31
- void isManifestHarness;
@@ -265,15 +265,6 @@ async function writeJsonFile(filePath, value) {
265
265
  });
266
266
  }
267
267
 
268
- async function fileExists(filePath) {
269
- try {
270
- await fs.stat(filePath);
271
- return true;
272
- } catch {
273
- return false;
274
- }
275
- }
276
-
277
268
  async function readTextFile(filePath, fallback = "") {
278
269
  try {
279
270
  return await fs.readFile(filePath, "utf8");
@@ -625,7 +616,7 @@ function isCodeLikePath(rawPath) {
625
616
  }
626
617
 
627
618
  function isMutatingTool(toolLower) {
628
- return /^(write|edit|multiedit|multi_edit|delete|applypatch|apply_patch)$/u.test(toolLower);
619
+ return /^(write|edit|multiedit|multi_edit|delete|applypatch|apply_patch|notebookedit|notebook_edit)$/u.test(toolLower);
629
620
  }
630
621
 
631
622
  function isExecutionOrMutatingTool(toolLower) {
@@ -871,8 +862,6 @@ async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
871
862
  const action = typeof row.action === "string" ? row.action : "action";
872
863
  return "- [" + confidence + " • " + stage + " • " + domain + "] " + trigger + " -> " + action;
873
864
  });
874
- const body =
875
- relevant.length > 0 ? relevant.join("\\n") : "(no matching entries for current stage)";
876
865
  return {
877
866
  digestLines: relevant,
878
867
  learningsCount
@@ -1164,7 +1153,7 @@ async function handlePromptGuard(runtime) {
1164
1153
  const payloadLower = toLower(payloadText);
1165
1154
  const reasons = [];
1166
1155
 
1167
- if (/^(write|edit|multiedit|multi_edit|delete|applypatch|runcommand|shell|terminal|execcommand)$/u.test(toolLower)) {
1156
+ if (/^(write|edit|multiedit|multi_edit|delete|applypatch|notebookedit|runcommand|shell|terminal|execcommand)$/u.test(toolLower)) {
1168
1157
  // Artifacts, runs, and knowledge writes are part of normal stage flow.
1169
1158
  // Guard only managed internals that should be mutated via installer/CLI.
1170
1159
  if (/\\.cclaw\\/(state|hooks|skills|commands|agents)/u.test(payloadLower)) {
@@ -1,5 +1,5 @@
1
1
  import { RUNTIME_ROOT } from "../constants.js";
2
- import { HOOK_MANIFEST, groupBindingsByEvent } from "./hook-manifest.js";
2
+ import { groupBindingsByEvent } from "./hook-manifest.js";
3
3
  function hookDispatcherCommand(hookName) {
4
4
  // Dispatch through the polyglot .cmd wrapper so Windows harnesses can run
5
5
  // hooks even when command execution happens under CMD-style shells.
@@ -78,9 +78,7 @@ export function codexHooksJsonWithObservation() {
78
78
  * manifest without importing the private generator helpers.
79
79
  */
80
80
  export function hookManifestSnapshot() {
81
- return (HOOK_MANIFEST.length === 0
82
- ? ["claude", "cursor", "codex"]
83
- : ["claude", "cursor", "codex"]).map((harness) => ({
81
+ return ["claude", "cursor", "codex"].map((harness) => ({
84
82
  harness,
85
83
  events: groupBindingsByEvent(harness)
86
84
  }));
@@ -463,11 +463,10 @@ export default function cclawPlugin(ctx) {
463
463
  * \`DEFAULT_STRICTNESS = advisory\`, so the plugin can no longer
464
464
  * accidentally be the stricter half of a mismatched pair.
465
465
  */
466
- function readConfigStrictness() {
466
+ async function readConfigStrictness() {
467
467
  try {
468
468
  if (!existsSync(configPath)) return "";
469
- const { readFileSync } = require("node:fs");
470
- const raw = readFileSync(configPath, "utf8");
469
+ const raw = await readFileText(configPath);
471
470
  if (typeof raw !== "string" || raw.length === 0) return "";
472
471
  const match = raw.match(/^\\s*strictness\\s*:\\s*([A-Za-z0-9_-]+)/m);
473
472
  return match && typeof match[1] === "string" ? match[1].trim().toLowerCase() : "";
@@ -476,7 +475,7 @@ export default function cclawPlugin(ctx) {
476
475
  }
477
476
  }
478
477
 
479
- function resolveStrictness() {
478
+ async function resolveStrictness() {
480
479
  const envRaw = typeof process.env.CCLAW_STRICTNESS === "string"
481
480
  ? process.env.CCLAW_STRICTNESS.trim().toLowerCase()
482
481
  : "";
@@ -484,7 +483,7 @@ export default function cclawPlugin(ctx) {
484
483
  if (envRaw === "advisory" || envRaw === "off" || envRaw === "disabled" || envRaw === "none") {
485
484
  return "advisory";
486
485
  }
487
- const fileRaw = readConfigStrictness();
486
+ const fileRaw = await readConfigStrictness();
488
487
  if (fileRaw === "strict") return "strict";
489
488
  return "advisory";
490
489
  }
@@ -683,7 +682,7 @@ export default function cclawPlugin(ctx) {
683
682
  );
684
683
  return;
685
684
  }
686
- const strictness = resolveStrictness();
685
+ const strictness = await resolveStrictness();
687
686
  if (strictness !== "strict") {
688
687
  // Advisory mode (the default) — every guard refusal is a hint,
689
688
  // not a hard stop. Users report the "failure" as a log line
@@ -47,7 +47,6 @@ export declare function stageSchema(stage: FlowStage, track?: FlowTrack): StageS
47
47
  export declare function orderedStageSchemas(track?: FlowTrack): StageSchema[];
48
48
  export declare function stageGateIds(stage: FlowStage, track?: FlowTrack): string[];
49
49
  export declare function stageRecommendedGateIds(stage: FlowStage, track?: FlowTrack): string[];
50
- export declare function nextCclawCommand(stage: FlowStage): string;
51
50
  export declare function buildTransitionRules(): TransitionRule[];
52
51
  export declare function stagePolicyNeedles(stage: FlowStage, track?: FlowTrack): string[];
53
52
  export declare function stageTrackRenderContext(track?: FlowTrack): import("./track-render-context.js").TrackRenderContext;
@@ -258,7 +258,7 @@ const REQUIRED_ARTIFACT_SECTIONS = {
258
258
  "Deployment & Rollout",
259
259
  "Completion Dashboard"
260
260
  ],
261
- spec: ["Acceptance Criteria", "Edge Cases", "Assumptions Before Finalization", "Testability Map", "Approval"],
261
+ spec: ["Acceptance Criteria", "Edge Cases", "Assumptions Before Finalization", "Acceptance Mapping", "Approval"],
262
262
  plan: ["Task List", "Dependency Batches", "Acceptance Mapping", "Execution Posture", "WAIT_FOR_CONFIRM"],
263
263
  tdd: ["Test Discovery", "System-Wide Impact Check", "RED Evidence", "GREEN Evidence", "REFACTOR Notes", "Traceability", "Verification Ladder"],
264
264
  review: ["Layer 1 Verdict", "Review Findings Contract", "Severity Summary", "Final Verdict"],
@@ -577,10 +577,6 @@ export function stageRecommendedGateIds(stage, track = "standard") {
577
577
  .filter((gate) => gate.tier === "recommended")
578
578
  .map((gate) => gate.id);
579
579
  }
580
- export function nextCclawCommand(stage) {
581
- const next = stageSchema(stage).next;
582
- return next === "done" ? "none" : `/cc-${next}`;
583
- }
584
580
  export function buildTransitionRules() {
585
581
  const rules = [];
586
582
  const seen = new Set();
@@ -92,7 +92,7 @@ ${conversationLanguagePolicyMarkdown()}
92
92
  12. Load the **first-stage skill for the chosen track** and its command file:
93
93
  - quick → \`.cclaw/skills/specification-authoring/SKILL.md\`
94
94
  - medium/standard → \`.cclaw/skills/brainstorming/SKILL.md\`
95
- - trivial fast-path → design or spec skill per Phase 0 decision.
95
+ - trivial fast-path → spec skill per Phase 0 decision.
96
96
  13. Execute that stage with the prompt + Phase 1/Phase 2 + seed context as initial input.
97
97
 
98
98
  ### Reclassification on discovery
@@ -0,0 +1,4 @@
1
+ import type { StageAutoSubagentDispatch } from "./stages/schema-types.js";
2
+ type SubagentContextSkillId = NonNullable<StageAutoSubagentDispatch["skill"]>;
3
+ export declare const SUBAGENT_CONTEXT_SKILLS: Record<SubagentContextSkillId, string>;
4
+ export {};
@@ -0,0 +1,122 @@
1
+ function skillFrontmatter(name, description) {
2
+ return [
3
+ "---",
4
+ `name: ${name}`,
5
+ `description: ${JSON.stringify(description)}`,
6
+ "---",
7
+ ""
8
+ ].join("\n");
9
+ }
10
+ function tddCycleEvidenceSkill() {
11
+ return `${skillFrontmatter("tdd-cycle-evidence", "Evidence contract for the mandatory test-author delegation during RED/GREEN/REFACTOR.")}# TDD Cycle Evidence
12
+
13
+ Use with the \`test-author\` delegation in the \`tdd\` stage.
14
+
15
+ ## Required Output
16
+
17
+ - RED evidence: failing test command, failing assertion/error, and why it fails for the intended reason.
18
+ - GREEN evidence: implementation summary plus relevant passing command.
19
+ - REFACTOR evidence: changed/unchanged behavior statement plus full-suite or highest available verification command.
20
+ - Trace refs: plan task ID, acceptance criterion ID, and touched test files.
21
+
22
+ ## Guardrails
23
+
24
+ - No production code before RED evidence exists.
25
+ - If a RED test cannot be expressed, stop and route back to design/spec with the blocker.
26
+ - Record command output summaries, not just "tests passed".
27
+ `;
28
+ }
29
+ function reviewSpecPassSkill() {
30
+ return `${skillFrontmatter("review-spec-pass", "Spec compliance pass for the mandatory reviewer delegation during review.")}# Review Spec Pass
31
+
32
+ Use with the \`reviewer\` delegation in the \`review\` stage before broader code-quality findings.
33
+
34
+ ## Required Output
35
+
36
+ - For each acceptance criterion: PASS / PARTIAL / FAIL.
37
+ - Evidence refs grounded in files, tests, artifacts, or command output.
38
+ - Any mismatch between scope/design/spec/plan and implementation.
39
+ - Explicit list of Critical/Important blockers before ship.
40
+
41
+ ## Guardrails
42
+
43
+ - Do not trust implementer summaries; verify by reading artifacts/code.
44
+ - Keep spec compliance separate from style suggestions.
45
+ `;
46
+ }
47
+ function securityAuditSkill() {
48
+ return `${skillFrontmatter("security-audit", "Mandatory security sweep contract for the security-reviewer delegation.")}# Security Audit
49
+
50
+ Use with the \`security-reviewer\` delegation in the \`review\` stage.
51
+
52
+ ## Required Output
53
+
54
+ - Trust-boundary map: auth/authz, input validation, secrets, filesystem/network/process access, third-party calls.
55
+ - Findings with severity, exploitability, affected file/path, and concrete mitigation.
56
+ - NO_CHANGE_ATTESTATION when no security-relevant surface moved, with evidence for why.
57
+
58
+ ## Guardrails
59
+
60
+ - Pattern-scan the diff and touched modules before attesting no change.
61
+ - Security is mandatory in review even for small diffs.
62
+ `;
63
+ }
64
+ function adversarialReviewSkill() {
65
+ return `${skillFrontmatter("adversarial-review", "Second-opinion reviewer lens for high-risk review scenarios.")}# Adversarial Review
66
+
67
+ Use only when the review dispatch trigger says risk justifies a second opinion.
68
+
69
+ ## Required Output
70
+
71
+ - Attack the implementation assumptions, not the author.
72
+ - Look for hidden coupling, rollback gaps, data loss, race conditions, and untested edge cases.
73
+ - Mark each finding as confirmed, disproven, or needs-human-decision.
74
+
75
+ ## Guardrails
76
+
77
+ - Do not duplicate the mandatory reviewer pass.
78
+ - If no additional risk is found, say so explicitly and cite what was checked.
79
+ `;
80
+ }
81
+ function receivingCodeReviewSkill() {
82
+ return `${skillFrontmatter("receiving-code-review", "Workflow for triaging external reviewer, bot, or CI feedback during review.")}# Receiving Code Review
83
+
84
+ Use when external comments, bot findings, or CI annotations appear after the initial review pass.
85
+
86
+ ## Required Output
87
+
88
+ - Queue every feedback item with source, severity, requested change, and evidence.
89
+ - Disposition: accepted, rejected-with-evidence, accepted-risk, duplicate, or needs-user-decision.
90
+ - Mirror the queue into the review artifact so unresolved feedback cannot disappear.
91
+
92
+ ## Guardrails
93
+
94
+ - Do not silently dismiss bot/CI feedback.
95
+ - Re-run relevant checks after accepted fixes.
96
+ `;
97
+ }
98
+ function stackAwareReviewSkill() {
99
+ return `${skillFrontmatter("stack-aware-review", "Language/runtime-specific review lens selected from detected repo signals.")}# Stack-Aware Review
100
+
101
+ Use after the default reviewer/security-reviewer passes when repo signals identify a relevant stack.
102
+
103
+ ## Required Output
104
+
105
+ - Detected stack signal and why this lens applies.
106
+ - Stack-specific risks checked: package/build/test config, type/runtime boundaries, framework conventions, and deployment assumptions.
107
+ - Findings with evidence and whether they affect ship readiness.
108
+
109
+ ## Guardrails
110
+
111
+ - Do not run every stack lens unconditionally.
112
+ - Keep the default general reviewer pass intact; this is additive context, not a replacement.
113
+ `;
114
+ }
115
+ export const SUBAGENT_CONTEXT_SKILLS = {
116
+ "tdd-cycle-evidence": tddCycleEvidenceSkill(),
117
+ "review-spec-pass": reviewSpecPassSkill(),
118
+ "security-audit": securityAuditSkill(),
119
+ "adversarial-review": adversarialReviewSkill(),
120
+ "receiving-code-review": receivingCodeReviewSkill(),
121
+ "stack-aware-review": stackAwareReviewSkill()
122
+ };
@@ -510,7 +510,7 @@ ${SEED_SHELF_SECTION}
510
510
  |---|---|---|---|
511
511
  | | | | accepted/rejected/open |
512
512
 
513
- ## Testability Map
513
+ ## Acceptance Mapping
514
514
  | Criterion ID | Verification approach | Command/manual steps |
515
515
  |---|---|---|
516
516
  | AC-1 | | |
@@ -93,7 +93,6 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
93
93
  satisfied: boolean;
94
94
  missing: string[];
95
95
  waived: string[];
96
- autoWaived: string[];
97
96
  staleIgnored: string[];
98
97
  /** Delegation rows missing required evidence under a role-switch fallback. */
99
98
  missingEvidence: string[];
@@ -268,7 +268,6 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
268
268
  .map((e) => `${e.agent}(runId=${e.runId ?? "unknown"})`);
269
269
  const missing = [];
270
270
  const waived = [];
271
- const autoWaived = [];
272
271
  const missingEvidence = [];
273
272
  const config = await readConfig(projectRoot).catch(() => null);
274
273
  const harnesses = config?.harnesses ?? [];
@@ -277,7 +276,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
277
276
  for (const agent of mandatory) {
278
277
  const rows = forRun.filter((e) => e.agent === agent);
279
278
  const completedRows = rows.filter((e) => e.status === "completed");
280
- const waivedRows = rows.filter((e) => e.status === "waived");
279
+ const waivedRows = rows.filter((e) => e.status === "waived" && e.mode === "mandatory");
281
280
  const hasCompleted = completedRows.length >= 1;
282
281
  const hasWaived = waivedRows.length > 0;
283
282
  const ok = hasWaived || hasCompleted;
@@ -301,7 +300,6 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
301
300
  satisfied: missing.length === 0 && missingEvidence.length === 0,
302
301
  missing,
303
302
  waived,
304
- autoWaived,
305
303
  staleIgnored,
306
304
  missingEvidence,
307
305
  expectedMode
package/dist/doctor.js CHANGED
@@ -1461,9 +1461,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1461
1461
  name: "warning:delegation:waived",
1462
1462
  ok: true,
1463
1463
  details: delegation.waived.length > 0
1464
- ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}${delegation.autoWaived.length > 0
1465
- ? ` (auto-waived due to harness limitation: ${delegation.autoWaived.join(", ")})`
1466
- : ""}`
1464
+ ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}`
1467
1465
  : "no waived mandatory delegations for current stage"
1468
1466
  });
1469
1467
  checks.push({
package/dist/install.js CHANGED
@@ -21,6 +21,7 @@ import { REVIEW_PROMPTS } from "./content/review-prompts.js";
21
21
  import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
22
22
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LANGUAGE_RULE_PACK_GENERATORS, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
23
23
  import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
24
+ import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
24
25
  import { createInitialFlowState } from "./flow-state.js";
25
26
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
26
27
  import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
@@ -70,12 +71,9 @@ const DEPRECATED_UTILITY_SKILL_FOLDERS = [
70
71
  "source-driven-development",
71
72
  "frontend-accessibility",
72
73
  "landscape-check",
73
- "adversarial-review",
74
- "security-audit",
75
74
  "knowledge-curation",
76
75
  "retrospective",
77
76
  "document-review",
78
- "receiving-code-review",
79
77
  "flow-status",
80
78
  "flow-tree",
81
79
  "flow-diff"
@@ -387,18 +385,45 @@ async function writeSkills(projectRoot, config) {
387
385
  for (const [fileName, markdown] of Object.entries(REVIEW_PROMPTS)) {
388
386
  await writeFileSafe(runtimePath(projectRoot, "skills", "review-prompts", fileName), markdown);
389
387
  }
388
+ for (const [folderName, markdown] of Object.entries(SUBAGENT_CONTEXT_SKILLS)) {
389
+ await writeFileSafe(runtimePath(projectRoot, "skills", folderName, "SKILL.md"), markdown);
390
+ }
390
391
  // Language rule packs live under .cclaw/rules/lang/<pack>.md. They are opt-in:
391
392
  // only the packs listed in config.languageRulePacks are materialised. Any
392
393
  // legacy per-language skill folders from v0.7.0 (.cclaw/skills/language-*)
393
394
  // are cleaned up below so the new rules/lang layout is the only truth.
394
395
  const enabledPacks = config?.languageRulePacks ?? [];
396
+ const enabledPackFileNames = new Set();
395
397
  for (const pack of enabledPacks) {
396
398
  const fileName = LANGUAGE_RULE_PACK_FILES[pack];
397
399
  const generator = LANGUAGE_RULE_PACK_GENERATORS[pack];
398
400
  if (!fileName || !generator)
399
401
  continue;
402
+ enabledPackFileNames.add(fileName);
400
403
  await writeFileSafe(runtimePath(projectRoot, ...LANGUAGE_RULE_PACK_DIR, fileName), generator());
401
404
  }
405
+ // Strict idempotence: once a pack is removed from config, its generated
406
+ // file under .cclaw/rules/lang/ must disappear on the next sync. Without
407
+ // this loop the directory accumulates a superset of every pack ever
408
+ // enabled, which silently keeps stale guidance alive.
409
+ const langDir = runtimePath(projectRoot, ...LANGUAGE_RULE_PACK_DIR);
410
+ if (await exists(langDir)) {
411
+ const knownPackFileNames = new Set(Object.values(LANGUAGE_RULE_PACK_FILES));
412
+ let entries = [];
413
+ try {
414
+ entries = await fs.readdir(langDir);
415
+ }
416
+ catch {
417
+ entries = [];
418
+ }
419
+ for (const entry of entries) {
420
+ if (!knownPackFileNames.has(entry))
421
+ continue;
422
+ if (enabledPackFileNames.has(entry))
423
+ continue;
424
+ await fs.rm(path.join(langDir, entry), { force: true });
425
+ }
426
+ }
402
427
  for (const legacyFolder of LEGACY_LANGUAGE_RULE_PACK_FOLDERS) {
403
428
  const legacyPath = runtimePath(projectRoot, "skills", legacyFolder);
404
429
  if (await exists(legacyPath)) {
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
- import { canTransition, createInitialCloseoutState, createInitialFlowState, FLOW_STATE_SCHEMA_VERSION, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
4
+ import { nextStage, createInitialCloseoutState, createInitialFlowState, FLOW_STATE_SCHEMA_VERSION, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
5
5
  import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
6
6
  import { FLOW_STAGES } from "./types.js";
7
7
  export class InvalidStageTransitionError extends Error {
@@ -38,8 +38,11 @@ function validateFlowTransition(prev, next) {
38
38
  if (prev.currentStage === next.currentStage) {
39
39
  return;
40
40
  }
41
- if (!canTransition(prev.currentStage, next.currentStage)) {
42
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}". Use /cc-next to advance stages or archive the run to reset.`);
41
+ const naturalForward = nextStage(prev.currentStage, prev.track);
42
+ const isNaturalForward = naturalForward === next.currentStage;
43
+ const isReviewRewind = prev.currentStage === "review" && next.currentStage === "tdd";
44
+ if (!isNaturalForward && !isReviewRewind) {
45
+ throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}" for track "${prev.track}". Use /cc-next to advance stages or archive the run to reset.`);
43
46
  }
44
47
  }
45
48
  function flowStatePath(projectRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.18",
3
+ "version": "0.51.21",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {