@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd

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/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
  3. package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
  4. package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
  5. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  8. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  9. package/dist/resources/extensions/gsd/auto.js +10 -3
  10. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
  12. package/dist/resources/extensions/gsd/gsd-db.js +52 -0
  13. package/dist/resources/extensions/gsd/migrate-external.js +13 -4
  14. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
  15. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  16. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  17. package/dist/resources/extensions/gsd/unit-registry.js +27 -2
  18. package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
  19. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  22. package/dist/web/standalone/.next/build-manifest.json +2 -2
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.html +1 -1
  41. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  48. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  50. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  51. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  52. package/integrations/hermes/CONTEXT.md +47 -0
  53. package/integrations/hermes/README.md +21 -0
  54. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  55. package/integrations/hermes/docs/setup.md +4 -0
  56. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  57. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  58. package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
  59. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  60. package/integrations/hermes/plugin.yaml +4 -0
  61. package/integrations/hermes/tests/test_config.py +6 -0
  62. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  63. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  64. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  65. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  66. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  67. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  68. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  69. package/package.json +1 -1
  70. package/packages/cloud-mcp-gateway/package.json +2 -2
  71. package/packages/contracts/package.json +1 -1
  72. package/packages/daemon/dist/session-manager.js +1 -1
  73. package/packages/daemon/dist/session-manager.js.map +1 -1
  74. package/packages/daemon/package.json +4 -4
  75. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  76. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  77. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  78. package/packages/gsd-agent-core/package.json +5 -5
  79. package/packages/gsd-agent-modes/package.json +7 -7
  80. package/packages/mcp-server/dist/server.d.ts +18 -5
  81. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/server.js +46 -5
  83. package/packages/mcp-server/dist/server.js.map +1 -1
  84. package/packages/mcp-server/dist/session-manager.js +1 -1
  85. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  86. package/packages/mcp-server/package.json +4 -4
  87. package/packages/native/package.json +1 -1
  88. package/packages/pi-agent-core/package.json +1 -1
  89. package/packages/pi-ai/package.json +1 -1
  90. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  91. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  95. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  96. package/packages/pi-coding-agent/package.json +7 -7
  97. package/packages/pi-tui/package.json +2 -2
  98. package/packages/rpc-client/package.json +2 -2
  99. package/pkg/package.json +1 -1
  100. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
  101. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
  102. package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
  103. package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
  104. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  105. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  106. package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
  107. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  108. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  109. package/src/resources/extensions/gsd/auto.ts +9 -3
  110. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  111. package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
  112. package/src/resources/extensions/gsd/gsd-db.ts +72 -0
  113. package/src/resources/extensions/gsd/migrate-external.ts +14 -4
  114. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
  115. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  116. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  117. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  118. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  119. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  120. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
  121. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  123. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
  124. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
  125. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  126. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  127. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  128. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  129. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
  130. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  131. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  132. package/src/resources/extensions/gsd/unit-registry.ts +34 -2
  133. package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
  134. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
  135. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { isAbsolute, join, relative } from "node:path";
3
- import { getAllMilestones, getMilestoneSlices, getSliceTasks, isDbAvailable, isMemoriesFtsAvailable, _getAdapter, } from "./gsd-db.js";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
+ import { deleteArtifactByPath, getAllMilestones, getMilestoneSlices, getSliceTasks, isDbAvailable, isMemoriesFtsAvailable, repairTaskCompletionFromSummary, _getAdapter, } from "./gsd-db.js";
4
4
  import { MEMORIES_FTS_REBUILT_KEY } from "./db-memory-fts-schema.js";
5
5
  import { isAfter, latestExplicitReopenAt } from "./milestone-reopen-events.js";
6
6
  import { gsdProjectionRoot, gsdRoot, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "./paths.js";
@@ -11,7 +11,11 @@ import { readEvents } from "./workflow-events.js";
11
11
  import { flushWorkflowProjections } from "./projection-flush.js";
12
12
  import { parseRoadmapSlices } from "./roadmap-slices.js";
13
13
  import { parsePlan } from "./parsers-legacy.js";
14
+ import { parseSummary } from "./files.js";
14
15
  const USER_AUTHORED_ARTIFACT_TYPES = new Set(["CONTEXT", "RESEARCH"]);
16
+ function escapeRegExp(value) {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
15
19
  function relativeFile(basePath, filePath) {
16
20
  return relative(basePath, filePath).split("\\").join("/");
17
21
  }
@@ -28,6 +32,17 @@ function userContentMissingMessage(path, artifactType) {
28
32
  const type = normalizedArtifactType(artifactType) || "UNKNOWN";
29
33
  return `Artifact \`${path}\` is a user-authored ${type} file recorded in the database but missing from disk. Re-run \`${userContentRecoveryCommand(type)}\` in this milestone to regenerate it.`;
30
34
  }
35
+ function artifactPathRelativeToGsd(artifactPath) {
36
+ const parts = artifactPath.split(/[\\/]+/);
37
+ const gsdIndex = parts.lastIndexOf(".gsd");
38
+ if (gsdIndex < 0 || gsdIndex === parts.length - 1)
39
+ return artifactPath;
40
+ return parts.slice(gsdIndex + 1).join("/");
41
+ }
42
+ function isPathInside(basePath, candidatePath) {
43
+ const rel = relative(basePath, candidatePath);
44
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
45
+ }
31
46
  function reportCheckboxDbStatusDivergence(issues, basePath, filePath, scope, unitId, status, checkboxDone) {
32
47
  const dbDone = isClosedStatus(status);
33
48
  if (checkboxDone === dbDone)
@@ -99,12 +114,97 @@ function isClearedByMilestoneShellProjectionFlush(basePath, issue, reRenderedMil
99
114
  return issue.file === relativeFile(basePath, roadmapPath);
100
115
  }
101
116
  function artifactExistsOnDisk(basePath, artifactPath) {
102
- if (isAbsolute(artifactPath))
103
- return existsSync(artifactPath);
104
- return [
105
- join(gsdProjectionRoot(basePath), artifactPath),
106
- join(gsdRoot(basePath), artifactPath),
107
- ].some((candidate) => existsSync(candidate));
117
+ return resolveArtifactDiskPath(basePath, artifactPath) !== null;
118
+ }
119
+ function resolveArtifactDiskPath(basePath, artifactPath) {
120
+ const relativeArtifactPath = artifactPathRelativeToGsd(artifactPath);
121
+ if (isAbsolute(relativeArtifactPath)) {
122
+ return existsSync(relativeArtifactPath) ? relativeArtifactPath : null;
123
+ }
124
+ for (const root of [gsdProjectionRoot(basePath), gsdRoot(basePath)]) {
125
+ const candidate = join(root, relativeArtifactPath);
126
+ if (isPathInside(root, candidate) && existsSync(candidate))
127
+ return candidate;
128
+ }
129
+ return null;
130
+ }
131
+ function taskExistsInPlan(basePath, milestoneId, sliceId, taskId) {
132
+ const planPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
133
+ if (!planPath || !existsSync(planPath))
134
+ return false;
135
+ try {
136
+ return parsePlan(readFileSync(planPath, "utf-8")).tasks.some((task) => task.id === taskId);
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ function isPassingVerificationResult(value) {
143
+ const normalized = value.trim().toLowerCase();
144
+ if (!normalized)
145
+ return false;
146
+ if (/\b(fail(?:ed|ing)?|error|blocked|mixed|untested)\b/.test(normalized))
147
+ return false;
148
+ if (/\b(?:not|never|no|didn't|did\s+not|cannot|can't|won't|couldn't)\s+(?:\w+\s+){0,3}?(?:pass(?:ed|ing)?|success(?:ful)?|succeeded)\b/.test(normalized)) {
149
+ return false;
150
+ }
151
+ return /\b(pass(?:ed|ing)?|success(?:ful)?|succeeded)\b/.test(normalized) || normalized === "all-pass";
152
+ }
153
+ function readTaskCompletionEvidenceFromSummary(basePath, row) {
154
+ if (!row.slice_id || !row.task_id)
155
+ return null;
156
+ if (!row.task_status && Number(row.task_count) > 0 && !taskExistsInPlan(basePath, row.milestone_id, row.slice_id, row.task_id)) {
157
+ return null;
158
+ }
159
+ const diskPath = resolveArtifactDiskPath(basePath, row.path);
160
+ if (!diskPath)
161
+ return null;
162
+ try {
163
+ const fullSummaryMd = readFileSync(diskPath, "utf-8");
164
+ const summary = parseSummary(fullSummaryMd);
165
+ const fm = summary.frontmatter;
166
+ if (fm.id !== row.task_id || fm.parent !== row.slice_id || fm.milestone !== row.milestone_id)
167
+ return null;
168
+ if (fm.blocker_discovered)
169
+ return null;
170
+ if (!isPassingVerificationResult(fm.verification_result))
171
+ return null;
172
+ const completedAt = fm.completed_at.trim();
173
+ if (!completedAt || !Number.isFinite(Date.parse(completedAt)))
174
+ return null;
175
+ return {
176
+ completedAt,
177
+ verificationResult: fm.verification_result.trim(),
178
+ title: summary.title.replace(new RegExp(`^${escapeRegExp(row.task_id)}:\\s*`), "").trim(),
179
+ oneLiner: summary.oneLiner,
180
+ narrative: summary.whatHappened,
181
+ duration: fm.duration,
182
+ blockerDiscovered: fm.blocker_discovered,
183
+ deviations: summary.deviations,
184
+ knownIssues: summary.knownLimitations,
185
+ keyFiles: fm.key_files.filter((file) => file !== "(none)"),
186
+ keyDecisions: fm.key_decisions.filter((decision) => decision !== "(none)"),
187
+ fullSummaryMd,
188
+ };
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ }
194
+ function repairTaskArtifactDbStatusDivergence(basePath, row) {
195
+ const evidence = readTaskCompletionEvidenceFromSummary(basePath, row);
196
+ if (!evidence || !row.slice_id || !row.task_id)
197
+ return false;
198
+ repairTaskCompletionFromSummary({
199
+ milestoneId: row.milestone_id,
200
+ sliceId: row.slice_id,
201
+ taskId: row.task_id,
202
+ ...evidence,
203
+ });
204
+ return true;
205
+ }
206
+ function artifactDbStatusDivergenceFixable(basePath, row) {
207
+ return readTaskCompletionEvidenceFromSummary(basePath, row) !== null;
108
208
  }
109
209
  function artifactUnitId(row) {
110
210
  if (!row.milestone_id)
@@ -124,6 +224,38 @@ function artifactScope(row) {
124
224
  return "milestone";
125
225
  return "project";
126
226
  }
227
+ function sameArtifactIdentity(left, right) {
228
+ return left.artifact_type === right.artifact_type &&
229
+ left.milestone_id === right.milestone_id &&
230
+ left.slice_id === right.slice_id &&
231
+ left.task_id === right.task_id;
232
+ }
233
+ function isMilestonesArtifactPath(artifactPath) {
234
+ return artifactPathRelativeToGsd(artifactPath).startsWith("milestones/");
235
+ }
236
+ function expectedMilestonesArtifactPath(row) {
237
+ if (!row.milestone_id)
238
+ return null;
239
+ const artifactType = normalizedArtifactType(row.artifact_type);
240
+ if (!artifactType)
241
+ return null;
242
+ if (row.slice_id && row.task_id) {
243
+ return `milestones/${row.milestone_id}/slices/${row.slice_id}/tasks/${row.task_id}-${artifactType}.md`;
244
+ }
245
+ if (row.slice_id) {
246
+ return `milestones/${row.milestone_id}/slices/${row.slice_id}/${row.slice_id}-${artifactType}.md`;
247
+ }
248
+ return `milestones/${row.milestone_id}/${row.milestone_id}-${artifactType}.md`;
249
+ }
250
+ function hasPresentMilestonesReplacement(basePath, row, artifactRows) {
251
+ const expectedPath = expectedMilestonesArtifactPath(row);
252
+ if (expectedPath && artifactExistsOnDisk(basePath, expectedPath))
253
+ return true;
254
+ return artifactRows.some((other) => other.path !== row.path &&
255
+ isMilestonesArtifactPath(other.path) &&
256
+ sameArtifactIdentity(row, other) &&
257
+ artifactExistsOnDisk(basePath, other.path));
258
+ }
127
259
  export async function checkEngineHealth(basePath, issues, fixesApplied, options) {
128
260
  const dbPath = resolveGsdPathContract(basePath).projectDb;
129
261
  if (!isDbAvailable() && existsSync(dbPath)) {
@@ -327,16 +459,25 @@ export async function checkEngineHealth(basePath, issues, fixesApplied, options)
327
459
  if (artifactExistsOnDisk(basePath, row.path))
328
460
  continue;
329
461
  const unitId = artifactUnitId(row);
462
+ const issuePath = artifactPathRelativeToGsd(row.path);
463
+ if (options?.repair && issuePath.startsWith("phases/") && hasPresentMilestonesReplacement(basePath, row, artifactRows)) {
464
+ // Route the write through the Single Writer owner (gsd-db.ts) instead
465
+ // of issuing raw DELETE SQL here — doctor is a read-only consumer and
466
+ // the single-writer invariant forbids write SQL outside the allowlist.
467
+ deleteArtifactByPath(row.path);
468
+ fixesApplied.push(`pruned stale flat-phase artifact row ${row.path}`);
469
+ continue;
470
+ }
330
471
  if (isUserAuthoredArtifactType(row.artifact_type)) {
331
472
  const artifactType = normalizedArtifactType(row.artifact_type);
332
- missingUserContentArtifacts.push({ path: row.path, artifactType });
473
+ missingUserContentArtifacts.push({ path: issuePath, artifactType });
333
474
  issues.push({
334
475
  severity: "warning",
335
476
  code: "artifact_user_content_missing",
336
477
  scope: artifactScope(row),
337
478
  unitId,
338
- message: userContentMissingMessage(row.path, artifactType),
339
- file: row.path,
479
+ message: userContentMissingMessage(issuePath, artifactType),
480
+ file: issuePath,
340
481
  fixable: false,
341
482
  });
342
483
  continue;
@@ -346,9 +487,9 @@ export async function checkEngineHealth(basePath, issues, fixesApplied, options)
346
487
  code: "artifact_file_missing",
347
488
  scope: artifactScope(row),
348
489
  unitId,
349
- message: `Artifact ${row.path} is recorded in the database as ${row.artifact_type || "UNKNOWN"} but no matching file exists on disk`,
350
- file: row.path,
351
- fixable: false,
490
+ message: `Artifact ${issuePath} is recorded in the database as ${row.artifact_type || "UNKNOWN"} but no matching file exists on disk`,
491
+ file: issuePath,
492
+ fixable: issuePath.startsWith("phases/") && hasPresentMilestonesReplacement(basePath, row, artifactRows),
352
493
  });
353
494
  }
354
495
  }
@@ -395,13 +536,30 @@ export async function checkEngineHealth(basePath, issues, fixesApplied, options)
395
536
  if (seen.has(unitId))
396
537
  continue;
397
538
  seen.add(unitId);
539
+ const fixable = artifactDbStatusDivergenceFixable(basePath, row);
540
+ let repairFailed = false;
541
+ if (options?.repair && fixable) {
542
+ try {
543
+ if (repairTaskArtifactDbStatusDivergence(basePath, row)) {
544
+ fixesApplied.push(`repaired task completion from SUMMARY artifact for ${unitId}`);
545
+ continue;
546
+ }
547
+ }
548
+ catch {
549
+ repairFailed = true;
550
+ }
551
+ }
398
552
  issues.push({
399
553
  severity: "error",
400
554
  code: "artifact_db_status_divergence",
401
555
  scope: row.task_id ? "task" : row.slice_id ? "slice" : "milestone",
402
556
  unitId,
403
- message: `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Runtime will not import it silently; run explicit recovery/repair after review.`,
404
- fixable: false,
557
+ message: repairFailed
558
+ ? `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Doctor found valid SUMMARY completion evidence but could not repair the database state.`
559
+ : fixable
560
+ ? `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Doctor can repair this from the SUMMARY completion evidence.`
561
+ : `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Runtime will not import it silently; run explicit recovery/repair after review.`,
562
+ fixable,
405
563
  });
406
564
  }
407
565
  }
@@ -410,6 +410,58 @@ export function insertTask(t) {
410
410
  export function updateTaskStatus(milestoneId, sliceId, taskId, status, completedAt) {
411
411
  applyStatusTransition({ entity: "task", milestoneId, sliceId, taskId, status, completedAt });
412
412
  }
413
+ export function repairTaskCompletionFromSummary(t) {
414
+ const db = getDbOrNull();
415
+ if (!db)
416
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
417
+ transaction(() => {
418
+ const seq = db.prepare(`SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence
419
+ FROM tasks
420
+ WHERE milestone_id = :mid AND slice_id = :sid`).get({ ":mid": t.milestoneId, ":sid": t.sliceId });
421
+ db.prepare(`INSERT INTO tasks (
422
+ milestone_id, slice_id, id, title, status, one_liner, narrative,
423
+ verification_result, duration, completed_at, blocker_discovered,
424
+ deviations, known_issues, key_files, key_decisions, full_summary_md,
425
+ sequence
426
+ ) VALUES (
427
+ :milestone_id, :slice_id, :id, :title, 'complete', :one_liner, :narrative,
428
+ :verification_result, :duration, :completed_at, :blocker_discovered,
429
+ :deviations, :known_issues, :key_files, :key_decisions, :full_summary_md,
430
+ :sequence
431
+ )
432
+ ON CONFLICT(milestone_id, slice_id, id) DO UPDATE SET
433
+ title = CASE WHEN NULLIF(:title, '') IS NOT NULL THEN :title ELSE tasks.title END,
434
+ status = 'complete',
435
+ one_liner = CASE WHEN NULLIF(:one_liner, '') IS NOT NULL THEN :one_liner ELSE tasks.one_liner END,
436
+ narrative = CASE WHEN NULLIF(:narrative, '') IS NOT NULL THEN :narrative ELSE tasks.narrative END,
437
+ verification_result = :verification_result,
438
+ duration = CASE WHEN NULLIF(:duration, '') IS NOT NULL THEN :duration ELSE tasks.duration END,
439
+ completed_at = :completed_at,
440
+ blocker_discovered = :blocker_discovered,
441
+ deviations = CASE WHEN NULLIF(:deviations, '') IS NOT NULL THEN :deviations ELSE tasks.deviations END,
442
+ known_issues = CASE WHEN NULLIF(:known_issues, '') IS NOT NULL THEN :known_issues ELSE tasks.known_issues END,
443
+ key_files = :key_files,
444
+ key_decisions = :key_decisions,
445
+ full_summary_md = :full_summary_md`).run({
446
+ ":milestone_id": t.milestoneId,
447
+ ":slice_id": t.sliceId,
448
+ ":id": t.taskId,
449
+ ":title": t.title ?? "",
450
+ ":one_liner": t.oneLiner ?? "",
451
+ ":narrative": t.narrative ?? "",
452
+ ":verification_result": t.verificationResult,
453
+ ":duration": t.duration ?? "",
454
+ ":completed_at": t.completedAt,
455
+ ":blocker_discovered": t.blockerDiscovered ? 1 : 0,
456
+ ":deviations": t.deviations ?? "",
457
+ ":known_issues": t.knownIssues ?? "",
458
+ ":key_files": JSON.stringify(t.keyFiles ?? []),
459
+ ":key_decisions": JSON.stringify(t.keyDecisions ?? []),
460
+ ":full_summary_md": t.fullSummaryMd,
461
+ ":sequence": seq?.next_sequence ?? 1,
462
+ });
463
+ });
464
+ }
413
465
  export function setTaskBlockerDiscovered(milestoneId, sliceId, taskId, discovered) {
414
466
  if (!getDbOrNull())
415
467
  return;
@@ -8,7 +8,7 @@
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, statSync, symlinkSync } from "node:fs";
10
10
  import { join } from "node:path";
11
- import { externalGsdRoot, isInsideWorktree } from "./repo-identity.js";
11
+ import { externalGsdRoot, externalStateAlreadyExistsForProject, isInsideWorktree } from "./repo-identity.js";
12
12
  import { getErrorMessage } from "./error-utils.js";
13
13
  import { hasGitTrackedGsdFiles } from "./gitignore.js";
14
14
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
@@ -74,6 +74,15 @@ export function migrateToExternalState(basePath) {
74
74
  return { migrated: false };
75
75
  }
76
76
  }
77
+ // If external state already contains project data, this is not a legacy
78
+ // migration source. It is likely an already-migrated project whose symlink
79
+ // was replaced by a real directory, so copying would risk data loss.
80
+ if (externalStateAlreadyExistsForProject(basePath)) {
81
+ return {
82
+ migrated: false,
83
+ error: "External state already exists for this project; leaving local .gsd directory untouched to avoid overwriting authoritative state",
84
+ };
85
+ }
77
86
  const externalPath = externalGsdRoot(basePath);
78
87
  const migratingPath = join(basePath, ".gsd.migrating");
79
88
  try {
@@ -81,13 +90,13 @@ export function migrateToExternalState(basePath) {
81
90
  mkdirSync(externalPath, { recursive: true });
82
91
  // Rename .gsd -> .gsd.migrating (atomic lock).
83
92
  // On Windows, NTFS may reject rename with EPERM if file descriptors are
84
- // open (VS Code watchers, antivirus on-access scan). Fall back to
85
- // copy+delete (#1292).
93
+ // open (VS Code watchers, antivirus on-access scan). WSL/DrvFs can report
94
+ // the same transient lock as EACCES. Fall back to copy+delete (#1292).
86
95
  try {
87
96
  renameSync(localGsd, migratingPath);
88
97
  }
89
98
  catch (renameErr) {
90
- if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
99
+ if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY" || renameErr?.code === "EACCES") {
91
100
  try {
92
101
  cpSync(localGsd, migratingPath, { recursive: true, force: true });
93
102
  rmSync(localGsd, { recursive: true, force: true });
@@ -1,10 +1,10 @@
1
1
  // Project/App: gsd-pi
2
2
  // File Purpose: Process-level transaction wrapper for milestone merge closeout.
3
3
  export function runMilestoneMergeTransaction(deps, input) {
4
- return deps.mergeMilestoneToMain(input.basePath, input.milestoneId, input.roadmapContent);
4
+ return deps.mergeMilestone(input.basePath, input.milestoneId, input.roadmapContent);
5
5
  }
6
- export function createMilestoneMergeTransaction(mergeMilestoneToMain) {
6
+ export function createMilestoneMergeTransaction(mergeMilestone) {
7
7
  return function mergeMilestoneTransaction(basePath, milestoneId, roadmapContent) {
8
- return runMilestoneMergeTransaction({ mergeMilestoneToMain }, { basePath, milestoneId, roadmapContent });
8
+ return runMilestoneMergeTransaction({ mergeMilestone }, { basePath, milestoneId, roadmapContent });
9
9
  };
10
10
  }
@@ -328,6 +328,22 @@ export function externalProjectsRoot() {
328
328
  const base = process.env.GSD_STATE_DIR || gsdHome();
329
329
  return join(base, "projects");
330
330
  }
331
+ /**
332
+ * Check whether this project already has authoritative external state.
333
+ *
334
+ * Used by legacy `.gsd/` migration to avoid re-copying a re-materialized local
335
+ * directory over newer state when an already-migrated symlink was replaced.
336
+ */
337
+ export function externalStateAlreadyExistsForProject(basePath) {
338
+ const base = process.env.GSD_STATE_DIR || gsdHome();
339
+ const computedPath = join(base, "projects", repoIdentity(basePath));
340
+ if (hasProjectState(computedPath))
341
+ return true;
342
+ const markerId = readGsdIdMarker(resolveGitRoot(basePath));
343
+ if (!markerId)
344
+ return false;
345
+ return hasProjectState(join(base, "projects", markerId));
346
+ }
331
347
  // ─── Numbered Variant Cleanup ────────────────────────────────────────────────
332
348
  /**
333
349
  * macOS collision pattern: `.gsd 2`, `.gsd 3`, `.gsd 4`, etc.
@@ -42,12 +42,14 @@ export function captureRootDirtySnapshot(rootPath) {
42
42
  if (!line.trim())
43
43
  continue;
44
44
  const code = line.slice(0, 2);
45
+ if (code !== "??")
46
+ continue;
45
47
  const path = line.slice(3).replace(/^"|"$/g, "");
46
48
  if (!path || isRootRuntimePath(path))
47
49
  continue;
48
50
  snapshot.set(path, {
49
51
  path,
50
- status: code.trim() || code,
52
+ status: code,
51
53
  fingerprint: fileFingerprint(rootPath, path),
52
54
  });
53
55
  }
@@ -58,7 +60,7 @@ export function detectRootWriteLeak(input) {
58
60
  const leaked = [];
59
61
  for (const entry of after.values()) {
60
62
  const prior = input.before?.get(entry.path);
61
- if (!prior || prior.status !== entry.status || prior.fingerprint !== entry.fingerprint) {
63
+ if (!prior || prior.fingerprint !== entry.fingerprint) {
62
64
  leaked.push(entry);
63
65
  }
64
66
  }
@@ -20,8 +20,9 @@
20
20
  //
21
21
  // Not yet declared here (remaining ADR-033 steps): the manifest data
22
22
  // (`UNIT_MANIFESTS` stays in unit-context-manifest.ts, already type-enforced
23
- // against the registry's `UnitType`) and prompt-template association (still
24
- // implicit in auto-prompts.ts builders).
23
+ // against the registry's `UnitType`). Prompt-template association is declared
24
+ // for direct one-template units; conditional/composite prompt selection stays
25
+ // with the prompt builders until it has a richer registry shape.
25
26
  import { BROWSER_CONTRACT_TOOL_NAMES } from "../shared/browser-contract.js";
26
27
  // ─── Shared tool-name constants (used by registry rows) ──────────────────
27
28
  export const RUN_UAT_WORKFLOW_TOOL_NAMES = [
@@ -50,6 +51,7 @@ export const UNIT_REGISTRY = {
50
51
  kind: "primary",
51
52
  scopeClass: "standard",
52
53
  phaseChain: ["research"],
54
+ promptTemplate: "research-milestone",
53
55
  toolContract: {
54
56
  allowedGsdTools: [
55
57
  "gsd_summary_save",
@@ -65,6 +67,7 @@ export const UNIT_REGISTRY = {
65
67
  kind: "primary",
66
68
  scopeClass: "standard",
67
69
  phaseChain: ["planning"],
70
+ promptTemplate: "plan-milestone",
68
71
  toolContract: {
69
72
  allowedGsdTools: [
70
73
  "gsd_milestone_status",
@@ -113,6 +116,7 @@ export const UNIT_REGISTRY = {
113
116
  kind: "primary",
114
117
  scopeClass: "section-close",
115
118
  phaseChain: ["validation", "planning"],
119
+ promptTemplate: "validate-milestone",
116
120
  toolContract: {
117
121
  allowedGsdTools: [
118
122
  "gsd_milestone_status",
@@ -130,6 +134,7 @@ export const UNIT_REGISTRY = {
130
134
  kind: "primary",
131
135
  scopeClass: "standard",
132
136
  phaseChain: ["completion"],
137
+ promptTemplate: "complete-milestone",
133
138
  toolContract: {
134
139
  allowedGsdTools: [
135
140
  "gsd_milestone_status",
@@ -153,6 +158,7 @@ export const UNIT_REGISTRY = {
153
158
  kind: "primary",
154
159
  scopeClass: "standard",
155
160
  phaseChain: ["research"],
161
+ promptTemplate: "research-slice",
156
162
  toolContract: {
157
163
  allowedGsdTools: [
158
164
  "gsd_milestone_status",
@@ -169,6 +175,7 @@ export const UNIT_REGISTRY = {
169
175
  kind: "primary",
170
176
  scopeClass: "standard",
171
177
  phaseChain: ["planning"],
178
+ promptTemplate: "plan-slice",
172
179
  toolContract: {
173
180
  allowedGsdTools: [
174
181
  "gsd_milestone_status",
@@ -187,6 +194,7 @@ export const UNIT_REGISTRY = {
187
194
  kind: "primary",
188
195
  scopeClass: "standard",
189
196
  phaseChain: ["planning"],
197
+ promptTemplate: "refine-slice",
190
198
  toolContract: {
191
199
  allowedGsdTools: [
192
200
  "gsd_milestone_status",
@@ -203,6 +211,7 @@ export const UNIT_REGISTRY = {
203
211
  kind: "primary",
204
212
  scopeClass: "standard",
205
213
  phaseChain: ["planning"],
214
+ promptTemplate: "replan-slice",
206
215
  toolContract: {
207
216
  allowedGsdTools: ["gsd_replan_slice", "gsd_decision_save"],
208
217
  requiredWorkflowTools: ["gsd_replan_slice"],
@@ -212,6 +221,7 @@ export const UNIT_REGISTRY = {
212
221
  kind: "primary",
213
222
  scopeClass: "section-close",
214
223
  phaseChain: ["completion"],
224
+ promptTemplate: "complete-slice",
215
225
  toolContract: {
216
226
  allowedGsdTools: [
217
227
  "gsd_milestone_status",
@@ -246,6 +256,7 @@ export const UNIT_REGISTRY = {
246
256
  kind: "primary",
247
257
  scopeClass: "standard",
248
258
  phaseChain: ["validation", "planning"],
259
+ promptTemplate: "reassess-roadmap",
249
260
  toolContract: {
250
261
  allowedGsdTools: ["gsd_milestone_status", "gsd_reassess_roadmap"],
251
262
  requiredWorkflowTools: ["gsd_milestone_status", "gsd_reassess_roadmap"],
@@ -255,6 +266,7 @@ export const UNIT_REGISTRY = {
255
266
  kind: "primary",
256
267
  scopeClass: "execute-task",
257
268
  phaseChain: ["execution"],
269
+ promptTemplate: "execute-task",
258
270
  toolContract: {
259
271
  allowedGsdTools: [
260
272
  "gsd_task_complete",
@@ -299,6 +311,7 @@ export const UNIT_REGISTRY = {
299
311
  kind: "primary",
300
312
  scopeClass: "execute-task",
301
313
  phaseChain: ["execution"],
314
+ promptTemplate: "reactive-execute",
302
315
  toolContract: {
303
316
  allowedGsdTools: [
304
317
  "gsd_milestone_status",
@@ -314,6 +327,7 @@ export const UNIT_REGISTRY = {
314
327
  kind: "primary",
315
328
  scopeClass: "standard",
316
329
  phaseChain: ["uat", "completion"],
330
+ promptTemplate: "run-uat",
317
331
  toolContract: {
318
332
  allowedGsdTools: [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent"],
319
333
  requiredWorkflowTools: [...RUN_UAT_WORKFLOW_TOOL_NAMES],
@@ -328,6 +342,7 @@ export const UNIT_REGISTRY = {
328
342
  kind: "primary",
329
343
  scopeClass: "standard",
330
344
  phaseChain: ["validation", "planning"],
345
+ promptTemplate: "gate-evaluate",
331
346
  toolContract: {
332
347
  allowedGsdTools: ["gsd_save_gate_result"],
333
348
  requiredWorkflowTools: ["gsd_save_gate_result"],
@@ -337,6 +352,7 @@ export const UNIT_REGISTRY = {
337
352
  kind: "primary",
338
353
  scopeClass: "standard",
339
354
  phaseChain: ["validation", "planning"],
355
+ promptTemplate: "rewrite-docs",
340
356
  toolContract: {
341
357
  allowedGsdTools: ["gsd_summary_save", "gsd_decision_save"],
342
358
  requiredWorkflowTools: [],
@@ -361,6 +377,7 @@ export const UNIT_REGISTRY = {
361
377
  kind: "primary",
362
378
  scopeClass: "standard",
363
379
  phaseChain: ["discuss", "planning"],
380
+ promptTemplate: "guided-workflow-preferences",
364
381
  toolContract: {
365
382
  allowedGsdTools: ["gsd_summary_save"],
366
383
  requiredWorkflowTools: [],
@@ -370,6 +387,7 @@ export const UNIT_REGISTRY = {
370
387
  kind: "primary",
371
388
  scopeClass: "standard",
372
389
  phaseChain: ["discuss", "planning"],
390
+ promptTemplate: "guided-discuss-project",
373
391
  toolContract: {
374
392
  allowedGsdTools: ["gsd_summary_save", "gsd_decision_save", "gsd_requirement_save"],
375
393
  requiredWorkflowTools: ["ask_user_questions", "gsd_summary_save"],
@@ -379,6 +397,7 @@ export const UNIT_REGISTRY = {
379
397
  kind: "primary",
380
398
  scopeClass: "standard",
381
399
  phaseChain: ["discuss", "planning"],
400
+ promptTemplate: "guided-discuss-requirements",
382
401
  toolContract: {
383
402
  allowedGsdTools: ["gsd_requirement_save", "gsd_summary_save"],
384
403
  requiredWorkflowTools: ["ask_user_questions", "gsd_requirement_save", "gsd_summary_save"],
@@ -388,6 +407,7 @@ export const UNIT_REGISTRY = {
388
407
  kind: "primary",
389
408
  scopeClass: "standard",
390
409
  phaseChain: ["discuss", "planning"],
410
+ promptTemplate: "guided-research-decision",
391
411
  toolContract: {
392
412
  allowedGsdTools: ["gsd_summary_save"],
393
413
  requiredWorkflowTools: ["ask_user_questions"],
@@ -400,6 +420,7 @@ export const UNIT_REGISTRY = {
400
420
  kind: "primary",
401
421
  scopeClass: "standard",
402
422
  phaseChain: ["research"],
423
+ promptTemplate: "guided-research-project",
403
424
  toolContract: {
404
425
  allowedGsdTools: [],
405
426
  requiredWorkflowTools: [],
@@ -419,3 +440,7 @@ export function getUnitDescriptor(unitType) {
419
440
  export function getUnitPhaseChain(unitType) {
420
441
  return getUnitDescriptor(unitType)?.phaseChain ?? null;
421
442
  }
443
+ /** Prompt template id for units with a direct one-template builder. */
444
+ export function getUnitPromptTemplate(unitType) {
445
+ return getUnitDescriptor(unitType)?.promptTemplate;
446
+ }