gsd-pi 2.78.1-dev.eccf86e27 → 2.79.0-dev.579e14e9b

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 (96) hide show
  1. package/README.md +94 -47
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
  4. package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
  5. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
  7. package/dist/resources/extensions/gsd/commands/context.js +1 -1
  8. package/dist/resources/extensions/gsd/preferences-types.js +20 -2
  9. package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
  10. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +41 -2
  11. package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
  12. package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
  13. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  16. package/dist/web/standalone/.next/build-manifest.json +2 -2
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/required-server-files.json +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.html +1 -1
  36. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  43. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  45. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  46. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  47. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
  48. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  49. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
  50. package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
  51. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
  52. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  53. package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
  54. package/dist/web/standalone/server.js +1 -1
  55. package/package.json +1 -1
  56. package/packages/daemon/package.json +2 -2
  57. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  58. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  59. package/packages/mcp-server/dist/workflow-tools.js +53 -0
  60. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  61. package/packages/mcp-server/package.json +2 -2
  62. package/packages/mcp-server/src/workflow-tools.test.ts +116 -0
  63. package/packages/mcp-server/src/workflow-tools.ts +81 -0
  64. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  65. package/packages/native/package.json +1 -1
  66. package/packages/pi-agent-core/package.json +1 -1
  67. package/packages/pi-ai/package.json +1 -1
  68. package/packages/pi-coding-agent/package.json +1 -1
  69. package/packages/pi-tui/package.json +1 -1
  70. package/packages/rpc-client/package.json +1 -1
  71. package/pkg/package.json +1 -1
  72. package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
  73. package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
  74. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  75. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
  76. package/src/resources/extensions/gsd/commands/context.ts +1 -1
  77. package/src/resources/extensions/gsd/preferences-types.ts +23 -4
  78. package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
  79. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +68 -1
  80. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
  81. package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
  82. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
  83. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
  84. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
  85. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
  86. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
  87. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
  88. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
  89. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
  90. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +100 -0
  91. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
  92. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
  93. package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
  94. package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
  95. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_buildManifest.js +0 -0
  96. /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_ssgManifest.js +0 -0
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
 
8
8
  import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
9
9
  import { resolveMilestoneFile } from "../paths.ts";
10
- import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow } from "../gsd-db.ts";
10
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask } from "../gsd-db.ts";
11
11
  import { clearParseCache } from "../files.ts";
12
12
  import { parseRoadmap } from "../parsers-legacy.ts";
13
13
  import { invalidateAllCaches } from "../cache.ts";
@@ -764,6 +764,73 @@ test("hasImplementationArtifacts finds implementation commits when .gsd/ is giti
764
764
  }
765
765
  });
766
766
 
767
+ test("hasImplementationArtifacts binds GSD-Task trailer to milestone via DB state when .gsd/ is gitignored", () => {
768
+ const base = makeGitBase();
769
+ try {
770
+ writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
771
+ mkdirSync(join(base, ".gsd"), { recursive: true });
772
+ openDatabase(join(base, ".gsd", "gsd.db"));
773
+ insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
774
+ insertSlice({
775
+ id: "S01",
776
+ milestoneId: "M001",
777
+ title: "Slice One",
778
+ status: "complete",
779
+ risk: "low",
780
+ depends: [],
781
+ });
782
+ insertTask({
783
+ id: "T01",
784
+ sliceId: "S01",
785
+ milestoneId: "M001",
786
+ title: "Task One",
787
+ status: "complete",
788
+ });
789
+
790
+ mkdirSync(join(base, "src"), { recursive: true });
791
+ writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
792
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
793
+ execFileSync(
794
+ "git",
795
+ ["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
796
+ { cwd: base, stdio: "ignore" },
797
+ );
798
+
799
+ const result = hasImplementationArtifacts(base, "M001");
800
+ assert.equal(
801
+ result,
802
+ "present",
803
+ "DB task ownership should bind S01/T01 implementation commits to M001 without explicit M001 text",
804
+ );
805
+ } finally {
806
+ cleanup(base);
807
+ }
808
+ });
809
+
810
+ test("hasImplementationArtifacts does not bind GSD-Task trailer without milestone ownership evidence", () => {
811
+ const base = makeGitBase();
812
+ try {
813
+ writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
814
+ mkdirSync(join(base, "src"), { recursive: true });
815
+ writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
816
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
817
+ execFileSync(
818
+ "git",
819
+ ["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
820
+ { cwd: base, stdio: "ignore" },
821
+ );
822
+
823
+ const result = hasImplementationArtifacts(base, "M001");
824
+ assert.equal(
825
+ result,
826
+ "absent",
827
+ "S01/T01 shape alone must not bind an implementation commit to M001",
828
+ );
829
+ } finally {
830
+ cleanup(base);
831
+ }
832
+ });
833
+
767
834
  test("hasImplementationArtifacts ignores malformed milestone IDs in commit-message fallback", () => {
768
835
  const base = makeGitBase();
769
836
  try {
@@ -31,9 +31,9 @@ describe("bootstrap deriveState DB guards (#3844)", () => {
31
31
  const compactIdx = registerHooksSrc.indexOf('pi.on("session_before_compact"');
32
32
  assert.ok(compactIdx > -1, "register-hooks should define session_before_compact");
33
33
  const compactSection = extractSourceRegion(registerHooksSrc, 'pi.on("session_before_compact"');
34
- const ensureIdx = compactSection.indexOf("ensureDbOpen()");
34
+ const ensureIdx = compactSection.indexOf("ensureDbOpen(basePath)");
35
35
  const deriveIdx = compactSection.indexOf("deriveGsdState(basePath)");
36
- assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen()");
36
+ assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen(basePath)");
37
37
  assert.ok(deriveIdx > -1, "session_before_compact should derive state");
38
38
  assert.ok(ensureIdx < deriveIdx, "session_before_compact should open DB before deriveState");
39
39
  });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Regression test — currentDirectoryRoot() uses os.homedir() as fallback
3
+ * when process.cwd() throws (e.g. worktree teardown deletes the cwd).
4
+ *
5
+ * Before the fix, the catch block used `process.env.HOME ?? "/"`. On
6
+ * Windows, HOME is typically unset so this resolved to "/", an invalid
7
+ * path. After the fix, os.homedir() is used — it checks USERPROFILE,
8
+ * HOMEDRIVE+HOMEPATH, etc., returning a valid path on all platforms.
9
+ *
10
+ * The test monkey-patches process.cwd() to throw ENOENT, simulating a
11
+ * deleted cwd. currentDirectoryRoot() must NOT propagate the raw error;
12
+ * instead it falls back to homedir(), which validateDirectory correctly
13
+ * rejects as "blocked", yielding GSDNoProjectError — the same controlled
14
+ * error path handlers already know how to catch.
15
+ *
16
+ * The error message is also asserted to match validateDirectory(homedir()),
17
+ * confirming the fallback resolved to homedir() specifically (not "/" or
18
+ * any other path).
19
+ */
20
+ import { describe, test, beforeEach, afterEach } from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { homedir } from "node:os";
23
+
24
+ import { currentDirectoryRoot, GSDNoProjectError } from "../commands/context.ts";
25
+ import { validateDirectory } from "../validate-directory.ts";
26
+
27
+ describe("currentDirectoryRoot() homedir() fallback on deleted cwd", () => {
28
+ const originalCwd = process.cwd.bind(process);
29
+
30
+ beforeEach(() => {
31
+ process.cwd = () => {
32
+ const err = new Error("ENOENT: no such file or directory, uv_cwd");
33
+ (err as NodeJS.ErrnoException).code = "ENOENT";
34
+ throw err;
35
+ };
36
+ });
37
+
38
+ afterEach(() => {
39
+ process.cwd = originalCwd;
40
+ });
41
+
42
+ test("does not propagate ENOENT — throws GSDNoProjectError via homedir() fallback", () => {
43
+ const expected = validateDirectory(homedir());
44
+ assert.equal(expected.severity, "blocked", "homedir() itself should be blocked");
45
+
46
+ assert.throws(
47
+ () => currentDirectoryRoot(),
48
+ (err: unknown) => {
49
+ assert.ok(
50
+ err instanceof GSDNoProjectError,
51
+ `expected GSDNoProjectError, got: ${err}`,
52
+ );
53
+ assert.equal(
54
+ (err as Error).message,
55
+ expected.reason ?? "GSD must be run inside a project directory.",
56
+ "error message must match validateDirectory(homedir()), confirming homedir() was the fallback",
57
+ );
58
+ return true;
59
+ },
60
+ "should throw GSDNoProjectError (homedir fallback validated), not raw ENOENT",
61
+ );
62
+ });
63
+ });
@@ -101,6 +101,20 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
101
101
  prompt.includes("Implement the thing"),
102
102
  "must include task plan body content from disk",
103
103
  );
104
+ assert.ok(prompt.includes("## Context Mode"), "execute-task should include standalone Context Mode guidance");
105
+ assert.ok(prompt.includes("execution lane"), "execute-task should render the execution lane");
106
+ });
107
+
108
+ test("buildExecuteTaskPrompt omits Context Mode when disabled", async () => {
109
+ writeFileSync(
110
+ join(base, ".gsd", "PREFERENCES.md"),
111
+ ["---", "context_mode:", " enabled: false", "---", ""].join("\n"),
112
+ );
113
+
114
+ const prompt = await buildExecuteTaskPrompt(MID, SID, S_TITLE, TID, T_TITLE, base);
115
+
116
+ assert.ok(!prompt.includes("## Context Mode"));
117
+ assert.ok(!prompt.includes("Context Mode (execution lane)"));
104
118
  });
105
119
 
106
120
  test("buildCompleteSlicePrompt carries the complete-slice contract", async () => {
@@ -147,4 +147,12 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
147
147
  prompt.includes(SKILL_ACTIVATION_SUBSTRING),
148
148
  `parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
149
149
  );
150
+ assert.ok(
151
+ prompt.includes("Context Mode (research lane):"),
152
+ "embedded parallel research subagent prompts should use nested Context Mode guidance",
153
+ );
154
+ assert.ok(
155
+ !prompt.includes("## Context Mode\n\nLane: **research lane**."),
156
+ "embedded parallel research subagent prompts should not use standalone Context Mode heading",
157
+ );
150
158
  });
@@ -111,6 +111,9 @@ test("#4551: buildPlanSlicePrompt injects fix section when priorPreExecFailure p
111
111
  },
112
112
  );
113
113
 
114
+ assert.ok(prompt.includes("## Context Mode"), "plan-slice should include standalone Context Mode guidance");
115
+ assert.ok(prompt.includes("planning lane"), "plan-slice should render the planning lane");
116
+
114
117
  assert.ok(
115
118
  prompt.includes("Fix these specific issues from the prior pre-exec check"),
116
119
  "prompt must contain the fix section heading",
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
7
  import { registerHooks } from "../bootstrap/register-hooks.ts";
8
+ import { autoSession } from "../auto-runtime-state.ts";
8
9
  import { parseContinue } from "../files.ts";
9
10
  import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
10
11
  import { deriveState, invalidateStateCache } from "../state.ts";
@@ -96,3 +97,87 @@ test("register-hooks writes CONTINUE checkpoint during planning phase without ac
96
97
  assert.match(parsed.completedWork, /planning phase/i, "completed-work should capture non-executing phase context");
97
98
  assert.match(parsed.nextAction, /slice S01/i, "next action should route resume to the active slice");
98
99
  });
100
+
101
+ test("register-hooks writes Context Mode snapshot before active auto cancels compaction", async (t) => {
102
+ const base = createPlanningFixtureBase();
103
+ const originalCwd = process.cwd();
104
+ process.chdir(base);
105
+ invalidateStateCache();
106
+ closeDatabase();
107
+ autoSession.reset();
108
+ autoSession.active = true;
109
+
110
+ t.after(() => {
111
+ autoSession.reset();
112
+ invalidateStateCache();
113
+ closeDatabase();
114
+ process.chdir(originalCwd);
115
+ rmSync(base, { recursive: true, force: true });
116
+ });
117
+
118
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
119
+ const pi = {
120
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
121
+ const existing = handlers.get(event) ?? [];
122
+ existing.push(handler);
123
+ handlers.set(event, existing);
124
+ },
125
+ } as any;
126
+
127
+ registerHooks(pi, []);
128
+
129
+ const compactHandlers = handlers.get("session_before_compact");
130
+ assert.ok(compactHandlers && compactHandlers.length > 0, "session_before_compact handler should be registered");
131
+
132
+ const result = await compactHandlers![0]({});
133
+
134
+ assert.deepEqual(result, { cancel: true }, "active auto should still cancel compaction");
135
+ const snapshotPath = join(base, ".gsd", "last-snapshot.md");
136
+ assert.ok(existsSync(snapshotPath), "active auto cancel should still leave a Context Mode snapshot");
137
+ assert.match(readFileSync(snapshotPath, "utf-8"), /GSD context snapshot/);
138
+ });
139
+
140
+ test("register-hooks does not write Context Mode snapshot when disabled", async (t) => {
141
+ const base = createPlanningFixtureBase();
142
+ writeFileSync(
143
+ join(base, ".gsd", "PREFERENCES.md"),
144
+ "---\ncontext_mode:\n enabled: false\n---\n",
145
+ "utf-8",
146
+ );
147
+ const originalCwd = process.cwd();
148
+ process.chdir(base);
149
+ invalidateStateCache();
150
+ closeDatabase();
151
+ autoSession.reset();
152
+
153
+ t.after(() => {
154
+ autoSession.reset();
155
+ invalidateStateCache();
156
+ closeDatabase();
157
+ process.chdir(originalCwd);
158
+ rmSync(base, { recursive: true, force: true });
159
+ });
160
+
161
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
162
+ const pi = {
163
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
164
+ const existing = handlers.get(event) ?? [];
165
+ existing.push(handler);
166
+ handlers.set(event, existing);
167
+ },
168
+ } as any;
169
+
170
+ registerHooks(pi, []);
171
+
172
+ const compactHandlers = handlers.get("session_before_compact");
173
+ assert.ok(compactHandlers && compactHandlers.length > 0, "session_before_compact handler should be registered");
174
+
175
+ for (const handler of compactHandlers ?? []) {
176
+ await handler({});
177
+ }
178
+
179
+ assert.ok(
180
+ !existsSync(join(base, ".gsd", "last-snapshot.md")),
181
+ "disabled Context Mode should not write a snapshot",
182
+ );
183
+ });
@@ -95,6 +95,8 @@ test("#4782 phase 3: buildRunUatPrompt inlines slice UAT, slice summary, project
95
95
 
96
96
  // Context wrapper present
97
97
  assert.match(prompt, /## Inlined Context \(preloaded — do not re-read these files\)/);
98
+ assert.match(prompt, /## Context Mode/);
99
+ assert.match(prompt, /verification lane/);
98
100
 
99
101
  // Artifacts from the manifest inline list, in declared order:
100
102
  // slice-uat → slice-summary → project (#4925 review).
@@ -211,6 +211,14 @@ test("buildReactiveExecutePrompt: output contains model string when subagentMode
211
211
  prompt.includes('model: "claude-opus-4-6"'),
212
212
  `Prompt should contain model instruction. Got:\n${prompt.slice(0, 500)}`,
213
213
  );
214
+ assert.ok(
215
+ prompt.includes("Context Mode (execution lane):"),
216
+ "embedded reactive-execute task prompt should use nested Context Mode guidance",
217
+ );
218
+ assert.ok(
219
+ prompt.includes("## Context Mode"),
220
+ "reactive-execute parent prompt should include standalone Context Mode guidance",
221
+ );
214
222
  });
215
223
 
216
224
  test("buildReactiveExecutePrompt: no model instruction when subagentModel omitted", async (t) => {
@@ -266,3 +274,54 @@ test("buildReactiveExecutePrompt: no model instruction when subagentModel omitte
266
274
  "Prompt should not contain model instruction when subagentModel is omitted",
267
275
  );
268
276
  });
277
+
278
+ test("buildGateEvaluatePrompt: embedded gate prompts use nested Context Mode guidance", async (t) => {
279
+ const { buildGateEvaluatePrompt } = await import("../auto-prompts.ts");
280
+ const { closeDatabase, insertGateRow, insertMilestone, insertSlice, openDatabase } = await import("../gsd-db.ts");
281
+ const repo = mkdtempSync(join(tmpdir(), "gsd-subagent-model-gate-"));
282
+ t.after(() => {
283
+ try { closeDatabase(); } catch { /* noop */ }
284
+ rmSync(repo, { recursive: true, force: true });
285
+ });
286
+
287
+ const sliceDir = join(repo, ".gsd", "milestones", "M001", "slices", "S01");
288
+ mkdirSync(sliceDir, { recursive: true });
289
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01 Plan\n\n## Verification\n- Run checks.\n");
290
+ openDatabase(join(repo, ".gsd", "gsd.db"));
291
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "active", depends_on: [] });
292
+ insertSlice({
293
+ id: "S01",
294
+ milestoneId: "M001",
295
+ title: "Test Slice",
296
+ status: "planned",
297
+ risk: "low",
298
+ depends: [],
299
+ demo: "",
300
+ sequence: 1,
301
+ });
302
+ insertGateRow({
303
+ milestoneId: "M001",
304
+ sliceId: "S01",
305
+ gateId: "Q3",
306
+ scope: "slice",
307
+ });
308
+
309
+ const prompt = await buildGateEvaluatePrompt(
310
+ "M001",
311
+ "Test Milestone",
312
+ "S01",
313
+ "Test Slice",
314
+ repo,
315
+ "claude-opus-4-6",
316
+ );
317
+
318
+ assert.ok(
319
+ prompt.includes("Context Mode (verification lane):"),
320
+ "embedded gate-evaluate prompt should use nested Context Mode guidance",
321
+ );
322
+ assert.ok(
323
+ prompt.includes("## Context Mode"),
324
+ "gate-evaluate parent prompt should include standalone Context Mode guidance",
325
+ );
326
+ assert.ok(prompt.includes('model: "claude-opus-4-6"'), "gate subagent prompt should preserve model instruction");
327
+ });
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
  import { tmpdir } from "node:os";
10
10
 
11
11
  import {
12
+ composeContextModeInstructions,
12
13
  composeInlinedContext,
13
14
  composeUnitContext,
14
15
  manifestBudgetChars,
@@ -101,6 +102,43 @@ test("#4782 composer: manifestBudgetChars returns declared budget", () => {
101
102
  assert.strictEqual(manifestBudgetChars("never-dispatched"), null);
102
103
  });
103
104
 
105
+ test("Context Mode composer: disabled, unknown, and none modes return empty string", () => {
106
+ assert.strictEqual(
107
+ composeContextModeInstructions("execute-task", { enabled: false, renderMode: "standalone" }),
108
+ "",
109
+ );
110
+ assert.strictEqual(
111
+ composeContextModeInstructions("never-dispatched", { enabled: true, renderMode: "standalone" }),
112
+ "",
113
+ );
114
+ assert.strictEqual(
115
+ composeContextModeInstructions("workflow-preferences", { enabled: true, renderMode: "standalone" }),
116
+ "",
117
+ );
118
+ });
119
+
120
+ test("Context Mode composer: standalone output starts with heading and includes required tools", () => {
121
+ const out = composeContextModeInstructions("execute-task", { enabled: true, renderMode: "standalone" });
122
+ assert.ok(out.startsWith("## Context Mode"));
123
+ assert.match(out, /execution lane/i);
124
+ assert.match(out, /`gsd_exec`/);
125
+ assert.match(out, /noisy scans, builds, and tests/);
126
+ assert.match(out, /`gsd_exec_search`/);
127
+ assert.match(out, /before repeating prior runs/);
128
+ assert.match(out, /`gsd_resume`/);
129
+ assert.match(out, /after compaction or resume/);
130
+ });
131
+
132
+ test("Context Mode composer: nested output is compact single sentence", () => {
133
+ const out = composeContextModeInstructions("gate-evaluate", { enabled: true, renderMode: "nested" });
134
+ assert.ok(!out.startsWith("## Context Mode"));
135
+ assert.match(out, /^Context Mode \(verification lane\): /);
136
+ assert.strictEqual(out.split(/\n/).length, 1);
137
+ assert.match(out, /`gsd_exec`/);
138
+ assert.match(out, /`gsd_exec_search`/);
139
+ assert.match(out, /`gsd_resume`/);
140
+ });
141
+
104
142
  // ─── Integration: migrated buildReassessRoadmapPrompt ─────────────────────
105
143
 
106
144
  function makeFixtureBase(): string {
@@ -12,6 +12,7 @@ import {
12
12
  UNIT_MANIFESTS,
13
13
  resolveManifest,
14
14
  type ArtifactKey,
15
+ type ContextModePolicy,
15
16
  type SkillsPolicy,
16
17
  type UnitContextManifest,
17
18
  } from "../unit-context-manifest.ts";
@@ -105,6 +106,37 @@ test("#4782 phase 1: every manifest has a positive maxSystemPromptChars", () =>
105
106
  }
106
107
  });
107
108
 
109
+ test("Context Mode: every manifest declares the expected contextMode lane", () => {
110
+ const expected: Record<string, ContextModePolicy> = {
111
+ "workflow-preferences": "none",
112
+ "research-decision": "none",
113
+ "discuss-project": "interview",
114
+ "discuss-requirements": "interview",
115
+ "discuss-milestone": "interview",
116
+ "research-project": "research",
117
+ "research-milestone": "research",
118
+ "research-slice": "research",
119
+ "plan-milestone": "planning",
120
+ "plan-slice": "planning",
121
+ "refine-slice": "planning",
122
+ "replan-slice": "planning",
123
+ "reassess-roadmap": "planning",
124
+ "execute-task": "execution",
125
+ "reactive-execute": "execution",
126
+ "run-uat": "verification",
127
+ "gate-evaluate": "verification",
128
+ "validate-milestone": "verification",
129
+ "complete-slice": "verification",
130
+ "complete-milestone": "verification",
131
+ "rewrite-docs": "docs",
132
+ };
133
+
134
+ assert.deepEqual(Object.keys(expected).sort(), [...KNOWN_UNIT_TYPES].sort());
135
+ for (const unitType of KNOWN_UNIT_TYPES) {
136
+ assert.strictEqual(UNIT_MANIFESTS[unitType].contextMode, expected[unitType]);
137
+ }
138
+ });
139
+
108
140
  test("#4782 phase 1: skills policy shapes are valid discriminated-union members", () => {
109
141
  for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
110
142
  const p = manifest.skills as SkillsPolicy;
@@ -11,7 +11,9 @@ import {
11
11
  _getAdapter,
12
12
  insertGateRow,
13
13
  upsertRequirement,
14
+ getAllMilestones,
14
15
  } from "../gsd-db.ts";
16
+ import { deriveState, invalidateStateCache } from "../state.ts";
15
17
  import { markApprovalGateVerified, markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot, setPendingGate } from "../bootstrap/write-gate.ts";
16
18
  import {
17
19
  executeCompleteMilestone,
@@ -737,6 +739,104 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
737
739
  }
738
740
  });
739
741
 
742
+ test("executeSummarySave registers PROJECT milestone sequence for the next run", async () => {
743
+ const base = makeTmpBase();
744
+ try {
745
+ openTestDb(base);
746
+
747
+ const result = await inProjectDir(base, () => executeSummarySave({
748
+ artifact_type: "PROJECT",
749
+ content: [
750
+ "# Project",
751
+ "",
752
+ "## What This Is",
753
+ "",
754
+ "Deep project setup output.",
755
+ "",
756
+ "## Project Shape",
757
+ "",
758
+ "**Complexity:** complex",
759
+ "**Why:** It spans multiple delivery steps.",
760
+ "",
761
+ "## Capability Contract",
762
+ "",
763
+ "See .gsd/REQUIREMENTS.md.",
764
+ "",
765
+ "## Milestone Sequence",
766
+ "",
767
+ "- [ ] M001: Foundation - Establish the first runnable slice.",
768
+ "- [ ] M002: Polish - Follow-up experience work.",
769
+ "",
770
+ ].join("\n"),
771
+ }, base));
772
+
773
+ assert.equal(result.isError, undefined);
774
+ assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
775
+
776
+ const milestones = getAllMilestones();
777
+ assert.deepEqual(
778
+ milestones.map((m) => [m.id, m.title, m.status]),
779
+ [
780
+ ["M001", "Foundation", "queued"],
781
+ ["M002", "Polish", "queued"],
782
+ ],
783
+ );
784
+
785
+ invalidateStateCache();
786
+ const state = await deriveState(base);
787
+ assert.equal(state.activeMilestone?.id, "M001");
788
+ assert.equal(state.phase, "pre-planning");
789
+ assert.equal(state.registry[0]?.status, "active");
790
+ assert.equal(state.registry[1]?.status, "pending");
791
+ } finally {
792
+ closeDatabase();
793
+ cleanup(base);
794
+ }
795
+ });
796
+
797
+ test("executeSummarySave keeps PROJECT artifact save successful if milestone registration fails", async () => {
798
+ const base = makeTmpBase();
799
+ try {
800
+ openTestDb(base);
801
+ const db = _getAdapter();
802
+ assert.ok(db, "DB should be open");
803
+ const originalPrepare = db.prepare.bind(db);
804
+ (db as any).prepare = (sql: string) => {
805
+ if (sql.includes("INSERT OR IGNORE INTO milestones")) {
806
+ throw new Error("simulated milestone registration failure");
807
+ }
808
+ return originalPrepare(sql);
809
+ };
810
+
811
+ const result = await inProjectDir(base, () => executeSummarySave({
812
+ artifact_type: "PROJECT",
813
+ content: [
814
+ "# Project",
815
+ "",
816
+ "## What This Is",
817
+ "",
818
+ "Deep project setup output.",
819
+ "",
820
+ "## Milestone Sequence",
821
+ "",
822
+ "- [ ] M001: Foundation - Establish the first runnable slice.",
823
+ "",
824
+ ].join("\n"),
825
+ }, base));
826
+
827
+ assert.equal(result.isError, undefined);
828
+ assert.equal(result.details.path, "PROJECT.md");
829
+ assert.equal(result.details.registeredMilestones, undefined);
830
+ assert.match(String(result.details.warning), /milestone registration failed/);
831
+ assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
832
+ const artifact = originalPrepare("SELECT path FROM artifacts WHERE path = ?").get("PROJECT.md");
833
+ assert.equal(artifact?.path, "PROJECT.md");
834
+ } finally {
835
+ closeDatabase();
836
+ cleanup(base);
837
+ }
838
+ });
839
+
740
840
  test("executeSummarySave blocks final root artifacts while approval gate is pending", async () => {
741
841
  const base = makeTmpBase();
742
842
  try {
@@ -229,6 +229,9 @@ test("worktree-aware prompt builders include the explicit working directory", as
229
229
  ),
230
230
  ]);
231
231
 
232
+ assert.ok(prompts[0].includes("## Context Mode"), "discuss-milestone should include standalone Context Mode guidance");
233
+ assert.ok(prompts[0].includes("interview lane"), "discuss-milestone should render the interview lane");
234
+
232
235
  for (const prompt of prompts) {
233
236
  assert.match(prompt, /working directory/i);
234
237
  assert.ok(prompt.includes(base), "prompt should include the provided working directory");