cclaw-cli 0.51.24 → 0.51.26
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 +110 -0
- package/dist/content/core-agents.js +255 -3
- package/dist/content/examples.js +8 -5
- package/dist/content/harness-doc.d.ts +1 -0
- package/dist/content/harness-doc.js +3 -0
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +189 -0
- 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/skills.js +42 -36
- package/dist/content/stage-common-guidance.js +19 -3
- package/dist/content/stage-schema.d.ts +12 -0
- package/dist/content/stage-schema.js +184 -28
- package/dist/content/stages/_lint-metadata/index.js +3 -2
- package/dist/content/stages/brainstorm.js +7 -3
- package/dist/content/stages/design.js +12 -3
- package/dist/content/stages/review.js +7 -5
- package/dist/content/stages/schema-types.d.ts +9 -2
- package/dist/content/stages/scope.js +8 -2
- 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 +17 -6
- package/dist/content/subagents.js +286 -40
- package/dist/content/templates.js +64 -3
- package/dist/content/tree-command.js +7 -1
- package/dist/delegation.d.ts +34 -1
- package/dist/delegation.js +168 -8
- package/dist/doctor-registry.js +9 -0
- package/dist/doctor.js +121 -6
- package/dist/gate-evidence.js +25 -2
- package/dist/harness-adapters.d.ts +6 -0
- package/dist/harness-adapters.js +28 -4
- package/dist/install.js +5 -10
- package/dist/internal/advance-stage.js +179 -26
- package/dist/run-persistence.js +21 -3
- 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/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;
|
|
@@ -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:
|