cclaw-cli 0.51.21 → 0.51.23

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 (48) hide show
  1. package/README.md +14 -13
  2. package/dist/config.d.ts +8 -1
  3. package/dist/config.js +9 -6
  4. package/dist/content/examples.js +2 -2
  5. package/dist/content/hook-manifest.d.ts +2 -4
  6. package/dist/content/hook-manifest.js +5 -7
  7. package/dist/content/learnings.js +5 -2
  8. package/dist/content/meta-skill.d.ts +1 -0
  9. package/dist/content/meta-skill.js +16 -9
  10. package/dist/content/next-command.js +2 -2
  11. package/dist/content/node-hooks.js +14 -4
  12. package/dist/content/review-loop.js +15 -5
  13. package/dist/content/review-prompts.js +1 -1
  14. package/dist/content/skills.js +16 -11
  15. package/dist/content/stage-command.d.ts +2 -0
  16. package/dist/content/stage-command.js +17 -0
  17. package/dist/content/stage-schema.js +1 -0
  18. package/dist/content/stages/brainstorm.js +3 -3
  19. package/dist/content/stages/design.js +18 -17
  20. package/dist/content/stages/plan.js +2 -1
  21. package/dist/content/stages/review.js +15 -15
  22. package/dist/content/stages/scope.js +14 -14
  23. package/dist/content/stages/spec.js +7 -5
  24. package/dist/content/stages/tdd.js +11 -4
  25. package/dist/content/start-command.d.ts +4 -3
  26. package/dist/content/start-command.js +21 -17
  27. package/dist/content/subagents.js +14 -4
  28. package/dist/content/templates.d.ts +1 -1
  29. package/dist/content/templates.js +49 -29
  30. package/dist/content/track-render-context.js +7 -0
  31. package/dist/content/view-command.js +3 -1
  32. package/dist/delegation.d.ts +2 -2
  33. package/dist/delegation.js +40 -13
  34. package/dist/doctor-registry.js +1 -1
  35. package/dist/doctor.js +222 -34
  36. package/dist/gate-evidence.js +19 -7
  37. package/dist/harness-adapters.d.ts +14 -11
  38. package/dist/harness-adapters.js +154 -22
  39. package/dist/install.js +116 -28
  40. package/dist/internal/advance-stage.js +90 -11
  41. package/dist/knowledge-store.d.ts +4 -1
  42. package/dist/knowledge-store.js +24 -14
  43. package/dist/retro-gate.d.ts +1 -0
  44. package/dist/retro-gate.js +9 -9
  45. package/dist/run-archive.js +19 -1
  46. package/dist/run-persistence.js +6 -2
  47. package/dist/tdd-cycle.js +6 -3
  48. package/package.json +1 -1
@@ -230,7 +230,7 @@ function validateGateEvidenceShape(stage, gateId, evidence) {
230
230
  function reviewLoopArtifactFixHint(stage, gateId) {
231
231
  if (AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage] !== gateId)
232
232
  return "";
233
- return " Add a `## Spec Review Loop` table to the artifact with rows like `| 1 | 0.80 | 0 |` plus `- Stop reason: quality_threshold_met`, `- Target score: 0.80`, and `- Max iterations: 3`; then omit this gate from manual evidence so stage-complete can auto-hydrate it.";
233
+ return ` Add a \`## ${stage === "scope" ? "Scope Outside Voice Loop" : "Design Outside Voice Loop"}\` table to the artifact with rows like \`| 1 | 0.80 | 0 |\` plus \`- Stop reason: quality_threshold_met\`, \`- Target score: 0.80\`, and \`- Max iterations: 3\`; then omit this gate from manual evidence so stage-complete can auto-hydrate it.`;
234
234
  }
235
235
  function parseStringList(raw) {
236
236
  if (!Array.isArray(raw))
@@ -401,9 +401,7 @@ async function hydrateReviewLoopEvidenceFromArtifact(projectRoot, stage, track,
401
401
  return;
402
402
  const existing = evidenceByGate[gateId];
403
403
  if (typeof existing === "string" && existing.trim().length > 0) {
404
- const existingIssue = validateGateEvidenceShape(stage, gateId, existing);
405
- if (!existingIssue)
406
- return;
404
+ return;
407
405
  }
408
406
  const resolved = await resolveArtifactPath(stage, {
409
407
  projectRoot,
@@ -617,11 +615,16 @@ function parseStartFlowArgs(tokens) {
617
615
  }
618
616
  return { track, className, prompt, reason, stack, forceReset, reclassify, quiet };
619
617
  }
620
- async function buildValidationReport(projectRoot, flowState) {
618
+ async function buildValidationReport(projectRoot, flowState, options = {}) {
621
619
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
622
620
  const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState);
623
621
  const completedStages = verifyCompletedStagesGateClosure(flowState);
624
- const ok = delegation.satisfied && gates.ok && gates.complete && completedStages.ok;
622
+ const blockedReviewRouteComplete = options.allowBlockedReviewRoute === true
623
+ && flowState.currentStage === "review"
624
+ && typeof flowState.guardEvidence.review_verdict_blocked === "string"
625
+ && flowState.guardEvidence.review_verdict_blocked.trim().length > 0
626
+ && !flowState.stageGateCatalog.review.passed.includes("review_criticals_resolved");
627
+ const ok = delegation.satisfied && gates.ok && (gates.complete || blockedReviewRouteComplete) && completedStages.ok;
625
628
  return {
626
629
  ok,
627
630
  stage: flowState.currentStage,
@@ -753,7 +756,11 @@ async function runAdvanceStage(projectRoot, args, io) {
753
756
  : requiredGateIds;
754
757
  const selectedGateIdSet = new Set(selectedGateIds);
755
758
  const selectedTransitionGuards = selectedGateIds.filter((gateId) => transitionGuardIds.has(gateId));
756
- const missingRequired = requiredGateIds.filter((gateId) => !selectedGateIdSet.has(gateId));
759
+ const blockedReviewRoute = args.stage === "review" && selectedGateIdSet.has("review_verdict_blocked");
760
+ const requiredForSelectedRoute = blockedReviewRoute
761
+ ? requiredGateIds.filter((gateId) => gateId !== "review_criticals_resolved")
762
+ : requiredGateIds;
763
+ const missingRequired = requiredForSelectedRoute.filter((gateId) => !selectedGateIdSet.has(gateId));
757
764
  if (missingRequired.length > 0) {
758
765
  io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
759
766
  return 1;
@@ -844,7 +851,9 @@ async function runAdvanceStage(projectRoot, args, io) {
844
851
  [args.stage]: nextStageCatalog
845
852
  }
846
853
  };
847
- const validation = await buildValidationReport(projectRoot, candidateState);
854
+ const validation = await buildValidationReport(projectRoot, candidateState, {
855
+ allowBlockedReviewRoute: blockedReviewRoute
856
+ });
848
857
  if (!validation.ok) {
849
858
  if (args.json) {
850
859
  io.stdout.write(`${JSON.stringify({
@@ -894,9 +903,11 @@ async function runAdvanceStage(projectRoot, args, io) {
894
903
  }
895
904
  const satisfiedGuards = new Set([...nextPassed, ...selectedTransitionGuards]);
896
905
  const successor = resolveSuccessorTransition(args.stage, flowState.track, transitionTargets, satisfiedGuards, new Set(selectedTransitionGuards));
897
- const completedStages = flowState.completedStages.includes(args.stage)
898
- ? [...flowState.completedStages]
899
- : [...flowState.completedStages, args.stage];
906
+ const completedStages = blockedReviewRoute
907
+ ? flowState.completedStages.filter((stage) => stage !== args.stage)
908
+ : flowState.completedStages.includes(args.stage)
909
+ ? [...flowState.completedStages]
910
+ : [...flowState.completedStages, args.stage];
900
911
  const finalState = {
901
912
  ...candidateState,
902
913
  completedStages,
@@ -969,6 +980,55 @@ function firstIncompleteStageForTrack(track, completedStages) {
969
980
  const stages = TRACK_STAGES[track];
970
981
  return stages.find((stage) => !completed.has(stage)) ?? stages[stages.length - 1] ?? "brainstorm";
971
982
  }
983
+ function carriedCompletedStageCatalog(current, fresh, stage) {
984
+ const previousCatalog = current.stageGateCatalog[stage];
985
+ const freshCatalog = fresh.stageGateCatalog[stage];
986
+ const allowed = new Set([...freshCatalog.required, ...freshCatalog.recommended]);
987
+ const previousPassed = new Set(previousCatalog.passed.filter((gateId) => allowed.has(gateId)));
988
+ const previousBlocked = new Set(previousCatalog.blocked.filter((gateId) => allowed.has(gateId)));
989
+ const orderedAllowed = [...freshCatalog.required, ...freshCatalog.recommended];
990
+ const evidence = {};
991
+ const passed = orderedAllowed.filter((gateId) => {
992
+ if (!previousPassed.has(gateId))
993
+ return false;
994
+ const note = current.guardEvidence[gateId];
995
+ if (typeof note !== "string" || note.trim().length === 0)
996
+ return false;
997
+ evidence[gateId] = note.trim();
998
+ return true;
999
+ });
1000
+ const passedSet = new Set(passed);
1001
+ return {
1002
+ catalog: {
1003
+ required: [...freshCatalog.required],
1004
+ recommended: [...freshCatalog.recommended],
1005
+ conditional: [],
1006
+ triggered: [],
1007
+ passed,
1008
+ blocked: orderedAllowed.filter((gateId) => previousBlocked.has(gateId) && !passedSet.has(gateId))
1009
+ },
1010
+ evidence
1011
+ };
1012
+ }
1013
+ function completedStageClosureEvidenceIssues(flowState) {
1014
+ const issues = [];
1015
+ for (const stage of flowState.completedStages) {
1016
+ const schema = stageSchema(stage, flowState.track);
1017
+ const catalog = flowState.stageGateCatalog[stage];
1018
+ const required = schema.requiredGates
1019
+ .filter((gate) => gate.tier === "required")
1020
+ .map((gate) => gate.id);
1021
+ for (const gateId of required) {
1022
+ if (!catalog.passed.includes(gateId))
1023
+ continue;
1024
+ const note = flowState.guardEvidence[gateId];
1025
+ if (typeof note !== "string" || note.trim().length === 0) {
1026
+ issues.push(`completed stage "${stage}" passed gate "${gateId}" is missing guardEvidence.`);
1027
+ }
1028
+ }
1029
+ }
1030
+ return issues;
1031
+ }
972
1032
  async function ensureProactiveDelegationTrace(projectRoot, stage) {
973
1033
  const proactiveRules = stageAutoSubagentDispatch(stage).filter((rule) => rule.mode === "proactive");
974
1034
  if (proactiveRules.length === 0)
@@ -1126,13 +1186,32 @@ async function runStartFlow(projectRoot, args, io) {
1126
1186
  if (args.reclassify) {
1127
1187
  const completedInNewTrack = current.completedStages.filter((stage) => TRACK_STAGES[args.track].includes(stage));
1128
1188
  const fresh = createInitialFlowState({ activeRunId: current.activeRunId, track: args.track });
1189
+ const stageGateCatalog = { ...fresh.stageGateCatalog };
1190
+ const guardEvidence = {};
1191
+ for (const stage of completedInNewTrack) {
1192
+ const carried = carriedCompletedStageCatalog(current, fresh, stage);
1193
+ stageGateCatalog[stage] = carried.catalog;
1194
+ Object.assign(guardEvidence, carried.evidence);
1195
+ }
1129
1196
  nextState = {
1130
1197
  ...fresh,
1131
1198
  completedStages: completedInNewTrack,
1132
1199
  currentStage: firstIncompleteStageForTrack(args.track, completedInNewTrack),
1200
+ guardEvidence,
1201
+ stageGateCatalog,
1133
1202
  rewinds: current.rewinds,
1134
1203
  staleStages: current.staleStages
1135
1204
  };
1205
+ const validation = await buildValidationReport(projectRoot, nextState);
1206
+ const evidenceIssues = completedStageClosureEvidenceIssues(nextState);
1207
+ if (!validation.completedStages.ok || evidenceIssues.length > 0) {
1208
+ io.stderr.write("cclaw internal start-flow: reclassification would leave completed stages without valid gate closure.\n");
1209
+ const issues = [...validation.completedStages.issues, ...evidenceIssues];
1210
+ if (issues.length > 0) {
1211
+ io.stderr.write(`- completed-stage closure issues: ${issues.join(" | ")}\n`);
1212
+ }
1213
+ return 1;
1214
+ }
1136
1215
  }
1137
1216
  else {
1138
1217
  nextState = createInitialFlowState({ track: args.track });
@@ -174,7 +174,10 @@ export declare function effectiveCompoundThreshold(baseThreshold: number, archiv
174
174
  * as ready.
175
175
  */
176
176
  export declare function computeCompoundReadiness(entries: KnowledgeEntry[], options?: ComputeCompoundReadinessOptions): CompoundReadiness;
177
- export declare function validateKnowledgeEntry(entry: unknown): {
177
+ export interface ValidateKnowledgeEntryOptions {
178
+ allowLegacyOriginFeature?: boolean;
179
+ }
180
+ export declare function validateKnowledgeEntry(entry: unknown, options?: ValidateKnowledgeEntryOptions): {
178
181
  ok: boolean;
179
182
  errors: string[];
180
183
  };
@@ -171,11 +171,13 @@ const KNOWLEDGE_REQUIRED_KEYS = [
171
171
  "project"
172
172
  ];
173
173
  const KNOWLEDGE_ALLOWED_KEYS = new Set(KNOWLEDGE_REQUIRED_KEYS);
174
- KNOWLEDGE_ALLOWED_KEYS.add("origin_feature");
175
174
  KNOWLEDGE_ALLOWED_KEYS.add("source");
176
175
  KNOWLEDGE_ALLOWED_KEYS.add("severity");
177
176
  KNOWLEDGE_ALLOWED_KEYS.add("supersedes");
178
177
  KNOWLEDGE_ALLOWED_KEYS.add("superseded_by");
178
+ function keyAllowedInKnowledgeEntry(key, options) {
179
+ return KNOWLEDGE_ALLOWED_KEYS.has(key) || (options.allowLegacyOriginFeature === true && key === "origin_feature");
180
+ }
179
181
  function knowledgePath(projectRoot) {
180
182
  return path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
181
183
  }
@@ -236,7 +238,7 @@ function parseKnowledgeSnapshot(raw) {
236
238
  continue;
237
239
  try {
238
240
  const parsed = JSON.parse(trimmed);
239
- const validated = validateKnowledgeEntry(parsed);
241
+ const validated = validateKnowledgeEntry(parsed, { allowLegacyOriginFeature: true });
240
242
  if (!validated.ok) {
241
243
  malformedLines += 1;
242
244
  continue;
@@ -294,20 +296,22 @@ function isNullableString(value) {
294
296
  function isNullableStage(value) {
295
297
  return value === null || (typeof value === "string" && FLOW_STAGE_SET.has(value));
296
298
  }
297
- export function validateKnowledgeEntry(entry) {
299
+ export function validateKnowledgeEntry(entry, options = {}) {
298
300
  const errors = [];
299
301
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
300
302
  return { ok: false, errors: ["Knowledge entry must be a JSON object."] };
301
303
  }
302
304
  const obj = entry;
303
305
  for (const key of Object.keys(obj)) {
304
- if (!KNOWLEDGE_ALLOWED_KEYS.has(key)) {
306
+ if (!keyAllowedInKnowledgeEntry(key, options)) {
305
307
  errors.push(`Unknown key "${key}" in knowledge entry.`);
306
308
  }
307
309
  }
308
310
  for (const key of KNOWLEDGE_REQUIRED_KEYS) {
309
311
  if (!Object.prototype.hasOwnProperty.call(obj, key)) {
310
- if (key !== "origin_run" || !Object.prototype.hasOwnProperty.call(obj, "origin_feature")) {
312
+ if (key !== "origin_run" ||
313
+ options.allowLegacyOriginFeature !== true ||
314
+ !Object.prototype.hasOwnProperty.call(obj, "origin_feature")) {
311
315
  errors.push(`Missing required key "${key}".`);
312
316
  }
313
317
  }
@@ -339,7 +343,9 @@ export function validateKnowledgeEntry(entry) {
339
343
  }
340
344
  const originRun = Object.prototype.hasOwnProperty.call(obj, "origin_run")
341
345
  ? obj.origin_run
342
- : obj.origin_feature;
346
+ : options.allowLegacyOriginFeature === true
347
+ ? obj.origin_feature
348
+ : undefined;
343
349
  if (!isNullableString(originRun)) {
344
350
  errors.push("origin_run must be string or null.");
345
351
  }
@@ -534,16 +540,15 @@ export async function selectRelevantLearnings(projectRoot, options = {}) {
534
540
  : 8;
535
541
  const ranked = entries.map((entry, index) => {
536
542
  let score = 0;
543
+ let stageScore = 0;
537
544
  if (stage) {
538
545
  if (entry.stage === stage) {
539
- score += 4;
546
+ stageScore = 4;
540
547
  }
541
548
  else if (entry.origin_stage === stage) {
542
- score += 3;
543
- }
544
- else if (entry.stage === null) {
545
- score += 1;
549
+ stageScore = 3;
546
550
  }
551
+ score += stageScore;
547
552
  }
548
553
  if (entry.confidence === "high")
549
554
  score += 2;
@@ -561,17 +566,22 @@ export async function selectRelevantLearnings(projectRoot, options = {}) {
561
566
  ...tokenizeText(entry.project)
562
567
  ];
563
568
  const searchSet = new Set(searchable);
569
+ let contextualScore = 0;
564
570
  for (const token of branchTokens) {
565
571
  if (searchSet.has(token))
566
- score += 2;
572
+ contextualScore += 2;
567
573
  }
568
574
  for (const token of diffTokens) {
569
575
  if (searchSet.has(token))
570
- score += 2;
576
+ contextualScore += 2;
571
577
  }
572
578
  for (const token of gateTokens) {
573
579
  if (searchSet.has(token))
574
- score += 2;
580
+ contextualScore += 2;
581
+ }
582
+ score += contextualScore;
583
+ if (stage && entry.stage === null && stageScore === 0 && contextualScore < 4) {
584
+ score = 0;
575
585
  }
576
586
  return {
577
587
  index,
@@ -4,5 +4,6 @@ export interface RetroGateStatus {
4
4
  completed: boolean;
5
5
  compoundEntries: number;
6
6
  hasRetroArtifact: boolean;
7
+ skipped: boolean;
7
8
  }
8
9
  export declare function evaluateRetroGate(projectRoot: string, state: FlowState): Promise<RetroGateStatus>;
@@ -42,11 +42,10 @@ export async function evaluateRetroGate(projectRoot, state) {
42
42
  hasRetroArtifact = false;
43
43
  }
44
44
  }
45
- let compoundEntries = state.retro.compoundEntries;
45
+ let compoundEntries = 0;
46
46
  let windowStartMs = parseIsoTimestamp(state.closeout.retroDraftedAt);
47
47
  let windowEndMs = parseIsoTimestamp(state.closeout.retroAcceptedAt) ?? parseIsoTimestamp(state.retro.completedAt);
48
- if (compoundEntries <= 0 &&
49
- hasRetroArtifact &&
48
+ if (hasRetroArtifact &&
50
49
  windowStartMs === null &&
51
50
  windowEndMs === null) {
52
51
  try {
@@ -61,8 +60,8 @@ export async function evaluateRetroGate(projectRoot, state) {
61
60
  // fallback scan remains disabled when mtime cannot be read
62
61
  }
63
62
  }
64
- const shouldFallbackScan = compoundEntries <= 0 && (windowStartMs !== null || windowEndMs !== null);
65
- if (shouldFallbackScan) {
63
+ const shouldScanCompoundEvidence = windowStartMs !== null || windowEndMs !== null;
64
+ if (shouldScanCompoundEvidence) {
66
65
  const countIfEligible = (parsed) => {
67
66
  if (parsed.type !== "compound") {
68
67
  return 0;
@@ -80,7 +79,6 @@ export async function evaluateRetroGate(projectRoot, state) {
80
79
  try {
81
80
  const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
82
81
  const { entries } = await readKnowledgeSafely(projectRoot);
83
- compoundEntries = 0;
84
82
  for (const parsed of entries) {
85
83
  compoundEntries += countIfEligible(parsed);
86
84
  }
@@ -111,12 +109,13 @@ export async function evaluateRetroGate(projectRoot, state) {
111
109
  // promoted during the retro window OR compound was explicitly skipped
112
110
  // after reviewing the draft), or
113
111
  // - the operator explicitly skipped the retro step itself
114
- // (`retroSkipped === true` with a reason). `retroSkipped` is an
112
+ // (`retroSkipped === true` with a non-empty reason). `retroSkipped` is an
115
113
  // operator-level override of the artifact requirement, so it must
116
114
  // bypass `hasRetroArtifact` — otherwise a run that legitimately had
117
115
  // nothing worth retro-ing dead-locks at closeout waiting for a
118
116
  // file that will never exist.
119
- const retroSkipped = state.closeout.retroSkipped === true;
117
+ const retroSkipReason = state.closeout.retroSkipReason?.trim() ?? "";
118
+ const retroSkipped = state.closeout.retroSkipped === true && retroSkipReason.length > 0;
120
119
  const compoundSkipped = state.closeout.compoundSkipped === true;
121
120
  const artifactPathComplete = hasRetroArtifact && (compoundEntries > 0 || compoundSkipped);
122
121
  const completed = required ? retroSkipped || artifactPathComplete : true;
@@ -124,6 +123,7 @@ export async function evaluateRetroGate(projectRoot, state) {
124
123
  required,
125
124
  completed,
126
125
  compoundEntries,
127
- hasRetroArtifact
126
+ hasRetroArtifact,
127
+ skipped: retroSkipped
128
128
  };
129
129
  }
@@ -83,6 +83,24 @@ async function resetCarryoverStateFiles(projectRoot, activeRunId) {
83
83
  await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "", { mode: 0o600 });
84
84
  await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`, { mode: 0o600 });
85
85
  }
86
+ async function restoreStateSnapshot(projectRoot, archiveStatePath) {
87
+ if (!(await exists(archiveStatePath)))
88
+ return;
89
+ const stateDir = stateDirPath(projectRoot);
90
+ await ensureDir(stateDir);
91
+ const entries = await fs.readdir(archiveStatePath, { withFileTypes: true });
92
+ for (const entry of entries) {
93
+ const from = path.join(archiveStatePath, entry.name);
94
+ const to = path.join(stateDir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ await fs.rm(to, { recursive: true, force: true });
97
+ await fs.cp(from, to, { recursive: true });
98
+ }
99
+ else if (entry.isFile()) {
100
+ await fs.copyFile(from, to);
101
+ }
102
+ }
103
+ }
86
104
  function toArchiveDate(date = new Date()) {
87
105
  const yyyy = date.getFullYear().toString();
88
106
  const mm = (date.getMonth() + 1).toString().padStart(2, "0");
@@ -296,8 +314,8 @@ export async function archiveRun(projectRoot, runName, options = {}) {
296
314
  }
297
315
  if (stateReset) {
298
316
  try {
317
+ await restoreStateSnapshot(projectRoot, path.join(archivePath, "state"));
299
318
  await writeFlowState(projectRoot, stateBeforeReset, { allowReset: true, skipLock: true });
300
- await resetCarryoverStateFiles(projectRoot, stateBeforeReset.activeRunId);
301
319
  }
302
320
  catch {
303
321
  // If rollback of state fails, keep sentinel + archive remnants for
@@ -248,8 +248,12 @@ function sanitizeCloseoutState(value) {
248
248
  let shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
249
249
  const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
250
250
  const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
251
- const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
252
- const retroSkipReason = typeof typed.retroSkipReason === "string" ? typed.retroSkipReason : undefined;
251
+ const retroSkipReason = typeof typed.retroSkipReason === "string"
252
+ ? typed.retroSkipReason.trim() || undefined
253
+ : undefined;
254
+ const retroSkipped = typed.retroSkipped === true && retroSkipReason !== undefined
255
+ ? true
256
+ : undefined;
253
257
  const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
254
258
  const compoundSkipped = typeof typed.compoundSkipped === "boolean" ? typed.compoundSkipped : undefined;
255
259
  const promotedRaw = typed.compoundPromoted;
package/dist/tdd-cycle.js CHANGED
@@ -146,7 +146,6 @@ export function validateTddCycleOrder(entries, options = {}) {
146
146
  issues.push(`slice ${slice}: refactor logged before green`);
147
147
  continue;
148
148
  }
149
- state = "need_red";
150
149
  }
151
150
  if (state === "red_open") {
152
151
  openRedSlices.push(slice);
@@ -191,8 +190,12 @@ export function pathMatchesTarget(candidate, target) {
191
190
  if (normalizedCandidate.length === 0 || normalizedTarget.length === 0) {
192
191
  return false;
193
192
  }
194
- return (normalizedCandidate === normalizedTarget ||
195
- normalizedCandidate.endsWith(`/${normalizedTarget}`));
193
+ if (normalizedCandidate === normalizedTarget) {
194
+ return true;
195
+ }
196
+ // Only allow suffix matching for multi-segment targets. A bare basename
197
+ // like `app.ts` is too broad and can match unrelated files in any folder.
198
+ return normalizedTarget.includes("/") && normalizedCandidate.endsWith(`/${normalizedTarget}`);
196
199
  }
197
200
  /**
198
201
  * Derive a lightweight Ralph Loop summary from parsed tdd-cycle-log entries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.21",
3
+ "version": "0.51.23",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {