@opengsd/gsd-pi 1.1.1-dev.154fd443 → 1.1.1-dev.1854a79a

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 (169) hide show
  1. package/dist/project-sessions.js +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/browser-tools/index.js +39 -22
  4. package/dist/resources/extensions/browser-tools/state.js +12 -0
  5. package/dist/resources/extensions/browser-tools/tools/session.js +3 -2
  6. package/dist/resources/extensions/browser-tools/utils.js +3 -3
  7. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -9
  8. package/dist/resources/extensions/gsd/auto/contracts.js +8 -1
  9. package/dist/resources/extensions/gsd/auto/orchestrator.js +659 -57
  10. package/dist/resources/extensions/gsd/auto-prompts.js +14 -1
  11. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  12. package/dist/resources/extensions/gsd/auto-tool-tracking.js +5 -0
  13. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +29 -0
  14. package/dist/resources/extensions/gsd/auto.js +62 -464
  15. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -1
  16. package/dist/resources/extensions/gsd/debug-logger.js +10 -0
  17. package/dist/resources/extensions/gsd/doctor-proactive.js +7 -2
  18. package/dist/resources/extensions/gsd/markdown-renderer.js +31 -32
  19. package/dist/resources/extensions/gsd/mcp-filter.js +6 -0
  20. package/dist/resources/extensions/gsd/native-git-bridge.js +9 -0
  21. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  22. package/dist/resources/extensions/gsd/schemas/parsers.js +6 -1
  23. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +21 -1
  24. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +169 -20
  25. package/dist/web/standalone/.next/BUILD_ID +1 -1
  26. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  27. package/dist/web/standalone/.next/build-manifest.json +2 -2
  28. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  49. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  50. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  51. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  52. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  53. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  54. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  55. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  57. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  58. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  59. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  60. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  61. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  62. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/index.html +1 -1
  81. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  88. package/dist/web/standalone/.next/server/chunks/5047.js +2 -0
  89. package/dist/web/standalone/.next/server/chunks/5124.js +1 -0
  90. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  91. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  93. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  94. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  95. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  96. package/package.json +6 -4
  97. package/packages/cloud-mcp-gateway/package.json +2 -2
  98. package/packages/contracts/package.json +1 -1
  99. package/packages/daemon/package.json +4 -4
  100. package/packages/gsd-agent-core/package.json +5 -5
  101. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  102. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js +21 -23
  103. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js.map +1 -1
  104. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  106. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +18 -11
  108. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  110. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +16 -0
  111. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  112. package/packages/gsd-agent-modes/package.json +7 -7
  113. package/packages/mcp-server/package.json +3 -3
  114. package/packages/native/package.json +1 -1
  115. package/packages/pi-agent-core/package.json +1 -1
  116. package/packages/pi-ai/dist/models.generated.d.ts +0 -34
  117. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  118. package/packages/pi-ai/dist/models.generated.js +0 -34
  119. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  120. package/packages/pi-ai/package.json +1 -1
  121. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/auth-storage.js +11 -3
  123. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  125. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  126. package/packages/pi-coding-agent/package.json +7 -7
  127. package/packages/pi-tui/package.json +2 -2
  128. package/packages/rpc-client/package.json +2 -2
  129. package/pkg/package.json +1 -1
  130. package/src/resources/extensions/browser-tools/index.ts +39 -22
  131. package/src/resources/extensions/browser-tools/state.ts +13 -0
  132. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +57 -0
  133. package/src/resources/extensions/browser-tools/tools/session.ts +4 -2
  134. package/src/resources/extensions/browser-tools/utils.ts +3 -3
  135. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -8
  136. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -2
  137. package/src/resources/extensions/gsd/auto/contracts.ts +8 -119
  138. package/src/resources/extensions/gsd/auto/orchestrator.ts +794 -58
  139. package/src/resources/extensions/gsd/auto-prompts.ts +21 -1
  140. package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -0
  141. package/src/resources/extensions/gsd/auto-tool-tracking.ts +5 -0
  142. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +33 -0
  143. package/src/resources/extensions/gsd/auto.ts +81 -500
  144. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -0
  145. package/src/resources/extensions/gsd/debug-logger.ts +11 -0
  146. package/src/resources/extensions/gsd/doctor-proactive.ts +8 -2
  147. package/src/resources/extensions/gsd/markdown-renderer.ts +38 -19
  148. package/src/resources/extensions/gsd/mcp-filter.ts +7 -0
  149. package/src/resources/extensions/gsd/native-git-bridge.ts +9 -0
  150. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  151. package/src/resources/extensions/gsd/schemas/parsers.ts +6 -1
  152. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +31 -10
  153. package/src/resources/extensions/gsd/tests/artifact-db-drift-memo.test.ts +66 -0
  154. package/src/resources/extensions/gsd/tests/auto-dispatch-baseline-harness.test.ts +53 -0
  155. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +590 -855
  156. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +38 -10
  157. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +15 -0
  158. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +64 -1
  159. package/src/resources/extensions/gsd/tests/markdown-renderer-parse-cache.test.ts +75 -0
  160. package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +127 -0
  161. package/src/resources/extensions/gsd/tests/parse-project-milestone-bridge.test.ts +77 -0
  162. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +4 -2
  163. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +19 -5
  164. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +24 -0
  165. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -3
  166. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +183 -21
  167. package/dist/web/standalone/.next/server/chunks/678.js +0 -2
  168. /package/dist/web/standalone/.next/static/{vAecbJ3K9eO213bAxU8Mi → h38jfi0dnRY0y3hbyBszg}/_buildManifest.js +0 -0
  169. /package/dist/web/standalone/.next/static/{vAecbJ3K9eO213bAxU8Mi → h38jfi0dnRY0y3hbyBszg}/_ssgManifest.js +0 -0
@@ -267,12 +267,18 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
267
267
  // Non-fatal — dispatch continues without STATE.md if rebuild fails
268
268
  }
269
269
 
270
+ // #442 Phase 1.7: resolve repo-ness once per gate. ensureWorkspaceGitReady
271
+ // has already run, and nothing below initializes/deinitializes the repo, so
272
+ // the two downstream blocks (integration-branch check, stale-changes
273
+ // snapshot) can share one nativeIsRepo result instead of querying twice.
274
+ const isRepo = nativeIsRepo(basePath);
275
+
270
276
  // ── Integration branch existence check ──
271
277
  // If the active milestone's recorded integration branch no longer exists in
272
278
  // git, the merge-back at the end of the milestone will fail. Block dispatch
273
279
  // now to surface this before work is lost.
274
280
  try {
275
- if (nativeIsRepo(basePath)) {
281
+ if (isRepo) {
276
282
  const state = await deriveState(basePath);
277
283
  if (state.activeMilestone) {
278
284
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
@@ -296,7 +302,7 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
296
302
  // If the working tree is dirty and no commit has happened recently,
297
303
  // create a safety snapshot so work isn't lost if the next unit crashes.
298
304
  try {
299
- if (nativeIsRepo(basePath)) {
305
+ if (isRepo) {
300
306
  const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
301
307
  // `git.snapshots: false` is the canonical toggle that disables WIP
302
308
  // snapshot commits — honour it before touching the threshold path (#4420).
@@ -9,11 +9,10 @@
9
9
  // Critical invariant: rendered markdown must round-trip through
10
10
  // parseRoadmap(), parsePlan(), parseSummary() in files.ts.
11
11
 
12
- import { readFileSync, existsSync, mkdirSync } from "node:fs";
12
+ import { readFileSync, existsSync, mkdirSync, statSync } from "node:fs";
13
13
  import { logWarning } from "./workflow-logger.js";
14
14
  import { isClosedStatus } from "./status-guards.js";
15
15
  import { dirname, join, relative } from "node:path";
16
- import { createRequire } from "node:module";
17
16
  import {
18
17
  getAllMilestones,
19
18
  getMilestone,
@@ -38,7 +37,8 @@ import {
38
37
  buildTaskFileName,
39
38
  buildSliceFileName,
40
39
  } from "./paths.js";
41
- import { saveFile, clearParseCache } from "./files.js";
40
+ import { saveFile, clearParseCache, registerCacheClearCallback } from "./files.js";
41
+ import { parseRoadmap, parsePlan } from "./parsers-legacy.js";
42
42
  import { invalidateStateCache } from "./state.js";
43
43
  import { clearPathCache } from "./paths.js";
44
44
  import type { RiskLevel } from "./types.js";
@@ -738,20 +738,41 @@ export interface StaleEntry {
738
738
  * Returns a list of stale entries with file path and reason.
739
739
  * Logs to stderr when stale files are detected.
740
740
  */
741
- export function detectStaleRenders(basePath: string): StaleEntry[] {
742
- // Lazy-load parsers intentional disk-vs-DB comparison requires parsers
743
- const _require = createRequire(import.meta.url);
744
- let parseRoadmap: Function, parsePlan: Function;
745
- try {
746
- // Prefer compiled JS for packaged/runtime installs; TS exists only in source/dev contexts.
747
- const m = _require("./parsers-legacy.js");
748
- parseRoadmap = m.parseRoadmap; parsePlan = m.parsePlan;
749
- } catch (e) {
750
- logWarning("renderer", `parsers-legacy.js require failed, falling back to .ts: ${(e as Error).message}`);
751
- const m = _require("./parsers-legacy.ts");
752
- parseRoadmap = m.parseRoadmap; parsePlan = m.parsePlan;
741
+ // #442 Phase 1.5: cache parsed ROADMAP/PLAN projections by file identity
742
+ // (path + mtimeMs + size) so an unchanged projection skips readFileSync AND
743
+ // the parse entirely on repeated dispatches. The DB-vs-disk comparison below
744
+ // still runs every call against fresh DB rows — only the disk-parse half is
745
+ // memoized, and parsed output depends solely on file bytes, so this is
746
+ // behavior-preserving. Invalidation rides the existing clearParseCache()
747
+ // callback chain (fired by invalidateCaches() after every projection write and
748
+ // by reconcileBeforeDispatch repairs), so a changed file always re-parses.
749
+ interface CachedProjection { mtimeMs: number; size: number; parsed: unknown }
750
+ const _projectionParseCache = new Map<string, CachedProjection>();
751
+ registerCacheClearCallback(() => _projectionParseCache.clear());
752
+
753
+ function parseProjectionByIdentity(path: string, parse: (content: string) => unknown): unknown {
754
+ let st: ReturnType<typeof statSync> | null = null;
755
+ try { st = statSync(path); } catch { st = null; }
756
+ if (st) {
757
+ const hit = _projectionParseCache.get(path);
758
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
759
+ return hit.parsed;
760
+ }
761
+ const parsed = parse(readFileSync(path, "utf-8"));
762
+ _projectionParseCache.set(path, { mtimeMs: st.mtimeMs, size: st.size, parsed });
763
+ return parsed;
753
764
  }
765
+ // stat failed (e.g. file vanished between existsSync and here) — fall back to
766
+ // the original plain read+parse so error handling is unchanged.
767
+ return parse(readFileSync(path, "utf-8"));
768
+ }
754
769
 
770
+ export function detectStaleRenders(basePath: string): StaleEntry[] {
771
+ // parseRoadmap/parsePlan are statically imported (#442 Phase 1.4): the
772
+ // per-call createRequire("./parsers-legacy") that used to live here ran on
773
+ // every dispatch. The static `./parsers-legacy.js` specifier resolves in
774
+ // both packaged (.js) and source (.ts via the strip-types loader) contexts —
775
+ // the same form a dozen other modules already use.
755
776
  const stale: StaleEntry[] = [];
756
777
  const milestones = getAllMilestones();
757
778
 
@@ -762,8 +783,7 @@ export function detectStaleRenders(basePath: string): StaleEntry[] {
762
783
  const roadmapPath = resolveRoadmapProjectionPath(basePath, milestone.id);
763
784
  if (existsSync(roadmapPath)) {
764
785
  try {
765
- const content = readFileSync(roadmapPath, "utf-8");
766
- const parsed = parseRoadmap(content);
786
+ const parsed = parseProjectionByIdentity(roadmapPath, parseRoadmap) as ReturnType<typeof parseRoadmap>;
767
787
 
768
788
  for (const slice of slices) {
769
789
  const isCompleteInDb = isClosedStatus(slice.status);
@@ -795,8 +815,7 @@ export function detectStaleRenders(basePath: string): StaleEntry[] {
795
815
  const planPath = resolveSliceFile(basePath, milestone.id, slice.id, "PLAN");
796
816
  if (planPath && existsSync(planPath)) {
797
817
  try {
798
- const content = readFileSync(planPath, "utf-8");
799
- const parsed = parsePlan(content);
818
+ const parsed = parseProjectionByIdentity(planPath, parsePlan) as ReturnType<typeof parsePlan>;
800
819
 
801
820
  for (const task of tasks) {
802
821
  const isDoneInDb = isClosedStatus(task.status);
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { resolve } from "node:path";
3
4
 
4
5
  import type { ClaudeCodeMcpConfig } from "./preferences-types.js";
@@ -115,6 +116,12 @@ export function discoverMcpServerNames(projectDir: string): string[] {
115
116
  return discoverMcpServers(projectDir).map((server) => server.name);
116
117
  }
117
118
 
119
+ export function discoverUserMcpServerNames(): string[] {
120
+ const userSettingsPath = resolve(homedir(), ".claude", "settings.json");
121
+ const userSettings = readJsonFile(userSettingsPath, true) as ClaudeSettingsFile | undefined;
122
+ return collectServerEntries(userSettings?.mcpServers).map((s) => s.name);
123
+ }
124
+
118
125
  export function computeMcpDisallowedTools(
119
126
  modelId: string,
120
127
  mcpConfig: ClaudeCodeMcpConfig | undefined,
@@ -13,6 +13,7 @@ import { GSDError, GSD_GIT_ERROR } from "./errors.js";
13
13
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
14
14
  import { getErrorMessage } from "./error-utils.js";
15
15
  import { isInfrastructureError } from "./auto/infra-errors.js";
16
+ import { debugCount } from "./debug-logger.js";
16
17
 
17
18
  // Issue #453: keep auto-mode bookkeeping on the stable git CLI path unless a
18
19
  // caller explicitly opts into the native helper.
@@ -140,6 +141,8 @@ function loadNative(): typeof nativeModule {
140
141
 
141
142
  /** Run a git command via execFileSync. Returns trimmed stdout. */
142
143
  function gitExec(basePath: string, args: string[], allowFailure = false): string {
144
+ // Counts git CLI shell-outs only (native libgit2 paths bypass this helper).
145
+ debugCount("gitInvocations");
143
146
  try {
144
147
  return execFileSync("git", args, {
145
148
  cwd: basePath,
@@ -169,6 +172,8 @@ function execGitFileSyncWithRetry(
169
172
  args: string[],
170
173
  options: Partial<ExecFileSyncOptionsWithStringEncoding>,
171
174
  ): string {
175
+ // Counts git CLI shell-outs only (native libgit2 paths bypass this helper).
176
+ debugCount("gitInvocations");
172
177
  try {
173
178
  return execFileSync("git", args, {
174
179
  cwd: basePath,
@@ -180,6 +185,8 @@ function execGitFileSyncWithRetry(
180
185
  } catch (err) {
181
186
  if (!isRetryableGitError(err)) throw err;
182
187
  sleepSync(GIT_RETRY_DELAY_MS);
188
+ // Retry is a second physical shell-out — count it too.
189
+ debugCount("gitInvocations");
183
190
  return execFileSync("git", args, {
184
191
  cwd: basePath,
185
192
  stdio: ["ignore", "pipe", "pipe"],
@@ -192,6 +199,8 @@ function execGitFileSyncWithRetry(
192
199
 
193
200
  /** Run a git command via execFileSync. Returns trimmed stdout. */
194
201
  function gitFileExec(basePath: string, args: string[], allowFailure = false): string {
202
+ // Counts git CLI shell-outs only (native libgit2 paths bypass this helper).
203
+ debugCount("gitInvocations");
195
204
  try {
196
205
  return execFileSync("git", args, {
197
206
  cwd: basePath,
@@ -38,7 +38,7 @@ If slice research is inlined, trust its architectural findings, but verify every
38
38
 
39
39
  1. If requirements are preloaded, identify owned and supporting Active requirements.
40
40
  2. Call `memory_query` with keywords from the slice title and source files.
41
- 3. Read `{{planTemplatePath}}` and `{{taskPlanTemplatePath}}`.
41
+ 3. Use the inlined Output Template sections already present in this prompt. Do not read template files from disk.
42
42
  4. {{skillActivation}} Record expected executor skills in each task plan's `skills_used` frontmatter.
43
43
  5. Define slice verification before tasks. Non-trivial slices need real tests or executable assertions; boundary contracts need contract-exercising checks. Tests must not read .gitignore/gitignored paths such as `.gsd/`, `.planning/`, or `.audits/`.
44
44
  6. Include Threat Surface (Q3), Requirement Impact (Q4), proof level, observability, integration closure, Failure Modes (Q5), Load Profile (Q6), and Negative Tests (Q7) only where applicable.
@@ -67,7 +67,12 @@ export interface ParsedRoadmap {
67
67
  const TEMPLATE_TOKEN_RE = /\{\{[^}]+\}\}/;
68
68
  const H2_RE = /^##\s+(.+)$/gm;
69
69
  const H3_RE = /^###\s+(.+)$/gm;
70
- const MILESTONE_LINE_RE = /^-\s+\[([ x])\]\s+(M\d{3}):\s+(.+?)\s+(?:—|--|-)\s+(.+)$/gm;
70
+ // A milestone line is single-line by construction. Every inter-token gap uses
71
+ // horizontal-whitespace classes (`[^\S\n]`) rather than `\s`, because `\s`
72
+ // matches newlines: a line missing a valid separator would otherwise let the
73
+ // `\s+(?:—|--|-)\s+` clause "bridge" onto the NEXT bullet's `- `, consuming it
74
+ // as the separator and silently swallowing the following well-formed milestone.
75
+ const MILESTONE_LINE_RE = /^-[^\S\n]+\[([ x])\][^\S\n]+(M\d{3}):[^\S\n]+(.+?)[^\S\n]+(?:—|--|-)[^\S\n]+(.+)$/gm;
71
76
  const SLICE_HEADER_RE = /^###\s+(S\d{2})\s*(?:—|--|-)\s+(.+)$/m;
72
77
  const REQUIREMENT_HEADER_RE = /^###\s+(R\d{3})\s*(?:—|--|-)\s+(.+)$/m;
73
78
 
@@ -335,21 +335,42 @@ function detectDiskSliceIdDivergenceForMilestone(
335
335
  return drifts;
336
336
  }
337
337
 
338
+ type ArtifactDbDrift =
339
+ | DiskSliceIdDivergenceDrift
340
+ | ArtifactDbStatusDivergenceDrift
341
+ | CompletedMilestoneReopenedDrift;
342
+
343
+ // #442 Phase 1.6: the three artifact/DB drift handlers (disk-slice-id,
344
+ // artifact-db-status, completed-milestone-reopened) each call
345
+ // detectArtifactDbDrift and then filter for their own kind — so the full
346
+ // milestone→slice→task walk + artifact SQL + disk scan would run THREE times
347
+ // per detection pass and discard 2/3 of the work. Memoize the result per
348
+ // DriftContext so the three handlers share one computation. The key is the
349
+ // ctx object, which detectAllDrift rebuilds for every pass (and which is
350
+ // unreferenced once the pass ends, so the WeakMap entry is GC'd) — DB/disk
351
+ // state is immutable within a single pass (repairs run only after detection),
352
+ // so this is behavior-preserving. A fresh ctx (e.g. the maintenance command's
353
+ // inline { basePath, state }) always recomputes.
354
+ const _artifactDbDriftMemo = new WeakMap<DriftContext, ArtifactDbDrift[]>();
355
+
338
356
  export function detectArtifactDbDrift(
357
+ state: GSDState,
358
+ ctx: DriftContext,
359
+ ): ArtifactDbDrift[] {
360
+ const cached = _artifactDbDriftMemo.get(ctx);
361
+ if (cached) return cached;
362
+ const computed = computeArtifactDbDrift(state, ctx);
363
+ _artifactDbDriftMemo.set(ctx, computed);
364
+ return computed;
365
+ }
366
+
367
+ function computeArtifactDbDrift(
339
368
  _state: GSDState,
340
369
  ctx: DriftContext,
341
- ): Array<
342
- | DiskSliceIdDivergenceDrift
343
- | ArtifactDbStatusDivergenceDrift
344
- | CompletedMilestoneReopenedDrift
345
- > {
370
+ ): ArtifactDbDrift[] {
346
371
  if (!isDbAvailable()) return [];
347
372
 
348
- const drifts: Array<
349
- | DiskSliceIdDivergenceDrift
350
- | ArtifactDbStatusDivergenceDrift
351
- | CompletedMilestoneReopenedDrift
352
- > = [];
373
+ const drifts: ArtifactDbDrift[] = [];
353
374
 
354
375
  for (const milestone of getAllMilestones()) {
355
376
  if (isClosedStatus(milestone.status)) continue;
@@ -0,0 +1,66 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: #442 Phase 1.6 — detectArtifactDbDrift is memoized per
3
+ // DriftContext so the three artifact/DB drift handlers share one
4
+ // milestone→slice→task walk per detection pass instead of recomputing it
5
+ // three times. Asserts external behavior: same ctx returns the same result
6
+ // (shared within a pass); a fresh ctx recomputes identical content (no
7
+ // cross-pass leakage).
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice } from "../gsd-db.ts";
16
+ import { detectArtifactDbDrift } from "../state-reconciliation/drift/artifact-db.ts";
17
+ import type { DriftContext } from "../state-reconciliation/types.ts";
18
+ import type { GSDState } from "../types.ts";
19
+
20
+ function stubState(): GSDState {
21
+ return {
22
+ activeMilestone: { id: "M001", title: "M" },
23
+ activeSlice: null,
24
+ activeTask: null,
25
+ phase: "executing",
26
+ recentDecisions: [],
27
+ blockers: [],
28
+ nextAction: "",
29
+ registry: [],
30
+ requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
31
+ progress: { milestones: { done: 0, total: 1 } },
32
+ };
33
+ }
34
+
35
+ test("#442: detectArtifactDbDrift is memoized per DriftContext", (t) => {
36
+ const base = mkdtempSync(join(tmpdir(), "gsd-artifact-memo-"));
37
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
38
+ mkdirSync(sliceDir, { recursive: true });
39
+ t.after(() => {
40
+ try { closeDatabase(); } catch { /* noop */ }
41
+ rmSync(base, { recursive: true, force: true });
42
+ });
43
+
44
+ openDatabase(join(base, ".gsd", "gsd.db"));
45
+ insertMilestone({ id: "M001", title: "M", status: "active" });
46
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
47
+ // A SUMMARY on disk while the slice is still pending = artifact/DB divergence.
48
+ writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# S01 Summary\n");
49
+
50
+ const state = stubState();
51
+
52
+ const ctx1: DriftContext = { basePath: base, state };
53
+ const first = detectArtifactDbDrift(state, ctx1);
54
+ const firstAgain = detectArtifactDbDrift(state, ctx1);
55
+
56
+ // Same ctx → cache hit → identical array reference (the 3 handlers in one
57
+ // pass share this exact result).
58
+ assert.strictEqual(firstAgain, first, "same ctx must return the memoized result");
59
+ assert.ok(first.length > 0, "fixture should produce at least one drift record");
60
+
61
+ // Fresh ctx → recomputed (distinct instance) but identical content.
62
+ const ctx2: DriftContext = { basePath: base, state };
63
+ const second = detectArtifactDbDrift(state, ctx2);
64
+ assert.notStrictEqual(second, first, "a fresh ctx must recompute, not reuse the prior pass");
65
+ assert.deepEqual(second, first, "recomputed result must be identical content");
66
+ });
@@ -0,0 +1,53 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Cover the getDebugCounters() snapshot getter added for the
3
+ // per-dispatch benchmark harness (issue #442, Phase 0.3). The harness itself
4
+ // (scripts/auto-dispatch-baseline.mjs) reads counters via this getter without
5
+ // disabling debug (which is what writeDebugSummary does).
6
+
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert';
9
+ import { mkdtempSync, mkdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { tmpdir } from 'node:os';
12
+
13
+ import {
14
+ enableDebug,
15
+ disableDebug,
16
+ debugCount,
17
+ getDebugCounters,
18
+ } from '../debug-logger.ts';
19
+
20
+ function tmpGsd(): string {
21
+ const tmp = mkdtempSync(join(tmpdir(), 'gsd-baseline-harness-'));
22
+ mkdirSync(join(tmp, '.gsd'), { recursive: true });
23
+ return tmp;
24
+ }
25
+
26
+ test('getDebugCounters returns a live snapshot of the hot-path counters', () => {
27
+ enableDebug(tmpGsd());
28
+
29
+ debugCount('deriveStateCalls', 3);
30
+ debugCount('parseRoadmapCalls');
31
+ debugCount('parsePlanCalls', 2);
32
+ debugCount('gitInvocations', 7);
33
+
34
+ const snap = getDebugCounters();
35
+ assert.strictEqual(snap.deriveStateCalls, 3);
36
+ assert.strictEqual(snap.parseRoadmapCalls, 1);
37
+ assert.strictEqual(snap.parsePlanCalls, 2);
38
+ assert.strictEqual(snap.gitInvocations, 7);
39
+
40
+ disableDebug();
41
+ });
42
+
43
+ test('getDebugCounters returns a copy — callers cannot mutate internal state', () => {
44
+ enableDebug(tmpGsd());
45
+ debugCount('gitInvocations', 5);
46
+
47
+ const snap = getDebugCounters() as Record<string, number>;
48
+ snap.gitInvocations = 999;
49
+
50
+ assert.strictEqual(getDebugCounters().gitInvocations, 5, 'internal counter must be unaffected by mutating the snapshot');
51
+
52
+ disableDebug();
53
+ });