cclaw-cli 0.51.19 → 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.
- package/dist/artifact-linter.js +89 -6
- package/dist/content/examples.js +1 -0
- package/dist/content/hook-events.js +1 -5
- package/dist/content/node-hooks.js +2 -13
- package/dist/content/observe.js +2 -4
- package/dist/content/opencode-plugin.js +5 -6
- package/dist/content/stage-schema.d.ts +0 -1
- package/dist/content/stage-schema.js +1 -5
- package/dist/content/start-command.js +1 -1
- package/dist/content/templates.js +1 -1
- package/dist/delegation.d.ts +0 -1
- package/dist/delegation.js +1 -3
- package/dist/doctor.js +1 -3
- package/dist/install.js +24 -0
- package/dist/run-persistence.js +6 -3
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -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
|
-
|
|
489
|
-
|
|
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
|
|
492
|
-
|
|
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
|
-
|
|
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;
|
package/dist/content/examples.js
CHANGED
|
@@ -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 {
|
|
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)) {
|
package/dist/content/observe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
2
|
-
import {
|
|
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 (
|
|
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
|
|
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", "
|
|
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 →
|
|
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
|
package/dist/delegation.d.ts
CHANGED
|
@@ -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[];
|
package/dist/delegation.js
CHANGED
|
@@ -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(", ")}
|
|
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
|
@@ -393,13 +393,37 @@ async function writeSkills(projectRoot, config) {
|
|
|
393
393
|
// legacy per-language skill folders from v0.7.0 (.cclaw/skills/language-*)
|
|
394
394
|
// are cleaned up below so the new rules/lang layout is the only truth.
|
|
395
395
|
const enabledPacks = config?.languageRulePacks ?? [];
|
|
396
|
+
const enabledPackFileNames = new Set();
|
|
396
397
|
for (const pack of enabledPacks) {
|
|
397
398
|
const fileName = LANGUAGE_RULE_PACK_FILES[pack];
|
|
398
399
|
const generator = LANGUAGE_RULE_PACK_GENERATORS[pack];
|
|
399
400
|
if (!fileName || !generator)
|
|
400
401
|
continue;
|
|
402
|
+
enabledPackFileNames.add(fileName);
|
|
401
403
|
await writeFileSafe(runtimePath(projectRoot, ...LANGUAGE_RULE_PACK_DIR, fileName), generator());
|
|
402
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
|
+
}
|
|
403
427
|
for (const legacyFolder of LEGACY_LANGUAGE_RULE_PACK_FOLDERS) {
|
|
404
428
|
const legacyPath = runtimePath(projectRoot, "skills", legacyFolder);
|
|
405
429
|
if (await exists(legacyPath)) {
|
package/dist/run-persistence.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
42
|
-
|
|
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) {
|