gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.42bd34838

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 (112) hide show
  1. package/README.md +1 -0
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +29 -15
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  6. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  7. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  9. package/dist/resources/extensions/gsd/auto.js +61 -2
  10. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  11. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  12. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  13. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  14. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  15. package/dist/resources/extensions/gsd/git-service.js +36 -4
  16. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  17. package/dist/resources/extensions/gsd/guided-flow.js +19 -4
  18. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  19. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  20. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  21. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  22. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  23. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  24. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  30. package/dist/web/standalone/.next/required-server-files.json +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/dist/web/standalone/server.js +1 -1
  60. package/package.json +3 -3
  61. package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
  62. package/packages/native/tsconfig.tsbuildinfo +1 -1
  63. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  64. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/agent-session.js +5 -0
  67. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  68. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  69. package/packages/pi-coding-agent/src/core/agent-session.ts +5 -0
  70. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  71. package/src/resources/extensions/gsd/auto/phases.ts +35 -20
  72. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  73. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  74. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  75. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  76. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  77. package/src/resources/extensions/gsd/auto.ts +69 -1
  78. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  79. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  80. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  81. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  82. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  83. package/src/resources/extensions/gsd/git-service.ts +46 -8
  84. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  85. package/src/resources/extensions/gsd/guided-flow.ts +32 -4
  86. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  87. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  88. package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  89. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  90. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  91. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  92. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +3 -1
  93. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  94. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  95. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  96. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  97. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  98. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  99. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  100. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  101. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  102. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -0
  103. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  104. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  105. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  106. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  107. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  108. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  109. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  110. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  111. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → tht0ltP8EIjsohPOA_WZf}/_buildManifest.js +0 -0
  112. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → tht0ltP8EIjsohPOA_WZf}/_ssgManifest.js +0 -0
@@ -22,6 +22,7 @@
22
22
  // intentionally independent store for cross-worktree claim races and is
23
23
  // excluded from this invariant.
24
24
  import { createRequire } from "node:module";
25
+ import { createHash } from "node:crypto";
25
26
  import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
26
27
  import { dirname } from "node:path";
27
28
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
@@ -36,7 +37,7 @@ import { rowToActiveDecision, rowToActiveRequirement, rowToDecision, rowToRequir
36
37
  import { rowToGate } from "./db-gate-rows.js";
37
38
  import { rowToArtifact, rowToMilestone } from "./db-milestone-artifact-rows.js";
38
39
  import { backupDatabaseBeforeMigration } from "./db-migration-backup.js";
39
- import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, } from "./db-migration-steps.js";
40
+ import { applyMigrationV2Artifacts, applyMigrationV3Memories, applyMigrationV4DecisionMadeBy, applyMigrationV5HierarchyTables, applyMigrationV6SliceSummaries, applyMigrationV7Dependencies, applyMigrationV8PlanningFields, applyMigrationV9Ordering, applyMigrationV10ReplanTrigger, applyMigrationV11TaskPlanning, applyMigrationV12QualityGates, applyMigrationV13HotPathIndexes, applyMigrationV14SliceDependencies, applyMigrationV15AuditTables, applyMigrationV16EscalationSource, applyMigrationV17TaskEscalation, applyMigrationV18MemorySources, applyMigrationV19MemoryFts, applyMigrationV20MemoryRelations, applyMigrationV21StructuredMemories, applyMigrationV22QualityGateRepair, applyMigrationV23MilestoneQueue, applyMigrationV26MilestoneCommitAttributions, applyMigrationV27ArtifactHash, applyMigrationV28MemoryLastHitAt, } from "./db-migration-steps.js";
40
41
  import { isMemoriesFtsAvailableSchema, tryCreateMemoriesFtsSchema } from "./db-memory-fts-schema.js";
41
42
  import { createDbOpenState } from "./db-open-state.js";
42
43
  import { createRuntimeKvTableV25 } from "./db-runtime-kv-schema.js";
@@ -52,7 +53,7 @@ const providerLoader = createSqliteProviderLoader({
52
53
  nodeVersion: process.versions.node,
53
54
  writeStderr: (message) => process.stderr.write(message),
54
55
  });
55
- export const SCHEMA_VERSION = 26;
56
+ export const SCHEMA_VERSION = 28;
56
57
  function initSchema(db, fileBacked) {
57
58
  if (fileBacked)
58
59
  db.exec("PRAGMA journal_mode=WAL");
@@ -250,6 +251,14 @@ function migrateSchema(db) {
250
251
  applyMigrationV26MilestoneCommitAttributions(db);
251
252
  recordSchemaVersion(db, 26);
252
253
  }
254
+ if (currentVersion < 27) {
255
+ applyMigrationV27ArtifactHash(db);
256
+ recordSchemaVersion(db, 27);
257
+ }
258
+ if (currentVersion < 28) {
259
+ applyMigrationV28MemoryLastHitAt(db);
260
+ recordSchemaVersion(db, 28);
261
+ }
253
262
  db.exec("COMMIT");
254
263
  }
255
264
  catch (err) {
@@ -821,8 +830,9 @@ export function clearArtifacts() {
821
830
  export function insertArtifact(a) {
822
831
  if (!currentDb)
823
832
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
824
- currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
825
- VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`).run({
833
+ const contentHash = createHash("sha256").update(a.full_content).digest("hex");
834
+ currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash)
835
+ VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at, :content_hash)`).run({
826
836
  ":path": a.path,
827
837
  ":artifact_type": a.artifact_type,
828
838
  ":milestone_id": a.milestone_id,
@@ -830,6 +840,7 @@ export function insertArtifact(a) {
830
840
  ":task_id": a.task_id,
831
841
  ":full_content": a.full_content,
832
842
  ":imported_at": new Date().toISOString(),
843
+ ":content_hash": contentHash,
833
844
  });
834
845
  }
835
846
  export function insertMilestone(m) {
@@ -1521,6 +1532,13 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1521
1532
  const hasEscalationAwaiting = wtTaskInfo.some((col) => col["name"] === "escalation_awaiting_review");
1522
1533
  const hasEscalationArtifact = wtTaskInfo.some((col) => col["name"] === "escalation_artifact_path");
1523
1534
  const hasEscalationOverride = wtTaskInfo.some((col) => col["name"] === "escalation_override_applied_at");
1535
+ const wtArtifactInfo = adapter.prepare("PRAGMA wt.table_info('artifacts')").all();
1536
+ const hasArtifactContentHash = wtArtifactInfo.some((col) => col["name"] === "content_hash");
1537
+ const wtMemoryInfo = adapter.prepare("PRAGMA wt.table_info('memories')").all();
1538
+ const hasMemoryScope = wtMemoryInfo.some((col) => col["name"] === "scope");
1539
+ const hasMemoryTags = wtMemoryInfo.some((col) => col["name"] === "tags");
1540
+ const hasMemoryStructuredFields = wtMemoryInfo.some((col) => col["name"] === "structured_fields");
1541
+ const hasMemoryLastHitAt = wtMemoryInfo.some((col) => col["name"] === "last_hit_at");
1524
1542
  const decConf = adapter.prepare(`SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR ${hasMadeBy ? "m.made_by != w.made_by" : "'agent' != 'agent'"} OR m.superseded_by IS NOT w.superseded_by`).all();
1525
1543
  for (const row of decConf)
1526
1544
  conflicts.push(`decision ${row["id"]}: modified in both`);
@@ -1553,12 +1571,17 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1553
1571
  supporting_slices, validation, notes, full_content, superseded_by
1554
1572
  FROM wt.requirements
1555
1573
  `).run());
1574
+ // V27: preserve content_hash. If the worktree predates V27 (no column),
1575
+ // fall back to the main DB's existing hash so reconcile doesn't null
1576
+ // out integrity fingerprints on artifacts that were unchanged in wt.
1556
1577
  merged.artifacts = countChanges(adapter.prepare(`
1557
1578
  INSERT OR REPLACE INTO artifacts (
1558
- path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1579
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at, content_hash
1559
1580
  )
1560
- SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
1561
- FROM wt.artifacts
1581
+ SELECT w.path, w.artifact_type, w.milestone_id, w.slice_id, w.task_id, w.full_content, w.imported_at,
1582
+ ${hasArtifactContentHash ? "w.content_hash" : "m.content_hash"}
1583
+ FROM wt.artifacts w
1584
+ LEFT JOIN artifacts m ON m.path = w.path
1562
1585
  `).run());
1563
1586
  // Merge milestones — worktree may have updated status/planning fields.
1564
1587
  // Never downgrade status: complete > active > pre-planning (#4372).
@@ -1656,15 +1679,25 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
1656
1679
  FROM wt.tasks w
1657
1680
  LEFT JOIN tasks m ON m.milestone_id = w.milestone_id AND m.slice_id = w.slice_id AND m.id = w.id
1658
1681
  `).run());
1659
- // Merge memories — keep worktree-learned insights
1682
+ // Merge memories — keep worktree-learned insights.
1683
+ // V18 (scope, tags), V21 (structured_fields), V28 (last_hit_at): for each
1684
+ // column the wt may not yet have (older worktree DB), fall back to the
1685
+ // main DB's existing value via LEFT JOIN so reconcile never silently
1686
+ // resets these fields to defaults on rows that already had them.
1660
1687
  merged.memories = countChanges(adapter.prepare(`
1661
1688
  INSERT OR REPLACE INTO memories (
1662
1689
  seq, id, category, content, confidence, source_unit_type, source_unit_id,
1663
- created_at, updated_at, superseded_by, hit_count
1690
+ created_at, updated_at, superseded_by, hit_count,
1691
+ scope, tags, structured_fields, last_hit_at
1664
1692
  )
1665
- SELECT seq, id, category, content, confidence, source_unit_type, source_unit_id,
1666
- created_at, updated_at, superseded_by, hit_count
1667
- FROM wt.memories
1693
+ SELECT w.seq, w.id, w.category, w.content, w.confidence, w.source_unit_type, w.source_unit_id,
1694
+ w.created_at, w.updated_at, w.superseded_by, w.hit_count,
1695
+ ${hasMemoryScope ? "w.scope" : "COALESCE(m.scope, 'project')"},
1696
+ ${hasMemoryTags ? "w.tags" : "COALESCE(m.tags, '[]')"},
1697
+ ${hasMemoryStructuredFields ? "w.structured_fields" : "m.structured_fields"},
1698
+ ${hasMemoryLastHitAt ? "w.last_hit_at" : "m.last_hit_at"}
1699
+ FROM wt.memories w
1700
+ LEFT JOIN memories m ON m.id = w.id
1668
1701
  `).run());
1669
1702
  // Merge verification evidence — append-only, use INSERT OR IGNORE to avoid duplicates
1670
1703
  merged.verification_evidence = countChanges(adapter.prepare(`
@@ -2423,7 +2456,7 @@ export function updateMemoryContentRow(id, content, confidence, updatedAt) {
2423
2456
  export function incrementMemoryHitCount(id, updatedAt) {
2424
2457
  if (!currentDb)
2425
2458
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2426
- currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id").run({ ":updated_at": updatedAt, ":id": id });
2459
+ currentDb.prepare("UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at, last_hit_at = :last_hit_at WHERE id = :id").run({ ":updated_at": updatedAt, ":last_hit_at": updatedAt, ":id": id });
2427
2460
  }
2428
2461
  export function supersedeMemoryRow(oldId, newId, updatedAt) {
2429
2462
  if (!currentDb)
@@ -51,6 +51,21 @@ export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesCon
51
51
  import { logWarning } from "./workflow-logger.js";
52
52
  import { deleteRuntimeKv } from "./db/runtime-kv.js";
53
53
  import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
54
+ function scheduleAutoStartAfterIdle(ctx, pi, basePath, verboseMode, options, launch = startAutoDetached) {
55
+ const waitForIdle = typeof ctx.waitForIdle === "function"
56
+ ? ctx.waitForIdle.bind(ctx)
57
+ : async () => { };
58
+ void waitForIdle()
59
+ .then(() => {
60
+ setTimeout(() => launch(ctx, pi, basePath, verboseMode, options), 0);
61
+ })
62
+ .catch((err) => {
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ ctx.ui.notify(`Auto-start failed while waiting for the prior turn to settle: ${message}`, "error");
65
+ logWarning("guided", `auto-start idle wait failed: ${message}`);
66
+ });
67
+ }
68
+ export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
54
69
  // ─── Scope-based validator wrappers ──────────────────────────────────────────
55
70
  // These thin wrappers accept a MilestoneScope so callers that already hold a
56
71
  // pinned scope never have to re-derive (basePath, milestoneId) separately.
@@ -325,7 +340,7 @@ async function dispatchNextDeepProjectSetupStage(entry) {
325
340
  const { DISPATCH_RULES, hasPendingDeepStage } = await import("./auto-dispatch.js");
326
341
  if (!hasPendingDeepStage(prefs, entry.basePath)) {
327
342
  pendingDeepProjectSetupMap.delete(entry.basePath);
328
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
343
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
329
344
  return true;
330
345
  }
331
346
  const state = await deriveState(entry.basePath);
@@ -359,14 +374,14 @@ async function dispatchNextDeepProjectSetupStage(entry) {
359
374
  }
360
375
  else if (hasPendingDeepStage(prefs, entry.basePath)) {
361
376
  pendingDeepProjectSetupMap.delete(entry.basePath);
362
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
377
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
363
378
  return true;
364
379
  }
365
380
  return false;
366
381
  }
367
382
  if (!USER_DRIVEN_DEEP_SETUP_UNITS.has(result.unitType)) {
368
383
  pendingDeepProjectSetupMap.delete(entry.basePath);
369
- startAutoDetached(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
384
+ scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
370
385
  return true;
371
386
  }
372
387
  entry.currentUnitType = result.unitType;
@@ -544,7 +559,7 @@ export function checkAutoStartAfterDiscuss() {
544
559
  }
545
560
  pendingAutoStartMap.delete(basePath);
546
561
  ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
547
- startAutoDetached(ctx, pi, basePath, false, { step });
562
+ scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step });
548
563
  return true;
549
564
  }
550
565
  /**
@@ -14,6 +14,27 @@ const CATEGORY_PRIORITY = {
14
14
  environment: 4,
15
15
  preference: 5,
16
16
  };
17
+ // ─── Scoring Helpers ─────────────────────────────────────────────────────────
18
+ /**
19
+ * Time-decay factor for memory relevance scoring.
20
+ * Returns 1.0 for never-hit or recently-hit memories, decaying linearly to
21
+ * 0.7 for memories not accessed in 90+ days. Floor at 0.7 keeps old-but-valid
22
+ * knowledge from being fully suppressed.
23
+ *
24
+ * Defensive parsing: invalid timestamp strings (NaN from Date.parse) are
25
+ * treated as "no decay" rather than propagating NaN into score arithmetic.
26
+ * Future timestamps (clock skew, manual DB edits) clamp to daysAgo=0 so the
27
+ * factor stays in the documented [0.7, 1.0] contract.
28
+ */
29
+ export function memoryDecayFactor(lastHitAt) {
30
+ if (!lastHitAt)
31
+ return 1.0;
32
+ const ts = Date.parse(lastHitAt);
33
+ if (!Number.isFinite(ts))
34
+ return 1.0;
35
+ const daysAgo = Math.max(0, (Date.now() - ts) / 86_400_000);
36
+ return Math.max(0.7, 1.0 - 0.3 * Math.min(1.0, daysAgo / 90));
37
+ }
17
38
  // ─── Row Mapping ────────────────────────────────────────────────────────────
18
39
  function rowToMemory(row) {
19
40
  return {
@@ -31,6 +52,7 @@ function rowToMemory(row) {
31
52
  scope: row['scope'] ?? 'project',
32
53
  tags: parseTags(row['tags']),
33
54
  structured_fields: parseStructuredFields(row['structured_fields']),
55
+ last_hit_at: row['last_hit_at'] ?? null,
34
56
  };
35
57
  }
36
58
  function parseStructuredFields(raw) {
@@ -114,15 +136,37 @@ export function queryMemoriesRanked(opts) {
114
136
  ? semanticSearch(adapter, opts.queryVector, activeClause, 50)
115
137
  : [];
116
138
  if (keywordHits.length === 0 && semanticHits.length === 0 && !trimmedQuery) {
117
- // No query at all — fall back to the existing ranked-by-score listing.
118
- return getActiveMemoriesRanked(k).map((memory) => ({
119
- memory,
120
- score: memory.confidence * (1 + memory.hit_count * 0.1),
121
- keywordRank: null,
122
- semanticRank: null,
123
- confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
124
- reason: 'ranked',
125
- })).filter((hit) => passesFilters(hit.memory, opts));
139
+ // No query at all — return top-k by decay-aware ranked score.
140
+ //
141
+ // Build the candidate pool from a direct SQL query that honors the
142
+ // request's activeClause (i.e. include_superseded). Using
143
+ // getActiveMemoriesRanked here would silently drop superseded rows even
144
+ // when the caller explicitly opted in, and would slice by raw score
145
+ // before decay/filters had a chance to reorder.
146
+ const candidatePool = Math.min(Math.max(k * 5, 50), 500);
147
+ const rows = adapter
148
+ .prepare(`SELECT * FROM memories ${activeClause}
149
+ ORDER BY (confidence * (1.0 + hit_count * 0.1)) DESC
150
+ LIMIT :limit`)
151
+ .all({ ':limit': candidatePool });
152
+ const ranked = [];
153
+ for (const row of rows) {
154
+ const memory = rowToMemory(row);
155
+ if (!passesFilters(memory, opts))
156
+ continue;
157
+ const decay = memoryDecayFactor(memory.last_hit_at);
158
+ const score = memory.confidence * (1 + memory.hit_count * 0.1) * decay;
159
+ ranked.push({
160
+ memory,
161
+ score,
162
+ keywordRank: null,
163
+ semanticRank: null,
164
+ confidenceBoost: score,
165
+ reason: 'ranked',
166
+ });
167
+ }
168
+ ranked.sort((a, b) => b.score - a.score);
169
+ return ranked.slice(0, k);
126
170
  }
127
171
  // 3) Reciprocal rank fusion — each hit contributes 1/(rrfK + rank).
128
172
  const fused = new Map();
@@ -155,7 +199,7 @@ export function queryMemoriesRanked(opts) {
155
199
  for (const entry of fused.values()) {
156
200
  if (!passesFilters(entry.memory, opts))
157
201
  continue;
158
- const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1);
202
+ const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1) * memoryDecayFactor(entry.memory.last_hit_at);
159
203
  const reason = entry.kwRank != null && entry.semRank != null
160
204
  ? 'both'
161
205
  : entry.kwRank != null
@@ -194,6 +238,7 @@ function passesFilters(memory, filters) {
194
238
  }
195
239
  return true;
196
240
  }
241
+ let ftsWarningEmitted = false;
197
242
  function keywordSearch(adapter, rawQuery, activeClause, limit) {
198
243
  const ftsAvailable = isFtsAvailable(adapter);
199
244
  if (ftsAvailable) {
@@ -215,14 +260,26 @@ function keywordSearch(adapter, rawQuery, activeClause, limit) {
215
260
  // fall through to LIKE
216
261
  }
217
262
  }
218
- // LIKE fallback — scans the candidate pool.
263
+ // LIKE fallback — scans a capped candidate pool.
264
+ if (!ftsWarningEmitted) {
265
+ ftsWarningEmitted = true;
266
+ logWarning('memory-store', 'FTS5 unavailable — using LIKE fallback scan (consider enabling FTS5)');
267
+ }
219
268
  const terms = rawQuery
220
269
  .toLowerCase()
221
270
  .split(/[^a-z0-9_]+/)
222
271
  .filter((t) => t.length >= 2);
223
272
  if (terms.length === 0)
224
273
  return [];
225
- const rows = adapter.prepare(`SELECT * FROM memories ${activeClause}`).all();
274
+ const preScanCap = Math.min(limit * 20, 2000);
275
+ // ORDER BY confidence-weighted hit_count DESC so the cap keeps the most
276
+ // valuable candidates instead of the oldest-by-rowid (which would silently
277
+ // exclude recently-stored memories on tables larger than preScanCap).
278
+ const rows = adapter
279
+ .prepare(`SELECT * FROM memories ${activeClause}
280
+ ORDER BY (confidence * (1.0 + hit_count * 0.1)) DESC
281
+ LIMIT :preScanCap`)
282
+ .all({ ':preScanCap': preScanCap });
226
283
  const scored = [];
227
284
  for (const row of rows) {
228
285
  const memory = rowToMemory(row);
@@ -111,15 +111,41 @@ function warmCache() {
111
111
  }
112
112
  }
113
113
  let warmCacheScheduled = false;
114
+ let warmCacheRan = false;
115
+ let warmCacheTimer;
116
+ /**
117
+ * Synchronously snapshot the prompt/template tree into the cache.
118
+ *
119
+ * Safe to call immediately after `initResources()` because that function uses
120
+ * synchronous fs APIs — there is no race window that requires deferring the
121
+ * cache warm via setTimeout. Idempotent: subsequent calls are no-ops.
122
+ *
123
+ * Cancels the fallback `scheduleWarmCache` timer if it is still pending.
124
+ */
125
+ export function primeCache() {
126
+ if (warmCacheRan)
127
+ return;
128
+ if (warmCacheTimer) {
129
+ clearTimeout(warmCacheTimer);
130
+ warmCacheTimer = undefined;
131
+ }
132
+ warmCacheScheduled = true;
133
+ warmCacheRan = true;
134
+ warmCache();
135
+ }
114
136
  function scheduleWarmCache() {
115
137
  if (warmCacheScheduled)
116
138
  return;
117
139
  warmCacheScheduled = true;
118
140
  const run = () => {
141
+ warmCacheTimer = undefined;
142
+ if (warmCacheRan)
143
+ return;
144
+ warmCacheRan = true;
119
145
  warmCache();
120
146
  };
121
- const timer = setTimeout(run, 1000);
122
- timer.unref?.();
147
+ warmCacheTimer = setTimeout(run, 1000);
148
+ warmCacheTimer.unref?.();
123
149
  }
124
150
  // Snapshot the full prompt/template tree after import so extension startup only
125
151
  // pays for prompts that are actually needed immediately.
@@ -29,14 +29,15 @@ Subagents report only; they do not write user source. Fold any findings into Dec
29
29
 
30
30
  ## Steps
31
31
 
32
- 1. Use the **Milestone Summary** output template from the inlined context above
33
- 2. {{skillActivation}}
34
- 3. **Verify code changes exist.** Compare milestone work against the integration branch (`main`, `master`, or recorded branch), using merge-base as older revision and `HEAD` as newer. If the diff lists non-`.gsd/` files, pass. If `HEAD` equals the integration branch/merge-base, treat it as a self-diff retry: inspect milestone-scoped commit evidence (`GSD-Unit: {{milestoneId}}` or production `GSD-Task: Sxx/Tyy` trailers touching `.gsd/milestones/{{milestoneId}}/`) and verify those commits touched non-`.gsd/` files. Record **verification failure** only when neither source shows implementation files.
35
- 4. Verify every **success criterion** from `{{roadmapPath}}`. If passing validation is present, summarize the validation evidence instead of re-auditing it; otherwise verify with evidence from summaries, tests, or observable behavior. Record unmet criteria as **verification failure**.
36
- 5. Verify **definition of done**: all slices `[x]`, summaries exist, and integrations work. If passing validation is present, trust its integration/verification verdict unless inconsistent with current artifacts. Record unmet items as **verification failure**.
37
- 6. If the roadmap includes a **Horizontal Checklist**, verify each item and note unchecked items in the summary.
38
- 7. Fill the **Decision Re-evaluation** table: compare each key `.gsd/DECISIONS.md` decision from this milestone with what shipped, and flag decisions to revisit.
39
- 8. Validate **requirement status transitions**. For each changed requirement, confirm evidence supports the new status. Requirements may move between Active, Validated, Deferred, Blocked, or Out of Scope only with proof.
32
+ 1. **Duplicate completion guard:** Call `gsd_milestone_status` for `{{milestoneId}}` before any durable writes. If the returned milestone **status is `complete`**, this is a stale or duplicate closeout turn: do NOT mutate requirements, do NOT refresh the project document, do NOT write LEARNINGS, and do NOT persist milestone completion again. Say: "Milestone {{milestoneId}} is already complete." and stop.
33
+ 2. Use the **Milestone Summary** output template from the inlined context above
34
+ 3. {{skillActivation}}
35
+ 4. **Verify code changes exist.** Compare milestone work against the integration branch (`main`, `master`, or recorded branch), using merge-base as older revision and `HEAD` as newer. If the diff lists non-`.gsd/` files, pass. If `HEAD` equals the integration branch/merge-base, treat it as a self-diff retry: inspect milestone-scoped commit evidence (`GSD-Unit: {{milestoneId}}` or production `GSD-Task: Sxx/Tyy` trailers touching `.gsd/milestones/{{milestoneId}}/`) and verify those commits touched non-`.gsd/` files. Record **verification failure** only when neither source shows implementation files.
36
+ 5. Verify every **success criterion** from `{{roadmapPath}}`. If passing validation is present, summarize the validation evidence instead of re-auditing it; otherwise verify with evidence from summaries, tests, or observable behavior. Record unmet criteria as **verification failure**.
37
+ 6. Verify **definition of done**: all slices `[x]`, summaries exist, and integrations work. If passing validation is present, trust its integration/verification verdict unless inconsistent with current artifacts. Record unmet items as **verification failure**.
38
+ 7. If the roadmap includes a **Horizontal Checklist**, verify each item and note unchecked items in the summary.
39
+ 8. Fill the **Decision Re-evaluation** table: compare each key `.gsd/DECISIONS.md` decision from this milestone with what shipped, and flag decisions to revisit.
40
+ 9. Validate **requirement status transitions**. For each changed requirement, confirm evidence supports the new status. Requirements may move between Active, Validated, Deferred, Blocked, or Out of Scope only with proof.
40
41
 
41
42
  **DB access safety:** Do NOT query `.gsd/gsd.db` directly via `sqlite3` or `node -e require('better-sqlite3')`; the engine owns the WAL connection. Use `gsd_milestone_status`, inlined context, or `gsd_*` tools; never direct SQL.
42
43
 
@@ -53,13 +54,13 @@ Subagents report only; they do not write user source. Fold any findings into Dec
53
54
 
54
55
  **Success path** (all verifications passed):
55
56
 
56
- 9. For each requirement whose status changed in step 8, call `gsd_requirement_update` with the requirement ID and updated `status` and `validation` fields — the tool regenerates `.gsd/REQUIREMENTS.md` automatically. Do this BEFORE completing the milestone so requirement updates are persisted.
57
- 10. Update `.gsd/PROJECT.md`: use the `write` tool with `path: ".gsd/PROJECT.md"` and `content` containing the full updated document reflecting milestone completion and current project state. Do NOT use the `edit` tool for this — PROJECT.md is a full-document refresh.
58
- 11. Extract structured learnings from this milestone and persist them to the GSD memory store. Follow the procedure block immediately below — it writes `{{milestoneId}}-LEARNINGS.md` as the audit trail and persists Patterns, Lessons, and Decisions via `capture_thought` (categories: pattern, gotcha/convention, architecture). The memory store is the single source of truth for cross-session durable knowledge (ADR-013).
57
+ 10. For each requirement whose status changed in step 9, call `gsd_requirement_update` with the requirement ID and updated `status` and `validation` fields — the tool regenerates `.gsd/REQUIREMENTS.md` automatically. Do this BEFORE completing the milestone so requirement updates are persisted.
58
+ 11. Update `.gsd/PROJECT.md`: use the `write` tool with `path: ".gsd/PROJECT.md"` and `content` containing the full updated document reflecting milestone completion and current project state. Do NOT use the `edit` tool for this — PROJECT.md is a full-document refresh.
59
+ 12. Extract structured learnings from this milestone and persist them to the GSD memory store. Follow the procedure block immediately below — it writes `{{milestoneId}}-LEARNINGS.md` as the audit trail and persists Patterns, Lessons, and Decisions via `capture_thought` (categories: pattern, gotcha/convention, architecture). The memory store is the single source of truth for cross-session durable knowledge (ADR-013).
59
60
 
60
61
  {{extractLearningsSteps}}
61
62
 
62
- 12. **Persist completion through `gsd_complete_milestone`.** Call it with the parameters below. This must be the final persistent write in the unit. The tool updates the milestone status in the DB, renders `{{milestoneSummaryPath}}`, and validates all slices are complete.
63
+ 13. **Persist completion through `gsd_complete_milestone`.** Call it with the parameters below. This must be the final persistent write in the unit. The tool updates the milestone status in the DB, renders `{{milestoneSummaryPath}}`, and validates all slices are complete.
63
64
 
64
65
  **Required parameters:**
65
66
  - `milestoneId` (string) — Milestone ID (e.g. M001)
@@ -78,7 +79,7 @@ Subagents report only; they do not write user source. Fold any findings into Dec
78
79
  - `followUps` (string) — Follow-up items for future milestones
79
80
  - `deviations` (string) — Deviations from the original plan
80
81
 
81
- 13. Do not commit manually — the system auto-commits your changes after this unit completes.
82
+ 14. Do not commit manually — the system auto-commits your changes after this unit completes.
82
83
  - Say: "Milestone {{milestoneId}} complete."
83
84
 
84
85
  **Important:** Do NOT skip code-change, success-criteria, or definition-of-done verification (steps 3-5). The summary must reflect verified outcomes. Verification failures block completion; there is no override. If a verification tool fails, errors, or returns unexpected output, treat it as failure.
@@ -256,6 +256,7 @@ function includeSupersededMemories(rankedActive) {
256
256
  scope: row["scope"] ?? "project",
257
257
  tags,
258
258
  structured_fields: structuredFields,
259
+ last_hit_at: row["last_hit_at"] ?? null,
259
260
  };
260
261
  });
261
262
  }
@@ -680,7 +680,7 @@ export async function executeMilestoneStatus(params, basePath = process.cwd()) {
680
680
  };
681
681
  return {
682
682
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
683
- details: { operation: "milestone_status", milestoneId: milestone.id, sliceCount: slices.length },
683
+ details: { operation: "milestone_status", ...result },
684
684
  };
685
685
  });
686
686
  }
@@ -4,6 +4,8 @@ import { atomicWriteSync } from "./atomic-write.js";
4
4
  import { gsdRoot, relSliceFile, relTaskFile, resolveSliceFile, resolveTaskFile, } from "./paths.js";
5
5
  import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
6
6
  import { parseUnitId } from "./unit-id.js";
7
+ import { getTask, isDbAvailable, refreshOpenDatabaseFromDisk } from "./gsd-db.js";
8
+ import { isClosedStatus } from "./status-guards.js";
7
9
  // Per-record advisory lock — prevents read-modify-write races between
8
10
  // concurrent writers updating disjoint fields of the same runtime record.
9
11
  // Within a single Node process this is moot (writeUnitRuntimeRecord is sync),
@@ -165,6 +167,12 @@ export async function inspectExecuteTaskDurability(basePath, unitId) {
165
167
  const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
168
  const taskChecked = !!planContent && new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m").test(planContent);
167
169
  const nextActionAdvanced = !new RegExp(`Execute ${tid}\\b`).test(stateContent);
170
+ let dbComplete = false;
171
+ if (isDbAvailable()) {
172
+ refreshOpenDatabaseFromDisk();
173
+ const task = getTask(mid, sid, tid);
174
+ dbComplete = !!task && isClosedStatus(task.status);
175
+ }
168
176
  // Must-have coverage: load task plan and count mentions in summary
169
177
  let mustHaveCount = 0;
170
178
  let mustHavesMentionedInSummary = 0;
@@ -188,11 +196,14 @@ export async function inspectExecuteTaskDurability(basePath, unitId) {
188
196
  summaryExists,
189
197
  taskChecked,
190
198
  nextActionAdvanced,
199
+ dbComplete,
191
200
  mustHaveCount,
192
201
  mustHavesMentionedInSummary,
193
202
  };
194
203
  }
195
204
  export function formatExecuteTaskRecoveryStatus(status) {
205
+ if (status.dbComplete)
206
+ return "DB task status is closed";
196
207
  const missing = [];
197
208
  if (!status.summaryExists)
198
209
  missing.push(`summary missing (${status.summaryPath})`);
@@ -22,7 +22,7 @@ import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js
22
22
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
23
23
  import { loadEffectiveGSDPreferences } from "./preferences.js";
24
24
  import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
25
- import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
25
+ import { claimMilestoneLease, refreshMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
26
26
  // ─── Path Comparison Helper ────────────────────────────────────────────────
27
27
  /**
28
28
  * Compare two paths for physical identity, tolerating trailing slashes,
@@ -119,24 +119,40 @@ export class WorktreeResolver {
119
119
  // milestone (re-entry within the same session).
120
120
  if (this.s.workerId) {
121
121
  if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
122
- // Already held no-op, the heartbeat in loop.ts refreshes TTL.
123
- }
124
- else {
125
- // If we held a different milestone, release it first so other
126
- // workers don't have to wait for TTL.
127
- if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
128
- try {
129
- releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
130
- }
131
- catch (err) {
132
- debugLog("WorktreeResolver", {
133
- action: "enterMilestone",
134
- milestoneId,
135
- releasePriorLeaseError: err instanceof Error ? err.message : String(err),
136
- });
137
- }
122
+ const refreshed = refreshMilestoneLease(this.s.workerId, milestoneId, this.s.milestoneLeaseToken);
123
+ if (refreshed) {
124
+ debugLog("WorktreeResolver", {
125
+ action: "enterMilestone",
126
+ milestoneId,
127
+ leaseRefreshed: true,
128
+ fencingToken: this.s.milestoneLeaseToken,
129
+ });
130
+ }
131
+ else {
132
+ debugLog("WorktreeResolver", {
133
+ action: "enterMilestone",
134
+ milestoneId,
135
+ staleLeaseToken: this.s.milestoneLeaseToken,
136
+ });
138
137
  this.s.milestoneLeaseToken = null;
139
138
  }
139
+ }
140
+ // If we held a different milestone, release it first so other
141
+ // workers don't have to wait for TTL.
142
+ if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
143
+ try {
144
+ releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
145
+ }
146
+ catch (err) {
147
+ debugLog("WorktreeResolver", {
148
+ action: "enterMilestone",
149
+ milestoneId,
150
+ releasePriorLeaseError: err instanceof Error ? err.message : String(err),
151
+ });
152
+ }
153
+ this.s.milestoneLeaseToken = null;
154
+ }
155
+ if (this.s.milestoneLeaseToken === null) {
140
156
  try {
141
157
  const claim = claimMilestoneLease(this.s.workerId, milestoneId);
142
158
  if (claim.ok) {