@opengsd/gsd-pi 1.3.0-dev.65546769 → 1.3.0-dev.eed73bea

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 (183) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +11 -2
  3. package/dist/resources/extensions/google-cli/stream-adapter.js +82 -15
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +12 -3
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +17 -14
  6. package/dist/resources/extensions/gsd/auto-prompts.js +43 -12
  7. package/dist/resources/extensions/gsd/auto-recovery.js +13 -6
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +103 -13
  9. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +6 -1
  10. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -3
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -19
  13. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +75 -1
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  15. package/dist/resources/extensions/gsd/commands-context.js +19 -1
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +16 -10
  17. package/dist/resources/extensions/gsd/commands-worktree.js +12 -10
  18. package/dist/resources/extensions/gsd/dashboard-overlay.js +32 -3
  19. package/dist/resources/extensions/gsd/db/queries.js +60 -0
  20. package/dist/resources/extensions/gsd/doctor-providers.js +92 -8
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +45 -9
  22. package/dist/resources/extensions/gsd/forensics.js +2 -32
  23. package/dist/resources/extensions/gsd/git-service.js +4 -4
  24. package/dist/resources/extensions/gsd/guided-flow-queue.js +59 -5
  25. package/dist/resources/extensions/gsd/health-widget.js +55 -29
  26. package/dist/resources/extensions/gsd/markdown-renderer.js +6 -2
  27. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +44 -21
  28. package/dist/resources/extensions/gsd/milestone-implementation-evidence.js +26 -20
  29. package/dist/resources/extensions/gsd/quick.js +45 -2
  30. package/dist/resources/extensions/gsd/session-forensics.js +11 -1
  31. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +52 -3
  32. package/dist/resources/extensions/gsd/tools/complete-slice.js +34 -3
  33. package/dist/resources/extensions/gsd/tools/complete-task.js +78 -16
  34. package/dist/resources/extensions/gsd/tools/exec-tool.js +7 -2
  35. package/dist/resources/extensions/gsd/unit-context-composer.js +23 -7
  36. package/dist/resources/extensions/gsd/unit-registry.js +25 -3
  37. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +33 -3
  38. package/dist/resources/extensions/gsd/validation-block-guard.js +9 -4
  39. package/dist/resources/extensions/gsd/workspace-git-preflight.js +30 -1
  40. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  41. package/dist/web/standalone/.next/BUILD_ID +1 -1
  42. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  43. package/dist/web/standalone/.next/build-manifest.json +3 -3
  44. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  45. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/web/standalone/.next/static/chunks/{796.e0bdc932325d7e03.js → 796.3976108148518f7d.js} +3 -3
  77. package/dist/web/standalone/.next/static/chunks/{webpack-f46ea08200a0227e.js → webpack-7c1d97e39be2da11.js} +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/dist/workflow.d.ts +1 -0
  81. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  82. package/packages/contracts/dist/workflow.js +2 -0
  83. package/packages/contracts/dist/workflow.js.map +1 -1
  84. package/packages/contracts/package.json +1 -1
  85. package/packages/daemon/package.json +4 -4
  86. package/packages/gsd-agent-core/package.json +5 -5
  87. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  88. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +21 -9
  89. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  90. package/packages/gsd-agent-modes/package.json +7 -7
  91. package/packages/mcp-server/README.md +1 -1
  92. package/packages/mcp-server/dist/server.d.ts +1 -1
  93. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/server.js +3 -3
  95. package/packages/mcp-server/dist/server.js.map +1 -1
  96. package/packages/mcp-server/dist/workflow-tools.d.ts +13 -1
  97. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/workflow-tools.js +34 -20
  99. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  100. package/packages/mcp-server/package.json +4 -4
  101. package/packages/native/package.json +1 -1
  102. package/packages/pi-agent-core/package.json +1 -1
  103. package/packages/pi-ai/package.json +1 -1
  104. package/packages/pi-coding-agent/package.json +7 -7
  105. package/packages/pi-tui/package.json +2 -2
  106. package/packages/rpc-client/package.json +2 -2
  107. package/pkg/package.json +1 -1
  108. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +20 -2
  109. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +80 -0
  110. package/src/resources/extensions/google-cli/stream-adapter.ts +106 -19
  111. package/src/resources/extensions/gsd/auto/orchestrator.ts +25 -11
  112. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -17
  113. package/src/resources/extensions/gsd/auto-prompts.ts +54 -12
  114. package/src/resources/extensions/gsd/auto-recovery.ts +13 -6
  115. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +125 -12
  116. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +6 -1
  117. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +9 -3
  119. package/src/resources/extensions/gsd/bootstrap/system-context.ts +52 -18
  120. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +82 -1
  121. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  122. package/src/resources/extensions/gsd/commands-context.ts +18 -1
  123. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -9
  124. package/src/resources/extensions/gsd/commands-worktree.ts +12 -10
  125. package/src/resources/extensions/gsd/dashboard-overlay.ts +32 -3
  126. package/src/resources/extensions/gsd/db/queries.ts +79 -0
  127. package/src/resources/extensions/gsd/doctor-providers.ts +103 -9
  128. package/src/resources/extensions/gsd/exec-sandbox.ts +49 -9
  129. package/src/resources/extensions/gsd/forensics.ts +2 -33
  130. package/src/resources/extensions/gsd/git-service.ts +5 -5
  131. package/src/resources/extensions/gsd/guided-flow-queue.ts +82 -4
  132. package/src/resources/extensions/gsd/health-widget.ts +69 -32
  133. package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
  134. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +51 -19
  135. package/src/resources/extensions/gsd/milestone-implementation-evidence.ts +35 -21
  136. package/src/resources/extensions/gsd/quick.ts +43 -2
  137. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  138. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +76 -8
  139. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -1
  140. package/src/resources/extensions/gsd/tests/commands-context.test.ts +26 -0
  141. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +80 -0
  142. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +11 -0
  143. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +48 -8
  144. package/src/resources/extensions/gsd/tests/complete-task.test.ts +75 -0
  145. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +55 -2
  146. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +26 -1
  147. package/src/resources/extensions/gsd/tests/doctor-forensics-db-open-regression.test.ts +70 -2
  148. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +107 -0
  149. package/src/resources/extensions/gsd/tests/exec-graceful-kill.test.ts +38 -0
  150. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +45 -1
  151. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +88 -0
  152. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +42 -0
  153. package/src/resources/extensions/gsd/tests/health-widget.test.ts +268 -3
  154. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +119 -1
  155. package/src/resources/extensions/gsd/tests/integration/queue-active-milestone-context-budget.test.ts +93 -0
  156. package/src/resources/extensions/gsd/tests/integration/quick-branch-lifecycle.test.ts +56 -9
  157. package/src/resources/extensions/gsd/tests/knowledge-cold-start.test.ts +14 -0
  158. package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +78 -0
  159. package/src/resources/extensions/gsd/tests/orchestrator-logs.test.ts +43 -1
  160. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +26 -0
  161. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +1 -1
  162. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +54 -1
  163. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +195 -1
  164. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +185 -0
  165. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +87 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -0
  167. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +68 -0
  168. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +26 -0
  169. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +193 -14
  170. package/src/resources/extensions/gsd/tests/unmerged-milestone-guard.test.ts +25 -0
  171. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +79 -0
  172. package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +66 -0
  173. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +151 -2
  174. package/src/resources/extensions/gsd/tools/complete-slice.ts +30 -3
  175. package/src/resources/extensions/gsd/tools/complete-task.ts +86 -16
  176. package/src/resources/extensions/gsd/tools/exec-tool.ts +7 -3
  177. package/src/resources/extensions/gsd/unit-context-composer.ts +33 -7
  178. package/src/resources/extensions/gsd/unit-registry.ts +25 -3
  179. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +41 -5
  180. package/src/resources/extensions/gsd/validation-block-guard.ts +13 -7
  181. package/src/resources/extensions/gsd/workspace-git-preflight.ts +31 -0
  182. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{BTKtGFF1Y-hvVJEGhBRo9 → SzEuqWX37DR9MEpEuQjP1}/_ssgManifest.js +0 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Behavioural regression test for the milestone-closeout UAT gate —
3
+ * `readUatGateVerdict`.
4
+ *
5
+ * The gate (ADR-017: DB-authoritative UAT sign-off) reads a slice's UAT
6
+ * verdict from its ASSESSMENT artifact via the *canonical* expected path
7
+ * (`resolveSliceFile` + a path-keyed `getAssessment`). When a milestone
8
+ * artifact-layout migration orphans the ASSESSMENT markdown from that canonical
9
+ * path (e.g. `phases/…` → `milestones/…`), the gate used to return `null` and
10
+ * block milestone closure with "missing UAT PASS verdict" — even though the
11
+ * verdict was correctly recorded in the `assessments` table by
12
+ * `gsd_uat_result_save`.
13
+ *
14
+ * The DB fallback added to `readUatGateVerdict` consults the authoritative
15
+ * `assessments` table by (milestoneId, sliceId, scope='run-uat') identity,
16
+ * independent of path. These tests pin that behaviour.
17
+ */
18
+
19
+ import { describe, test, beforeEach, afterEach } from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+ import * as os from 'node:os';
24
+
25
+ import {
26
+ openDatabase,
27
+ closeDatabase,
28
+ insertMilestone,
29
+ insertSlice,
30
+ insertAssessment,
31
+ } from '../gsd-db.ts';
32
+ import { readUatGateVerdict } from '../auto-dispatch.ts';
33
+
34
+ function tempDbPath(): string {
35
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-uat-gate-'));
36
+ return path.join(dir, 'test.db');
37
+ }
38
+
39
+ function cleanupDb(dbPath: string): void {
40
+ closeDatabase();
41
+ try { fs.rmSync(path.dirname(dbPath), { recursive: true, force: true }); } catch { /* */ }
42
+ }
43
+
44
+ const MID = 'M001';
45
+ const SLICE = 'S01';
46
+
47
+ /** Canonical on-disk ASSESSMENT path produced by `resolveSliceFile`. */
48
+ function canonicalAssessmentPath(basePath: string): string {
49
+ return path.join(basePath, '.gsd', 'milestones', MID, 'slices', SLICE, `${SLICE}-ASSESSMENT.md`);
50
+ }
51
+
52
+ /** An ASSESSMENT body that declares a runtime-executable UAT type and a PASS verdict. */
53
+ const RUNTIME_PASS_BODY = [
54
+ '---',
55
+ 'verdict: pass',
56
+ '---',
57
+ '',
58
+ '# S01 UAT Assessment',
59
+ '',
60
+ '## UAT Type',
61
+ '- UAT mode: runtime-executable',
62
+ '',
63
+ '## Result',
64
+ 'All checks passed.',
65
+ ].join('\n');
66
+
67
+ describe('readUatGateVerdict — DB fallback for orphaned ASSESSMENT', () => {
68
+ let dbPath: string;
69
+ let basePath: string;
70
+
71
+ beforeEach(() => {
72
+ dbPath = tempDbPath();
73
+ openDatabase(dbPath);
74
+ basePath = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-uat-gate-proj-'));
75
+ insertMilestone({ id: MID });
76
+ insertSlice({ id: SLICE, milestoneId: MID });
77
+ });
78
+
79
+ afterEach(() => {
80
+ cleanupDb(dbPath);
81
+ try { fs.rmSync(basePath, { recursive: true, force: true }); } catch { /* */ }
82
+ });
83
+
84
+ test('returns pass when the ASSESSMENT is keyed by a legacy/orphaned path (the bug)', async () => {
85
+ // Reproduces milestone 15: `gsd_uat_result_save` wrote S01's assessment row
86
+ // under a now-migrated path; the canonical file never existed on disk and
87
+ // the `assessments.path` is not what `resolveSliceFile` computes.
88
+ insertAssessment({
89
+ // Deliberately non-canonical — a legacy `phases/…` path.
90
+ path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
91
+ milestoneId: MID,
92
+ sliceId: SLICE,
93
+ status: 'pass',
94
+ scope: 'run-uat',
95
+ fullContent: RUNTIME_PASS_BODY,
96
+ });
97
+
98
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
99
+
100
+ assert.ok(result, 'expected the DB fallback to resolve a verdict, got null');
101
+ assert.equal(result!.verdict, 'pass');
102
+ });
103
+
104
+ test('the DB fallback derives uatType from the assessment body when no file exists', async () => {
105
+ insertAssessment({
106
+ path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
107
+ milestoneId: MID,
108
+ sliceId: SLICE,
109
+ status: 'pass',
110
+ scope: 'run-uat',
111
+ fullContent: RUNTIME_PASS_BODY,
112
+ });
113
+
114
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
115
+
116
+ assert.ok(result);
117
+ assert.equal(result!.verdict, 'pass');
118
+ assert.equal(result!.uatType, 'runtime-executable');
119
+ });
120
+
121
+ test('canonical ASSESSMENT file on disk still resolves (regression guard)', async () => {
122
+ // When the file is present at the canonical path, the existing path-keyed
123
+ // lookup must resolve it without needing the fallback.
124
+ const file = canonicalAssessmentPath(basePath);
125
+ fs.mkdirSync(path.dirname(file), { recursive: true });
126
+ fs.writeFileSync(file, RUNTIME_PASS_BODY);
127
+ // Also seed the path-keyed assessments row, mirroring a normal save.
128
+ insertAssessment({
129
+ path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
130
+ milestoneId: MID,
131
+ sliceId: SLICE,
132
+ status: 'pass',
133
+ scope: 'run-uat',
134
+ fullContent: RUNTIME_PASS_BODY,
135
+ });
136
+
137
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
138
+
139
+ assert.ok(result);
140
+ assert.equal(result!.verdict, 'pass');
141
+ assert.equal(result!.uatType, 'runtime-executable');
142
+ });
143
+
144
+ test('a roadmap-scoped assessment does NOT satisfy the UAT gate', async () => {
145
+ // `reassess-roadmap` writes roadmap-scoped assessments to the same
146
+ // S##-ASSESSMENT path; those must never be treated as a UAT verdict. The
147
+ // legacy-path fallback queries scope='run-uat', so a roadmap-only row is
148
+ // invisible and the gate returns null.
149
+ insertAssessment({
150
+ path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
151
+ milestoneId: MID,
152
+ sliceId: SLICE,
153
+ status: 'pass',
154
+ scope: 'roadmap',
155
+ fullContent: RUNTIME_PASS_BODY,
156
+ });
157
+
158
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
159
+
160
+ assert.equal(result, null, 'roadmap-scoped assessments must not satisfy the UAT gate');
161
+ });
162
+
163
+ test('returns null when no assessment and no file exist (fallback does not hallucinate)', async () => {
164
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
165
+ assert.equal(result, null);
166
+ });
167
+
168
+ test('surfaces a recorded non-pass verdict via the DB fallback', async () => {
169
+ // A failing verdict stored under a legacy path must surface (not be masked
170
+ // as "missing") so the gate's non-PASS branch can act on it.
171
+ insertAssessment({
172
+ path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
173
+ milestoneId: MID,
174
+ sliceId: SLICE,
175
+ status: 'fail',
176
+ scope: 'run-uat',
177
+ fullContent: RUNTIME_PASS_BODY.replace('verdict: pass', 'verdict: fail'),
178
+ });
179
+
180
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
181
+
182
+ assert.ok(result);
183
+ assert.equal(result!.verdict, 'fail');
184
+ });
185
+ });
@@ -970,6 +970,93 @@ test("register-hooks agent_end does not re-arm deferred gate after workflow MCP
970
970
  });
971
971
  });
972
972
 
973
+ test("register-hooks message_update uses in-memory write-gate snapshot instead of disk reconcile", async (t) => {
974
+ const dir = makeTempDir("message-update-memory-snapshot");
975
+ const originalCwd = process.cwd();
976
+ const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
977
+ process.chdir(dir);
978
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
979
+ resetWriteGateState(dir);
980
+ clearPendingAutoStart(dir);
981
+
982
+ const gateId = "depth_verification_M012_confirm";
983
+ const statePath = join(dir, ".gsd", "runtime", "write-gate-state.json");
984
+
985
+ t.after(() => {
986
+ try {
987
+ resetWriteGateState(dir);
988
+ clearPendingAutoStart(dir);
989
+ } finally {
990
+ if (originalEnv === undefined) {
991
+ delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
992
+ } else {
993
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
994
+ }
995
+ process.chdir(originalCwd);
996
+ rmSync(dir, { recursive: true, force: true });
997
+ }
998
+ });
999
+
1000
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
1001
+ const pi = {
1002
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
1003
+ const existing = handlers.get(event) ?? [];
1004
+ existing.push(handler);
1005
+ handlers.set(event, existing);
1006
+ },
1007
+ } as any;
1008
+
1009
+ const notices: Array<{ text: string; level: string }> = [];
1010
+ const ctx = {
1011
+ cwd: dir,
1012
+ ui: { notify: (text: string, level: string) => notices.push({ text, level }) },
1013
+ } as any;
1014
+
1015
+ registerHooks(pi, []);
1016
+ setPendingAutoStart(dir, {
1017
+ basePath: dir,
1018
+ milestoneId: "M012",
1019
+ ctx,
1020
+ pi: { sendMessage: () => undefined } as any,
1021
+ });
1022
+
1023
+ mkdirSync(join(dir, ".gsd", "runtime"), { recursive: true });
1024
+ writeFileSync(statePath, JSON.stringify({
1025
+ verifiedDepthMilestones: ["M012"],
1026
+ verifiedApprovalGates: [gateId],
1027
+ activeQueuePhase: false,
1028
+ pendingGateId: null,
1029
+ }, null, 2), "utf-8");
1030
+
1031
+ const approvalMessage = {
1032
+ role: "assistant",
1033
+ content: [
1034
+ { type: "text", text: "Here is the milestone plan.\n\nDid I capture the project correctly?" },
1035
+ ],
1036
+ };
1037
+
1038
+ for (const handler of handlers.get("message_update") ?? []) {
1039
+ await handler({ message: approvalMessage }, ctx);
1040
+ }
1041
+
1042
+ assert.equal(
1043
+ notices.some((n) => /discuss-milestone M012 is waiting for your approval - pausing/.test(n.text)),
1044
+ true,
1045
+ "streaming hook must not suppress the pause from a disk-only verification",
1046
+ );
1047
+ assert.equal(
1048
+ shouldBlockContextArtifactSave("CONTEXT", "M012", null, dir).block,
1049
+ true,
1050
+ "streaming hook must not reconcile disk-only verification into the in-memory snapshot",
1051
+ );
1052
+
1053
+ for (const handler of handlers.get("agent_end") ?? []) {
1054
+ await handler({ messages: [] }, ctx);
1055
+ }
1056
+
1057
+ assert.equal(getPendingGate(dir), null, "agent_end still reconciles disk and suppresses durable re-arm");
1058
+ });
1059
+
973
1060
  // ── External-engine post-hoc gate replay (write-gate two-process sync) ──────
974
1061
  // On claude-code-cli, pi ingests the SDK turn's tool blocks after the workflow
975
1062
  // MCP child already executed them. The depth gate can therefore arrive at
@@ -42,6 +42,7 @@ import {
42
42
  type ReconciliationDeps,
43
43
  } from "../state-reconciliation.ts";
44
44
  import { classifyFailure } from "../recovery-classification.ts";
45
+ import { staleRenderHandler } from "../state-reconciliation/drift/stale-render.ts";
45
46
  import type { GSDState } from "../types.ts";
46
47
 
47
48
  function makeState(overrides: Partial<GSDState> = {}): GSDState {
@@ -696,6 +697,81 @@ test("ADR-017 (#5702): stale-render drift detected and repaired end-to-end", asy
696
697
  assert.match(repairedContent, /\[x\][^\n]*\*\*T02\*\*/, "T02 checkbox should be checked after repair");
697
698
  });
698
699
 
700
+ test("#1003: stale-render plan repair reopens DB before rendering", async (t) => {
701
+ const base = mkdtempSync(join(tmpdir(), "gsd-stale-render-reopen-"));
702
+ const sliceDir = join(base, ".gsd", "phases", "01-test");
703
+ mkdirSync(sliceDir, { recursive: true });
704
+ t.after(() => {
705
+ try { closeDatabase(); } catch { /* noop */ }
706
+ rmTreeQuiet(base);
707
+ });
708
+
709
+ openDatabase(join(base, ".gsd", "gsd.db"));
710
+ clearRendererCaches();
711
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
712
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
713
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "First task", status: "done" });
714
+
715
+ const planPath = join(sliceDir, "01-01-PLAN.md");
716
+ writeFileSync(planPath, makeStalePlanContent("S01", [
717
+ { id: "T01", title: "First task", done: false },
718
+ ]));
719
+ closeDatabase();
720
+
721
+ await staleRenderHandler.repair(
722
+ {
723
+ kind: "stale-render",
724
+ renderPath: planPath,
725
+ reason: "T01 is done in DB but unchecked in plan",
726
+ },
727
+ { basePath: base, state: makeState() },
728
+ );
729
+
730
+ const repairedContent = readFileSync(planPath, "utf-8");
731
+ assert.match(repairedContent, /\[x\][^\n]*\*\*T01\*\*/, "T01 checkbox should be checked after DB reopen repair");
732
+ assert.equal(getSliceTasks("M001", "S01").length, 1, "DB should be reopened on the original project database");
733
+ });
734
+
735
+ test("#1003: stale-render plan repair switches back from an open wrong DB", async (t) => {
736
+ const base = mkdtempSync(join(tmpdir(), "gsd-stale-render-wrong-db-"));
737
+ const wrongBase = mkdtempSync(join(tmpdir(), "gsd-stale-render-other-db-"));
738
+ const sliceDir = join(base, ".gsd", "phases", "01-test");
739
+ mkdirSync(sliceDir, { recursive: true });
740
+ mkdirSync(join(wrongBase, ".gsd"), { recursive: true });
741
+ t.after(() => {
742
+ try { closeDatabase(); } catch { /* noop */ }
743
+ rmTreeQuiet(base);
744
+ rmTreeQuiet(wrongBase);
745
+ });
746
+
747
+ openDatabase(join(base, ".gsd", "gsd.db"));
748
+ clearRendererCaches();
749
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
750
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
751
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "First task", status: "done" });
752
+
753
+ const planPath = join(sliceDir, "01-01-PLAN.md");
754
+ writeFileSync(planPath, makeStalePlanContent("S01", [
755
+ { id: "T01", title: "First task", done: false },
756
+ ]));
757
+ closeDatabase();
758
+
759
+ openDatabase(join(wrongBase, ".gsd", "gsd.db"));
760
+
761
+ await staleRenderHandler.repair(
762
+ {
763
+ kind: "stale-render",
764
+ renderPath: planPath,
765
+ reason: "T01 is done in DB but unchecked in plan",
766
+ },
767
+ { basePath: base, state: makeState() },
768
+ );
769
+
770
+ const repairedContent = readFileSync(planPath, "utf-8");
771
+ assert.match(repairedContent, /\[x\][^\n]*\*\*T01\*\*/, "T01 checkbox should be checked after switching back to the project DB");
772
+ assert.equal(getSliceTasks("M001", "S01").length, 1, "repair should leave the project DB active");
773
+ });
774
+
699
775
  test("ADR-017 (#5702): stale-render detector reason strings match repair contract", (t) => {
700
776
  t.skip("TODO(flat-phase): stale-render detection temporarily disabled during layout transition"); return;
701
777
  const base = mkdtempSync(join(tmpdir(), "gsd-adr017-render-reasons-"));
@@ -10,6 +10,7 @@ import {
10
10
  resetToolCallLoopGuard,
11
11
  disableToolCallLoopGuard,
12
12
  getToolCallLoopCount,
13
+ getToolCallCountForTool,
13
14
  } from '../bootstrap/tool-call-loop-guard.ts';
14
15
 
15
16
 
@@ -177,3 +178,70 @@ console.log('\n── Loop guard: nested key order is normalized ──');
177
178
  }
178
179
 
179
180
  // ═══════════════════════════════════════════════════════════════════════════
181
+ // Per-tool-name cap (#783 Brief C) — catches improvisation loops with varied args
182
+ // ═══════════════════════════════════════════════════════════════════════════
183
+
184
+ console.log('\n── Loop guard: per-tool cap blocks varied-args improvisation (#783) ──');
185
+
186
+ {
187
+ resetToolCallLoopGuard();
188
+
189
+ // A one-shot workflow tool called with DIFFERENT args each time (the reported
190
+ // improvisation pattern). The identical-signature streak alone would reset
191
+ // every call; the per-tool cap must catch it.
192
+ for (let i = 1; i <= 6; i++) {
193
+ const result = checkToolCallLoop('gsd_complete_milestone', { milestone: `M${i}` });
194
+ assert.ok(result.block === false, `one-shot call ${i} (varied args) should be allowed`);
195
+ assert.deepStrictEqual(getToolCallCountForTool('gsd_complete_milestone'), i, `per-tool count should be ${i}`);
196
+ }
197
+ // 7th call (cap 6 + 1) must be blocked by the per-tool guard.
198
+ const blocked = checkToolCallLoop('gsd_complete_milestone', { milestone: 'M7' });
199
+ assert.ok(blocked.block === true, '7th one-shot call (varied args) should be blocked by per-tool cap');
200
+ assert.ok(blocked.reason!.includes('repeated tool'), 'reason should identify the per-tool guard');
201
+ assert.ok(blocked.reason!.includes('gsd_complete_milestone'), 'reason should name the tool');
202
+ assert.ok(blocked.reason!.includes('7'), 'reason should mention the count');
203
+ }
204
+
205
+ // ═══════════════════════════════════════════════════════════════════════════
206
+ // Repeatable tools get the higher cap
207
+ // ═══════════════════════════════════════════════════════════════════════════
208
+
209
+ console.log('\n── Loop guard: repeatable tools get the higher cap (#783) ──');
210
+
211
+ {
212
+ resetToolCallLoopGuard();
213
+
214
+ // bash is repeatable: varied commands are legitimate up to the higher cap.
215
+ for (let i = 1; i <= 15; i++) {
216
+ const result = checkToolCallLoop('bash', { command: `echo ${i}` });
217
+ assert.ok(result.block === false, `bash call ${i} (varied args) should be allowed`);
218
+ }
219
+ // 16th call (cap 15 + 1) is blocked by the per-tool guard — this is the
220
+ // improvisation-through-bash case from the forensics (~51 calls).
221
+ const blocked = checkToolCallLoop('bash', { command: 'echo 16' });
222
+ assert.ok(blocked.block === true, '16th bash call (varied args) should be blocked by per-tool cap');
223
+ assert.ok(blocked.reason!.includes('cap 15'), 'reason should mention the repeatable cap');
224
+ }
225
+
226
+ // ═══════════════════════════════════════════════════════════════════════════
227
+ // Per-tool counts are independent per tool and reset together
228
+ // ═══════════════════════════════════════════════════════════════════════════
229
+
230
+ console.log('\n── Loop guard: per-tool counts are independent and reset together (#783) ──');
231
+
232
+ {
233
+ resetToolCallLoopGuard();
234
+
235
+ // Two different tools tracked separately.
236
+ for (let i = 0; i < 3; i++) checkToolCallLoop('read', { path: `f${i}` });
237
+ for (let i = 0; i < 3; i++) checkToolCallLoop('write', { path: `g${i}` });
238
+ assert.deepStrictEqual(getToolCallCountForTool('read'), 3, 'read tracked separately');
239
+ assert.deepStrictEqual(getToolCallCountForTool('write'), 3, 'write tracked separately');
240
+ assert.deepStrictEqual(getToolCallCountForTool('edit'), 0, 'uncalled tool reports 0');
241
+
242
+ resetToolCallLoopGuard();
243
+ assert.deepStrictEqual(getToolCallCountForTool('read'), 0, 'per-tool counts cleared on reset');
244
+ assert.deepStrictEqual(getToolCallCountForTool('write'), 0, 'per-tool counts cleared on reset');
245
+ }
246
+
247
+ // ═══════════════════════════════════════════════════════════════════════════
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { test } from "node:test";
15
15
  import assert from "node:assert/strict";
16
+ import { SUMMARY_SAVE_CONTENT_MAX_LENGTH } from "@opengsd/contracts";
16
17
  import { registerDbTools } from "../bootstrap/db-tools.ts";
17
18
  import AjvModule from "ajv";
18
19
 
@@ -86,6 +87,31 @@ test("gsd_summary_save — validates UAT assessment params", () => {
86
87
  assert.strictEqual(valid, true, `UAT assessment params should validate but got errors: ${JSON.stringify(validate.errors)}`);
87
88
  });
88
89
 
90
+ test("gsd_summary_save — content has a provider-safe maxLength", () => {
91
+ const tool = getTool("gsd_summary_save");
92
+ assert.ok(tool, "gsd_summary_save must be registered");
93
+
94
+ const contentSchema = tool.parameters.properties.content;
95
+ assert.strictEqual(contentSchema.maxLength, SUMMARY_SAVE_CONTENT_MAX_LENGTH);
96
+
97
+ const validAtLimit = validateSchema(tool, {
98
+ milestone_id: "M001",
99
+ artifact_type: "CONTEXT-DRAFT",
100
+ content: "x".repeat(SUMMARY_SAVE_CONTENT_MAX_LENGTH),
101
+ });
102
+ assert.deepEqual(validAtLimit, []);
103
+
104
+ const overLimit = validateSchema(tool, {
105
+ milestone_id: "M001",
106
+ artifact_type: "CONTEXT-DRAFT",
107
+ content: "x".repeat(SUMMARY_SAVE_CONTENT_MAX_LENGTH + 1),
108
+ });
109
+ assert.ok(
110
+ overLimit.some((error) => error.includes(`must NOT have more than ${SUMMARY_SAVE_CONTENT_MAX_LENGTH} characters`)),
111
+ `expected maxLength validation error, got: ${overLimit.join("; ")}`,
112
+ );
113
+ });
114
+
89
115
  // ─── gsd_slice_complete: enrichment arrays must be optional ──────────────────
90
116
 
91
117
  test("gsd_slice_complete — enrichment arrays are optional", () => {