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.
Files changed (45) hide show
  1. package/README.md +135 -414
  2. package/dist/artifact-linter.js +10 -6
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +28 -3
  5. package/dist/content/core-agents.d.ts +110 -0
  6. package/dist/content/core-agents.js +255 -3
  7. package/dist/content/examples.js +8 -5
  8. package/dist/content/harness-doc.d.ts +1 -0
  9. package/dist/content/harness-doc.js +3 -0
  10. package/dist/content/hooks.d.ts +1 -0
  11. package/dist/content/hooks.js +189 -0
  12. package/dist/content/next-command.js +10 -6
  13. package/dist/content/reference-patterns.d.ts +18 -0
  14. package/dist/content/reference-patterns.js +391 -0
  15. package/dist/content/skills.js +42 -36
  16. package/dist/content/stage-common-guidance.js +19 -3
  17. package/dist/content/stage-schema.d.ts +12 -0
  18. package/dist/content/stage-schema.js +184 -28
  19. package/dist/content/stages/_lint-metadata/index.js +3 -2
  20. package/dist/content/stages/brainstorm.js +7 -3
  21. package/dist/content/stages/design.js +12 -3
  22. package/dist/content/stages/review.js +7 -5
  23. package/dist/content/stages/schema-types.d.ts +9 -2
  24. package/dist/content/stages/scope.js +8 -2
  25. package/dist/content/stages/ship.js +3 -2
  26. package/dist/content/stages/tdd.js +18 -13
  27. package/dist/content/start-command.js +3 -2
  28. package/dist/content/status-command.js +17 -6
  29. package/dist/content/subagents.js +286 -40
  30. package/dist/content/templates.js +64 -3
  31. package/dist/content/tree-command.js +7 -1
  32. package/dist/delegation.d.ts +34 -1
  33. package/dist/delegation.js +168 -8
  34. package/dist/doctor-registry.js +9 -0
  35. package/dist/doctor.js +121 -6
  36. package/dist/gate-evidence.js +25 -2
  37. package/dist/harness-adapters.d.ts +6 -0
  38. package/dist/harness-adapters.js +28 -4
  39. package/dist/install.js +5 -10
  40. package/dist/internal/advance-stage.js +179 -26
  41. package/dist/run-persistence.js +21 -3
  42. package/dist/tdd-verification-evidence.d.ts +17 -0
  43. package/dist/tdd-verification-evidence.js +43 -0
  44. package/dist/types.d.ts +10 -0
  45. 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(evidence.trim());
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 && args.waiverReason.length > 0
777
- ? args.waiverReason
778
- : "manual_waiver";
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 = nextPassed.flatMap((gateId) => {
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
- return issue ? [`${gateId}: ${issue}${reviewLoopArtifactFixHint(args.stage, gateId)}`] : [];
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) {
@@ -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
- for (const completed of prev.completedStages) {
34
- if (!next.completedStages.includes(completed)) {
35
- throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage "${completed}" was previously completed but is missing from the new state.`);
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.24",
3
+ "version": "0.51.26",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {