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.
- package/README.md +3 -1
- package/dist/artifact-linter.d.ts +7 -0
- package/dist/artifact-linter.js +169 -8
- package/dist/config.d.ts +6 -6
- package/dist/config.js +22 -0
- package/dist/constants.d.ts +10 -1
- package/dist/constants.js +19 -10
- package/dist/content/contracts.d.ts +1 -1
- package/dist/content/contracts.js +1 -1
- package/dist/content/{harnesses-doc.js → harness-doc.js} +32 -1
- package/dist/content/harness-playbooks.js +4 -4
- package/dist/content/ideate-command.js +19 -19
- package/dist/content/skills.js +2 -2
- package/dist/content/stage-schema.js +54 -15
- package/dist/content/stages/design.js +2 -2
- package/dist/content/stages/review.js +1 -1
- package/dist/content/stages/ship.js +2 -0
- package/dist/content/stages/tdd.js +8 -4
- package/dist/content/templates.js +4 -3
- package/dist/delegation.js +107 -26
- package/dist/doctor.js +77 -9
- package/dist/flow-state.d.ts +8 -0
- package/dist/flow-state.js +11 -8
- package/dist/gate-evidence.js +26 -2
- package/dist/harness-adapters.d.ts +2 -2
- package/dist/harness-adapters.js +2 -2
- package/dist/install.js +28 -6
- package/dist/internal/advance-stage.js +53 -16
- package/dist/internal/detect-public-api-changes.d.ts +5 -0
- package/dist/internal/detect-public-api-changes.js +45 -0
- package/dist/policy.js +3 -2
- package/dist/retro-gate.js +30 -3
- package/dist/run-persistence.js +16 -5
- package/dist/tdd-cycle.js +19 -1
- package/dist/types.d.ts +6 -1
- package/package.json +4 -1
- /package/dist/content/{harnesses-doc.d.ts → harness-doc.d.ts} +0 -0
package/dist/gate-evidence.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { checkReviewVerdictConsistency, extractMarkdownSectionBody, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
|
|
3
|
+
import { checkReviewSecurityNoChangeAttestation, checkReviewVerdictConsistency, extractMarkdownSectionBody, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
|
|
4
4
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
5
5
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
|
+
import { readDelegationLedger } from "./delegation.js";
|
|
6
7
|
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
8
|
+
import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
|
|
7
9
|
import { readFlowState, writeFlowState } from "./runs.js";
|
|
8
10
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
9
11
|
import { FLOW_STAGES } from "./types.js";
|
|
@@ -228,6 +230,10 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
228
230
|
if (!verdictConsistency.ok) {
|
|
229
231
|
issues.push(`review verdict inconsistency: ${verdictConsistency.errors.join("; ")}`);
|
|
230
232
|
}
|
|
233
|
+
const securityAttestation = await checkReviewSecurityNoChangeAttestation(projectRoot);
|
|
234
|
+
if (!securityAttestation.ok) {
|
|
235
|
+
issues.push(`review security attestation failed: ${securityAttestation.errors.join("; ")}`);
|
|
236
|
+
}
|
|
231
237
|
const traceGateRequired = schema.requiredGates.some((gate) => gate.id === "review_trace_matrix_clean" && gate.tier === "required");
|
|
232
238
|
if (traceGateRequired) {
|
|
233
239
|
const trace = await buildTraceMatrix(projectRoot);
|
|
@@ -266,7 +272,12 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
266
272
|
.map((line) => line.trim())
|
|
267
273
|
.filter((line) => line.length > 0)
|
|
268
274
|
.filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
|
|
269
|
-
|
|
275
|
+
// `<fill-in>` needs its own check because `\b` does not match
|
|
276
|
+
// around `<`/`>` (non-word characters), so the previous combined
|
|
277
|
+
// pattern `\b(?:...|<fill-in>)\b` silently never matched placeholder
|
|
278
|
+
// templates that used angle-bracket form.
|
|
279
|
+
const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
|
|
280
|
+
!/<fill-in>/iu.test(line));
|
|
270
281
|
if (nonPlaceholder.length === 0) {
|
|
271
282
|
missingSections.push(`${section} (empty or placeholder)`);
|
|
272
283
|
}
|
|
@@ -277,6 +288,19 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
|
|
|
277
288
|
}
|
|
278
289
|
}
|
|
279
290
|
}
|
|
291
|
+
if (stage === "tdd") {
|
|
292
|
+
const docsDriftDetection = await detectPublicApiChanges(projectRoot);
|
|
293
|
+
if (docsDriftDetection.triggered) {
|
|
294
|
+
const ledger = await readDelegationLedger(projectRoot);
|
|
295
|
+
const hasDocUpdaterCompletion = ledger.entries.some((entry) => entry.runId === flowState.activeRunId &&
|
|
296
|
+
entry.stage === "tdd" &&
|
|
297
|
+
entry.agent === "doc-updater" &&
|
|
298
|
+
entry.status === "completed");
|
|
299
|
+
if (!hasDocUpdaterCompletion) {
|
|
300
|
+
issues.push(`tdd docs drift gate blocked (tdd_docs_drift_check): public surface changes detected (${docsDriftDetection.changedFiles.join(", ")}) but no completed doc-updater delegation exists for the active run.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
280
304
|
}
|
|
281
305
|
const passedSet = new Set(catalog.passed);
|
|
282
306
|
const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
|
|
@@ -17,8 +17,8 @@ export type SubagentFallback =
|
|
|
17
17
|
*/
|
|
18
18
|
| "role-switch"
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Reserved escape hatch for future harnesses with no parity path.
|
|
21
|
+
* Current shipped harnesses do not use this fallback.
|
|
22
22
|
*/
|
|
23
23
|
| "waiver";
|
|
24
24
|
/**
|
package/dist/harness-adapters.js
CHANGED
|
@@ -222,7 +222,7 @@ When in doubt, prefer **non-trivial** — the quick track is opt-in and only saf
|
|
|
222
222
|
|---|---|
|
|
223
223
|
| \`/cc\` | **Entry point.** No args = resume current stage. With prompt = classify task and start the right flow. |
|
|
224
224
|
| \`/cc-next\` | **Progression.** Advances to the next stage when current is complete. |
|
|
225
|
-
| \`/cc-ideate\` | **
|
|
225
|
+
| \`/cc-ideate\` | **Ideate mode.** Generates a ranked repo-improvement backlog before implementation. |
|
|
226
226
|
| \`/cc-view\` | **Read-only router.** Unified entry for status/tree/diff views. |
|
|
227
227
|
| \`/cc-ops\` | **Operations router.** Unified entry for feature/tdd-log/retro/compound/archive/rewind actions. |
|
|
228
228
|
|
|
@@ -356,7 +356,7 @@ function codexSkillDescription(command) {
|
|
|
356
356
|
case "next":
|
|
357
357
|
return `Advance the cclaw flow to the next stage. Use when the user types \`/cc-next\` or asks to "move to the next stage", "continue the flow", "advance cclaw", "progress the workflow", or when the current stage skill reports completion and gates have passed.`;
|
|
358
358
|
case "ideate":
|
|
359
|
-
return `Read-only repo-improvement
|
|
359
|
+
return `Read-only repo-improvement ideate mode for cclaw. Use when the user types \`/cc-ideate\` or asks to "ideate", "scan the repo for TODOs/tech debt", "generate a backlog", or wants a ranked list of candidate ideas before committing to a single flow. Does not mutate \`.cclaw/state/flow-state.json\`.`;
|
|
360
360
|
case "view":
|
|
361
361
|
return `Read-only router for cclaw flow views. Use when the user types \`/cc-view\`, \`/cc-view status\`, \`/cc-view tree\`, \`/cc-view diff\`, or asks to "show cclaw status", "show the flow tree", "diff flow state", or wants a snapshot without mutation.`;
|
|
362
362
|
case "ops":
|
package/dist/install.js
CHANGED
|
@@ -2,9 +2,9 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
-
import { CCLAW_VERSION,
|
|
5
|
+
import { CCLAW_VERSION, FLOW_VERSION, REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
|
|
6
6
|
import { writeConfig, createDefaultConfig, readConfig, configPath, detectLanguageRulePacks, detectAdvancedKeys } from "./config.js";
|
|
7
|
-
import {
|
|
7
|
+
import { stageCommandContract } from "./content/contracts.js";
|
|
8
8
|
import { contextModeFiles, createInitialContextModeState } from "./content/contexts.js";
|
|
9
9
|
import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js";
|
|
10
10
|
import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
|
|
@@ -36,7 +36,7 @@ import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LANGUAGE_RULE_PACK_GE
|
|
|
36
36
|
import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
|
|
37
37
|
import { HARNESS_TOOL_REFS_DIR, HARNESS_TOOL_REFS_INDEX_MD, harnessToolRefMarkdown } from "./content/harness-tool-refs.js";
|
|
38
38
|
import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
|
|
39
|
-
import { harnessDocsOverviewMarkdown, harnessIntegrationDocMarkdown } from "./content/
|
|
39
|
+
import { harnessDocsOverviewMarkdown, harnessIntegrationDocMarkdown } from "./content/harness-doc.js";
|
|
40
40
|
import { HARNESS_PLAYBOOKS_DIR, harnessPlaybookFileName, harnessPlaybookMarkdown, harnessPlaybooksIndexMarkdown } from "./content/harness-playbooks.js";
|
|
41
41
|
import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./content/hook-events.js";
|
|
42
42
|
import { createInitialFlowState } from "./flow-state.js";
|
|
@@ -45,6 +45,7 @@ import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
|
|
|
45
45
|
import { HARNESS_ADAPTERS, harnessShimFileNames, harnessTier, syncHarnessShims, removeCclawFromAgentsMd } from "./harness-adapters.js";
|
|
46
46
|
import { validateHookDocument } from "./hook-schema.js";
|
|
47
47
|
import { ensureRunSystem, readFlowState } from "./runs.js";
|
|
48
|
+
import { FLOW_STAGES } from "./types.js";
|
|
48
49
|
const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
|
|
49
50
|
const CURSOR_RULE_REL_PATH = ".cursor/rules/cclaw-workflow.mdc";
|
|
50
51
|
const GIT_HOOK_MANAGED_MARKER = "cclaw-managed-git-hook";
|
|
@@ -177,8 +178,8 @@ async function ensureStructure(projectRoot) {
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
async function writeCommandContracts(projectRoot) {
|
|
180
|
-
for (const stage of
|
|
181
|
-
await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`),
|
|
181
|
+
for (const stage of FLOW_STAGES) {
|
|
182
|
+
await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandContract(stage));
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
async function writeArtifactTemplates(projectRoot) {
|
|
@@ -214,7 +215,7 @@ async function writeEvalScaffold(projectRoot) {
|
|
|
214
215
|
}
|
|
215
216
|
async function writeSkills(projectRoot, config) {
|
|
216
217
|
const skillTrack = config?.defaultTrack ?? "standard";
|
|
217
|
-
for (const stage of
|
|
218
|
+
for (const stage of FLOW_STAGES) {
|
|
218
219
|
const folder = stageSkillFolder(stage);
|
|
219
220
|
await writeFileSafe(runtimePath(projectRoot, "skills", folder, "SKILL.md"), stageSkillMarkdown(stage, skillTrack));
|
|
220
221
|
// Progressive disclosure (A.2#8): materialize the full example artifact as
|
|
@@ -1114,6 +1115,27 @@ async function cleanLegacyArtifacts(projectRoot) {
|
|
|
1114
1115
|
// best-effort cleanup
|
|
1115
1116
|
}
|
|
1116
1117
|
}
|
|
1118
|
+
// D-4 terminology migration: rename historical ideation artifacts to the
|
|
1119
|
+
// canonical ideate-* naming without deleting user-authored content.
|
|
1120
|
+
const artifactsDir = runtimePath(projectRoot, "artifacts");
|
|
1121
|
+
try {
|
|
1122
|
+
const entries = await fs.readdir(artifactsDir);
|
|
1123
|
+
for (const entry of entries) {
|
|
1124
|
+
const match = /^ideation-(.+\.md)$/u.exec(entry);
|
|
1125
|
+
if (!match)
|
|
1126
|
+
continue;
|
|
1127
|
+
const nextName = `ideate-${match[1]}`;
|
|
1128
|
+
const from = path.join(artifactsDir, entry);
|
|
1129
|
+
const to = path.join(artifactsDir, nextName);
|
|
1130
|
+
if (await exists(to)) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
await fs.rename(from, to);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
// no artifacts directory yet (fresh init) or read-only FS
|
|
1138
|
+
}
|
|
1117
1139
|
}
|
|
1118
1140
|
async function cleanStaleFiles(projectRoot) {
|
|
1119
1141
|
const expectedShimFiles = new Set(harnessShimFileNames());
|
|
@@ -16,21 +16,36 @@ function unique(values) {
|
|
|
16
16
|
const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|pnpm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)\b/iu;
|
|
17
17
|
const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
18
18
|
const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const SHIP_FINALIZATION_MODE_PATTERN = /\bFINALIZE_(?:MERGE_LOCAL|OPEN_PR|QUEUE|HANDOFF|SKIP)\b/u;
|
|
20
|
+
// Per-gate validators keyed by `${stage}:${gateId}`. Returning a non-null
|
|
21
|
+
// string surfaces the reason as an `advance-stage` failure so evidence is
|
|
22
|
+
// guaranteed to carry the structural breadcrumbs downstream tooling
|
|
23
|
+
// expects. Previously only `tdd:tdd_verified_before_complete` was checked.
|
|
24
|
+
const GATE_EVIDENCE_VALIDATORS = {
|
|
25
|
+
"tdd:tdd_verified_before_complete": (evidence) => {
|
|
26
|
+
if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
|
|
27
|
+
return "must include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).";
|
|
28
|
+
}
|
|
29
|
+
if (!SHA_WITH_LABEL_PATTERN.test(evidence)) {
|
|
30
|
+
return "must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).";
|
|
31
|
+
}
|
|
32
|
+
if (!PASS_STATUS_PATTERN.test(evidence)) {
|
|
33
|
+
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
"ship:ship_finalization_executed": (evidence) => {
|
|
38
|
+
if (!SHIP_FINALIZATION_MODE_PATTERN.test(evidence)) {
|
|
39
|
+
return "must name the finalization mode that ran (for example `FINALIZE_MERGE_LOCAL`, `FINALIZE_OPEN_PR`, `FINALIZE_HANDOFF`, `FINALIZE_QUEUE`, or `FINALIZE_SKIP`).";
|
|
40
|
+
}
|
|
21
41
|
return null;
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
if (!PASS_STATUS_PATTERN.test(trimmed)) {
|
|
31
|
-
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
43
|
+
};
|
|
44
|
+
function validateGateEvidenceShape(stage, gateId, evidence) {
|
|
45
|
+
const validator = GATE_EVIDENCE_VALIDATORS[`${stage}:${gateId}`];
|
|
46
|
+
if (!validator)
|
|
47
|
+
return null;
|
|
48
|
+
return validator(evidence.trim());
|
|
34
49
|
}
|
|
35
50
|
function parseStringList(raw) {
|
|
36
51
|
if (!Array.isArray(raw))
|
|
@@ -58,10 +73,23 @@ function parseGuardEvidence(value) {
|
|
|
58
73
|
}
|
|
59
74
|
return next;
|
|
60
75
|
}
|
|
76
|
+
function emptyGateState() {
|
|
77
|
+
return {
|
|
78
|
+
required: [],
|
|
79
|
+
recommended: [],
|
|
80
|
+
conditional: [],
|
|
81
|
+
triggered: [],
|
|
82
|
+
passed: [],
|
|
83
|
+
blocked: []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
61
86
|
function parseCandidateGateCatalog(value, fallback) {
|
|
62
87
|
const next = {};
|
|
63
88
|
for (const stage of FLOW_STAGES) {
|
|
64
|
-
|
|
89
|
+
// Guard against stale on-disk flow-state files that persisted a partial
|
|
90
|
+
// stageGateCatalog (missing a stage key). Previously `fallback[stage]`
|
|
91
|
+
// could be undefined and the spread below would throw at runtime.
|
|
92
|
+
const base = fallback[stage] ?? emptyGateState();
|
|
65
93
|
next[stage] = {
|
|
66
94
|
required: [...base.required],
|
|
67
95
|
recommended: [...base.recommended],
|
|
@@ -81,7 +109,7 @@ function parseCandidateGateCatalog(value, fallback) {
|
|
|
81
109
|
continue;
|
|
82
110
|
}
|
|
83
111
|
const typed = rawStage;
|
|
84
|
-
const base = fallback[stage];
|
|
112
|
+
const base = fallback[stage] ?? emptyGateState();
|
|
85
113
|
const allowed = new Set([...base.required, ...base.recommended, ...base.conditional]);
|
|
86
114
|
const conditional = new Set(base.conditional);
|
|
87
115
|
const passed = unique(parseStringList(typed.passed)).filter((gateId) => allowed.has(gateId));
|
|
@@ -114,13 +142,22 @@ function coerceCandidateFlowState(raw, fallback) {
|
|
|
114
142
|
const completedStages = unique(parseStringList(typed.completedStages).filter((stage) => isFlowStageValue(stage)));
|
|
115
143
|
const skippedStagesRaw = parseStringList(typed.skippedStages).filter((stage) => isFlowStageValue(stage));
|
|
116
144
|
const skippedStages = skippedStagesRaw.length > 0 ? skippedStagesRaw : fallback.skippedStages;
|
|
145
|
+
// When the candidate payload omits `guardEvidence` entirely we must keep
|
|
146
|
+
// the on-disk fallback — otherwise a partial update (e.g. a tooling call
|
|
147
|
+
// that only passes stage + passedGateIds) would silently wipe every
|
|
148
|
+
// previously recorded evidence string and fail the next
|
|
149
|
+
// `verifyCurrentStageGateEvidence` check.
|
|
150
|
+
const candidateEvidence = parseGuardEvidence(typed.guardEvidence);
|
|
151
|
+
const guardEvidence = typed.guardEvidence === undefined
|
|
152
|
+
? { ...fallback.guardEvidence }
|
|
153
|
+
: candidateEvidence;
|
|
117
154
|
return {
|
|
118
155
|
...fallback,
|
|
119
156
|
currentStage,
|
|
120
157
|
completedStages,
|
|
121
158
|
track,
|
|
122
159
|
skippedStages,
|
|
123
|
-
guardEvidence
|
|
160
|
+
guardEvidence,
|
|
124
161
|
stageGateCatalog: parseCandidateGateCatalog(typed.stageGateCatalog, fallback.stageGateCatalog)
|
|
125
162
|
};
|
|
126
163
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const PUBLIC_SURFACE_PATH_PATTERNS = [
|
|
5
|
+
/(^|\/)(cli|types?|config)\.[cm]?[jt]s$/iu,
|
|
6
|
+
/(^|\/)(openapi|swagger|schema)(\/|[-_.])/iu,
|
|
7
|
+
/(^|\/)(api|commands?|flags?)(\/|[-_.])/iu,
|
|
8
|
+
/(^|\/)(package|tsconfig)\.json$/iu
|
|
9
|
+
];
|
|
10
|
+
async function resolveDiffBase(projectRoot) {
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD~1"], {
|
|
13
|
+
cwd: projectRoot
|
|
14
|
+
});
|
|
15
|
+
const base = stdout.trim();
|
|
16
|
+
return base.length > 0 ? base : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function detectPublicApiChanges(projectRoot) {
|
|
23
|
+
const base = await resolveDiffBase(projectRoot);
|
|
24
|
+
if (!base) {
|
|
25
|
+
return { triggered: false, changedFiles: [] };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const range = `${base}..HEAD`;
|
|
29
|
+
const { stdout } = await execFileAsync("git", ["diff", "--name-only", range], {
|
|
30
|
+
cwd: projectRoot
|
|
31
|
+
});
|
|
32
|
+
const changedFiles = stdout
|
|
33
|
+
.split(/\r?\n/gu)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter((line) => line.length > 0)
|
|
36
|
+
.filter((filePath) => PUBLIC_SURFACE_PATH_PATTERNS.some((pattern) => pattern.test(filePath)));
|
|
37
|
+
return {
|
|
38
|
+
triggered: changedFiles.length > 0,
|
|
39
|
+
changedFiles
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { triggered: false, changedFiles: [] };
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/policy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
|
+
import { FLOW_STAGES } from "./types.js";
|
|
4
5
|
import { stageSchema, stagePolicyNeedles } from "./content/stage-schema.js";
|
|
5
6
|
import { stageSkillFolder } from "./content/skills.js";
|
|
6
7
|
import { exists } from "./fs-utils.js";
|
|
@@ -10,7 +11,7 @@ export async function policyChecks(projectRoot, options = {}) {
|
|
|
10
11
|
const checks = [];
|
|
11
12
|
const rules = [...POLICY_RULES];
|
|
12
13
|
const activeHarnesses = new Set(options.harnesses && options.harnesses.length > 0 ? options.harnesses : ALL_HARNESSES);
|
|
13
|
-
for (const stage of
|
|
14
|
+
for (const stage of FLOW_STAGES) {
|
|
14
15
|
const folder = stageSkillFolder(stage);
|
|
15
16
|
const schema = stageSchema(stage);
|
|
16
17
|
const commandFile = `${RUNTIME_ROOT}/commands/${stage}.md`;
|
package/dist/retro-gate.js
CHANGED
|
@@ -8,6 +8,7 @@ function activeArtifactsPath(projectRoot) {
|
|
|
8
8
|
function retroArtifactPath(projectRoot) {
|
|
9
9
|
return path.join(activeArtifactsPath(projectRoot), "09-retro.md");
|
|
10
10
|
}
|
|
11
|
+
const RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
11
12
|
function parseIsoTimestamp(value) {
|
|
12
13
|
if (!value || value.trim().length === 0)
|
|
13
14
|
return null;
|
|
@@ -35,8 +36,24 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
let compoundEntries = state.retro.compoundEntries;
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
let windowStartMs = parseIsoTimestamp(state.closeout.retroDraftedAt);
|
|
40
|
+
let windowEndMs = parseIsoTimestamp(state.closeout.retroAcceptedAt) ?? parseIsoTimestamp(state.retro.completedAt);
|
|
41
|
+
if (compoundEntries <= 0 &&
|
|
42
|
+
hasRetroArtifact &&
|
|
43
|
+
windowStartMs === null &&
|
|
44
|
+
windowEndMs === null) {
|
|
45
|
+
try {
|
|
46
|
+
const stats = await fs.stat(artifactFile);
|
|
47
|
+
const anchor = stats.mtimeMs;
|
|
48
|
+
if (Number.isFinite(anchor) && anchor > 0) {
|
|
49
|
+
windowStartMs = anchor - RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS;
|
|
50
|
+
windowEndMs = anchor + RETRO_ARTIFACT_MTIME_FALLBACK_WINDOW_MS;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// fallback scan remains disabled when mtime cannot be read
|
|
55
|
+
}
|
|
56
|
+
}
|
|
40
57
|
const shouldFallbackScan = compoundEntries <= 0 && (windowStartMs !== null || windowEndMs !== null);
|
|
41
58
|
const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
42
59
|
if (shouldFallbackScan && (await exists(knowledgeFile))) {
|
|
@@ -73,7 +90,17 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
73
90
|
compoundEntries = 0;
|
|
74
91
|
}
|
|
75
92
|
}
|
|
76
|
-
|
|
93
|
+
// A retro is considered complete when either:
|
|
94
|
+
// - at least one compound learning was promoted during the retro window, or
|
|
95
|
+
// - the operator explicitly skipped retro or compound (`retroSkipped` /
|
|
96
|
+
// `compoundSkipped` recorded in the closeout substate) after reviewing
|
|
97
|
+
// the draft. Previously the gate required `compoundEntries > 0`
|
|
98
|
+
// unconditionally, which dead-locked ship closeout whenever the retro
|
|
99
|
+
// yielded no new patterns worth promoting.
|
|
100
|
+
const explicitSkip = Boolean(state.closeout.retroSkipped || state.closeout.compoundSkipped);
|
|
101
|
+
const completed = required
|
|
102
|
+
? hasRetroArtifact && (compoundEntries > 0 || explicitSkip)
|
|
103
|
+
: true;
|
|
77
104
|
return {
|
|
78
105
|
required,
|
|
79
106
|
completed,
|
package/dist/run-persistence.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
4
|
import { canTransition, createInitialCloseoutState, createInitialFlowState, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
|
|
5
5
|
import { ensureFeatureSystem, syncActiveFeatureSnapshot } from "./feature-system.js";
|
|
6
6
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
7
|
+
import { FLOW_STAGES } from "./types.js";
|
|
7
8
|
export class InvalidStageTransitionError extends Error {
|
|
8
9
|
from;
|
|
9
10
|
to;
|
|
@@ -17,7 +18,7 @@ export class InvalidStageTransitionError extends Error {
|
|
|
17
18
|
const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
18
19
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
19
20
|
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
20
|
-
const FLOW_STAGE_SET = new Set(
|
|
21
|
+
const FLOW_STAGE_SET = new Set(FLOW_STAGES);
|
|
21
22
|
function validateFlowTransition(prev, next) {
|
|
22
23
|
if (prev.activeRunId !== next.activeRunId) {
|
|
23
24
|
// New run — only reset paths may change the runId, but those set allowReset.
|
|
@@ -85,7 +86,7 @@ function sanitizeGuardEvidence(value) {
|
|
|
85
86
|
function sanitizeStageGateCatalog(value, fallback) {
|
|
86
87
|
const uniqueStrings = (items) => [...new Set(items)];
|
|
87
88
|
const next = {};
|
|
88
|
-
for (const stage of
|
|
89
|
+
for (const stage of FLOW_STAGES) {
|
|
89
90
|
const base = fallback[stage];
|
|
90
91
|
next[stage] = {
|
|
91
92
|
required: [...base.required],
|
|
@@ -100,7 +101,7 @@ function sanitizeStageGateCatalog(value, fallback) {
|
|
|
100
101
|
return next;
|
|
101
102
|
}
|
|
102
103
|
const rawCatalog = value;
|
|
103
|
-
for (const stage of
|
|
104
|
+
for (const stage of FLOW_STAGES) {
|
|
104
105
|
const rawStage = rawCatalog[stage];
|
|
105
106
|
if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
|
|
106
107
|
continue;
|
|
@@ -235,7 +236,7 @@ function sanitizeCloseoutState(value) {
|
|
|
235
236
|
return fallback;
|
|
236
237
|
}
|
|
237
238
|
const typed = value;
|
|
238
|
-
|
|
239
|
+
let shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
|
|
239
240
|
const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
|
|
240
241
|
const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
|
|
241
242
|
const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
|
|
@@ -246,6 +247,16 @@ function sanitizeCloseoutState(value) {
|
|
|
246
247
|
const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
|
|
247
248
|
? Math.floor(promotedRaw)
|
|
248
249
|
: 0;
|
|
250
|
+
// Demote shipSubstate when its retro invariant is violated on disk. A
|
|
251
|
+
// hand-edited flow-state could claim `ready_to_archive` or `compound_review`
|
|
252
|
+
// without ever going through the retro step, which would let `archive`
|
|
253
|
+
// proceed and skip the gate. Compound completion is not independently
|
|
254
|
+
// tracked in all flows (some runs rely on knowledge.jsonl + the retro
|
|
255
|
+
// window), so we only demote when the retro leg is missing outright.
|
|
256
|
+
const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
|
|
257
|
+
if (!retroDone && (shipSubstate === "ready_to_archive" || shipSubstate === "compound_review")) {
|
|
258
|
+
shipSubstate = "retro_review";
|
|
259
|
+
}
|
|
249
260
|
return {
|
|
250
261
|
shipSubstate,
|
|
251
262
|
retroDraftedAt,
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -31,6 +31,7 @@ export function parseTddCycleLog(text) {
|
|
|
31
31
|
}
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
|
+
const SLICE_ID_PATTERN = /^S-\d+$/u;
|
|
34
35
|
export function validateTddCycleOrder(entries, options = {}) {
|
|
35
36
|
const targetRun = options.runId;
|
|
36
37
|
const filtered = targetRun
|
|
@@ -44,6 +45,15 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
44
45
|
}
|
|
45
46
|
const issues = [];
|
|
46
47
|
const openRedSlices = [];
|
|
48
|
+
// Reject slices whose ID does not match the stable `S-<number>` contract.
|
|
49
|
+
// Entries that drop the slice field entirely were previously coerced to
|
|
50
|
+
// `S-unknown` and silently bucketed together, which means multiple distinct
|
|
51
|
+
// cycles could appear to share a RED/GREEN pair.
|
|
52
|
+
for (const slice of bySlice.keys()) {
|
|
53
|
+
if (!SLICE_ID_PATTERN.test(slice)) {
|
|
54
|
+
issues.push(`slice "${slice}": id must match /^S-\\d+$/ (e.g. S-1)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
47
57
|
for (const [slice, sliceEntries] of bySlice.entries()) {
|
|
48
58
|
let state = "need_red";
|
|
49
59
|
for (const entry of sliceEntries) {
|
|
@@ -79,7 +89,15 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
79
89
|
state = "green_done";
|
|
80
90
|
continue;
|
|
81
91
|
}
|
|
82
|
-
// refactor
|
|
92
|
+
// refactor — must preserve the passing state established by green.
|
|
93
|
+
if (entry.exitCode === undefined) {
|
|
94
|
+
issues.push(`slice ${slice}: refactor entry must record exitCode 0`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (entry.exitCode !== 0) {
|
|
98
|
+
issues.push(`slice ${slice}: refactor entry exitCode must be 0 (tests must stay green)`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
83
101
|
if (state !== "green_done") {
|
|
84
102
|
issues.push(`slice ${slice}: refactor logged before green`);
|
|
85
103
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -109,7 +109,7 @@ export interface TddPathConfig {
|
|
|
109
109
|
export interface CompoundConfig {
|
|
110
110
|
recurrenceThreshold?: number;
|
|
111
111
|
}
|
|
112
|
-
export interface
|
|
112
|
+
export interface CclawConfig {
|
|
113
113
|
version: string;
|
|
114
114
|
flowVersion: string;
|
|
115
115
|
harnesses: HarnessId[];
|
|
@@ -141,6 +141,7 @@ export interface VibyConfig {
|
|
|
141
141
|
/**
|
|
142
142
|
* Legacy alias for test-side path detection in workflow-guard.
|
|
143
143
|
* Prefer `tdd.testPathPatterns` in new configs.
|
|
144
|
+
* @deprecated Use `tdd.testPathPatterns` instead.
|
|
144
145
|
*/
|
|
145
146
|
tddTestGlobs?: string[];
|
|
146
147
|
/** Path-pattern routing for TDD test/production write classification. */
|
|
@@ -173,6 +174,10 @@ export interface VibyConfig {
|
|
|
173
174
|
*/
|
|
174
175
|
sliceReview?: SliceReviewConfig;
|
|
175
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* @deprecated Use `CclawConfig` instead.
|
|
179
|
+
*/
|
|
180
|
+
export type VibyConfig = CclawConfig;
|
|
176
181
|
export interface TransitionRule {
|
|
177
182
|
from: FlowStage;
|
|
178
183
|
to: FlowStage;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cclaw-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Installer-first flow toolkit for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:watch": "vitest",
|
|
23
23
|
"test:coverage": "vitest run --coverage",
|
|
24
|
+
"test:mutation": "stryker run",
|
|
24
25
|
"smoke:runtime": "npm run build && node scripts/smoke-init.mjs",
|
|
25
26
|
"lint:hooks": "npm run build && node scripts/lint-generated-hooks.mjs",
|
|
26
27
|
"build:harness-docs": "npm run build && node scripts/build-harness-docs.mjs",
|
|
@@ -44,6 +45,8 @@
|
|
|
44
45
|
"yaml": "^2.8.1"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
48
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
49
|
+
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
47
50
|
"@types/node": "^24.7.2",
|
|
48
51
|
"@vitest/coverage-v8": "^3.2.4",
|
|
49
52
|
"typescript": "^5.9.3",
|
|
File without changes
|