cclaw-cli 0.51.23 → 0.51.25
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 +135 -414
- package/dist/artifact-linter.js +10 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +28 -3
- package/dist/content/core-agents.d.ts +128 -2
- package/dist/content/core-agents.js +291 -13
- package/dist/content/examples.js +21 -10
- package/dist/content/next-command.js +10 -6
- package/dist/content/reference-patterns.d.ts +18 -0
- package/dist/content/reference-patterns.js +391 -0
- package/dist/content/seed-shelf.js +73 -8
- package/dist/content/skills.js +39 -34
- package/dist/content/stage-common-guidance.js +19 -3
- package/dist/content/stage-schema.d.ts +12 -0
- package/dist/content/stage-schema.js +224 -24
- package/dist/content/stages/_lint-metadata/index.js +3 -2
- package/dist/content/stages/brainstorm.js +27 -18
- package/dist/content/stages/design.js +27 -18
- package/dist/content/stages/review.js +20 -9
- package/dist/content/stages/schema-types.d.ts +9 -2
- package/dist/content/stages/scope.js +21 -10
- package/dist/content/stages/ship.js +3 -2
- package/dist/content/stages/tdd.js +18 -13
- package/dist/content/start-command.js +3 -2
- package/dist/content/status-command.js +9 -4
- package/dist/content/subagents.js +336 -38
- package/dist/content/templates.js +182 -25
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +27 -6
- package/dist/doctor.js +167 -25
- package/dist/flow-state.d.ts +1 -0
- package/dist/flow-state.js +1 -0
- package/dist/gate-evidence.js +25 -2
- package/dist/install.js +72 -8
- package/dist/internal/advance-stage.js +179 -26
- package/dist/knowledge-store.js +30 -6
- package/dist/run-archive.js +11 -0
- package/dist/run-persistence.js +35 -10
- package/dist/tdd-verification-evidence.d.ts +17 -0
- package/dist/tdd-verification-evidence.js +43 -0
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { resolveArtifactPath } from "../artifact-paths.js";
|
|
6
6
|
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
7
|
+
import { ensureDir } from "../fs-utils.js";
|
|
7
8
|
import { stageAutoSubagentDispatch, stageSchema } from "../content/stage-schema.js";
|
|
8
9
|
import { appendDelegation, checkMandatoryDelegations, readDelegationLedger } from "../delegation.js";
|
|
9
10
|
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
@@ -18,6 +19,7 @@ import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
|
18
19
|
import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
|
|
19
20
|
import { runTddRedEvidenceCommand } from "./tdd-red-evidence.js";
|
|
20
21
|
import { extractReviewLoopEnvelopeFromArtifact } from "../content/review-loop.js";
|
|
22
|
+
import { PASS_STATUS_PATTERN, TEST_COMMAND_HINT_PATTERN, validateTddVerificationEvidence } from "../tdd-verification-evidence.js";
|
|
21
23
|
const AUTO_REVIEW_LOOP_GATE_BY_STAGE = {
|
|
22
24
|
design: "design_architecture_locked"
|
|
23
25
|
};
|
|
@@ -52,9 +54,6 @@ function resolveSuccessorTransition(stage, track, transitionTargets, satisfiedGu
|
|
|
52
54
|
}
|
|
53
55
|
return natural;
|
|
54
56
|
}
|
|
55
|
-
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;
|
|
56
|
-
const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
57
|
-
const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
58
57
|
const SHIP_FINALIZATION_MODE_PATTERN = new RegExp(`\\b(?:${SHIP_FINALIZATION_MODES.join("|")})\\b`, "u");
|
|
59
58
|
const SHIP_FINALIZATION_MODE_HINT = SHIP_FINALIZATION_MODES.join(", ");
|
|
60
59
|
const REVIEW_LOOP_STOP_REASONS = new Set([
|
|
@@ -191,18 +190,6 @@ function validateUserApprovalEvidence(evidence) {
|
|
|
191
190
|
// guaranteed to carry the structural breadcrumbs downstream tooling
|
|
192
191
|
// expects. Previously only `tdd:tdd_verified_before_complete` was checked.
|
|
193
192
|
const GATE_EVIDENCE_VALIDATORS = {
|
|
194
|
-
"tdd:tdd_verified_before_complete": (evidence) => {
|
|
195
|
-
if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
|
|
196
|
-
return "must include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).";
|
|
197
|
-
}
|
|
198
|
-
if (!SHA_WITH_LABEL_PATTERN.test(evidence)) {
|
|
199
|
-
return "must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).";
|
|
200
|
-
}
|
|
201
|
-
if (!PASS_STATUS_PATTERN.test(evidence)) {
|
|
202
|
-
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
205
|
-
},
|
|
206
193
|
"review:review_trace_matrix_clean": (evidence) => {
|
|
207
194
|
if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
|
|
208
195
|
return "must include the fresh verification command that was run before ship handoff (for example `npm test`, `pytest`, `go test`, or equivalent).";
|
|
@@ -221,11 +208,16 @@ const GATE_EVIDENCE_VALIDATORS = {
|
|
|
221
208
|
"scope:scope_user_approved": (evidence) => validateUserApprovalEvidence(evidence),
|
|
222
209
|
"design:design_architecture_locked": (evidence) => validateReviewLoopGateEvidence("design", evidence)
|
|
223
210
|
};
|
|
224
|
-
function validateGateEvidenceShape(stage, gateId, evidence) {
|
|
211
|
+
async function validateGateEvidenceShape(projectRoot, stage, gateId, evidence) {
|
|
212
|
+
const normalized = evidence.trim();
|
|
213
|
+
if (stage === "tdd" && gateId === "tdd_verified_before_complete") {
|
|
214
|
+
const result = await validateTddVerificationEvidence(projectRoot, normalized);
|
|
215
|
+
return result.ok ? null : result.issues.join(" ");
|
|
216
|
+
}
|
|
225
217
|
const validator = GATE_EVIDENCE_VALIDATORS[`${stage}:${gateId}`];
|
|
226
218
|
if (!validator)
|
|
227
219
|
return null;
|
|
228
|
-
return validator(
|
|
220
|
+
return validator(normalized);
|
|
229
221
|
}
|
|
230
222
|
function reviewLoopArtifactFixHint(stage, gateId) {
|
|
231
223
|
if (AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage] !== gateId)
|
|
@@ -540,6 +532,50 @@ function parseVerifyCurrentStateArgs(tokens) {
|
|
|
540
532
|
}
|
|
541
533
|
return { quiet };
|
|
542
534
|
}
|
|
535
|
+
function parseRewindArgs(tokens) {
|
|
536
|
+
let quiet = false;
|
|
537
|
+
let json = false;
|
|
538
|
+
const positional = [];
|
|
539
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
540
|
+
const token = tokens[i];
|
|
541
|
+
const nextToken = tokens[i + 1];
|
|
542
|
+
if (token === "--quiet") {
|
|
543
|
+
quiet = true;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (token === "--json") {
|
|
547
|
+
json = true;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (token === "--ack") {
|
|
551
|
+
if (!nextToken || nextToken.startsWith("--")) {
|
|
552
|
+
throw new Error("--ack requires a stage value.");
|
|
553
|
+
}
|
|
554
|
+
if (!isFlowStageValue(nextToken)) {
|
|
555
|
+
throw new Error(`--ack stage must be one of: ${FLOW_STAGES.join(", ")}.`);
|
|
556
|
+
}
|
|
557
|
+
i += 1;
|
|
558
|
+
return { mode: "ack", targetStage: nextToken, quiet, json };
|
|
559
|
+
}
|
|
560
|
+
if (token.startsWith("--ack=")) {
|
|
561
|
+
const stage = token.slice("--ack=".length);
|
|
562
|
+
if (!isFlowStageValue(stage)) {
|
|
563
|
+
throw new Error(`--ack stage must be one of: ${FLOW_STAGES.join(", ")}.`);
|
|
564
|
+
}
|
|
565
|
+
return { mode: "ack", targetStage: stage, quiet, json };
|
|
566
|
+
}
|
|
567
|
+
positional.push(token);
|
|
568
|
+
}
|
|
569
|
+
const [targetStage, ...reasonParts] = positional;
|
|
570
|
+
if (!isFlowStageValue(targetStage)) {
|
|
571
|
+
throw new Error(`internal rewind requires a target stage (${FLOW_STAGES.join(", ")}) or --ack <stage>.`);
|
|
572
|
+
}
|
|
573
|
+
const reason = reasonParts.join(" ").trim();
|
|
574
|
+
if (reason.length === 0) {
|
|
575
|
+
throw new Error('internal rewind requires a reason, for example: cclaw internal rewind tdd "review_blocked_by_critical".');
|
|
576
|
+
}
|
|
577
|
+
return { mode: "rewind", targetStage, reason, quiet, json };
|
|
578
|
+
}
|
|
543
579
|
function parseHookArgs(tokens) {
|
|
544
580
|
const [hookName, ...rest] = tokens;
|
|
545
581
|
const normalizedHook = typeof hookName === "string" ? hookName.trim() : "";
|
|
@@ -633,6 +669,7 @@ async function buildValidationReport(projectRoot, flowState, options = {}) {
|
|
|
633
669
|
missing: delegation.missing,
|
|
634
670
|
waived: delegation.waived,
|
|
635
671
|
missingEvidence: delegation.missingEvidence,
|
|
672
|
+
staleWorkers: delegation.staleWorkers,
|
|
636
673
|
expectedMode: delegation.expectedMode
|
|
637
674
|
},
|
|
638
675
|
gates: {
|
|
@@ -773,9 +810,11 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
773
810
|
}
|
|
774
811
|
}
|
|
775
812
|
if (args.waiveDelegations.length > 0) {
|
|
776
|
-
const waiverReason = args.waiverReason
|
|
777
|
-
|
|
778
|
-
: "
|
|
813
|
+
const waiverReason = args.waiverReason?.trim();
|
|
814
|
+
if (!waiverReason) {
|
|
815
|
+
io.stderr.write("cclaw internal advance-stage: --waive-delegation requires an explicit non-empty --waiver-reason.\n");
|
|
816
|
+
return 1;
|
|
817
|
+
}
|
|
779
818
|
for (const agent of args.waiveDelegations) {
|
|
780
819
|
await appendDelegation(projectRoot, {
|
|
781
820
|
stage: args.stage,
|
|
@@ -812,7 +851,8 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
812
851
|
io.stderr.write(`cclaw internal advance-stage: missing --evidence-json entries for passed gates: ${missingGuardEvidence.join(", ")}.\n`);
|
|
813
852
|
return 1;
|
|
814
853
|
}
|
|
815
|
-
const malformedGateEvidence =
|
|
854
|
+
const malformedGateEvidence = [];
|
|
855
|
+
for (const gateId of nextPassed) {
|
|
816
856
|
const provided = args.evidenceByGate[gateId];
|
|
817
857
|
const existing = flowState.guardEvidence[gateId];
|
|
818
858
|
const effectiveEvidence = typeof provided === "string" && provided.trim().length > 0
|
|
@@ -820,9 +860,11 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
820
860
|
: typeof existing === "string" && existing.trim().length > 0
|
|
821
861
|
? existing
|
|
822
862
|
: "";
|
|
823
|
-
const issue = validateGateEvidenceShape(args.stage, gateId, effectiveEvidence);
|
|
824
|
-
|
|
825
|
-
|
|
863
|
+
const issue = await validateGateEvidenceShape(projectRoot, args.stage, gateId, effectiveEvidence);
|
|
864
|
+
if (issue) {
|
|
865
|
+
malformedGateEvidence.push(`${gateId}: ${issue}${reviewLoopArtifactFixHint(args.stage, gateId)}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
826
868
|
if (malformedGateEvidence.length > 0) {
|
|
827
869
|
io.stderr.write(`cclaw internal advance-stage: gate evidence format check failed: ${malformedGateEvidence.join(" | ")}.\n`);
|
|
828
870
|
return 1;
|
|
@@ -871,6 +913,9 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
871
913
|
...(validation.delegation.missingEvidence.length > 0
|
|
872
914
|
? ["Add evidenceRefs for role-switch delegation completion or use an explicit waiver reason."]
|
|
873
915
|
: []),
|
|
916
|
+
...(validation.delegation.staleWorkers.length > 0
|
|
917
|
+
? ["Resolve scheduled delegation span(s) without terminal lifecycle evidence before advancing."]
|
|
918
|
+
: []),
|
|
874
919
|
...(validation.gates.issues.length > 0
|
|
875
920
|
? ["Fix the artifact/gate issue shown in gates.issues, then rerun stage-complete."]
|
|
876
921
|
: []),
|
|
@@ -888,6 +933,9 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
888
933
|
if (validation.delegation.missingEvidence.length > 0) {
|
|
889
934
|
io.stderr.write(`- role-switch evidence missing: ${validation.delegation.missingEvidence.join(", ")}\n`);
|
|
890
935
|
}
|
|
936
|
+
if (validation.delegation.staleWorkers.length > 0) {
|
|
937
|
+
io.stderr.write(`- stale scheduled delegations: ${validation.delegation.staleWorkers.join(", ")}\n`);
|
|
938
|
+
}
|
|
891
939
|
if (validation.gates.issues.length > 0) {
|
|
892
940
|
io.stderr.write(`- gate issues: ${validation.gates.issues.join(" | ")}\n`);
|
|
893
941
|
}
|
|
@@ -1231,6 +1279,108 @@ async function runStartFlow(projectRoot, args, io) {
|
|
|
1231
1279
|
}
|
|
1232
1280
|
return 0;
|
|
1233
1281
|
}
|
|
1282
|
+
function rewindLogPath(projectRoot) {
|
|
1283
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", "rewind-log.jsonl");
|
|
1284
|
+
}
|
|
1285
|
+
function rewindId(date = new Date()) {
|
|
1286
|
+
return `rewind-${date.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1287
|
+
}
|
|
1288
|
+
function stagesInvalidatedByRewind(current, targetStage) {
|
|
1289
|
+
const ordered = TRACK_STAGES[current.track];
|
|
1290
|
+
const targetIndex = ordered.indexOf(targetStage);
|
|
1291
|
+
const currentIndex = ordered.indexOf(current.currentStage);
|
|
1292
|
+
if (targetIndex < 0 || currentIndex < 0 || targetIndex > currentIndex) {
|
|
1293
|
+
return [];
|
|
1294
|
+
}
|
|
1295
|
+
return ordered.slice(targetIndex, currentIndex + 1);
|
|
1296
|
+
}
|
|
1297
|
+
async function appendRewindLog(projectRoot, payload) {
|
|
1298
|
+
const logPath = rewindLogPath(projectRoot);
|
|
1299
|
+
await ensureDir(path.dirname(logPath));
|
|
1300
|
+
await fs.appendFile(logPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
1301
|
+
}
|
|
1302
|
+
async function runRewind(projectRoot, args, io) {
|
|
1303
|
+
const current = await readFlowState(projectRoot);
|
|
1304
|
+
const now = new Date().toISOString();
|
|
1305
|
+
if (args.mode === "ack") {
|
|
1306
|
+
const marker = current.staleStages[args.targetStage];
|
|
1307
|
+
if (!marker) {
|
|
1308
|
+
io.stderr.write(`cclaw internal rewind: no stale marker exists for "${args.targetStage}".\n`);
|
|
1309
|
+
return 1;
|
|
1310
|
+
}
|
|
1311
|
+
if (current.currentStage !== args.targetStage) {
|
|
1312
|
+
io.stderr.write(`cclaw internal rewind: cannot ack "${args.targetStage}" while currentStage is "${current.currentStage}". Re-run the stale stage before acknowledging it.\n`);
|
|
1313
|
+
return 1;
|
|
1314
|
+
}
|
|
1315
|
+
const staleStages = { ...current.staleStages };
|
|
1316
|
+
delete staleStages[args.targetStage];
|
|
1317
|
+
const nextState = { ...current, staleStages };
|
|
1318
|
+
await writeFlowState(projectRoot, nextState);
|
|
1319
|
+
const payload = {
|
|
1320
|
+
ok: true,
|
|
1321
|
+
command: "rewind",
|
|
1322
|
+
action: "ack",
|
|
1323
|
+
stage: args.targetStage,
|
|
1324
|
+
acknowledgedAt: now,
|
|
1325
|
+
rewindId: marker.rewindId
|
|
1326
|
+
};
|
|
1327
|
+
await appendRewindLog(projectRoot, payload);
|
|
1328
|
+
if (!args.quiet) {
|
|
1329
|
+
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1330
|
+
}
|
|
1331
|
+
return 0;
|
|
1332
|
+
}
|
|
1333
|
+
const invalidatedStages = stagesInvalidatedByRewind(current, args.targetStage);
|
|
1334
|
+
if (invalidatedStages.length === 0) {
|
|
1335
|
+
io.stderr.write(`cclaw internal rewind: target "${args.targetStage}" is not an earlier or current stage on track "${current.track}" from "${current.currentStage}".\n`);
|
|
1336
|
+
return 1;
|
|
1337
|
+
}
|
|
1338
|
+
const id = rewindId();
|
|
1339
|
+
const completedInvalidated = new Set(invalidatedStages);
|
|
1340
|
+
const staleStages = { ...current.staleStages };
|
|
1341
|
+
for (const stage of invalidatedStages) {
|
|
1342
|
+
staleStages[stage] = {
|
|
1343
|
+
rewindId: id,
|
|
1344
|
+
reason: args.reason ?? "rewind",
|
|
1345
|
+
markedAt: now
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
const record = {
|
|
1349
|
+
id,
|
|
1350
|
+
fromStage: current.currentStage,
|
|
1351
|
+
toStage: args.targetStage,
|
|
1352
|
+
reason: args.reason ?? "rewind",
|
|
1353
|
+
timestamp: now,
|
|
1354
|
+
invalidatedStages
|
|
1355
|
+
};
|
|
1356
|
+
const nextState = {
|
|
1357
|
+
...current,
|
|
1358
|
+
currentStage: args.targetStage,
|
|
1359
|
+
completedStages: current.completedStages.filter((stage) => !completedInvalidated.has(stage)),
|
|
1360
|
+
staleStages,
|
|
1361
|
+
rewinds: [...current.rewinds, record]
|
|
1362
|
+
};
|
|
1363
|
+
await writeFlowState(projectRoot, nextState);
|
|
1364
|
+
const payload = {
|
|
1365
|
+
ok: true,
|
|
1366
|
+
command: "rewind",
|
|
1367
|
+
action: "rewind",
|
|
1368
|
+
rewind: record,
|
|
1369
|
+
currentStage: nextState.currentStage,
|
|
1370
|
+
completedStages: nextState.completedStages,
|
|
1371
|
+
staleStages: Object.keys(nextState.staleStages),
|
|
1372
|
+
nextActions: [
|
|
1373
|
+
`Re-run ${args.targetStage} stage work and update its artifact evidence.`,
|
|
1374
|
+
`Then run cclaw internal rewind --ack ${args.targetStage}.`,
|
|
1375
|
+
"Continue with /cc-next after the stale marker is acknowledged."
|
|
1376
|
+
]
|
|
1377
|
+
};
|
|
1378
|
+
await appendRewindLog(projectRoot, payload);
|
|
1379
|
+
if (!args.quiet) {
|
|
1380
|
+
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1381
|
+
}
|
|
1382
|
+
return 0;
|
|
1383
|
+
}
|
|
1234
1384
|
async function runHookCommand(projectRoot, args, io) {
|
|
1235
1385
|
const runHookPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
1236
1386
|
try {
|
|
@@ -1269,7 +1419,7 @@ async function runHookCommand(projectRoot, args, io) {
|
|
|
1269
1419
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
1270
1420
|
const [subcommand, ...tokens] = argv;
|
|
1271
1421
|
if (!subcommand) {
|
|
1272
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n");
|
|
1422
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n");
|
|
1273
1423
|
return 1;
|
|
1274
1424
|
}
|
|
1275
1425
|
try {
|
|
@@ -1279,6 +1429,9 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
1279
1429
|
if (subcommand === "start-flow") {
|
|
1280
1430
|
return await runStartFlow(projectRoot, parseStartFlowArgs(tokens), io);
|
|
1281
1431
|
}
|
|
1432
|
+
if (subcommand === "rewind") {
|
|
1433
|
+
return await runRewind(projectRoot, parseRewindArgs(tokens), io);
|
|
1434
|
+
}
|
|
1282
1435
|
if (subcommand === "verify-flow-state-diff") {
|
|
1283
1436
|
return await runVerifyFlowStateDiff(projectRoot, parseVerifyFlowStateDiffArgs(tokens), io);
|
|
1284
1437
|
}
|
|
@@ -1303,7 +1456,7 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
1303
1456
|
if (subcommand === "hook") {
|
|
1304
1457
|
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
1305
1458
|
}
|
|
1306
|
-
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n`);
|
|
1459
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness | hook-manifest | hook\n`);
|
|
1307
1460
|
return 1;
|
|
1308
1461
|
}
|
|
1309
1462
|
catch (err) {
|
package/dist/knowledge-store.js
CHANGED
|
@@ -505,18 +505,40 @@ export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
|
|
|
505
505
|
appendedEntries
|
|
506
506
|
};
|
|
507
507
|
}
|
|
508
|
+
const SHORT_TECHNICAL_TOKEN_SET = new Set(["ci", "db", "ui", "qa", "ux"]);
|
|
508
509
|
function tokenizeText(value) {
|
|
509
510
|
if (!value)
|
|
510
511
|
return [];
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
512
|
+
const tokens = [];
|
|
513
|
+
const matches = value.matchAll(/[A-Za-z0-9]+/gu);
|
|
514
|
+
for (const match of matches) {
|
|
515
|
+
const raw = match[0] ?? "";
|
|
516
|
+
const normalized = raw.toLowerCase();
|
|
517
|
+
if (normalized.length >= 3) {
|
|
518
|
+
tokens.push(normalized);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (/^[A-Z]{2}$/u.test(raw) || SHORT_TECHNICAL_TOKEN_SET.has(normalized)) {
|
|
522
|
+
tokens.push(normalized);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return tokens;
|
|
516
526
|
}
|
|
517
527
|
function uniqueTokens(values) {
|
|
518
528
|
return [...new Set(values)];
|
|
519
529
|
}
|
|
530
|
+
function supersededTriggerSet(entries) {
|
|
531
|
+
const superseded = new Set();
|
|
532
|
+
for (const entry of entries) {
|
|
533
|
+
for (const trigger of entry.supersedes ?? []) {
|
|
534
|
+
superseded.add(normalizeText(trigger));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return superseded;
|
|
538
|
+
}
|
|
539
|
+
function isSupersededLearning(entry, supersededTriggers) {
|
|
540
|
+
return entry.superseded_by !== undefined || supersededTriggers.has(normalizeText(entry.trigger));
|
|
541
|
+
}
|
|
520
542
|
function pathTokens(paths) {
|
|
521
543
|
if (!Array.isArray(paths) || paths.length === 0)
|
|
522
544
|
return [];
|
|
@@ -538,7 +560,9 @@ export async function selectRelevantLearnings(projectRoot, options = {}) {
|
|
|
538
560
|
const limit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0
|
|
539
561
|
? Math.floor(options.limit)
|
|
540
562
|
: 8;
|
|
541
|
-
const
|
|
563
|
+
const staleTriggers = supersededTriggerSet(entries);
|
|
564
|
+
const activeEntries = entries.filter((entry) => !isSupersededLearning(entry, staleTriggers));
|
|
565
|
+
const ranked = activeEntries.map((entry, index) => {
|
|
542
566
|
let score = 0;
|
|
543
567
|
let stageScore = 0;
|
|
544
568
|
if (stage) {
|
package/dist/run-archive.js
CHANGED
|
@@ -35,6 +35,13 @@ function stateDirPath(projectRoot) {
|
|
|
35
35
|
function archiveLockPath(projectRoot) {
|
|
36
36
|
return path.join(projectRoot, RUNTIME_ROOT, "state", ".archive.lock");
|
|
37
37
|
}
|
|
38
|
+
function compoundCloseoutComplete(state) {
|
|
39
|
+
return (state.closeout.compoundCompletedAt !== undefined ||
|
|
40
|
+
state.closeout.compoundPromoted > 0 ||
|
|
41
|
+
(state.closeout.compoundSkipped === true &&
|
|
42
|
+
typeof state.closeout.compoundSkipReason === "string" &&
|
|
43
|
+
state.closeout.compoundSkipReason.trim().length > 0));
|
|
44
|
+
}
|
|
38
45
|
async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
39
46
|
const sourceDir = stateDirPath(projectRoot);
|
|
40
47
|
if (!(await exists(sourceDir))) {
|
|
@@ -209,6 +216,10 @@ export async function archiveRun(projectRoot, runName, options = {}) {
|
|
|
209
216
|
sourceState.closeout.retroSkipReason.trim().length > 0;
|
|
210
217
|
const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
|
|
211
218
|
const inShipCloseout = sourceState.currentStage === "ship";
|
|
219
|
+
if (readyForArchive && !compoundCloseoutComplete(sourceState)) {
|
|
220
|
+
throw new Error("Archive blocked: compound closeout is incomplete. " +
|
|
221
|
+
"Promote compound guidance or skip compound review with an explicit reason before archiving.");
|
|
222
|
+
}
|
|
212
223
|
if (inShipCloseout && skipRetro) {
|
|
213
224
|
throw new Error("Archive blocked: --skip-retro is not allowed while current stage is ship. " +
|
|
214
225
|
"Complete closeout to ready_to_archive via /cc-next.");
|
package/dist/run-persistence.js
CHANGED
|
@@ -30,10 +30,28 @@ function validateFlowTransition(prev, next) {
|
|
|
30
30
|
if (prev.track !== next.track) {
|
|
31
31
|
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change track from "${prev.track}" to "${next.track}" mid-run (activeRunId="${prev.activeRunId}"). Archive the run and start a new one to switch tracks.`);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const newRewind = next.rewinds.length === prev.rewinds.length + 1
|
|
34
|
+
? next.rewinds[next.rewinds.length - 1]
|
|
35
|
+
: undefined;
|
|
36
|
+
const isManagedRewind = newRewind !== undefined
|
|
37
|
+
&& newRewind.fromStage === prev.currentStage
|
|
38
|
+
&& newRewind.toStage === next.currentStage
|
|
39
|
+
&& newRewind.invalidatedStages.includes(next.currentStage);
|
|
40
|
+
const removedCompletedStages = prev.completedStages.filter((stage) => !next.completedStages.includes(stage));
|
|
41
|
+
if (removedCompletedStages.length > 0 && !isManagedRewind) {
|
|
42
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage(s) ${removedCompletedStages.map((stage) => `"${stage}"`).join(", ")} were previously completed but are missing from the new state.`);
|
|
43
|
+
}
|
|
44
|
+
if (isManagedRewind) {
|
|
45
|
+
const invalidated = new Set(newRewind.invalidatedStages);
|
|
46
|
+
const unexpectedRemoved = removedCompletedStages.filter((stage) => !invalidated.has(stage));
|
|
47
|
+
const missingMarkers = newRewind.invalidatedStages.filter((stage) => {
|
|
48
|
+
const marker = next.staleStages[stage];
|
|
49
|
+
return !marker || marker.rewindId !== newRewind.id;
|
|
50
|
+
});
|
|
51
|
+
if (unexpectedRemoved.length > 0 || missingMarkers.length > 0) {
|
|
52
|
+
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `managed rewind state is inconsistent: unexpectedRemoved=${unexpectedRemoved.join(",") || "none"}; missingMarkers=${missingMarkers.join(",") || "none"}.`);
|
|
36
53
|
}
|
|
54
|
+
return;
|
|
37
55
|
}
|
|
38
56
|
if (prev.currentStage === next.currentStage) {
|
|
39
57
|
return;
|
|
@@ -255,21 +273,27 @@ function sanitizeCloseoutState(value) {
|
|
|
255
273
|
? true
|
|
256
274
|
: undefined;
|
|
257
275
|
const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
|
|
258
|
-
const
|
|
276
|
+
const compoundSkipReason = typeof typed.compoundSkipReason === "string"
|
|
277
|
+
? typed.compoundSkipReason.trim() || undefined
|
|
278
|
+
: undefined;
|
|
279
|
+
const compoundSkipped = typed.compoundSkipped === true && compoundSkipReason !== undefined
|
|
280
|
+
? true
|
|
281
|
+
: undefined;
|
|
259
282
|
const promotedRaw = typed.compoundPromoted;
|
|
260
283
|
const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
|
|
261
284
|
? Math.floor(promotedRaw)
|
|
262
285
|
: 0;
|
|
263
|
-
// Demote shipSubstate when its
|
|
264
|
-
// hand-edited flow-state could claim `ready_to_archive`
|
|
265
|
-
//
|
|
266
|
-
// proceed and skip the gate. Compound completion is not independently
|
|
267
|
-
// tracked in all flows (some runs rely on knowledge.jsonl + the retro
|
|
268
|
-
// window), so we only demote when the retro leg is missing outright.
|
|
286
|
+
// Demote shipSubstate when its closeout invariants are violated on disk. A
|
|
287
|
+
// hand-edited flow-state could claim `ready_to_archive` without completing
|
|
288
|
+
// the compound leg, which would let `archive` skip durable closeout proof.
|
|
269
289
|
const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
|
|
290
|
+
const compoundDone = compoundCompletedAt !== undefined || compoundPromoted > 0 || compoundSkipped === true;
|
|
270
291
|
if (!retroDone && (shipSubstate === "ready_to_archive" || shipSubstate === "compound_review")) {
|
|
271
292
|
shipSubstate = "retro_review";
|
|
272
293
|
}
|
|
294
|
+
else if (shipSubstate === "ready_to_archive" && !compoundDone) {
|
|
295
|
+
shipSubstate = "compound_review";
|
|
296
|
+
}
|
|
273
297
|
return {
|
|
274
298
|
shipSubstate,
|
|
275
299
|
retroDraftedAt,
|
|
@@ -278,6 +302,7 @@ function sanitizeCloseoutState(value) {
|
|
|
278
302
|
retroSkipReason,
|
|
279
303
|
compoundCompletedAt,
|
|
280
304
|
compoundSkipped,
|
|
305
|
+
compoundSkipReason,
|
|
281
306
|
compoundPromoted
|
|
282
307
|
};
|
|
283
308
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const TEST_COMMAND_HINT_PATTERN: RegExp;
|
|
2
|
+
export declare const SHA_WITH_LABEL_PATTERN: RegExp;
|
|
3
|
+
export declare const PASS_STATUS_PATTERN: RegExp;
|
|
4
|
+
export declare const NO_VCS_ATTESTATION_PATTERN: RegExp;
|
|
5
|
+
export declare const NO_VCS_HASH_PATTERN: RegExp;
|
|
6
|
+
export type TddVerificationRefMode = "auto" | "required" | "disabled";
|
|
7
|
+
export interface TddVerificationEvidenceOptions {
|
|
8
|
+
requireCommand?: boolean;
|
|
9
|
+
requirePassStatus?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface TddVerificationEvidenceResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
issues: string[];
|
|
14
|
+
mode: TddVerificationRefMode;
|
|
15
|
+
gitPresent: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function validateTddVerificationEvidence(projectRoot: string, evidence: string, options?: TddVerificationEvidenceOptions): Promise<TddVerificationEvidenceResult>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readConfig } from "./config.js";
|
|
3
|
+
import { exists } from "./fs-utils.js";
|
|
4
|
+
export const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|npm run test(?::[\w:-]+)?|pnpm test|pnpm [\w:-]*test[\w:-]*|yarn test|yarn [\w:-]*test[\w:-]*|bun test|bun run test(?::[\w:-]+)?|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|\.\/gradlew test|dotnet test)\b/iu;
|
|
5
|
+
export const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
6
|
+
export const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
7
|
+
export const NO_VCS_ATTESTATION_PATTERN = /\b(?:no[-_ ]?vcs|no git|not a git repo|vcs\s*[:=]\s*none)\b/iu;
|
|
8
|
+
export const NO_VCS_HASH_PATTERN = /\b(?:content|artifact)[-_ ]?hash\s*[:=]\s*(?:sha256:)?[0-9a-f]{16,64}\b|\bsha256\s*[:=]\s*[0-9a-f]{16,64}\b/iu;
|
|
9
|
+
export async function validateTddVerificationEvidence(projectRoot, evidence, options = {}) {
|
|
10
|
+
const normalized = evidence.trim();
|
|
11
|
+
const config = await readConfig(projectRoot);
|
|
12
|
+
const mode = config.tdd?.verificationRef ?? "auto";
|
|
13
|
+
const configuredVcs = config.vcs ?? "git-local-only";
|
|
14
|
+
const gitPresent = configuredVcs !== "none" && await exists(path.join(projectRoot, ".git"));
|
|
15
|
+
const issues = [];
|
|
16
|
+
if (options.requireCommand !== false && !TEST_COMMAND_HINT_PATTERN.test(normalized)) {
|
|
17
|
+
issues.push("must include the fresh verification command that was run (for example `npm test`, `pytest`, `go test`, or equivalent).");
|
|
18
|
+
}
|
|
19
|
+
if (options.requirePassStatus !== false && !PASS_STATUS_PATTERN.test(normalized)) {
|
|
20
|
+
issues.push("must include explicit success status (for example `PASS` or `GREEN`).");
|
|
21
|
+
}
|
|
22
|
+
const hasSha = SHA_WITH_LABEL_PATTERN.test(normalized);
|
|
23
|
+
const hasNoVcs = NO_VCS_ATTESTATION_PATTERN.test(normalized);
|
|
24
|
+
const hasNoVcsHash = NO_VCS_HASH_PATTERN.test(normalized);
|
|
25
|
+
if (mode !== "disabled" && configuredVcs === "none") {
|
|
26
|
+
if (!hasNoVcs) {
|
|
27
|
+
issues.push("must include an explicit no-VCS reason because `vcs` is `none`.");
|
|
28
|
+
}
|
|
29
|
+
if (!hasNoVcsHash) {
|
|
30
|
+
issues.push("must include a content/artifact hash for no-VCS TDD evidence (for example `artifact-hash: sha256:<hash>`).");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (mode === "required" && !hasSha) {
|
|
34
|
+
issues.push("must include a commit SHA token prefixed with `sha` or `commit` because `tdd.verificationRef` is `required`.");
|
|
35
|
+
}
|
|
36
|
+
else if (mode === "auto" && gitPresent && !hasSha) {
|
|
37
|
+
issues.push("must include a commit SHA token prefixed with `sha` or `commit` (for example `sha: abc1234`).");
|
|
38
|
+
}
|
|
39
|
+
else if (mode === "auto" && !gitPresent && !hasSha && !hasNoVcs) {
|
|
40
|
+
issues.push("must include either a commit SHA or an explicit no-VCS attestation (for example `no-vcs: project has no .git directory`).");
|
|
41
|
+
}
|
|
42
|
+
return { ok: issues.length === 0, issues, mode, gitPresent };
|
|
43
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -99,6 +99,13 @@ export interface SliceReviewConfig {
|
|
|
99
99
|
export interface TddPathConfig {
|
|
100
100
|
testPathPatterns?: string[];
|
|
101
101
|
productionPathPatterns?: string[];
|
|
102
|
+
/**
|
|
103
|
+
* Verification reference policy for tdd_verified_before_complete evidence.
|
|
104
|
+
* - auto: require commit SHA when .git exists; with vcs:none require no-VCS reason plus content/artifact hash.
|
|
105
|
+
* - required: always require a commit SHA except explicit vcs:none still uses no-VCS hash evidence.
|
|
106
|
+
* - disabled: command + pass status are enough.
|
|
107
|
+
*/
|
|
108
|
+
verificationRef?: "auto" | "required" | "disabled";
|
|
102
109
|
}
|
|
103
110
|
/**
|
|
104
111
|
* Compound-stage clustering policy.
|
|
@@ -141,10 +148,13 @@ export interface ReviewLoopExternalSecondOpinionConfig {
|
|
|
141
148
|
export interface ReviewLoopConfig {
|
|
142
149
|
externalSecondOpinion?: ReviewLoopExternalSecondOpinionConfig;
|
|
143
150
|
}
|
|
151
|
+
export type VcsMode = "git-with-remote" | "git-local-only" | "none";
|
|
144
152
|
export interface CclawConfig {
|
|
145
153
|
version: string;
|
|
146
154
|
flowVersion: string;
|
|
147
155
|
harnesses: HarnessId[];
|
|
156
|
+
/** Repository evidence mode for stages that need durable verification refs. */
|
|
157
|
+
vcs?: VcsMode;
|
|
148
158
|
/**
|
|
149
159
|
* Single knob that controls enforcement behaviour of all hook-driven guards
|
|
150
160
|
* (prompt guard, workflow guard, TDD enforcement, iron laws). Default:
|