gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -1,165 +0,0 @@
1
- /**
2
- * Stuck detection and loop recovery for auto-mode unit dispatch.
3
- *
4
- * Tracks dispatch counts per unit, enforces lifetime caps, and attempts
5
- * stub/artifact recovery before stopping.
6
- *
7
- * Extracted from dispatchNextUnit() in auto.ts. Returns action values
8
- * instead of calling stopAuto/dispatchNextUnit — the caller handles
9
- * control flow.
10
- */
11
- import { inspectExecuteTaskDurability, } from "./unit-runtime.js";
12
- import { verifyExpectedArtifact, diagnoseExpectedArtifact, skipExecuteTask, persistCompletedKey, buildLoopRemediationSteps, } from "./auto-recovery.js";
13
- import { closeoutUnit } from "./auto-unit-closeout.js";
14
- import { saveActivityLog } from "./activity-log.js";
15
- import { invalidateAllCaches } from "./cache.js";
16
- import { sendDesktopNotification } from "./notifications.js";
17
- import { debugLog } from "./debug-logger.js";
18
- import { resolveMilestonePath, resolveSlicePath, resolveTasksDir, buildTaskFileName, } from "./paths.js";
19
- import { MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES, } from "./auto/session.js";
20
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
21
- import { join } from "node:path";
22
- import { parseUnitId } from "./unit-id.js";
23
- /**
24
- * Check dispatch counts, enforce lifetime cap and MAX_UNIT_DISPATCHES,
25
- * attempt stub/artifact recovery. Returns an action for the caller.
26
- */
27
- export async function checkStuckAndRecover(sctx) {
28
- const { s, ctx, unitType, unitId, basePath, buildSnapshotOpts } = sctx;
29
- const dispatchKey = `${unitType}/${unitId}`;
30
- const prevCount = s.unitDispatchCount.get(dispatchKey) ?? 0;
31
- // Real dispatch reached — clear the consecutive-skip counter for this unit.
32
- s.unitConsecutiveSkips.delete(dispatchKey);
33
- debugLog("dispatch-unit", {
34
- type: unitType,
35
- id: unitId,
36
- cycle: prevCount + 1,
37
- lifetime: (s.unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1,
38
- });
39
- // Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
40
- const lifetimeCount = (s.unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1;
41
- s.unitLifetimeDispatches.set(dispatchKey, lifetimeCount);
42
- if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
43
- if (s.currentUnit) {
44
- await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts());
45
- }
46
- else {
47
- saveActivityLog(ctx, s.basePath, unitType, unitId);
48
- }
49
- const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
50
- return {
51
- action: "stop",
52
- reason: `Hard loop: ${unitType} ${unitId}`,
53
- notifyMessage: `Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles).${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
54
- };
55
- }
56
- if (prevCount >= MAX_UNIT_DISPATCHES) {
57
- if (s.currentUnit) {
58
- await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts());
59
- }
60
- else {
61
- saveActivityLog(ctx, s.basePath, unitType, unitId);
62
- }
63
- // Final reconciliation pass for execute-task
64
- if (unitType === "execute-task") {
65
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
66
- if (mid && sid && tid) {
67
- const status = await inspectExecuteTaskDurability(basePath, unitId);
68
- if (status) {
69
- const reconciled = skipExecuteTask(basePath, mid, sid, tid, status, "loop-recovery", prevCount);
70
- if (reconciled && verifyExpectedArtifact(unitType, unitId, basePath)) {
71
- ctx.ui.notify(`Loop recovery: ${unitId} reconciled after ${prevCount + 1} dispatches — blocker artifacts written, pipeline advancing.\n Review ${status.summaryPath} and replace the placeholder with real work.`, "warning");
72
- const reconciledKey = `${unitType}/${unitId}`;
73
- persistCompletedKey(basePath, reconciledKey);
74
- s.completedKeySet.add(reconciledKey);
75
- s.unitDispatchCount.delete(dispatchKey);
76
- invalidateAllCaches();
77
- return { action: "recovered", dispatchAgain: true };
78
- }
79
- }
80
- }
81
- }
82
- // General reconciliation: artifact appeared on last attempt
83
- if (verifyExpectedArtifact(unitType, unitId, basePath)) {
84
- ctx.ui.notify(`Loop recovery: ${unitType} ${unitId} — artifact verified after ${prevCount + 1} dispatches. Advancing.`, "info");
85
- persistCompletedKey(basePath, dispatchKey);
86
- s.completedKeySet.add(dispatchKey);
87
- s.unitDispatchCount.delete(dispatchKey);
88
- invalidateAllCaches();
89
- return { action: "recovered", dispatchAgain: true };
90
- }
91
- // Last resort for complete-milestone: generate stub summary
92
- if (unitType === "complete-milestone") {
93
- try {
94
- const mPath = resolveMilestonePath(basePath, unitId);
95
- if (mPath) {
96
- const stubPath = join(mPath, `${unitId}-SUMMARY.md`);
97
- if (!existsSync(stubPath)) {
98
- writeFileSync(stubPath, `# ${unitId} Summary\n\nAuto-generated stub — milestone tasks completed but summary generation failed after ${prevCount + 1} attempts.\nReview and replace this stub with a proper summary.\n`);
99
- ctx.ui.notify(`Generated stub summary for ${unitId} to unblock pipeline. Review later.`, "warning");
100
- persistCompletedKey(basePath, dispatchKey);
101
- s.completedKeySet.add(dispatchKey);
102
- s.unitDispatchCount.delete(dispatchKey);
103
- invalidateAllCaches();
104
- return { action: "recovered", dispatchAgain: true };
105
- }
106
- }
107
- }
108
- catch { /* non-fatal — fall through to normal stop */ }
109
- }
110
- const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
111
- const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
112
- sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
113
- return {
114
- action: "stop",
115
- reason: `Loop: ${unitType} ${unitId}`,
116
- notifyMessage: `Loop detected: ${unitType} ${unitId} dispatched ${prevCount + 1} times total. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}${remediation ? `\n\n Remediation steps:\n${remediation}` : "\n Check branch state and .gsd/ artifacts."}`,
117
- };
118
- }
119
- s.unitDispatchCount.set(dispatchKey, prevCount + 1);
120
- if (prevCount > 0) {
121
- // Adaptive self-repair: each retry attempts a different remediation step.
122
- if (unitType === "execute-task") {
123
- const status = await inspectExecuteTaskDurability(basePath, unitId);
124
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
125
- if (status && mid && sid && tid) {
126
- if (status.summaryExists && !status.taskChecked) {
127
- const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
128
- if (repaired && verifyExpectedArtifact(unitType, unitId, basePath)) {
129
- ctx.ui.notify(`Self-repaired ${unitId}: summary existed but checkbox was unmarked. Marked [x] and advancing.`, "warning");
130
- const repairedKey = `${unitType}/${unitId}`;
131
- persistCompletedKey(basePath, repairedKey);
132
- s.completedKeySet.add(repairedKey);
133
- s.unitDispatchCount.delete(dispatchKey);
134
- invalidateAllCaches();
135
- return { action: "recovered", dispatchAgain: true };
136
- }
137
- }
138
- else if (prevCount >= STUB_RECOVERY_THRESHOLD && !status.summaryExists) {
139
- const tasksDir = resolveTasksDir(basePath, mid, sid);
140
- const sDir = resolveSlicePath(basePath, mid, sid);
141
- const targetDir = tasksDir ?? (sDir ? join(sDir, "tasks") : null);
142
- if (targetDir) {
143
- if (!existsSync(targetDir))
144
- mkdirSync(targetDir, { recursive: true });
145
- const summaryPath = join(targetDir, buildTaskFileName(tid, "SUMMARY"));
146
- if (!existsSync(summaryPath)) {
147
- const stubContent = [
148
- `# PARTIAL RECOVERY — attempt ${prevCount + 1} of ${MAX_UNIT_DISPATCHES}`,
149
- ``,
150
- `Task \`${tid}\` in slice \`${sid}\` (milestone \`${mid}\`) has not yet produced a real summary.`,
151
- `This placeholder was written by auto-mode after ${prevCount} dispatch attempts.`,
152
- ``,
153
- `The next agent session will retry this task. Replace this file with real work when done.`,
154
- ].join("\n");
155
- writeFileSync(summaryPath, stubContent, "utf-8");
156
- ctx.ui.notify(`Stub recovery (attempt ${prevCount + 1}/${MAX_UNIT_DISPATCHES}): ${unitId} stub summary placeholder written. Retrying with recovery context.`, "warning");
157
- }
158
- }
159
- }
160
- }
161
- }
162
- ctx.ui.notify(`${unitType} ${unitId} didn't produce expected artifact. Retrying (${prevCount + 1}/${MAX_UNIT_DISPATCHES}).`, "warning");
163
- }
164
- return { action: "proceed" };
165
- }
@@ -1,351 +0,0 @@
1
- /**
2
- * Mechanical Completion — deterministic post-verification artifact generation.
3
- *
4
- * Pure functions that aggregate task-level outputs into slice/milestone summaries,
5
- * UAT stubs, roadmap checkbox updates, and validation reports. Zero orchestration
6
- * dependencies — operates on filesystem paths and parsed structures only.
7
- *
8
- * ADR-003: replaces LLM-driven complete-slice and validate-milestone units with
9
- * mechanical aggregation when the data is sufficient.
10
- */
11
- import { readFileSync, existsSync, readdirSync } from "node:fs";
12
- import { join } from "node:path";
13
- import { atomicWriteSync } from "./atomic-write.js";
14
- import { loadFile, parseSummary } from "./files.js";
15
- import { extractMarkdownSection } from "./auto-prompts.js";
16
- import { resolveTaskFiles, resolveTaskJsonFiles, resolveTasksDir, resolveSliceFile, resolveSlicePath, resolveMilestoneFile, resolveMilestonePath, resolveGsdRootFile, } from "./paths.js";
17
- // ─── Slice Completion ────────────────────────────────────────────────────────
18
- /**
19
- * Mechanically complete a slice by aggregating task summaries into:
20
- * - S##-SUMMARY.md (aggregated frontmatter + task one-liners)
21
- * - S##-UAT.md (extracted from plan Verification section)
22
- * - Roadmap checkbox [x] update
23
- *
24
- * Returns true if completion succeeded, false if data is insufficient
25
- * (serves as quality gate — caller falls back to LLM completion).
26
- */
27
- export async function mechanicalSliceCompletion(base, mid, sid) {
28
- const tDir = resolveTasksDir(base, mid, sid);
29
- if (!tDir)
30
- return false;
31
- // Read all task summaries
32
- const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
33
- if (summaryFiles.length === 0)
34
- return false;
35
- const taskSummaries = [];
36
- for (const file of summaryFiles) {
37
- const content = readFileSync(join(tDir, file), "utf-8");
38
- if (!content.trim())
39
- continue;
40
- const summary = parseSummary(content);
41
- const taskId = file.match(/^(T\d+)/)?.[1] ?? file;
42
- taskSummaries.push({ taskId, summary });
43
- }
44
- if (taskSummaries.length === 0)
45
- return false;
46
- // Quality gate: multi-task slices need substantive summaries
47
- if (taskSummaries.length > 1) {
48
- const totalContent = taskSummaries
49
- .map(ts => ts.summary.whatHappened || ts.summary.oneLiner || "")
50
- .join("");
51
- if (totalContent.length < 200)
52
- return false;
53
- }
54
- // Aggregate frontmatter
55
- const aggregated = aggregateFrontmatter(taskSummaries.map(ts => ts.summary.frontmatter));
56
- // Build SUMMARY.md
57
- const summaryLines = [
58
- "---",
59
- `id: ${sid}`,
60
- `parent: ${mid}`,
61
- `milestone: ${mid}`,
62
- ];
63
- if (aggregated.provides.length > 0)
64
- summaryLines.push(`provides:\n${aggregated.provides.map(p => ` - ${p}`).join("\n")}`);
65
- if (aggregated.key_files.length > 0)
66
- summaryLines.push(`key_files:\n${aggregated.key_files.map(f => ` - ${f}`).join("\n")}`);
67
- if (aggregated.key_decisions.length > 0)
68
- summaryLines.push(`key_decisions:\n${aggregated.key_decisions.map(d => ` - ${d}`).join("\n")}`);
69
- if (aggregated.patterns_established.length > 0)
70
- summaryLines.push(`patterns_established:\n${aggregated.patterns_established.map(p => ` - ${p}`).join("\n")}`);
71
- if (aggregated.affects.length > 0)
72
- summaryLines.push(`affects:\n${aggregated.affects.map(a => ` - ${a}`).join("\n")}`);
73
- if (aggregated.observability_surfaces.length > 0)
74
- summaryLines.push(`observability_surfaces:\n${aggregated.observability_surfaces.map(o => ` - ${o}`).join("\n")}`);
75
- const allPassed = taskSummaries.every(ts => ts.summary.frontmatter.verification_result === "passed");
76
- summaryLines.push(`verification_result: ${allPassed ? "passed" : "mixed"}`);
77
- summaryLines.push(`completed_at: ${new Date().toISOString()}`);
78
- summaryLines.push("---");
79
- summaryLines.push("");
80
- summaryLines.push(`# ${sid}: Slice Summary`);
81
- summaryLines.push("");
82
- // Task one-liners
83
- for (const { taskId, summary } of taskSummaries) {
84
- const line = summary.oneLiner || summary.title || taskId;
85
- summaryLines.push(`- **${taskId}**: ${line}`);
86
- }
87
- summaryLines.push("");
88
- const sDir = resolveSlicePath(base, mid, sid);
89
- if (!sDir)
90
- return false;
91
- const summaryPath = join(sDir, `${sid}-SUMMARY.md`);
92
- atomicWriteSync(summaryPath, summaryLines.join("\n"));
93
- process.stderr.write(`gsd-mechanical: wrote ${summaryPath}\n`);
94
- // Build UAT.md from plan's Verification section
95
- const planPath = resolveSliceFile(base, mid, sid, "PLAN");
96
- if (planPath) {
97
- const planContent = readFileSync(planPath, "utf-8");
98
- const verification = extractMarkdownSection(planContent, "Verification");
99
- if (verification) {
100
- const uatContent = [
101
- "---",
102
- `id: ${sid}`,
103
- `parent: ${mid}`,
104
- "type: artifact-driven",
105
- "---",
106
- "",
107
- `# ${sid}: UAT`,
108
- "",
109
- verification,
110
- "",
111
- ].join("\n");
112
- const uatPath = join(sDir, `${sid}-UAT.md`);
113
- atomicWriteSync(uatPath, uatContent);
114
- process.stderr.write(`gsd-mechanical: wrote ${uatPath}\n`);
115
- }
116
- }
117
- // Mark slice [x] in ROADMAP
118
- await markSliceInRoadmap(base, mid, sid);
119
- // Append new decisions if any
120
- await appendNewDecisions(base, taskSummaries.map(ts => ts.summary));
121
- // Update requirements if all passed
122
- if (allPassed) {
123
- await mechanicalRequirementsUpdate(base, mid, sid, taskSummaries.map(ts => ts.summary));
124
- }
125
- return true;
126
- }
127
- // ─── Requirements Update ─────────────────────────────────────────────────────
128
- /**
129
- * Conservative requirements update: mark requirements Validated only if
130
- * all tasks' verification passed.
131
- */
132
- export async function mechanicalRequirementsUpdate(_base, _mid, _sid, _taskSummaries) {
133
- // Conservative: requirements validation requires human or LLM judgment
134
- // about whether the requirement is truly met. Mechanical completion only
135
- // marks the slice done — requirement status updates are left to the
136
- // existing validation pipeline.
137
- }
138
- // ─── Decision Aggregation ────────────────────────────────────────────────────
139
- /**
140
- * Collect key_decisions from task summaries, deduplicate against existing
141
- * DECISIONS.md, and append new ones.
142
- */
143
- export async function appendNewDecisions(base, taskSummaries) {
144
- const allDecisions = taskSummaries.flatMap(s => s.frontmatter.key_decisions);
145
- if (allDecisions.length === 0)
146
- return;
147
- const decisionsPath = resolveGsdRootFile(base, "DECISIONS");
148
- const existing = existsSync(decisionsPath)
149
- ? readFileSync(decisionsPath, "utf-8")
150
- : "";
151
- // Deduplicate — skip decisions whose text already appears in the file
152
- const newDecisions = allDecisions.filter(d => d.trim() && !existing.includes(d.trim()));
153
- if (newDecisions.length === 0)
154
- return;
155
- const entries = newDecisions
156
- .map(d => `- ${d} _(auto-aggregated from task summaries)_`)
157
- .join("\n");
158
- const updated = existing.trimEnd() + "\n\n### Auto-aggregated Decisions\n\n" + entries + "\n";
159
- atomicWriteSync(decisionsPath, updated);
160
- process.stderr.write(`gsd-mechanical: appended ${newDecisions.length} decision(s) to DECISIONS.md\n`);
161
- }
162
- /**
163
- * Aggregate T##-VERIFY.json files and S##-UAT-RESULT.md files across all
164
- * slices in a milestone to produce VALIDATION.md.
165
- */
166
- export async function aggregateMilestoneVerification(base, mid) {
167
- const mDir = resolveMilestonePath(base, mid);
168
- if (!mDir)
169
- return { verdict: "failed", checks: [], uatResults: [], markdown: "" };
170
- const allChecks = [];
171
- const allUatResults = [];
172
- // Scan all slices
173
- const slicesDir = join(mDir, "slices");
174
- if (!existsSync(slicesDir))
175
- return { verdict: "failed", checks: [], uatResults: [], markdown: "" };
176
- const sliceDirs = readdirSyncSafe(slicesDir).filter(name => /^S\d+/i.test(name)).sort();
177
- for (const sliceName of sliceDirs) {
178
- const sid = sliceName.match(/^(S\d+)/i)?.[1] ?? sliceName;
179
- const tDir = resolveTasksDir(base, mid, sid);
180
- if (tDir) {
181
- const verifyFiles = resolveTaskJsonFiles(tDir, "VERIFY");
182
- for (const vf of verifyFiles) {
183
- try {
184
- const content = readFileSync(join(tDir, vf), "utf-8");
185
- const evidence = JSON.parse(content);
186
- allChecks.push(evidence);
187
- }
188
- catch {
189
- // Skip malformed JSON
190
- }
191
- }
192
- }
193
- // Check for UAT result
194
- const uatResultPath = resolveSliceFile(base, mid, sid, "UAT-RESULT");
195
- if (uatResultPath) {
196
- try {
197
- const uatContent = readFileSync(uatResultPath, "utf-8");
198
- allUatResults.push(`### ${sid}\n\n${uatContent}`);
199
- }
200
- catch {
201
- // Non-fatal
202
- }
203
- }
204
- }
205
- // Determine verdict
206
- const allPassed = allChecks.length > 0 && allChecks.every(c => c.passed);
207
- const anyFailed = allChecks.some(c => !c.passed);
208
- const verdict = allPassed
209
- ? "passed"
210
- : anyFailed
211
- ? (allChecks.some(c => c.passed) ? "mixed" : "failed")
212
- : "passed"; // No checks = vacuously passed
213
- // Build VALIDATION.md
214
- const mdLines = [
215
- "---",
216
- `milestone: ${mid}`,
217
- `verdict: ${verdict}`,
218
- "remediation_round: 0",
219
- `validated_at: ${new Date().toISOString()}`,
220
- "---",
221
- "",
222
- `# ${mid}: Milestone Validation`,
223
- "",
224
- `**Verdict:** ${verdict}`,
225
- "",
226
- "## Verification Results",
227
- "",
228
- ];
229
- if (allChecks.length === 0) {
230
- mdLines.push("_No verification evidence found._");
231
- }
232
- else {
233
- mdLines.push("| Task | Passed | Checks | Failed |");
234
- mdLines.push("|------|--------|--------|--------|");
235
- for (const check of allChecks) {
236
- const failedCount = check.checks.filter(c => c.verdict === "fail").length;
237
- mdLines.push(`| ${check.taskId} | ${check.passed ? "yes" : "no"} | ${check.checks.length} | ${failedCount} |`);
238
- }
239
- }
240
- if (allUatResults.length > 0) {
241
- mdLines.push("");
242
- mdLines.push("## UAT Results");
243
- mdLines.push("");
244
- mdLines.push(...allUatResults);
245
- }
246
- mdLines.push("");
247
- const markdown = mdLines.join("\n");
248
- // Write VALIDATION.md
249
- const validationPath = join(mDir, `${mid}-VALIDATION.md`);
250
- atomicWriteSync(validationPath, markdown);
251
- process.stderr.write(`gsd-mechanical: wrote ${validationPath}\n`);
252
- return { verdict, checks: allChecks, uatResults: allUatResults, markdown };
253
- }
254
- // ─── Milestone Summary ──────────────────────────────────────────────────────
255
- /**
256
- * Read all S##-SUMMARY.md files and produce M##-SUMMARY.md.
257
- */
258
- export async function generateMilestoneSummary(base, mid) {
259
- const mDir = resolveMilestonePath(base, mid);
260
- if (!mDir)
261
- return "";
262
- const slicesDir = join(mDir, "slices");
263
- if (!existsSync(slicesDir))
264
- return "";
265
- const sliceDirs = readdirSyncSafe(slicesDir).filter(name => /^S\d+/i.test(name)).sort();
266
- const aggregatedProvides = [];
267
- const aggregatedKeyFiles = [];
268
- const aggregatedKeyDecisions = [];
269
- const aggregatedPatterns = [];
270
- const sliceOneLinerList = [];
271
- for (const sliceName of sliceDirs) {
272
- const sid = sliceName.match(/^(S\d+)/i)?.[1] ?? sliceName;
273
- const summaryPath = resolveSliceFile(base, mid, sid, "SUMMARY");
274
- if (!summaryPath)
275
- continue;
276
- try {
277
- const content = readFileSync(summaryPath, "utf-8");
278
- const summary = parseSummary(content);
279
- aggregatedProvides.push(...summary.frontmatter.provides);
280
- aggregatedKeyFiles.push(...summary.frontmatter.key_files);
281
- aggregatedKeyDecisions.push(...summary.frontmatter.key_decisions);
282
- aggregatedPatterns.push(...summary.frontmatter.patterns_established);
283
- sliceOneLinerList.push(`- **${sid}**: ${summary.oneLiner || summary.title || sid}`);
284
- }
285
- catch {
286
- sliceOneLinerList.push(`- **${sid}**: _(summary unavailable)_`);
287
- }
288
- }
289
- const mdLines = [
290
- "---",
291
- `id: ${mid}`,
292
- ];
293
- if (dedup(aggregatedProvides).length > 0)
294
- mdLines.push(`provides:\n${dedup(aggregatedProvides).map(p => ` - ${p}`).join("\n")}`);
295
- if (dedup(aggregatedKeyFiles).length > 0)
296
- mdLines.push(`key_files:\n${dedup(aggregatedKeyFiles).map(f => ` - ${f}`).join("\n")}`);
297
- if (dedup(aggregatedKeyDecisions).length > 0)
298
- mdLines.push(`key_decisions:\n${dedup(aggregatedKeyDecisions).map(d => ` - ${d}`).join("\n")}`);
299
- if (dedup(aggregatedPatterns).length > 0)
300
- mdLines.push(`patterns_established:\n${dedup(aggregatedPatterns).map(p => ` - ${p}`).join("\n")}`);
301
- mdLines.push(`completed_at: ${new Date().toISOString()}`);
302
- mdLines.push("---");
303
- mdLines.push("");
304
- mdLines.push(`# ${mid}: Milestone Summary`);
305
- mdLines.push("");
306
- mdLines.push("## Slices");
307
- mdLines.push("");
308
- mdLines.push(...sliceOneLinerList);
309
- mdLines.push("");
310
- const content = mdLines.join("\n");
311
- // Write M##-SUMMARY.md
312
- const summaryPath = join(mDir, `${mid}-SUMMARY.md`);
313
- atomicWriteSync(summaryPath, content);
314
- process.stderr.write(`gsd-mechanical: wrote ${summaryPath}\n`);
315
- return content;
316
- }
317
- // ─── Helpers ─────────────────────────────────────────────────────────────────
318
- function aggregateFrontmatter(fms) {
319
- return {
320
- provides: dedup(fms.flatMap(f => f.provides)),
321
- key_files: dedup(fms.flatMap(f => f.key_files)),
322
- key_decisions: dedup(fms.flatMap(f => f.key_decisions)),
323
- patterns_established: dedup(fms.flatMap(f => f.patterns_established)),
324
- affects: dedup(fms.flatMap(f => f.affects)),
325
- observability_surfaces: dedup(fms.flatMap(f => f.observability_surfaces)),
326
- };
327
- }
328
- function dedup(arr) {
329
- return [...new Set(arr.filter(s => s.trim()))];
330
- }
331
- async function markSliceInRoadmap(base, mid, sid) {
332
- const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
333
- if (!roadmapPath)
334
- return;
335
- const content = await loadFile(roadmapPath);
336
- if (!content)
337
- return;
338
- const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"), `$1[x] **${sid}:`);
339
- if (updated !== content) {
340
- atomicWriteSync(roadmapPath, updated);
341
- process.stderr.write(`gsd-mechanical: marked ${sid} done in ROADMAP\n`);
342
- }
343
- }
344
- function readdirSyncSafe(dir) {
345
- try {
346
- return readdirSync(dir);
347
- }
348
- catch {
349
- return [];
350
- }
351
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Shared constants for auto-mode modules (auto.ts, auto-post-unit.ts, etc.).
3
- */
4
-
5
- /** Throttle STATE.md rebuilds — at most once per 30 seconds. */
6
- export const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
@@ -1,151 +0,0 @@
1
- /**
2
- * Idempotency checks for auto-mode unit dispatch.
3
- *
4
- * Handles completed-key membership, artifact cross-validation,
5
- * consecutive skip counting, phantom skip loop detection, key eviction,
6
- * and fallback persistence.
7
- *
8
- * Extracted from dispatchNextUnit() in auto.ts. Pure decision logic
9
- * with set mutations — does NOT call dispatchNextUnit or stopAuto.
10
- */
11
-
12
- import { invalidateAllCaches } from "./cache.js";
13
- import {
14
- verifyExpectedArtifact,
15
- persistCompletedKey,
16
- removePersistedKey,
17
- } from "./auto-recovery.js";
18
- import { resolveMilestoneFile } from "./paths.js";
19
- import { MAX_CONSECUTIVE_SKIPS, MAX_LIFETIME_DISPATCHES } from "./auto/session.js";
20
- import type { AutoSession } from "./auto/session.js";
21
- import { parseUnitId } from "./unit-id.js";
22
-
23
- export interface IdempotencyContext {
24
- s: AutoSession;
25
- unitType: string;
26
- unitId: string;
27
- basePath: string;
28
- /** Notification callback */
29
- notify: (message: string, level: "info" | "warning" | "error") => void;
30
- }
31
-
32
- export type IdempotencyResult =
33
- | { action: "skip"; reason: string }
34
- | { action: "rerun"; reason: string }
35
- | { action: "proceed" }
36
- | { action: "stop"; reason: string };
37
-
38
- /**
39
- * Check whether a unit should be skipped (already completed), rerun
40
- * (stale completion record), or dispatched normally.
41
- *
42
- * Mutates s.completedKeySet, s.unitConsecutiveSkips, s.unitLifetimeDispatches,
43
- * and s.recentlyEvictedKeys as needed.
44
- */
45
- export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
46
- const { s, unitType, unitId, basePath, notify } = ictx;
47
- const idempotencyKey = `${unitType}/${unitId}`;
48
-
49
- // ── Primary path: key exists in completed set ──
50
- if (s.completedKeySet.has(idempotencyKey)) {
51
- const artifactExists = verifyExpectedArtifact(unitType, unitId, basePath);
52
- if (artifactExists) {
53
- // Guard against infinite skip loops
54
- const skipCount = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
55
- s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
56
- if (skipCount > MAX_CONSECUTIVE_SKIPS) {
57
- // Cross-check: verify the unit's milestone is still active (#790)
58
- const skippedMid = parseUnitId(unitId).milestone;
59
- const skippedMilestoneComplete = skippedMid
60
- ? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
61
- : false;
62
- if (skippedMilestoneComplete) {
63
- s.unitConsecutiveSkips.delete(idempotencyKey);
64
- invalidateAllCaches();
65
- notify(
66
- `Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
67
- "info",
68
- );
69
- return { action: "skip", reason: "phantom-loop-cleared" };
70
- }
71
- s.unitConsecutiveSkips.delete(idempotencyKey);
72
- s.completedKeySet.delete(idempotencyKey);
73
- s.recentlyEvictedKeys.add(idempotencyKey);
74
- removePersistedKey(basePath, idempotencyKey);
75
- invalidateAllCaches();
76
- notify(
77
- `Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
78
- "warning",
79
- );
80
- return { action: "skip", reason: "evicted" };
81
- }
82
- // Count toward lifetime cap
83
- const lifeSkip = (s.unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
84
- s.unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
85
- if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
86
- return { action: "stop", reason: `Hard loop: ${unitType} ${unitId} (skip cycle)` };
87
- }
88
- notify(
89
- `Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
90
- "info",
91
- );
92
- return { action: "skip", reason: "completed" };
93
- } else {
94
- // Stale completion record — artifact missing. Remove and re-run.
95
- s.completedKeySet.delete(idempotencyKey);
96
- removePersistedKey(basePath, idempotencyKey);
97
- notify(
98
- `Re-running ${unitType} ${unitId} — marked complete but expected artifact missing.`,
99
- "warning",
100
- );
101
- return { action: "rerun", reason: "stale-key" };
102
- }
103
- }
104
-
105
- // ── Fallback: key missing but artifact exists ──
106
- if (verifyExpectedArtifact(unitType, unitId, basePath) && !s.recentlyEvictedKeys.has(idempotencyKey)) {
107
- persistCompletedKey(basePath, idempotencyKey);
108
- s.completedKeySet.add(idempotencyKey);
109
- invalidateAllCaches();
110
- // Same consecutive-skip guard as the primary path
111
- const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
112
- s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
113
- if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
114
- const skippedMid2 = parseUnitId(unitId).milestone;
115
- const skippedMilestoneComplete2 = skippedMid2
116
- ? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
117
- : false;
118
- if (skippedMilestoneComplete2) {
119
- s.unitConsecutiveSkips.delete(idempotencyKey);
120
- invalidateAllCaches();
121
- notify(
122
- `Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
123
- "info",
124
- );
125
- return { action: "skip", reason: "phantom-loop-cleared" };
126
- }
127
- s.unitConsecutiveSkips.delete(idempotencyKey);
128
- s.completedKeySet.delete(idempotencyKey);
129
- removePersistedKey(basePath, idempotencyKey);
130
- invalidateAllCaches();
131
- notify(
132
- `Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
133
- "warning",
134
- );
135
- return { action: "skip", reason: "evicted" };
136
- }
137
- // Count toward lifetime cap
138
- const lifeSkip2 = (s.unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
139
- s.unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
140
- if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
141
- return { action: "stop", reason: `Hard loop: ${unitType} ${unitId} (skip cycle)` };
142
- }
143
- notify(
144
- `Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
145
- "info",
146
- );
147
- return { action: "skip", reason: "fallback-persisted" };
148
- }
149
-
150
- return { action: "proceed" };
151
- }