gsd-pi 2.80.0-dev.0d763dc29 → 2.80.0-dev.2be7caf18

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 (119) hide show
  1. package/README.md +3 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  4. package/dist/resources/extensions/gsd/auto.js +7 -2
  5. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  7. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  8. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  9. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  10. package/dist/resources/extensions/gsd/quick.js +34 -2
  11. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  12. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  13. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  14. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  15. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  16. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  17. package/dist/web/standalone/.next/BUILD_ID +1 -1
  18. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  19. package/dist/web/standalone/.next/build-manifest.json +2 -2
  20. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  21. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.html +1 -1
  38. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  45. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/package.json +1 -1
  50. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  51. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  52. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  53. package/packages/mcp-server/src/workflow-tools.test.ts +66 -0
  54. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  55. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  56. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  58. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  60. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  62. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  64. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  65. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  66. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  67. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  68. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  70. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  72. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  74. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  77. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  79. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  82. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
  84. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  85. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  86. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  87. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  88. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  89. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  90. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  91. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  92. package/packages/pi-tui/dist/tui.js +1 -1
  93. package/packages/pi-tui/dist/tui.js.map +1 -1
  94. package/packages/pi-tui/src/tui.ts +1 -1
  95. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  96. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  97. package/src/resources/extensions/gsd/auto.ts +11 -2
  98. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  99. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  100. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  101. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  102. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  103. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  104. package/src/resources/extensions/gsd/quick.ts +37 -2
  105. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
  106. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  107. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  108. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  109. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  110. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +17 -0
  111. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  112. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  113. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  114. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  115. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  116. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  117. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  118. /package/dist/web/standalone/.next/static/{1J8mL3ANp9jqY3SU5BbVW → 3EDDd9ULaybSupPoA_vf-}/_buildManifest.js +0 -0
  119. /package/dist/web/standalone/.next/static/{1J8mL3ANp9jqY3SU5BbVW → 3EDDd9ULaybSupPoA_vf-}/_ssgManifest.js +0 -0
@@ -108,6 +108,21 @@ test('executeExecSearch: returns helpful empty-state message when no matches', (
108
108
  }
109
109
  });
110
110
 
111
+ test('executeExecSearch: returns disabled error when context_mode.enabled=false', () => {
112
+ const base = freshBase();
113
+ try {
114
+ writeRun(base, 'should-not-surface', { stdout: 'hidden\n' });
115
+ const result = executeExecSearch(
116
+ { query: 'hidden' },
117
+ { baseDir: base, preferences: { context_mode: { enabled: false } } },
118
+ );
119
+ assert.equal(result.isError, true);
120
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });
125
+
111
126
  test('executeExecSearch: includes stdout_path and preview in details', () => {
112
127
  const base = freshBase();
113
128
  try {
@@ -7,6 +7,7 @@ import { join } from 'node:path';
7
7
  import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
8
8
  import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
9
9
  import { isContextModeEnabled } from '../preferences-types.ts';
10
+ import { validatePreferences } from '../preferences-validation.ts';
10
11
 
11
12
  function freshBase(): string {
12
13
  return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
@@ -174,6 +175,52 @@ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
174
175
  }
175
176
  });
176
177
 
178
+ test('executeGsdExec: forwards custom exec_env_allowlist from preferences', async () => {
179
+ const base = freshBase();
180
+ try {
181
+ const result = await executeGsdExec(
182
+ {
183
+ runtime: 'bash',
184
+ script: 'printf "allowed=%s blocked=%s\\n" "$GSD_ALLOWED" "$GSD_BLOCKED"',
185
+ },
186
+ {
187
+ baseDir: base,
188
+ preferences: {
189
+ context_mode: {
190
+ enabled: true,
191
+ exec_env_allowlist: ['GSD_ALLOWED'],
192
+ },
193
+ },
194
+ env: {
195
+ PATH: '/usr/bin:/bin',
196
+ HOME: '/tmp',
197
+ GSD_ALLOWED: 'yes',
198
+ GSD_BLOCKED: 'no',
199
+ },
200
+ },
201
+ );
202
+ assert.ok(!result.isError);
203
+ assert.match(result.content[0].text, /allowed=yes blocked=/);
204
+ assert.doesNotMatch(result.content[0].text, /blocked=no/);
205
+ } finally {
206
+ cleanup(base);
207
+ }
208
+ });
209
+
210
+ test('executeGsdExec: enforces per-call timeout override end-to-end', async () => {
211
+ const base = freshBase();
212
+ try {
213
+ const result = await executeGsdExec(
214
+ { runtime: 'bash', script: 'sleep 2', timeout_ms: 1 },
215
+ { baseDir: base, preferences: { context_mode: { enabled: true, exec_timeout_ms: 10_000 } } },
216
+ );
217
+ assert.equal(result.details.timed_out, true);
218
+ assert.equal(result.isError, true);
219
+ } finally {
220
+ cleanup(base);
221
+ }
222
+ });
223
+
177
224
  test('executeGsdExec: rejects empty script', async () => {
178
225
  const base = freshBase();
179
226
  try {
@@ -188,6 +235,24 @@ test('executeGsdExec: rejects empty script', async () => {
188
235
  }
189
236
  });
190
237
 
238
+ test('validatePreferences: rejects invalid context_mode preference values', () => {
239
+ const result = validatePreferences({
240
+ context_mode: {
241
+ enabled: 'false',
242
+ exec_timeout_ms: 999,
243
+ exec_stdout_cap_bytes: 1,
244
+ exec_digest_chars: -1,
245
+ exec_env_allowlist: ['GOOD_NAME', 'bad-name'],
246
+ },
247
+ } as any);
248
+ assert.ok(result.errors.length > 0);
249
+ assert.ok(result.errors.includes('context_mode.enabled must be a boolean'));
250
+ assert.ok(result.errors.includes('context_mode.exec_timeout_ms must be a number between 1000 and 600000'));
251
+ assert.ok(result.errors.includes('context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216'));
252
+ assert.ok(result.errors.includes('context_mode.exec_digest_chars must be a number between 0 and 4000'));
253
+ assert.ok(result.errors.includes('context_mode.exec_env_allowlist must be an array of valid env var names'));
254
+ });
255
+
191
256
  test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
192
257
  assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
193
258
  assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname } from "node:path";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const promptsDir = join(__dirname, "..", "prompts");
11
+
12
+ test("prompt templates do not reference legacy milestone-root .gsd paths", () => {
13
+ const offenders: string[] = [];
14
+ for (const file of readdirSync(promptsDir)) {
15
+ if (!file.endsWith(".md")) continue;
16
+ const content = readFileSync(join(promptsDir, file), "utf-8");
17
+ const legacyPatterns = [
18
+ /\.gsd\/\{\{(?:milestoneId|mid)\}\}\//g,
19
+ /\.gsd\/<milestone-id>\//g,
20
+ /\.gsd\/<ID>\//g,
21
+ ];
22
+ for (const pattern of legacyPatterns) {
23
+ if (pattern.test(content)) {
24
+ offenders.push(`${file}: ${pattern.source}`);
25
+ }
26
+ }
27
+ }
28
+
29
+ assert.deepEqual(
30
+ offenders,
31
+ [],
32
+ "Milestone artifacts must use .gsd/milestones/<MID>/..., not legacy .gsd/<MID>/...",
33
+ );
34
+ });
35
+
36
+ test("quick task prompt delegates commit policy to quick.ts", () => {
37
+ const content = readFileSync(join(promptsDir, "quick-task.md"), "utf-8");
38
+ assert.match(content, /\{\{commitInstruction\}\}/);
39
+ assert.doesNotMatch(content, /Stage only relevant files/);
40
+ });
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { buildQuickCommitInstruction } from "../quick.ts";
8
+
9
+ function git(cwd: string, args: string[]): void {
10
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
11
+ assert.equal(result.status, 0, result.stderr || result.stdout);
12
+ }
13
+
14
+ test("quick task commit instruction does not ask agents to stage external .gsd quick files", { skip: process.platform === "win32" }, () => {
15
+ const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-quick-ext-")));
16
+ const repo = join(tempRoot, "repo");
17
+ const externalGsd = join(tempRoot, "state");
18
+ mkdirSync(repo);
19
+ mkdirSync(externalGsd);
20
+
21
+ const previousCwd = process.cwd();
22
+ try {
23
+ git(repo, ["init"]);
24
+ git(repo, ["config", "user.email", "test@example.com"]);
25
+ git(repo, ["config", "user.name", "Test User"]);
26
+ writeFileSync(join(repo, "README.md"), "# Test\n", "utf-8");
27
+ git(repo, ["add", "README.md"]);
28
+ git(repo, ["commit", "-m", "init"]);
29
+ symlinkSync(externalGsd, join(repo, ".gsd"), "dir");
30
+
31
+ const instruction = buildQuickCommitInstruction(repo, join(repo, ".gsd"));
32
+
33
+ assert.match(instruction, /do not stage or commit `\.gsd\/quick\/\.\.\.`/);
34
+ assert.match(instruction, /nothing in the project repo to commit/);
35
+ assert.match(instruction, /Write the quick summary file directly/);
36
+ } finally {
37
+ process.chdir(previousCwd);
38
+ rmSync(tempRoot, { recursive: true, force: true });
39
+ }
40
+ });
@@ -96,6 +96,23 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
96
96
  );
97
97
  });
98
98
 
99
+ test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
100
+ const autoSrc = readGsdFile("auto.ts");
101
+ const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
102
+ const startAutoBody = autoSrc.slice(startAutoIdx);
103
+
104
+ const preBootstrapRegisterIdx = startAutoBody.indexOf("registerAutoWorkerForSession(s, base);");
105
+ const bootstrapCallIdx = startAutoBody.indexOf("const ready = await bootstrapAutoSession(");
106
+
107
+ assert.ok(startAutoIdx > -1, "startAuto should exist");
108
+ assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
109
+ assert.ok(bootstrapCallIdx > -1, "startAuto should call bootstrapAutoSession");
110
+ assert.ok(
111
+ preBootstrapRegisterIdx < bootstrapCallIdx,
112
+ "worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
113
+ );
114
+ });
115
+
99
116
  test("startAutoDetached reports failures asynchronously (#3733)", () => {
100
117
  const autoSrc = readGsdFile("auto.ts");
101
118
 
@@ -44,4 +44,13 @@ describe('status opens DB before deriveState (#3691)', () => {
44
44
  assert.match(quickSrc, /getIsolationMode\(\)\s*!==\s*"none"/,
45
45
  'quick.ts should compare isolation mode against "none"');
46
46
  });
47
+
48
+ test('quick task prompt handles external .gsd without staging quick files', () => {
49
+ assert.match(quickSrc, /isExternalGsdRoot/,
50
+ 'quick.ts should detect whether .gsd resolves outside the project repo');
51
+ assert.match(quickSrc, /do not stage or commit `\.gsd\/quick\/\.\.\.`/,
52
+ 'external-state quick tasks must tell the agent not to stage .gsd/quick files');
53
+ assert.match(quickSrc, /nothing in the project repo to commit/,
54
+ 'external-state quick tasks should allow summary-only work without a git commit');
55
+ });
47
56
  });
@@ -21,12 +21,18 @@ import type {
21
21
  ComputedArtifactRegistry,
22
22
  UnitContextManifest,
23
23
  } from "../unit-context-manifest.ts";
24
- import { UNIT_MANIFESTS } from "../unit-context-manifest.ts";
25
- import { buildReassessRoadmapPrompt } from "../auto-prompts.ts";
24
+ import { KNOWN_UNIT_TYPES, UNIT_MANIFESTS } from "../unit-context-manifest.ts";
25
+ import {
26
+ buildExecuteTaskPrompt,
27
+ buildGateEvaluatePrompt,
28
+ buildReassessRoadmapPrompt,
29
+ buildWorkflowPreferencesPrompt,
30
+ } from "../auto-prompts.ts";
26
31
  import { invalidateAllCaches } from "../cache.ts";
27
32
  import {
28
33
  openDatabase,
29
34
  closeDatabase,
35
+ insertGateRow,
30
36
  insertMilestone,
31
37
  upsertMilestonePlanning,
32
38
  insertSlice,
@@ -121,7 +127,7 @@ test("Context Mode composer: standalone output starts with heading and includes
121
127
  assert.ok(out.startsWith("## Context Mode"));
122
128
  assert.match(out, /execution lane/i);
123
129
  assert.match(out, /`gsd_exec`/);
124
- assert.match(out, /noisy commands/);
130
+ assert.match(out, /builds, tests, and diagnostics/);
125
131
  assert.match(out, /`gsd_exec_search`/);
126
132
  assert.match(out, /before reruns/);
127
133
  assert.match(out, /`gsd_resume`/);
@@ -136,7 +142,45 @@ test("Context Mode composer: nested output is compact single sentence", () => {
136
142
  assert.match(out, /`gsd_exec`/);
137
143
  assert.match(out, /`gsd_exec_search`/);
138
144
  assert.match(out, /`gsd_resume`/);
139
- assert.ok(out.length < 180, `nested guidance should stay compact, got ${out.length} chars`);
145
+ assert.ok(out.length < 240, `nested guidance should stay compact, got ${out.length} chars`);
146
+ });
147
+
148
+ const laneLabelByMode: Record<string, string> = {
149
+ interview: "interview",
150
+ research: "research",
151
+ planning: "planning",
152
+ execution: "execution",
153
+ verification: "verification",
154
+ orchestration: "orchestration",
155
+ docs: "documentation",
156
+ };
157
+
158
+ test("Context Mode composer: every known eligible unit renders its configured lane and required tools", () => {
159
+ for (const unitType of KNOWN_UNIT_TYPES) {
160
+ const manifest = UNIT_MANIFESTS[unitType];
161
+ assert.ok(manifest, `missing manifest for ${unitType}`);
162
+ const out = composeContextModeInstructions(unitType, { enabled: true, renderMode: "standalone" });
163
+ if (manifest.contextMode === "none") {
164
+ assert.strictEqual(out, "", `${unitType} should not render Context Mode`);
165
+ continue;
166
+ }
167
+ assert.ok(out.startsWith("## Context Mode"), `${unitType} should render standalone Context Mode heading`);
168
+ assert.match(out, new RegExp(`Lane: \\*\\*${laneLabelByMode[manifest.contextMode]} lane\\*\\*\\.`, "i"));
169
+ assert.match(out, /`gsd_exec`/, `${unitType} should mention gsd_exec`);
170
+ assert.match(out, /`gsd_exec_search`/, `${unitType} should mention gsd_exec_search`);
171
+ assert.match(out, /`gsd_resume`/, `${unitType} should mention gsd_resume`);
172
+ }
173
+ });
174
+
175
+ test("Context Mode composer: workflow-preferences and research-decision render no Context Mode block", () => {
176
+ assert.strictEqual(
177
+ composeContextModeInstructions("workflow-preferences", { enabled: true, renderMode: "standalone" }),
178
+ "",
179
+ );
180
+ assert.strictEqual(
181
+ composeContextModeInstructions("research-decision", { enabled: true, renderMode: "standalone" }),
182
+ "",
183
+ );
140
184
  });
141
185
 
142
186
  // ─── Integration: migrated buildReassessRoadmapPrompt ─────────────────────
@@ -220,6 +264,94 @@ test("#4782 phase 2: buildReassessRoadmapPrompt emits composer-shaped context wi
220
264
  assert.ok(!prompt.includes("Slice Context (from discussion)"));
221
265
  });
222
266
 
267
+ test("Context Mode resume injection: eligible prompts include one bounded snapshot block above inlined context", async (t) => {
268
+ const base = makeFixtureBase();
269
+ t.after(() => cleanup(base));
270
+ invalidateAllCaches();
271
+
272
+ seed(base, "M001");
273
+ writeArtifacts(base);
274
+ writeFileSync(
275
+ join(base, ".gsd", "last-snapshot.md"),
276
+ "# GSD context snapshot\n\nResume evidence.\n",
277
+ "utf-8",
278
+ );
279
+
280
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
281
+
282
+ assert.equal(prompt.match(/## Context Snapshot/g)?.length, 1);
283
+ assert.match(prompt, /Source: `\.gsd\/last-snapshot\.md`/);
284
+ assert.match(prompt, /Resume evidence/);
285
+ assert.ok(prompt.indexOf("## Context Mode") < prompt.indexOf("## Context Snapshot"));
286
+ assert.ok(prompt.indexOf("## Context Snapshot") < prompt.indexOf("## Inlined Context"));
287
+ });
288
+
289
+ test("Context Mode resume injection: missing snapshot does not add an empty block", async (t) => {
290
+ const base = makeFixtureBase();
291
+ t.after(() => cleanup(base));
292
+ invalidateAllCaches();
293
+
294
+ seed(base, "M001");
295
+ writeArtifacts(base);
296
+
297
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
298
+
299
+ assert.match(prompt, /## Context Mode/);
300
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
301
+ });
302
+
303
+ test("Context Mode resume injection: disabled mode suppresses guidance and snapshot reads", async (t) => {
304
+ const base = makeFixtureBase();
305
+ t.after(() => cleanup(base));
306
+ invalidateAllCaches();
307
+
308
+ seed(base, "M001");
309
+ writeArtifacts(base);
310
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ncontext_mode:\n enabled: false\n---\n", "utf-8");
311
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nDo not inject.\n", "utf-8");
312
+
313
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
314
+
315
+ assert.doesNotMatch(prompt, /## Context Mode/);
316
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
317
+ assert.doesNotMatch(prompt, /Do not inject/);
318
+ });
319
+
320
+ test("Context Mode resume injection: none-mode units do not inject snapshots", async () => {
321
+ const base = makeFixtureBase();
322
+ try {
323
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nNo lane.\n", "utf-8");
324
+ const prompt = await buildWorkflowPreferencesPrompt(base);
325
+ assert.doesNotMatch(prompt, /## Context Mode/);
326
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
327
+ assert.doesNotMatch(prompt, /No lane/);
328
+ } finally {
329
+ cleanup(base);
330
+ }
331
+ });
332
+
333
+ test("Context Mode prompt suppression: disabled inlined, phase-anchor, and nested prompts omit Context Mode", async (t) => {
334
+ const base = makeFixtureBase();
335
+ t.after(() => cleanup(base));
336
+ invalidateAllCaches();
337
+
338
+ seed(base, "M001");
339
+ writeArtifacts(base);
340
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
341
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ncontext_mode:\n enabled: false\n---\n", "utf-8");
342
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nDo not inject.\n", "utf-8");
343
+
344
+ const inlinedPrompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
345
+ assert.doesNotMatch(inlinedPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
346
+
347
+ const phaseAnchorPrompt = await buildExecuteTaskPrompt("M001", "S01", "First", "T01", "Task", base);
348
+ assert.doesNotMatch(phaseAnchorPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
349
+
350
+ const nestedPrompt = await buildGateEvaluatePrompt("M001", "Test", "S01", "First", base);
351
+ assert.match(nestedPrompt, /Use this as the prompt for a `subagent` call/);
352
+ assert.doesNotMatch(nestedPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
353
+ });
354
+
223
355
  // ─── v2 surface (#4924) ───────────────────────────────────────────────────
224
356
 
225
357
  const fakeBase: BaseResolverContext = {
@@ -0,0 +1,25 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Shared Context Mode tool result helpers.
3
+
4
+ export interface ToolExecutionResult {
5
+ content: Array<{ type: "text"; text: string }>;
6
+ details: Record<string, unknown>;
7
+ isError?: boolean;
8
+ }
9
+
10
+ export type ContextModeToolName = "gsd_exec" | "gsd_exec_search" | "gsd_resume";
11
+
12
+ export function contextModeDisabledResult(operation: ContextModeToolName): ToolExecutionResult {
13
+ return {
14
+ content: [
15
+ {
16
+ type: "text",
17
+ text:
18
+ `${operation} is disabled by \`context_mode.enabled: false\` in preferences. ` +
19
+ "Remove that override or set it to true to re-enable Context Mode tools.",
20
+ },
21
+ ],
22
+ details: { operation, error: "context_mode_disabled" },
23
+ isError: true,
24
+ };
25
+ }
@@ -4,6 +4,8 @@
4
4
  // re-discover past runs without re-executing. Read-only; no DB writes.
5
5
 
6
6
  import { searchExecHistory, type ExecSearchOptions } from "../exec-history.js";
7
+ import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
8
+ import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
7
9
 
8
10
  export interface ExecSearchToolParams {
9
11
  query?: string;
@@ -12,16 +14,14 @@ export interface ExecSearchToolParams {
12
14
  limit?: number;
13
15
  }
14
16
 
15
- export interface ToolExecutionResult {
16
- content: Array<{ type: "text"; text: string }>;
17
- details: Record<string, unknown>;
18
- isError?: boolean;
19
- }
20
-
21
17
  export function executeExecSearch(
22
18
  params: ExecSearchToolParams,
23
- opts: { baseDir: string },
19
+ opts: { baseDir: string; preferences?: { context_mode?: ContextModeConfig } | null },
24
20
  ): ToolExecutionResult {
21
+ if (!isContextModeEnabled(opts.preferences)) {
22
+ return contextModeDisabledResult("gsd_exec_search");
23
+ }
24
+
25
25
  const searchOpts: ExecSearchOptions = {
26
26
  query: typeof params.query === "string" ? params.query : undefined,
27
27
  runtime: params.runtime,
@@ -12,6 +12,7 @@ import {
12
12
  type ExecSandboxResult,
13
13
  } from "../exec-sandbox.js";
14
14
  import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
15
+ import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
15
16
 
16
17
  export interface ExecToolParams {
17
18
  runtime: ExecSandboxRequest["runtime"];
@@ -20,17 +21,12 @@ export interface ExecToolParams {
20
21
  timeout_ms?: number;
21
22
  }
22
23
 
23
- export interface ToolExecutionResult {
24
- content: Array<{ type: "text"; text: string }>;
25
- details: Record<string, unknown>;
26
- isError?: boolean;
27
- }
28
-
29
24
  export interface ExecToolDeps {
30
25
  baseDir: string;
31
26
  preferences: { context_mode?: ContextModeConfig } | null;
32
27
  /** Optional override for testing. */
33
28
  run?: (req: ExecSandboxRequest, opts: ExecSandboxOptions) => Promise<ExecSandboxResult>;
29
+ env?: NodeJS.ProcessEnv;
34
30
  now?: () => Date;
35
31
  generateId?: () => string;
36
32
  }
@@ -77,21 +73,6 @@ function isEnabled(prefs: ExecToolDeps["preferences"]): boolean {
77
73
  return isContextModeEnabled(prefs);
78
74
  }
79
75
 
80
- function disabledResult(): ToolExecutionResult {
81
- return {
82
- content: [
83
- {
84
- type: "text",
85
- text:
86
- "gsd_exec is disabled by `context_mode.enabled: false` in preferences. Remove that " +
87
- "override (or set it to true) to re-enable sandboxed tool-output execution.",
88
- },
89
- ],
90
- details: { operation: "gsd_exec", error: "context_mode_disabled" },
91
- isError: true,
92
- };
93
- }
94
-
95
76
  function paramError(message: string): ToolExecutionResult {
96
77
  return {
97
78
  content: [{ type: "text", text: `Error: ${message}` }],
@@ -104,7 +85,7 @@ export async function executeGsdExec(
104
85
  params: ExecToolParams,
105
86
  deps: ExecToolDeps,
106
87
  ): Promise<ToolExecutionResult> {
107
- if (!isEnabled(deps.preferences)) return disabledResult();
88
+ if (!isEnabled(deps.preferences)) return contextModeDisabledResult("gsd_exec");
108
89
 
109
90
  const runtime = params.runtime;
110
91
  if (runtime !== "bash" && runtime !== "node" && runtime !== "python") {
@@ -121,7 +102,7 @@ export async function executeGsdExec(
121
102
  const opts = buildExecOptions(
122
103
  deps.baseDir,
123
104
  deps.preferences?.context_mode,
124
- { now: deps.now, generateId: deps.generateId },
105
+ { env: deps.env, now: deps.now, generateId: deps.generateId },
125
106
  );
126
107
  const run = deps.run ?? runExecSandbox;
127
108
 
@@ -3,22 +3,22 @@
3
3
  // re-deriving project memory state.
4
4
 
5
5
  import { readCompactionSnapshot } from "../compaction-snapshot.js";
6
+ import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
7
+ import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
6
8
 
7
9
  export interface ResumeToolParams {
8
10
  /** Ignored — reserved for future variant (e.g. dated snapshots). */
9
11
  _variant?: string;
10
12
  }
11
13
 
12
- export interface ToolExecutionResult {
13
- content: Array<{ type: "text"; text: string }>;
14
- details: Record<string, unknown>;
15
- isError?: boolean;
16
- }
17
-
18
14
  export function executeResume(
19
15
  _params: ResumeToolParams,
20
- opts: { baseDir: string },
16
+ opts: { baseDir: string; preferences?: { context_mode?: ContextModeConfig } | null },
21
17
  ): ToolExecutionResult {
18
+ if (!isContextModeEnabled(opts.preferences)) {
19
+ return contextModeDisabledResult("gsd_resume");
20
+ }
21
+
22
22
  const snapshot = readCompactionSnapshot(opts.baseDir);
23
23
  if (snapshot == null) {
24
24
  return {
@@ -112,8 +112,22 @@ const CONTEXT_MODE_LANE_LABELS: Record<Exclude<ContextModePolicy, "none">, strin
112
112
  docs: "documentation",
113
113
  };
114
114
 
115
- const CONTEXT_MODE_GUIDANCE =
116
- "Use `gsd_exec` for noisy commands, `gsd_exec_search` before reruns, and `gsd_resume` after compaction or resume.";
115
+ const CONTEXT_MODE_GUIDANCE_BY_LANE: Record<Exclude<ContextModePolicy, "none">, string> = {
116
+ interview:
117
+ "Use `gsd_resume` to restore prior discussion, `gsd_exec` for noisy discovery, and `gsd_exec_search` before repeating scans.",
118
+ research:
119
+ "Use `gsd_exec` for noisy research scans, `gsd_exec_search` before reruns, and `gsd_resume` to restore prior findings.",
120
+ planning:
121
+ "Use `gsd_resume` for planning continuity, `gsd_exec` for noisy checks, and `gsd_exec_search` before rerunning diagnostics.",
122
+ execution:
123
+ "Use `gsd_exec` for builds, tests, and diagnostics, `gsd_exec_search` before reruns, and `gsd_resume` after compaction or resume.",
124
+ verification:
125
+ "Use `gsd_exec` for verification commands, `gsd_exec_search` to reuse prior evidence, and `gsd_resume` after compaction or resume.",
126
+ orchestration:
127
+ "Use `gsd_resume` before resuming orchestration, `gsd_exec_search` to reuse prior runs, and `gsd_exec` for noisy coordination checks.",
128
+ docs:
129
+ "Use `gsd_resume` for prior context, `gsd_exec_search` for saved evidence, and `gsd_exec` for noisy doc validation commands.",
130
+ };
117
131
 
118
132
  /**
119
133
  * Render the Context Mode instruction lane for a unit type. Unknown unit
@@ -129,15 +143,16 @@ export function composeContextModeInstructions(
129
143
  if (!manifest || manifest.contextMode === "none") return "";
130
144
 
131
145
  const lane = CONTEXT_MODE_LANE_LABELS[manifest.contextMode];
146
+ const guidance = CONTEXT_MODE_GUIDANCE_BY_LANE[manifest.contextMode];
132
147
  if (opts.renderMode === "nested") {
133
- return `Context Mode (${lane} lane): ${CONTEXT_MODE_GUIDANCE}`;
148
+ return `Context Mode (${lane} lane): ${guidance}`;
134
149
  }
135
150
 
136
151
  return [
137
152
  "## Context Mode",
138
153
  "",
139
154
  `Lane: **${lane} lane**.`,
140
- CONTEXT_MODE_GUIDANCE,
155
+ guidance,
141
156
  ].join("\n");
142
157
  }
143
158