gsd-pi 2.45.0-dev.fdcf73c → 2.46.0-dev.cc9d310

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 (180) hide show
  1. package/dist/resources/extensions/gsd/auto/phases.js +14 -35
  2. package/dist/resources/extensions/gsd/auto/session.js +0 -11
  3. package/dist/resources/extensions/gsd/auto-artifact-paths.js +112 -0
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +25 -96
  5. package/dist/resources/extensions/gsd/auto-start.js +2 -3
  6. package/dist/resources/extensions/gsd/auto.js +8 -52
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +18 -0
  8. package/dist/resources/extensions/gsd/commands/context.js +0 -4
  9. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +1 -1
  10. package/dist/resources/extensions/gsd/crash-recovery.js +2 -4
  11. package/dist/resources/extensions/gsd/dashboard-overlay.js +0 -44
  12. package/dist/resources/extensions/gsd/doctor-checks.js +166 -1
  13. package/dist/resources/extensions/gsd/doctor.js +3 -1
  14. package/dist/resources/extensions/gsd/gsd-db.js +11 -2
  15. package/dist/resources/extensions/gsd/guided-flow.js +1 -2
  16. package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
  17. package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -18
  18. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  19. package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -23
  20. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
  21. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -15
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  24. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  28. package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
  29. package/dist/resources/extensions/gsd/prompts/quick-task.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -3
  32. package/dist/resources/extensions/gsd/prompts/rethink.md +7 -2
  33. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  34. package/dist/resources/extensions/gsd/session-lock.js +1 -3
  35. package/dist/resources/extensions/gsd/state.js +7 -0
  36. package/dist/resources/extensions/gsd/sync-lock.js +89 -0
  37. package/dist/resources/extensions/gsd/tools/complete-milestone.js +58 -12
  38. package/dist/resources/extensions/gsd/tools/complete-slice.js +56 -11
  39. package/dist/resources/extensions/gsd/tools/complete-task.js +50 -2
  40. package/dist/resources/extensions/gsd/tools/plan-milestone.js +37 -1
  41. package/dist/resources/extensions/gsd/tools/plan-slice.js +30 -1
  42. package/dist/resources/extensions/gsd/tools/plan-task.js +27 -1
  43. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +32 -2
  44. package/dist/resources/extensions/gsd/tools/reopen-slice.js +86 -0
  45. package/dist/resources/extensions/gsd/tools/reopen-task.js +90 -0
  46. package/dist/resources/extensions/gsd/tools/replan-slice.js +32 -2
  47. package/dist/resources/extensions/gsd/unit-ownership.js +85 -0
  48. package/dist/resources/extensions/gsd/workflow-events.js +102 -0
  49. package/dist/resources/extensions/gsd/workflow-logger.js +56 -1
  50. package/dist/resources/extensions/gsd/workflow-manifest.js +244 -0
  51. package/dist/resources/extensions/gsd/workflow-migration.js +280 -0
  52. package/dist/resources/extensions/gsd/workflow-projections.js +373 -0
  53. package/dist/resources/extensions/gsd/workflow-reconcile.js +411 -0
  54. package/dist/resources/extensions/gsd/write-intercept.js +84 -0
  55. package/dist/web/standalone/.next/BUILD_ID +1 -1
  56. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  57. package/dist/web/standalone/.next/build-manifest.json +2 -2
  58. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  59. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  60. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.html +1 -1
  76. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  83. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  84. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  85. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  86. package/package.json +1 -1
  87. package/packages/pi-coding-agent/package.json +1 -1
  88. package/pkg/package.json +1 -1
  89. package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -19
  90. package/src/resources/extensions/gsd/auto/phases.ts +11 -35
  91. package/src/resources/extensions/gsd/auto/session.ts +0 -18
  92. package/src/resources/extensions/gsd/auto-artifact-paths.ts +131 -0
  93. package/src/resources/extensions/gsd/auto-dashboard.ts +0 -1
  94. package/src/resources/extensions/gsd/auto-post-unit.ts +25 -106
  95. package/src/resources/extensions/gsd/auto-start.ts +1 -3
  96. package/src/resources/extensions/gsd/auto.ts +4 -80
  97. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  98. package/src/resources/extensions/gsd/commands/context.ts +0 -5
  99. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +1 -1
  100. package/src/resources/extensions/gsd/crash-recovery.ts +1 -5
  101. package/src/resources/extensions/gsd/dashboard-overlay.ts +0 -50
  102. package/src/resources/extensions/gsd/doctor-checks.ts +179 -1
  103. package/src/resources/extensions/gsd/doctor-types.ts +7 -1
  104. package/src/resources/extensions/gsd/doctor.ts +4 -1
  105. package/src/resources/extensions/gsd/gsd-db.ts +11 -2
  106. package/src/resources/extensions/gsd/guided-flow.ts +1 -2
  107. package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
  108. package/src/resources/extensions/gsd/parallel-orchestrator.ts +5 -21
  109. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  110. package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -23
  111. package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
  112. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -15
  113. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  114. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  115. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  116. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  117. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  118. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  119. package/src/resources/extensions/gsd/prompts/queue.md +2 -2
  120. package/src/resources/extensions/gsd/prompts/quick-task.md +2 -0
  121. package/src/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
  122. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -3
  123. package/src/resources/extensions/gsd/prompts/rethink.md +7 -2
  124. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  125. package/src/resources/extensions/gsd/session-lock.ts +0 -4
  126. package/src/resources/extensions/gsd/state.ts +8 -0
  127. package/src/resources/extensions/gsd/sync-lock.ts +94 -0
  128. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +5 -13
  129. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +6 -10
  130. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +264 -228
  131. package/src/resources/extensions/gsd/tests/complete-task.test.ts +317 -250
  132. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +2 -8
  133. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +0 -3
  134. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  135. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +1 -1
  136. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +15 -24
  137. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -3
  138. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  139. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  140. package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +8 -9
  141. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +0 -1
  142. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +0 -7
  143. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +7 -8
  144. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +20 -24
  145. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +0 -2
  146. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +9 -6
  147. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +171 -0
  148. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +174 -0
  149. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +15 -14
  150. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +155 -0
  151. package/src/resources/extensions/gsd/tests/reopen-task.test.ts +165 -0
  152. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +1 -4
  153. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +2 -3
  154. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +122 -0
  155. package/src/resources/extensions/gsd/tests/unit-ownership.test.ts +175 -0
  156. package/src/resources/extensions/gsd/tests/workflow-events.test.ts +205 -0
  157. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +186 -0
  158. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +171 -0
  159. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +76 -0
  160. package/src/resources/extensions/gsd/tools/complete-milestone.ts +70 -13
  161. package/src/resources/extensions/gsd/tools/complete-slice.ts +68 -11
  162. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -1
  163. package/src/resources/extensions/gsd/tools/plan-milestone.ts +45 -0
  164. package/src/resources/extensions/gsd/tools/plan-slice.ts +38 -0
  165. package/src/resources/extensions/gsd/tools/plan-task.ts +35 -1
  166. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +39 -1
  167. package/src/resources/extensions/gsd/tools/reopen-slice.ts +125 -0
  168. package/src/resources/extensions/gsd/tools/reopen-task.ts +129 -0
  169. package/src/resources/extensions/gsd/tools/replan-slice.ts +38 -1
  170. package/src/resources/extensions/gsd/types.ts +8 -0
  171. package/src/resources/extensions/gsd/unit-ownership.ts +104 -0
  172. package/src/resources/extensions/gsd/workflow-events.ts +154 -0
  173. package/src/resources/extensions/gsd/workflow-logger.ts +51 -1
  174. package/src/resources/extensions/gsd/workflow-manifest.ts +334 -0
  175. package/src/resources/extensions/gsd/workflow-migration.ts +345 -0
  176. package/src/resources/extensions/gsd/workflow-projections.ts +425 -0
  177. package/src/resources/extensions/gsd/workflow-reconcile.ts +503 -0
  178. package/src/resources/extensions/gsd/write-intercept.ts +90 -0
  179. /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_buildManifest.js +0 -0
  180. /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_ssgManifest.js +0 -0
@@ -2,6 +2,7 @@
2
2
  // Centralized warning/error accumulator for the workflow engine pipeline.
3
3
  // Captures structured entries that the auto-loop can drain after each unit
4
4
  // to surface root causes for stuck loops, silent degradation, and blocked writes.
5
+ // All entries are also persisted to .gsd/audit-log.jsonl for post-mortem analysis.
5
6
  //
6
7
  // Stderr policy: every logWarning/logError call writes immediately to stderr
7
8
  // for terminal visibility. This is intentional — unlike debug-logger (which is
@@ -12,9 +13,20 @@
12
13
  // a process. The auto-loop must call _resetLogs() (or drainAndSummarize()) at
13
14
  // the start of each unit to prevent log bleed between units running in the same
14
15
  // Node process.
15
- // ─── Buffer ─────────────────────────────────────────────────────────────
16
+ import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ // ─── Buffer & Persistent Audit ──────────────────────────────────────────
16
19
  const MAX_BUFFER = 100;
17
20
  let _buffer = [];
21
+ let _auditBasePath = null;
22
+ /**
23
+ * Set the base path for persistent audit log writes.
24
+ * Should be called once at engine init with the project root.
25
+ * Until set, log entries are buffered in-memory only.
26
+ */
27
+ export function setLogBasePath(basePath) {
28
+ _auditBasePath = basePath;
29
+ }
18
30
  // ─── Public API ─────────────────────────────────────────────────────────
19
31
  /**
20
32
  * Record a warning. Also writes to stderr for terminal visibility.
@@ -110,12 +122,43 @@ export function formatForNotification(entries) {
110
122
  .map((e) => `[${e.component}] ${e.message}`)
111
123
  .join("\n");
112
124
  }
125
+ /**
126
+ * Read all entries from the persistent audit log.
127
+ * Returns empty array if no basePath is set or the file doesn't exist.
128
+ */
129
+ export function readAuditLog(basePath) {
130
+ const bp = basePath ?? _auditBasePath;
131
+ if (!bp)
132
+ return [];
133
+ const auditPath = join(bp, ".gsd", "audit-log.jsonl");
134
+ if (!existsSync(auditPath))
135
+ return [];
136
+ try {
137
+ const content = readFileSync(auditPath, "utf-8");
138
+ return content
139
+ .split("\n")
140
+ .filter((l) => l.length > 0)
141
+ .map((l) => {
142
+ try {
143
+ return JSON.parse(l);
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ })
149
+ .filter((e) => e !== null);
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
113
155
  /**
114
156
  * Reset buffer. Call at the start of each auto-loop unit to prevent log bleed
115
157
  * between units running in the same process. Also used in tests via _resetLogs().
116
158
  */
117
159
  export function _resetLogs() {
118
160
  _buffer = [];
161
+ _auditBasePath = null;
119
162
  }
120
163
  // ─── Internal ───────────────────────────────────────────────────────────
121
164
  function _push(severity, component, message, context) {
@@ -135,4 +178,16 @@ function _push(severity, component, message, context) {
135
178
  if (_buffer.length > MAX_BUFFER) {
136
179
  _buffer.shift();
137
180
  }
181
+ // Persist to .gsd/audit-log.jsonl so entries survive context resets
182
+ if (_auditBasePath) {
183
+ try {
184
+ const auditDir = join(_auditBasePath, ".gsd");
185
+ mkdirSync(auditDir, { recursive: true });
186
+ appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(entry) + "\n", "utf-8");
187
+ }
188
+ catch (auditErr) {
189
+ // Best-effort — never let audit write failures bubble up
190
+ process.stderr.write(`[gsd:audit] failed to persist log entry: ${auditErr.message}\n`);
191
+ }
192
+ }
138
193
  }
@@ -0,0 +1,244 @@
1
+ import { _getAdapter, transaction, } from "./gsd-db.js";
2
+ import { atomicWriteSync } from "./atomic-write.js";
3
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ // ─── helpers ─────────────────────────────────────────────────────────────
6
+ function requireDb() {
7
+ const db = _getAdapter();
8
+ if (!db)
9
+ throw new Error("workflow-manifest: No database open");
10
+ return db;
11
+ }
12
+ // ─── snapshotState ───────────────────────────────────────────────────────
13
+ /**
14
+ * Capture complete DB state as a StateManifest.
15
+ * Reads all rows from milestones, slices, tasks, decisions, verification_evidence.
16
+ *
17
+ * Note: rows returned from raw queries are plain objects with TEXT columns for
18
+ * JSON arrays. We parse them into typed Row objects using the same logic as
19
+ * gsd-db helper functions.
20
+ */
21
+ export function snapshotState() {
22
+ const db = requireDb();
23
+ // Wrap all reads in a deferred transaction so the snapshot is consistent
24
+ // (all SELECTs see the same DB state even if a concurrent write lands between them).
25
+ db.exec("BEGIN DEFERRED");
26
+ try {
27
+ const rawMilestones = db.prepare("SELECT * FROM milestones ORDER BY id").all();
28
+ const milestones = rawMilestones.map((r) => ({
29
+ id: r["id"],
30
+ title: r["title"],
31
+ status: r["status"],
32
+ depends_on: JSON.parse(r["depends_on"] || "[]"),
33
+ created_at: r["created_at"],
34
+ completed_at: r["completed_at"] ?? null,
35
+ vision: r["vision"] ?? "",
36
+ success_criteria: JSON.parse(r["success_criteria"] || "[]"),
37
+ key_risks: JSON.parse(r["key_risks"] || "[]"),
38
+ proof_strategy: JSON.parse(r["proof_strategy"] || "[]"),
39
+ verification_contract: r["verification_contract"] ?? "",
40
+ verification_integration: r["verification_integration"] ?? "",
41
+ verification_operational: r["verification_operational"] ?? "",
42
+ verification_uat: r["verification_uat"] ?? "",
43
+ definition_of_done: JSON.parse(r["definition_of_done"] || "[]"),
44
+ requirement_coverage: r["requirement_coverage"] ?? "",
45
+ boundary_map_markdown: r["boundary_map_markdown"] ?? "",
46
+ }));
47
+ const rawSlices = db.prepare("SELECT * FROM slices ORDER BY milestone_id, sequence, id").all();
48
+ const slices = rawSlices.map((r) => ({
49
+ milestone_id: r["milestone_id"],
50
+ id: r["id"],
51
+ title: r["title"],
52
+ status: r["status"],
53
+ risk: r["risk"],
54
+ depends: JSON.parse(r["depends"] || "[]"),
55
+ demo: r["demo"] ?? "",
56
+ created_at: r["created_at"],
57
+ completed_at: r["completed_at"] ?? null,
58
+ full_summary_md: r["full_summary_md"] ?? "",
59
+ full_uat_md: r["full_uat_md"] ?? "",
60
+ goal: r["goal"] ?? "",
61
+ success_criteria: r["success_criteria"] ?? "",
62
+ proof_level: r["proof_level"] ?? "",
63
+ integration_closure: r["integration_closure"] ?? "",
64
+ observability_impact: r["observability_impact"] ?? "",
65
+ sequence: r["sequence"] ?? 0,
66
+ replan_triggered_at: r["replan_triggered_at"] ?? null,
67
+ }));
68
+ const rawTasks = db.prepare("SELECT * FROM tasks ORDER BY milestone_id, slice_id, sequence, id").all();
69
+ const tasks = rawTasks.map((r) => ({
70
+ milestone_id: r["milestone_id"],
71
+ slice_id: r["slice_id"],
72
+ id: r["id"],
73
+ title: r["title"],
74
+ status: r["status"],
75
+ one_liner: r["one_liner"] ?? "",
76
+ narrative: r["narrative"] ?? "",
77
+ verification_result: r["verification_result"] ?? "",
78
+ duration: r["duration"] ?? "",
79
+ completed_at: r["completed_at"] ?? null,
80
+ blocker_discovered: r["blocker_discovered"] === 1,
81
+ deviations: r["deviations"] ?? "",
82
+ known_issues: r["known_issues"] ?? "",
83
+ key_files: JSON.parse(r["key_files"] || "[]"),
84
+ key_decisions: JSON.parse(r["key_decisions"] || "[]"),
85
+ full_summary_md: r["full_summary_md"] ?? "",
86
+ description: r["description"] ?? "",
87
+ estimate: r["estimate"] ?? "",
88
+ files: JSON.parse(r["files"] || "[]"),
89
+ verify: r["verify"] ?? "",
90
+ inputs: JSON.parse(r["inputs"] || "[]"),
91
+ expected_output: JSON.parse(r["expected_output"] || "[]"),
92
+ observability_impact: r["observability_impact"] ?? "",
93
+ full_plan_md: r["full_plan_md"] ?? "",
94
+ sequence: r["sequence"] ?? 0,
95
+ }));
96
+ const rawDecisions = db.prepare("SELECT * FROM decisions ORDER BY seq").all();
97
+ const decisions = rawDecisions.map((r) => ({
98
+ seq: r["seq"],
99
+ id: r["id"],
100
+ when_context: r["when_context"] ?? "",
101
+ scope: r["scope"] ?? "",
102
+ decision: r["decision"] ?? "",
103
+ choice: r["choice"] ?? "",
104
+ rationale: r["rationale"] ?? "",
105
+ revisable: r["revisable"] ?? "",
106
+ made_by: r["made_by"] ?? "agent",
107
+ superseded_by: r["superseded_by"] ?? null,
108
+ }));
109
+ const rawEvidence = db.prepare("SELECT * FROM verification_evidence ORDER BY id").all();
110
+ const verification_evidence = rawEvidence.map((r) => ({
111
+ id: r["id"],
112
+ task_id: r["task_id"],
113
+ slice_id: r["slice_id"],
114
+ milestone_id: r["milestone_id"],
115
+ command: r["command"],
116
+ exit_code: r["exit_code"] ?? null,
117
+ verdict: r["verdict"] ?? "",
118
+ duration_ms: r["duration_ms"] ?? null,
119
+ created_at: r["created_at"],
120
+ }));
121
+ const result = {
122
+ version: 1,
123
+ exported_at: new Date().toISOString(),
124
+ milestones,
125
+ slices,
126
+ tasks,
127
+ decisions,
128
+ verification_evidence,
129
+ };
130
+ db.exec("COMMIT");
131
+ return result;
132
+ }
133
+ catch (err) {
134
+ try {
135
+ db.exec("ROLLBACK");
136
+ }
137
+ catch { /* ignore rollback failure */ }
138
+ throw err;
139
+ }
140
+ }
141
+ // ─── restore ─────────────────────────────────────────────────────────────
142
+ /**
143
+ * Atomically replace all workflow state from a manifest.
144
+ * Runs inside a transaction — if any insert fails, no tables are modified.
145
+ * Only touches engine tables + decisions. Does NOT modify artifacts or memories.
146
+ */
147
+ function restore(manifest) {
148
+ const db = requireDb();
149
+ transaction(() => {
150
+ // Clear engine tables (order matters for foreign-key-like consistency)
151
+ db.exec("DELETE FROM verification_evidence");
152
+ db.exec("DELETE FROM tasks");
153
+ db.exec("DELETE FROM slices");
154
+ db.exec("DELETE FROM milestones");
155
+ db.exec("DELETE FROM decisions WHERE 1=1");
156
+ // Restore milestones
157
+ const msStmt = db.prepare(`INSERT INTO milestones (id, title, status, depends_on, created_at, completed_at,
158
+ vision, success_criteria, key_risks, proof_strategy,
159
+ verification_contract, verification_integration, verification_operational, verification_uat,
160
+ definition_of_done, requirement_coverage, boundary_map_markdown)
161
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
162
+ for (const m of manifest.milestones) {
163
+ msStmt.run(m.id, m.title, m.status, JSON.stringify(m.depends_on), m.created_at, m.completed_at, m.vision, JSON.stringify(m.success_criteria), JSON.stringify(m.key_risks), JSON.stringify(m.proof_strategy), m.verification_contract, m.verification_integration, m.verification_operational, m.verification_uat, JSON.stringify(m.definition_of_done), m.requirement_coverage, m.boundary_map_markdown);
164
+ }
165
+ // Restore slices
166
+ const slStmt = db.prepare(`INSERT INTO slices (milestone_id, id, title, status, risk, depends, demo,
167
+ created_at, completed_at, full_summary_md, full_uat_md,
168
+ goal, success_criteria, proof_level, integration_closure, observability_impact,
169
+ sequence, replan_triggered_at)
170
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
171
+ for (const s of manifest.slices) {
172
+ slStmt.run(s.milestone_id, s.id, s.title, s.status, s.risk, JSON.stringify(s.depends), s.demo, s.created_at, s.completed_at, s.full_summary_md, s.full_uat_md, s.goal, s.success_criteria, s.proof_level, s.integration_closure, s.observability_impact, s.sequence, s.replan_triggered_at);
173
+ }
174
+ // Restore tasks
175
+ const tkStmt = db.prepare(`INSERT INTO tasks (milestone_id, slice_id, id, title, status,
176
+ one_liner, narrative, verification_result, duration, completed_at,
177
+ blocker_discovered, deviations, known_issues, key_files, key_decisions,
178
+ full_summary_md, description, estimate, files, verify,
179
+ inputs, expected_output, observability_impact, sequence)
180
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
181
+ for (const t of manifest.tasks) {
182
+ tkStmt.run(t.milestone_id, t.slice_id, t.id, t.title, t.status, t.one_liner, t.narrative, t.verification_result, t.duration, t.completed_at, t.blocker_discovered ? 1 : 0, t.deviations, t.known_issues, JSON.stringify(t.key_files), JSON.stringify(t.key_decisions), t.full_summary_md, t.description, t.estimate, JSON.stringify(t.files), t.verify, JSON.stringify(t.inputs), JSON.stringify(t.expected_output), t.observability_impact, t.sequence);
183
+ }
184
+ // Restore decisions
185
+ const dcStmt = db.prepare(`INSERT INTO decisions (seq, id, when_context, scope, decision, choice, rationale, revisable, made_by, superseded_by)
186
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
187
+ for (const d of manifest.decisions) {
188
+ dcStmt.run(d.seq, d.id, d.when_context, d.scope, d.decision, d.choice, d.rationale, d.revisable, d.made_by, d.superseded_by);
189
+ }
190
+ // Restore verification evidence
191
+ const evStmt = db.prepare(`INSERT INTO verification_evidence (task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at)
192
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
193
+ for (const e of manifest.verification_evidence) {
194
+ evStmt.run(e.task_id, e.slice_id, e.milestone_id, e.command, e.exit_code, e.verdict, e.duration_ms, e.created_at);
195
+ }
196
+ });
197
+ }
198
+ // ─── writeManifest ───────────────────────────────────────────────────────
199
+ /**
200
+ * Write current DB state to .gsd/state-manifest.json via atomicWriteSync.
201
+ * Uses JSON.stringify with 2-space indent for git three-way merge friendliness.
202
+ */
203
+ export function writeManifest(basePath) {
204
+ const manifest = snapshotState();
205
+ const json = JSON.stringify(manifest, null, 2);
206
+ const dir = join(basePath, ".gsd");
207
+ mkdirSync(dir, { recursive: true });
208
+ atomicWriteSync(join(dir, "state-manifest.json"), json);
209
+ }
210
+ // ─── readManifest ────────────────────────────────────────────────────────
211
+ /**
212
+ * Read state-manifest.json and return parsed manifest, or null if not found.
213
+ */
214
+ export function readManifest(basePath) {
215
+ const manifestPath = join(basePath, ".gsd", "state-manifest.json");
216
+ if (!existsSync(manifestPath)) {
217
+ return null;
218
+ }
219
+ const raw = readFileSync(manifestPath, "utf-8");
220
+ const parsed = JSON.parse(raw);
221
+ if (parsed.version !== 1) {
222
+ throw new Error(`Unsupported manifest version: ${parsed.version}`);
223
+ }
224
+ // Validate required fields to avoid cryptic errors during restore
225
+ if (!Array.isArray(parsed.milestones) || !Array.isArray(parsed.slices) ||
226
+ !Array.isArray(parsed.tasks) || !Array.isArray(parsed.decisions) ||
227
+ !Array.isArray(parsed.verification_evidence)) {
228
+ throw new Error("Malformed manifest: missing or invalid required arrays");
229
+ }
230
+ return parsed;
231
+ }
232
+ // ─── bootstrapFromManifest ──────────────────────────────────────────────
233
+ /**
234
+ * Read state-manifest.json and restore DB state from it.
235
+ * Returns true if bootstrap succeeded, false if manifest file doesn't exist.
236
+ */
237
+ export function bootstrapFromManifest(basePath) {
238
+ const manifest = readManifest(basePath);
239
+ if (!manifest) {
240
+ return false;
241
+ }
242
+ restore(manifest);
243
+ return true;
244
+ }
@@ -0,0 +1,280 @@
1
+ // GSD Extension — Legacy Markdown to Engine Migration
2
+ // Converts legacy markdown-only projects to engine state by parsing
3
+ // existing ROADMAP.md, *-PLAN.md, and *-SUMMARY.md files.
4
+ // Populates data into the already-existing v10 schema tables.
5
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { _getAdapter, transaction } from "./gsd-db.js";
8
+ import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
9
+ // ─── needsAutoMigration ───────────────────────────────────────────────────
10
+ /**
11
+ * Returns true when engine tables are empty AND a .gsd/milestones/ directory
12
+ * with markdown files exists — signals that this is a legacy project that needs
13
+ * one-time migration from markdown to engine state.
14
+ */
15
+ export function needsAutoMigration(basePath) {
16
+ const db = _getAdapter();
17
+ if (!db)
18
+ return false;
19
+ // If milestones table already has rows, migration already done
20
+ try {
21
+ const row = db.prepare("SELECT COUNT(*) as cnt FROM milestones").get();
22
+ if (row && row["cnt"] > 0)
23
+ return false;
24
+ }
25
+ catch {
26
+ // Table might not exist yet — that's fine, we can still migrate
27
+ return false;
28
+ }
29
+ // Check if .gsd/milestones/ directory exists
30
+ const milestonesDir = join(basePath, ".gsd", "milestones");
31
+ if (!existsSync(milestonesDir))
32
+ return false;
33
+ return true;
34
+ }
35
+ // ─── migrateFromMarkdown ──────────────────────────────────────────────────
36
+ /**
37
+ * Migrate legacy markdown-only .gsd/ projects to engine DB state.
38
+ * Reads .gsd/milestones/<ID>/ directories and parses ROADMAP.md, *-PLAN.md
39
+ * files. All inserts are wrapped in a transaction.
40
+ *
41
+ * This function only INSERTs data into the already-existing v10 schema tables
42
+ * (milestones, slices, tasks). It does NOT create tables or run migrations.
43
+ *
44
+ * Handles all directory shapes:
45
+ * - No DB: caller is responsible for openDatabase + initSchema before calling
46
+ * - Stale DB (empty tables): inserts succeed normally
47
+ * - No markdown at all: returns early with stderr message
48
+ * - Orphaned summary files: logs warning, skips without crash
49
+ */
50
+ export function migrateFromMarkdown(basePath) {
51
+ const db = _getAdapter();
52
+ if (!db) {
53
+ process.stderr.write("workflow-migration: no database connection, cannot migrate\n");
54
+ return;
55
+ }
56
+ const milestonesDir = join(basePath, ".gsd", "milestones");
57
+ if (!existsSync(milestonesDir)) {
58
+ process.stderr.write("workflow-migration: no .gsd/milestones/ directory found, nothing to migrate\n");
59
+ return;
60
+ }
61
+ // Discover milestone directories (any directory at the top level of milestones/)
62
+ let milestoneDirs;
63
+ try {
64
+ milestoneDirs = readdirSync(milestonesDir, { withFileTypes: true })
65
+ .filter(e => e.isDirectory())
66
+ .map(e => e.name);
67
+ }
68
+ catch {
69
+ process.stderr.write("workflow-migration: failed to read milestones directory\n");
70
+ return;
71
+ }
72
+ if (milestoneDirs.length === 0) {
73
+ process.stderr.write("workflow-migration: no milestone directories found in .gsd/milestones/\n");
74
+ return;
75
+ }
76
+ // Collect all data before the transaction
77
+ const migratedMilestoneIds = [];
78
+ const milestoneInserts = [];
79
+ const sliceInserts = [];
80
+ const taskInserts = [];
81
+ for (const mId of milestoneDirs) {
82
+ const mDir = join(milestonesDir, mId);
83
+ // Determine milestone status: done if a milestone-level SUMMARY.md exists
84
+ const milestoneSummaryPath = join(mDir, "SUMMARY.md");
85
+ const milestoneDone = existsSync(milestoneSummaryPath);
86
+ const milestoneStatus = milestoneDone ? "done" : "active";
87
+ // Parse ROADMAP.md for slices list
88
+ const roadmapPath = join(mDir, "ROADMAP.md");
89
+ let roadmapSlices = [];
90
+ if (existsSync(roadmapPath)) {
91
+ try {
92
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
93
+ const roadmap = parseRoadmap(roadmapContent);
94
+ // Extract milestone title from roadmap
95
+ const mTitle = roadmap.title || mId;
96
+ milestoneInserts.push({ id: mId, title: mTitle, status: milestoneStatus });
97
+ roadmapSlices = roadmap.slices.map(s => ({
98
+ id: s.id,
99
+ title: s.title,
100
+ done: s.done,
101
+ risk: s.risk || "low",
102
+ }));
103
+ }
104
+ catch (err) {
105
+ process.stderr.write(`workflow-migration: failed to parse ROADMAP.md for ${mId}: ${err.message}\n`);
106
+ // Still add milestone with ID as title
107
+ milestoneInserts.push({ id: mId, title: mId, status: milestoneStatus });
108
+ }
109
+ }
110
+ else {
111
+ // No ROADMAP.md — add milestone entry anyway using directory name
112
+ milestoneInserts.push({ id: mId, title: mId, status: milestoneStatus });
113
+ }
114
+ migratedMilestoneIds.push(mId);
115
+ // Collect slices from ROADMAP + their tasks from PLAN files
116
+ const knownSliceIds = new Set(roadmapSlices.map(s => s.id));
117
+ for (let sIdx = 0; sIdx < roadmapSlices.length; sIdx++) {
118
+ const slice = roadmapSlices[sIdx];
119
+ // Per Pitfall #5: if milestone is done, force all child slices to done
120
+ const sliceStatus = milestoneDone ? "done" : (slice.done ? "done" : "pending");
121
+ sliceInserts.push({
122
+ id: slice.id,
123
+ milestoneId: mId,
124
+ title: slice.title,
125
+ status: sliceStatus,
126
+ risk: slice.risk,
127
+ sequence: sIdx,
128
+ forceDone: milestoneDone,
129
+ });
130
+ // Read *-PLAN.md for this slice
131
+ const planPath = join(mDir, `${slice.id}-PLAN.md`);
132
+ if (existsSync(planPath)) {
133
+ try {
134
+ const planContent = readFileSync(planPath, "utf-8");
135
+ const plan = parsePlan(planContent);
136
+ for (let tIdx = 0; tIdx < plan.tasks.length; tIdx++) {
137
+ const task = plan.tasks[tIdx];
138
+ // Per Pitfall #5: if milestone is done, force all tasks to done
139
+ const taskStatus = milestoneDone ? "done" : (task.done ? "done" : "pending");
140
+ taskInserts.push({
141
+ id: task.id,
142
+ sliceId: slice.id,
143
+ milestoneId: mId,
144
+ title: task.title,
145
+ status: taskStatus,
146
+ sequence: tIdx,
147
+ });
148
+ }
149
+ }
150
+ catch (err) {
151
+ process.stderr.write(`workflow-migration: failed to parse ${slice.id}-PLAN.md for ${mId}: ${err.message}\n`);
152
+ }
153
+ }
154
+ }
155
+ // Check for orphaned summary files (summary for a slice not in ROADMAP)
156
+ try {
157
+ const files = readdirSync(mDir);
158
+ const summaryFiles = files.filter(f => f.endsWith("-SUMMARY.md") && f !== "SUMMARY.md");
159
+ for (const summaryFile of summaryFiles) {
160
+ const sliceId = summaryFile.replace("-SUMMARY.md", "");
161
+ if (!knownSliceIds.has(sliceId)) {
162
+ process.stderr.write(`workflow-migration: orphaned summary file ${summaryFile} in ${mId} (slice not found in ROADMAP.md), skipping\n`);
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // Non-fatal
168
+ }
169
+ }
170
+ // Execute all inserts atomically
171
+ const now = new Date().toISOString();
172
+ if (migratedMilestoneIds.length === 0) {
173
+ process.stderr.write("workflow-migration: no milestones collected, nothing to insert\n");
174
+ return;
175
+ }
176
+ const placeholders = migratedMilestoneIds.map(() => "?").join(",");
177
+ transaction(() => {
178
+ // Clear existing data to handle stale DB shape (DELETE ... IN (...))
179
+ db.prepare(`DELETE FROM tasks WHERE milestone_id IN (${placeholders})`).run(...migratedMilestoneIds);
180
+ db.prepare(`DELETE FROM slices WHERE milestone_id IN (${placeholders})`).run(...migratedMilestoneIds);
181
+ db.prepare(`DELETE FROM milestones WHERE id IN (${placeholders})`).run(...migratedMilestoneIds);
182
+ // Insert milestones
183
+ const insertMilestone = db.prepare("INSERT INTO milestones (id, title, status, created_at) VALUES (?, ?, ?, ?)");
184
+ for (const m of milestoneInserts) {
185
+ insertMilestone.run(m.id, m.title, m.status, now);
186
+ }
187
+ // Insert slices (using v10 column names: depends, sequence)
188
+ const insertSlice = db.prepare("INSERT INTO slices (id, milestone_id, title, status, risk, depends, sequence, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
189
+ for (const s of sliceInserts) {
190
+ insertSlice.run(s.id, s.milestoneId, s.title, s.status, s.risk, "[]", s.sequence, now);
191
+ }
192
+ // Insert tasks (using v10 column names: sequence, blocker_discovered, full_summary_md)
193
+ const insertTask = db.prepare("INSERT INTO tasks (id, slice_id, milestone_id, title, description, status, estimate, files, sequence) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
194
+ for (const t of taskInserts) {
195
+ insertTask.run(t.id, t.sliceId, t.milestoneId, t.title, "", t.status, "", "[]", t.sequence);
196
+ }
197
+ });
198
+ }
199
+ // ─── validateMigration ────────────────────────────────────────────────────
200
+ /**
201
+ * D-14: Validate that engine state matches what markdown parsers report.
202
+ * Compares milestone count, slice count, task count, and status distributions.
203
+ * Logs each discrepancy to stderr but does NOT throw.
204
+ * Returns array of discrepancy strings (empty = clean migration).
205
+ */
206
+ export function validateMigration(basePath) {
207
+ const db = _getAdapter();
208
+ if (!db) {
209
+ return { discrepancies: ["No database connection for validation"] };
210
+ }
211
+ const discrepancies = [];
212
+ // Get engine counts
213
+ const engMilestones = db.prepare("SELECT COUNT(*) as cnt FROM milestones").get();
214
+ const engSlices = db.prepare("SELECT COUNT(*) as cnt FROM slices").get();
215
+ const engTasks = db.prepare("SELECT COUNT(*) as cnt FROM tasks").get();
216
+ const engineMilestoneCount = engMilestones ? engMilestones["cnt"] : 0;
217
+ const engineSliceCount = engSlices ? engSlices["cnt"] : 0;
218
+ const engineTaskCount = engTasks ? engTasks["cnt"] : 0;
219
+ // Count from markdown
220
+ const milestonesDir = join(basePath, ".gsd", "milestones");
221
+ if (!existsSync(milestonesDir)) {
222
+ return { discrepancies };
223
+ }
224
+ let mdMilestoneCount = 0;
225
+ let mdSliceCount = 0;
226
+ let mdTaskCount = 0;
227
+ try {
228
+ const milestoneDirs = readdirSync(milestonesDir, { withFileTypes: true })
229
+ .filter(e => e.isDirectory())
230
+ .map(e => e.name);
231
+ mdMilestoneCount = milestoneDirs.length;
232
+ for (const mId of milestoneDirs) {
233
+ const mDir = join(milestonesDir, mId);
234
+ const roadmapPath = join(mDir, "ROADMAP.md");
235
+ if (existsSync(roadmapPath)) {
236
+ try {
237
+ const content = readFileSync(roadmapPath, "utf-8");
238
+ const roadmap = parseRoadmap(content);
239
+ mdSliceCount += roadmap.slices.length;
240
+ for (const slice of roadmap.slices) {
241
+ const planPath = join(mDir, `${slice.id}-PLAN.md`);
242
+ if (existsSync(planPath)) {
243
+ try {
244
+ const planContent = readFileSync(planPath, "utf-8");
245
+ const plan = parsePlan(planContent);
246
+ mdTaskCount += plan.tasks.length;
247
+ }
248
+ catch {
249
+ // Skip unreadable plan
250
+ }
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // Skip unreadable roadmap
256
+ }
257
+ }
258
+ }
259
+ }
260
+ catch {
261
+ return { discrepancies: ["Failed to read markdown for validation"] };
262
+ }
263
+ // Compare counts
264
+ if (engineMilestoneCount !== mdMilestoneCount) {
265
+ const msg = `Milestone count mismatch: engine=${engineMilestoneCount}, markdown=${mdMilestoneCount}`;
266
+ discrepancies.push(msg);
267
+ process.stderr.write(`workflow-migration: ${msg}\n`);
268
+ }
269
+ if (engineSliceCount !== mdSliceCount) {
270
+ const msg = `Slice count mismatch: engine=${engineSliceCount}, markdown=${mdSliceCount}`;
271
+ discrepancies.push(msg);
272
+ process.stderr.write(`workflow-migration: ${msg}\n`);
273
+ }
274
+ if (engineTaskCount !== mdTaskCount) {
275
+ const msg = `Task count mismatch: engine=${engineTaskCount}, markdown=${mdTaskCount}`;
276
+ discrepancies.push(msg);
277
+ process.stderr.write(`workflow-migration: ${msg}\n`);
278
+ }
279
+ return { discrepancies };
280
+ }