cclaw-cli 0.51.21 → 0.51.22
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/dist/config.d.ts +8 -1
- package/dist/config.js +9 -6
- package/dist/content/hook-manifest.d.ts +2 -4
- package/dist/content/hook-manifest.js +4 -3
- package/dist/content/meta-skill.js +7 -9
- package/dist/content/next-command.js +2 -2
- package/dist/content/node-hooks.js +13 -3
- package/dist/content/review-loop.js +15 -5
- package/dist/content/review-prompts.js +1 -1
- package/dist/content/skills.js +3 -2
- package/dist/content/stage-schema.js +1 -0
- package/dist/content/stages/brainstorm.js +3 -3
- package/dist/content/stages/design.js +18 -17
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/review.js +10 -10
- package/dist/content/stages/scope.js +13 -13
- package/dist/content/stages/spec.js +7 -5
- package/dist/content/stages/tdd.js +2 -2
- package/dist/content/start-command.d.ts +4 -3
- package/dist/content/start-command.js +21 -17
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +48 -28
- package/dist/content/view-command.js +3 -1
- package/dist/delegation.js +28 -8
- package/dist/doctor.js +147 -21
- package/dist/gate-evidence.js +19 -7
- package/dist/harness-adapters.js +1 -5
- package/dist/install.js +87 -24
- package/dist/internal/advance-stage.js +90 -11
- package/dist/knowledge-store.d.ts +4 -1
- package/dist/knowledge-store.js +24 -14
- package/dist/retro-gate.d.ts +1 -0
- package/dist/retro-gate.js +9 -9
- package/dist/run-archive.js +19 -1
- package/dist/run-persistence.js +6 -2
- package/dist/tdd-cycle.js +6 -3
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
898
|
-
?
|
|
899
|
-
:
|
|
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
|
|
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
|
};
|
package/dist/knowledge-store.js
CHANGED
|
@@ -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 (!
|
|
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" ||
|
|
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
|
-
:
|
|
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
|
-
|
|
546
|
+
stageScore = 4;
|
|
540
547
|
}
|
|
541
548
|
else if (entry.origin_stage === stage) {
|
|
542
|
-
|
|
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
|
-
|
|
572
|
+
contextualScore += 2;
|
|
567
573
|
}
|
|
568
574
|
for (const token of diffTokens) {
|
|
569
575
|
if (searchSet.has(token))
|
|
570
|
-
|
|
576
|
+
contextualScore += 2;
|
|
571
577
|
}
|
|
572
578
|
for (const token of gateTokens) {
|
|
573
579
|
if (searchSet.has(token))
|
|
574
|
-
|
|
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,
|
package/dist/retro-gate.d.ts
CHANGED
package/dist/retro-gate.js
CHANGED
|
@@ -42,11 +42,10 @@ export async function evaluateRetroGate(projectRoot, state) {
|
|
|
42
42
|
hasRetroArtifact = false;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
let 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 (
|
|
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
|
|
65
|
-
if (
|
|
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
|
|
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
|
}
|
package/dist/run-archive.js
CHANGED
|
@@ -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
|
package/dist/run-persistence.js
CHANGED
|
@@ -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
|
|
252
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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.
|