@opengsd/gsd-pi 1.8.1-dev.897b3c90 → 1.9.0-dev.5c1a3ff2

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 (194) hide show
  1. package/README.md +10 -8
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/GSD-WORKFLOW.md +2 -2
  4. package/dist/resources/extensions/gsd/artifact-verification.js +3 -4
  5. package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -8
  6. package/dist/resources/extensions/gsd/auto-prompts.js +90 -17
  7. package/dist/resources/extensions/gsd/auto-recovery.js +10 -5
  8. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +4 -0
  9. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +1 -1
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -5
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +10 -3
  13. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -24
  14. package/dist/resources/extensions/gsd/constants.js +12 -13
  15. package/dist/resources/extensions/gsd/context-masker.js +97 -21
  16. package/dist/resources/extensions/gsd/docs/preferences-reference.md +22 -0
  17. package/dist/resources/extensions/gsd/doctor-engine-checks.js +3 -6
  18. package/dist/resources/extensions/gsd/git-service.js +19 -7
  19. package/dist/resources/extensions/gsd/guided-flow.js +9 -3
  20. package/dist/resources/extensions/gsd/markdown-renderer.js +2 -12
  21. package/dist/resources/extensions/gsd/md-importer.js +5 -6
  22. package/dist/resources/extensions/gsd/model-cost-table.js +2 -0
  23. package/dist/resources/extensions/gsd/model-router.js +11 -0
  24. package/dist/resources/extensions/gsd/paths.js +51 -10
  25. package/dist/resources/extensions/gsd/planning-subagent-policy.js +37 -0
  26. package/dist/resources/extensions/gsd/planning-subagent-registry.js +25 -0
  27. package/dist/resources/extensions/gsd/preferences-types.js +5 -0
  28. package/dist/resources/extensions/gsd/preferences-validation.js +48 -1
  29. package/dist/resources/extensions/gsd/preferences.js +17 -0
  30. package/dist/resources/extensions/gsd/provider-payload-policy.js +16 -5
  31. package/dist/resources/extensions/gsd/safety/destructive-guard.js +1 -1
  32. package/dist/resources/extensions/gsd/skill-activation.js +17 -3
  33. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
  34. package/dist/resources/extensions/gsd/tools/complete-task.js +12 -5
  35. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +14 -5
  36. package/dist/resources/extensions/gsd/tools/reopen-slice.js +15 -5
  37. package/dist/resources/extensions/gsd/tools/reopen-task.js +16 -10
  38. package/dist/resources/extensions/gsd/unit-context-composer.js +28 -5
  39. package/dist/resources/extensions/gsd/workflow-projections.js +2 -3
  40. package/dist/resources/extensions/gsd/workspace.js +2 -1
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  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/index.html +1 -1
  63. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  70. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  76. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  77. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  78. package/integrations/hermes/pyproject.toml +1 -1
  79. package/package.json +6 -6
  80. package/packages/cloud-mcp-gateway/package.json +2 -2
  81. package/packages/contracts/package.json +1 -1
  82. package/packages/daemon/package.json +4 -4
  83. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  84. package/packages/gsd-agent-core/dist/compaction/compaction.js +36 -4
  85. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  86. package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
  87. package/packages/gsd-agent-core/dist/sdk.js +1 -0
  88. package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
  89. package/packages/gsd-agent-core/package.json +5 -5
  90. package/packages/gsd-agent-modes/package.json +7 -7
  91. package/packages/gsd-cloud/package.json +1 -1
  92. package/packages/mcp-server/README.md +7 -2
  93. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/server.js +5 -5
  95. package/packages/mcp-server/dist/server.js.map +1 -1
  96. package/packages/mcp-server/dist/workflow-tools.d.ts +4 -3
  97. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  98. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  99. package/packages/mcp-server/package.json +4 -4
  100. package/packages/native/package.json +1 -1
  101. package/packages/pi-agent-core/dist/agent.d.ts +5 -1
  102. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  103. package/packages/pi-agent-core/dist/agent.js +2 -0
  104. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  105. package/packages/pi-agent-core/package.json +1 -1
  106. package/packages/pi-ai/dist/models.generated.d.ts +59 -0
  107. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.js +53 -0
  109. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/anthropic.js +6 -1
  111. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  112. package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
  113. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  114. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  115. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  116. package/packages/pi-ai/package.json +1 -1
  117. package/packages/pi-coding-agent/dist/core/capability-patches.js +1 -1
  118. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
  120. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +11 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  124. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  125. package/packages/pi-coding-agent/package.json +7 -7
  126. package/packages/pi-tui/package.json +2 -2
  127. package/packages/rpc-client/package.json +2 -2
  128. package/pkg/package.json +1 -1
  129. package/src/resources/GSD-WORKFLOW.md +2 -2
  130. package/src/resources/extensions/gsd/artifact-verification.ts +3 -8
  131. package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -7
  132. package/src/resources/extensions/gsd/auto-prompts.ts +104 -15
  133. package/src/resources/extensions/gsd/auto-recovery.ts +10 -8
  134. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +4 -0
  135. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +1 -1
  136. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -0
  137. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +39 -10
  138. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -3
  139. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +7 -29
  140. package/src/resources/extensions/gsd/constants.ts +14 -13
  141. package/src/resources/extensions/gsd/context-masker.ts +98 -2
  142. package/src/resources/extensions/gsd/docs/preferences-reference.md +22 -0
  143. package/src/resources/extensions/gsd/doctor-engine-checks.ts +3 -5
  144. package/src/resources/extensions/gsd/git-service.ts +19 -6
  145. package/src/resources/extensions/gsd/guided-flow.ts +9 -2
  146. package/src/resources/extensions/gsd/markdown-renderer.ts +2 -14
  147. package/src/resources/extensions/gsd/md-importer.ts +5 -5
  148. package/src/resources/extensions/gsd/model-cost-table.ts +2 -0
  149. package/src/resources/extensions/gsd/model-router.ts +12 -0
  150. package/src/resources/extensions/gsd/paths.ts +58 -10
  151. package/src/resources/extensions/gsd/planning-subagent-policy.ts +58 -0
  152. package/src/resources/extensions/gsd/planning-subagent-registry.ts +31 -0
  153. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  154. package/src/resources/extensions/gsd/preferences-validation.ts +62 -0
  155. package/src/resources/extensions/gsd/preferences.ts +25 -0
  156. package/src/resources/extensions/gsd/provider-payload-policy.ts +18 -4
  157. package/src/resources/extensions/gsd/safety/destructive-guard.ts +1 -1
  158. package/src/resources/extensions/gsd/skill-activation.ts +17 -3
  159. package/src/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
  160. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +7 -7
  161. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +39 -0
  162. package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +55 -0
  163. package/src/resources/extensions/gsd/tests/complete-task.test.ts +76 -1
  164. package/src/resources/extensions/gsd/tests/context-masker.test.ts +204 -8
  165. package/src/resources/extensions/gsd/tests/db-tools-alias-suppression.test.ts +62 -0
  166. package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +33 -0
  167. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +14 -0
  168. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +22 -4
  169. package/src/resources/extensions/gsd/tests/integration-branch-meta-location.test.ts +62 -0
  170. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +5 -5
  171. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
  172. package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +7 -5
  174. package/src/resources/extensions/gsd/tests/preferences.test.ts +84 -0
  175. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +4 -0
  176. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +66 -0
  177. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +39 -0
  178. package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
  179. package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +33 -7
  180. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -16
  181. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +13 -7
  182. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -0
  183. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +39 -0
  184. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +4 -4
  185. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +34 -0
  186. package/src/resources/extensions/gsd/tools/complete-task.ts +20 -5
  187. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +24 -5
  188. package/src/resources/extensions/gsd/tools/reopen-slice.ts +25 -5
  189. package/src/resources/extensions/gsd/tools/reopen-task.ts +23 -9
  190. package/src/resources/extensions/gsd/unit-context-composer.ts +34 -8
  191. package/src/resources/extensions/gsd/workflow-projections.ts +2 -3
  192. package/src/resources/extensions/gsd/workspace.ts +2 -1
  193. /package/dist/web/standalone/.next/static/{TezjdjSHBVRRr54XjAvte → 6YWV7VAPg-jqjfPswrKZb}/_buildManifest.js +0 -0
  194. /package/dist/web/standalone/.next/static/{TezjdjSHBVRRr54XjAvte → 6YWV7VAPg-jqjfPswrKZb}/_ssgManifest.js +0 -0
@@ -823,6 +823,90 @@ test("pre-dispatch hook action validation via validatePreferences", () => {
823
823
  assert.ok(e3.some(e => e.includes("invalid action")));
824
824
  });
825
825
 
826
+ test("planning_subagents validation accepts configured read-only planning specialists", () => {
827
+ const { preferences, errors } = validatePreferences({
828
+ planning_subagents: {
829
+ "plan-milestone": { allowed: ["security", "reviewer"] },
830
+ "plan-slice": { allowed: ["scout", "security", "security"] },
831
+ },
832
+ } as any);
833
+
834
+ assert.equal(errors.length, 0);
835
+ assert.deepEqual(preferences.planning_subagents?.["plan-milestone"]?.allowed, ["security", "reviewer"]);
836
+ assert.deepEqual(preferences.planning_subagents?.["plan-slice"]?.allowed, ["scout", "security"]);
837
+ });
838
+
839
+ test("planning_subagents validation rejects unsupported units and unsafe agents", () => {
840
+ const { preferences, errors } = validatePreferences({
841
+ planning_subagents: {
842
+ "execute-task": { allowed: ["security"] },
843
+ "plan-slice": { allowed: ["worker"] },
844
+ },
845
+ } as any);
846
+
847
+ assert.equal(preferences.planning_subagents, undefined);
848
+ assert.ok(errors.some(e => e.includes("unsupported unit \"execute-task\"")));
849
+ assert.ok(errors.some(e => e.includes("\"worker\"") && e.includes("read-only planning specialist")));
850
+ });
851
+
852
+ test("loadEffectiveGSDPreferences merges planning_subagents allowlists", (t) => {
853
+ const originalCwd = process.cwd();
854
+ const originalGsdHome = process.env.GSD_HOME;
855
+ const tempProject = mkdtempSync(join(tmpdir(), "gsd-planning-subagents-project-"));
856
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-planning-subagents-home-"));
857
+
858
+ t.after(() => {
859
+ clearGSDPreferencesCache();
860
+ process.chdir(originalCwd);
861
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
862
+ else process.env.GSD_HOME = originalGsdHome;
863
+ rmSync(tempProject, { recursive: true, force: true });
864
+ rmSync(tempGsdHome, { recursive: true, force: true });
865
+ });
866
+
867
+ mkdirSync(join(tempProject, ".gsd"), { recursive: true });
868
+ process.env.GSD_HOME = tempGsdHome;
869
+ process.chdir(tempProject);
870
+ clearGSDPreferencesCache();
871
+
872
+ writeFileSync(
873
+ getGlobalGSDPreferencesPath(),
874
+ [
875
+ "---",
876
+ "version: 1",
877
+ "planning_subagents:",
878
+ " plan-slice:",
879
+ " allowed: [scout, reviewer]",
880
+ "---",
881
+ "",
882
+ ].join("\n"),
883
+ "utf-8",
884
+ );
885
+ writeFileSync(
886
+ getProjectGSDPreferencesPath(tempProject),
887
+ [
888
+ "---",
889
+ "version: 1",
890
+ "planning_subagents:",
891
+ " plan-slice:",
892
+ " allowed: [security]",
893
+ " plan-milestone:",
894
+ " allowed: [planner]",
895
+ "---",
896
+ "",
897
+ ].join("\n"),
898
+ "utf-8",
899
+ );
900
+
901
+ const loaded = loadEffectiveGSDPreferences(tempProject);
902
+ assert.deepEqual(loaded?.preferences.planning_subagents?.["plan-slice"]?.allowed, [
903
+ "scout",
904
+ "reviewer",
905
+ "security",
906
+ ]);
907
+ assert.deepEqual(loaded?.preferences.planning_subagents?.["plan-milestone"]?.allowed, ["planner"]);
908
+ });
909
+
826
910
  // ── Model config parsing ─────────────────────────────────────────────────────
827
911
 
828
912
  test("parses OpenRouter model config with org/model IDs and fallbacks", () => {
@@ -54,6 +54,10 @@ const PREF_SAMPLE_VALUES: Record<string, unknown> = {
54
54
  },
55
55
  post_unit_hooks: [{ command: "npm test" }],
56
56
  pre_dispatch_hooks: [{ command: "npm run lint" }],
57
+ planning_subagents: {
58
+ "plan-milestone": { allowed: ["scout", "planner"] },
59
+ "plan-slice": { allowed: ["scout"] },
60
+ },
57
61
  dynamic_routing: { enabled: true },
58
62
  disabled_model_providers: ["slow-provider"],
59
63
  uok: { enabled: true },
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
13
13
  import { fileURLToPath } from "node:url";
14
14
 
15
15
  import { buildExecuteTaskPrompt, buildPlanSlicePrompt, buildReactiveExecutePrompt, buildResearchSlicePrompt, inlineDependencySummaries } from "../auto-prompts.js";
16
+ import { buildDiscussSlicePrompt } from "../guided-flow.js";
16
17
  import { computeBudgets, truncateAtSectionBoundary } from "../context-budget.js";
17
18
 
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -735,3 +736,68 @@ describe("prompt-budget: modelRegistry + sessionContextWindow behavior", () => {
735
736
  }
736
737
  });
737
738
  });
739
+
740
+ // ─── execute-task + discuss-slice inline caps (plan 039) ──────────────────────
741
+
742
+ describe("prompt-budget: execute-task inline cap (039)", () => {
743
+ it("keeps the authoritative task plan whole but drops trailing templates when the plan alone exceeds the inline budget", async () => {
744
+ const base = createFixtureBase();
745
+ try {
746
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
747
+ const taskDir = join(sliceDir, "tasks");
748
+ mkdirSync(taskDir, { recursive: true });
749
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# Roadmap\n");
750
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# Slice Plan\n");
751
+ // Oversized task plan (~60K) — well past the 20K inline ceiling.
752
+ const bigPlan = "# Task Plan\n\nMARKER_TASKPLAN_HEAD\n\n" + Array.from({ length: 30 }, (_, i) =>
753
+ `### Step ${i}\n\n${"Implementation detail. ".repeat(90)}`
754
+ ).join("\n\n");
755
+ writeFileSync(join(taskDir, "T01-PLAN.md"), bigPlan);
756
+
757
+ const prompt = await buildExecuteTaskPrompt("M001", "S01", "Slice", "T01", "Task", base, {
758
+ level: "standard",
759
+ sessionContextWindow: 32_000,
760
+ });
761
+
762
+ // The task plan is the authoritative execution contract — never truncated;
763
+ // its content survives whole even when it alone exceeds the inline budget.
764
+ assert.match(prompt, /MARKER_TASKPLAN_HEAD/);
765
+ assert.match(prompt, /### Step 29/);
766
+ // Trailing lower-priority templates are dropped — the on-demand decisions
767
+ // template that "standard" normally inlines is gone because the task plan
768
+ // consumed the entire inline budget. (Baseline: the tiny-plan test above
769
+ // asserts this block IS present.)
770
+ assert.doesNotMatch(prompt, /### On-demand Decisions Template/);
771
+ } finally {
772
+ cleanup(base);
773
+ }
774
+ });
775
+ });
776
+
777
+ describe("prompt-budget: discuss-slice inline cap (039)", () => {
778
+ it("caps the inlined block — roadmap survives first, the trailing decisions register truncates", async () => {
779
+ const base = createFixtureBase();
780
+ try {
781
+ const msDir = join(base, ".gsd", "milestones", "M001");
782
+ mkdirSync(join(msDir, "slices", "S01"), { recursive: true });
783
+ writeFileSync(join(msDir, "M001-ROADMAP.md"), "# Roadmap\n\nMARKER_ROADMAP\n\n- [ ] **S01: Current** `risk:low`\n");
784
+ writeFileSync(join(msDir, "M001-CONTEXT.md"), "# Context\n\nMARKER_CONTEXT\n");
785
+ // Huge decisions register (~60K) — the unbounded-growth surface the cap bounds.
786
+ const bigDecisions = "# Decisions\n\n" + Array.from({ length: 40 }, (_, i) =>
787
+ `### D${String(i).padStart(3, "0")}\n\n${"Decision rationale detail. ".repeat(60)}`
788
+ ).join("\n\n");
789
+ writeFileSync(join(base, ".gsd", "DECISIONS.md"), bigDecisions);
790
+
791
+ const prompt = await buildDiscussSlicePrompt("M001", "S01", "Current", base);
792
+
793
+ // Roadmap is first in the inlined block → survives the cap.
794
+ assert.match(prompt, /MARKER_ROADMAP/);
795
+ // The cap fired (capPreamble ceiling is 20K; the block is ~60K).
796
+ assert.match(prompt, /\[\.\.\.truncated/);
797
+ // The last decision (trailing content) is dropped.
798
+ assert.doesNotMatch(prompt, /### D039/);
799
+ } finally {
800
+ cleanup(base);
801
+ }
802
+ });
803
+ });
@@ -608,3 +608,42 @@ test("getDependencyTaskSummaryPaths handles missing dependency summaries gracefu
608
608
  rmSync(repo, { recursive: true, force: true });
609
609
  }
610
610
  });
611
+
612
+ test("#1343: getPriorTaskSummaryPaths excludes sibling-slice summaries in flat-phase", async () => {
613
+ const { getPriorTaskSummaryPaths } = await import("../auto-prompts.ts");
614
+ const repo = mkdtempSync(join(tmpdir(), "gsd-reactive-flat-prior-"));
615
+ try {
616
+ // Flat-phase: slices S01 and S02 share the phase dir and overlap task ids.
617
+ const phaseDir = join(repo, ".gsd", "phases", "01-test");
618
+ mkdirSync(phaseDir, { recursive: true });
619
+ writeFileSync(join(phaseDir, "S01-T01-SUMMARY.md"), "---\nid: T01\n---\n# S01 T01\n");
620
+ writeFileSync(join(phaseDir, "S02-T01-SUMMARY.md"), "---\nid: T01\n---\n# S02 T01\n");
621
+
622
+ // S02/T02 prior summaries must not pull the sibling S01-T01 summary.
623
+ const paths = await getPriorTaskSummaryPaths("M001", "S02", "T02", repo);
624
+ assert.equal(paths.length, 1, "Should only return the current slice's prior summary");
625
+ assert.ok(paths[0].includes("S02-T01-SUMMARY"), "Should include S02's T01");
626
+ assert.ok(!paths.some((p) => p.includes("S01-T01-SUMMARY")), "Should NOT include sibling S01's T01");
627
+ } finally {
628
+ rmSync(repo, { recursive: true, force: true });
629
+ }
630
+ });
631
+
632
+ test("#1343: getDependencyTaskSummaryPaths excludes sibling-slice summaries in flat-phase", async () => {
633
+ const { getDependencyTaskSummaryPaths } = await import("../auto-prompts.ts");
634
+ const repo = mkdtempSync(join(tmpdir(), "gsd-reactive-flat-dep-"));
635
+ try {
636
+ const phaseDir = join(repo, ".gsd", "phases", "01-test");
637
+ mkdirSync(phaseDir, { recursive: true });
638
+ writeFileSync(join(phaseDir, "S01-T01-SUMMARY.md"), "---\nid: T01\n---\n# S01 T01\n");
639
+ writeFileSync(join(phaseDir, "S02-T01-SUMMARY.md"), "---\nid: T01\n---\n# S02 T01\n");
640
+
641
+ // S02/T02 depends on T01 — must resolve S02's T01, not the sibling S01's.
642
+ const paths = await getDependencyTaskSummaryPaths("M001", "S02", "T02", ["T01"], repo);
643
+ assert.equal(paths.length, 1, "Should only return the current slice's dependency summary");
644
+ assert.ok(paths[0].includes("S02-T01-SUMMARY"), "Should include S02's T01");
645
+ assert.ok(!paths.some((p) => p.includes("S01-T01-SUMMARY")), "Should NOT include sibling S01's T01");
646
+ } finally {
647
+ rmSync(repo, { recursive: true, force: true });
648
+ }
649
+ });
@@ -35,6 +35,10 @@ describe("supportsServiceTier", () => {
35
35
  assert.equal(supportsServiceTier("gpt-5.5"), false);
36
36
  });
37
37
 
38
+ test("returns false for gpt-5.6 until service_tier payload support is verified", () => {
39
+ assert.equal(supportsServiceTier("gpt-5.6"), false);
40
+ });
41
+
38
42
  test("returns false for provider-only identifier without gpt-5.4 model suffix", () => {
39
43
  assert.equal(supportsServiceTier("vibeproxy-openai"), false);
40
44
  });
@@ -7,8 +7,9 @@
7
7
  * self-identify (Stream 2: actor identity passthrough).
8
8
  *
9
9
  * 2. The 3 reversibility handlers (reopen-task/slice/milestone) are
10
- * registered as MCP tools with both canonical and alias names
11
- * (Stream 3: reversibility tools).
10
+ * registered as MCP tools under their canonical names, and additionally
11
+ * under their alias names when GSD_ADVERTISE_TOOL_ALIASES=1 (plan 035
12
+ * suppresses alias schemas by default) (Stream 3: reversibility tools).
12
13
  *
13
14
  * 3. The reopen tools accept the documented core params plus optional
14
15
  * reason/actorName/triggerReason without rejecting valid payloads.
@@ -36,6 +37,18 @@ function getTool(name: string) {
36
37
  return pi.tools.find((t: any) => t.name === name);
37
38
  }
38
39
 
40
+ function withEnv(name: string, value: string | undefined, fn: () => void): void {
41
+ const previous = process.env[name];
42
+ if (value === undefined) delete process.env[name];
43
+ else process.env[name] = value;
44
+ try {
45
+ fn();
46
+ } finally {
47
+ if (previous === undefined) delete process.env[name];
48
+ else process.env[name] = previous;
49
+ }
50
+ }
51
+
39
52
  function getRequiredProps(tool: any): string[] {
40
53
  return tool.parameters.required ?? [];
41
54
  }
@@ -78,7 +91,9 @@ for (const name of ACTOR_TOOLS) {
78
91
  });
79
92
  }
80
93
 
81
- // ─── Stream 3: reopen tools registered with canonical + alias names ─────────
94
+ // ─── Stream 3: reopen tools registered under canonical names ────────────────
95
+ // Plan 035 suppresses alias schemas by default; the canonical reopen tools are
96
+ // always registered, and their aliases only when GSD_ADVERTISE_TOOL_ALIASES=1.
82
97
 
83
98
  const REOPEN_TOOLS = [
84
99
  { canonical: "gsd_task_reopen", alias: "gsd_reopen_task" },
@@ -87,13 +102,24 @@ const REOPEN_TOOLS = [
87
102
  ];
88
103
 
89
104
  for (const { canonical, alias } of REOPEN_TOOLS) {
90
- test(`${canonical} — registered with alias ${alias}`, () => {
105
+ test(`${canonical} — registered under its canonical name, no alias by default`, () => {
91
106
  const canonicalTool = getTool(canonical);
92
- const aliasTool = getTool(alias);
93
107
  assert.ok(canonicalTool, `${canonical} must be registered`);
94
- assert.ok(aliasTool, `${alias} must be registered as alias`);
95
108
  assert.ok(typeof canonicalTool.execute === "function", `${canonical} must have an execute function`);
96
- assert.ok(typeof aliasTool.execute === "function", `${alias} must have an execute function`);
109
+ assert.ok(!getTool(alias), `${alias} must not be registered by default (plan 035 alias suppression)`);
110
+ });
111
+
112
+ test(`${canonical} — registered with alias ${alias} when GSD_ADVERTISE_TOOL_ALIASES=1`, () => {
113
+ withEnv("GSD_ADVERTISE_TOOL_ALIASES", "1", () => {
114
+ const aliasPi = makeMockPi();
115
+ registerDbTools(aliasPi);
116
+ const canonicalTool = aliasPi.tools.find((t: any) => t.name === canonical);
117
+ const aliasTool = aliasPi.tools.find((t: any) => t.name === alias);
118
+ assert.ok(canonicalTool, `${canonical} must be registered`);
119
+ assert.ok(aliasTool, `${alias} must be registered as alias`);
120
+ assert.ok(typeof canonicalTool.execute === "function", `${canonical} must have an execute function`);
121
+ assert.ok(typeof aliasTool.execute === "function", `${alias} must have an execute function`);
122
+ });
97
123
  });
98
124
  }
99
125
 
@@ -56,6 +56,10 @@ function buildBlock(
56
56
  });
57
57
  }
58
58
 
59
+ function expectedSkillRead(base: string, name: string): string {
60
+ return `Read the skill file at \`${join(base, "skills", name, "SKILL.md")}\``;
61
+ }
62
+
59
63
  test("buildSkillActivationBlock does not auto-activate skills via broad context heuristic", () => {
60
64
  const base = makeTempBase();
61
65
  try {
@@ -92,7 +96,7 @@ test("buildSkillActivationBlock activates skills via prefer_skills when context
92
96
  prefer_skills: ["react"],
93
97
  });
94
98
 
95
- assert.match(result, /Read the installed 'react' skill file from <available_skills>/);
99
+ assert.ok(result.includes(expectedSkillRead(base, "react")));
96
100
  assert.doesNotMatch(result, /swiftui/);
97
101
  } finally {
98
102
  cleanup(base);
@@ -111,7 +115,7 @@ test("buildSkillActivationBlock includes always_use_skills using read-based skil
111
115
 
112
116
  assert.equal(
113
117
  result,
114
- "<skill_activation>Read the installed 'swift-testing' skill file from <available_skills>.</skill_activation>",
118
+ `<skill_activation>Read the skill file at \`${join(base, "skills", "swift-testing", "SKILL.md")}\`.</skill_activation>`,
115
119
  );
116
120
  } finally {
117
121
  cleanup(base);
@@ -140,8 +144,8 @@ test("buildSkillActivationBlock includes skill_rules matches and task-plan skill
140
144
  skill_rules: [{ when: "prisma database schema", use: ["prisma"] }],
141
145
  });
142
146
 
143
- assert.match(result, /Read the installed 'accessibility' skill file from <available_skills>/);
144
- assert.match(result, /Read the installed 'prisma' skill file from <available_skills>/);
147
+ assert.ok(result.includes(expectedSkillRead(base, "accessibility")));
148
+ assert.ok(result.includes(expectedSkillRead(base, "prisma")));
145
149
  } finally {
146
150
  cleanup(base);
147
151
  }
@@ -163,7 +167,7 @@ test("buildSkillActivationBlock matches skill_rules against exact unit type cont
163
167
  ],
164
168
  });
165
169
 
166
- assert.match(result, /Read the installed 'complete-slice-policies' skill file from <available_skills>/);
170
+ assert.ok(result.includes(expectedSkillRead(base, "complete-slice-policies")));
167
171
  assert.doesNotMatch(result, /slice-broad/);
168
172
  } finally {
169
173
  cleanup(base);
@@ -267,8 +271,8 @@ test("buildSkillActivationBlock allows valid skill names and rejects invalid one
267
271
  });
268
272
 
269
273
  assert.match(result, /skill_activation/);
270
- assert.match(result, /Read the installed 'react' skill file from <available_skills>/);
271
- assert.match(result, /Read the installed 'good-skill-2' skill file from <available_skills>/);
274
+ assert.ok(result.includes(expectedSkillRead(base, "react")));
275
+ assert.ok(result.includes(expectedSkillRead(base, "good-skill-2")));
272
276
  assert.doesNotMatch(result, /bad'name/);
273
277
  } finally {
274
278
  cleanup(base);
@@ -292,8 +296,8 @@ test("buildSkillActivationBlock: explicit always_use_skills bypass the unit-type
292
296
  always_use_skills: ["write-docs", "swiftui"],
293
297
  });
294
298
 
295
- assert.match(result, /Read the installed 'write-docs' skill file from <available_skills>/);
296
- assert.match(result, /Read the installed 'swiftui' skill file from <available_skills>/);
299
+ assert.ok(result.includes(expectedSkillRead(base, "write-docs")));
300
+ assert.ok(result.includes(expectedSkillRead(base, "swiftui")));
297
301
  } finally {
298
302
  cleanup(base);
299
303
  }
@@ -310,7 +314,7 @@ test("buildSkillActivationBlock falls through to all skills for unknown unit typ
310
314
  });
311
315
 
312
316
  // Unknown unit type = wildcard fallback (pre-manifest behavior).
313
- assert.match(result, /Read the installed 'swiftui' skill file from <available_skills>/);
317
+ assert.ok(result.includes(expectedSkillRead(base, "swiftui")));
314
318
  } finally {
315
319
  cleanup(base);
316
320
  }
@@ -327,7 +331,7 @@ test("buildSkillActivationBlock without unitType preserves pre-manifest behavior
327
331
  always_use_skills: ["swiftui"],
328
332
  });
329
333
 
330
- assert.match(result, /Read the installed 'swiftui' skill file from <available_skills>/);
334
+ assert.ok(result.includes(expectedSkillRead(base, "swiftui")));
331
335
  } finally {
332
336
  cleanup(base);
333
337
  }
@@ -344,12 +348,12 @@ test("milestone prompt builders propagate always_use_skills through buildSkillAc
344
348
  loadOnlyTestSkills(base);
345
349
 
346
350
  const researchPrompt = await buildResearchMilestonePrompt("M001", "Test", base);
347
- assert.match(researchPrompt, /Read the installed 'write-docs' skill file from <available_skills>/);
348
- assert.match(researchPrompt, /Read the installed 'swiftui' skill file from <available_skills>/);
351
+ assert.ok(researchPrompt.includes(expectedSkillRead(base, "write-docs")));
352
+ assert.ok(researchPrompt.includes(expectedSkillRead(base, "swiftui")));
349
353
 
350
354
  const planPrompt = await buildPlanMilestonePrompt("M001", "Test", base);
351
- assert.match(planPrompt, /Read the installed 'write-docs' skill file from <available_skills>/);
352
- assert.match(planPrompt, /Read the installed 'swiftui' skill file from <available_skills>/);
355
+ assert.ok(planPrompt.includes(expectedSkillRead(base, "write-docs")));
356
+ assert.ok(planPrompt.includes(expectedSkillRead(base, "swiftui")));
353
357
  } finally {
354
358
  cleanup(base);
355
359
  }
@@ -380,7 +384,7 @@ test("complete-slice prompt propagates always_use_skills through buildSkillActiv
380
384
 
381
385
  const prompt = await buildCompleteSlicePrompt("M001", "Test", "S01", "Slice", base);
382
386
 
383
- assert.match(prompt, /Read the installed 'write-docs' skill file from <available_skills>/);
387
+ assert.ok(prompt.includes(expectedSkillRead(base, "write-docs")));
384
388
  } finally {
385
389
  cleanup(base);
386
390
  }
@@ -4,7 +4,13 @@
4
4
  import { describe, test } from "node:test";
5
5
  import assert from "node:assert/strict";
6
6
 
7
- import { buildContextMessage, stripVolatileCodebaseMetadata } from "../bootstrap/system-context.ts";
7
+ import {
8
+ buildContextMessage,
9
+ GSD_CONTEXT_MESSAGE_SENTINEL,
10
+ stripVolatileCodebaseMetadata,
11
+ } from "../bootstrap/system-context.ts";
12
+
13
+ const SENTINEL = `${GSD_CONTEXT_MESSAGE_SENTINEL}\n`;
8
14
 
9
15
  describe("stripVolatileCodebaseMetadata (#847 — KV cache stability)", () => {
10
16
  const map = [
@@ -63,7 +69,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
63
69
  });
64
70
  assert.ok(result, "expected a context message");
65
71
  assert.equal(result.customType, "gsd-memory");
66
- assert.equal(result.content, "[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]\nrule one\nrule two");
72
+ assert.equal(result.content, `${SENTINEL}[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]\nrule one\nrule two`);
67
73
  assert.equal(result.display, false);
68
74
  });
69
75
 
@@ -75,7 +81,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
75
81
  });
76
82
  assert.ok(result);
77
83
  assert.equal(result.customType, "gsd-guided-context");
78
- assert.equal(result.content, "[GUIDED]\nexecute T01");
84
+ assert.equal(result.content, `${SENTINEL}[GUIDED]\nexecute T01`);
79
85
  });
80
86
 
81
87
  test("forensics injection alone emits gsd-forensics", () => {
@@ -86,7 +92,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
86
92
  });
87
93
  assert.ok(result);
88
94
  assert.equal(result.customType, "gsd-forensics");
89
- assert.equal(result.content, "[FORENSICS]\ninvestigation context");
95
+ assert.equal(result.content, `${SENTINEL}[FORENSICS]\ninvestigation context`);
90
96
  });
91
97
 
92
98
  test("memory + guided injection: memory prepended, customType is gsd-guided-context", () => {
@@ -97,7 +103,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
97
103
  });
98
104
  assert.ok(result);
99
105
  assert.equal(result.customType, "gsd-guided-context");
100
- assert.equal(result.content, `${markedMemory}\n\n[GUIDED]\nexecute T01`);
106
+ assert.equal(result.content, `${SENTINEL}${markedMemory}\n\n[GUIDED]\nexecute T01`);
101
107
  });
102
108
 
103
109
  test("memory + forensics: memory prepended, customType is gsd-forensics", () => {
@@ -108,7 +114,7 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
108
114
  });
109
115
  assert.ok(result);
110
116
  assert.equal(result.customType, "gsd-forensics");
111
- assert.equal(result.content, `${markedMemory}\n\n[FORENSICS]\ninvestigation context`);
117
+ assert.equal(result.content, `${SENTINEL}${markedMemory}\n\n[FORENSICS]\ninvestigation context`);
112
118
  });
113
119
 
114
120
  test("guided takes precedence over forensics when both are somehow present", () => {
@@ -122,6 +128,6 @@ describe("buildContextMessage (#5019 — memory routing)", () => {
122
128
  });
123
129
  assert.ok(result);
124
130
  assert.equal(result.customType, "gsd-guided-context");
125
- assert.equal(result.content, "[GUIDED]");
131
+ assert.equal(result.content, `${SENTINEL}[GUIDED]`);
126
132
  });
127
133
  });
@@ -25,8 +25,16 @@ function makeMockPi() {
25
25
 
26
26
  console.log('\n── Tool naming: registration count ──');
27
27
 
28
+ // Aliases are hidden from the model-facing surface by default (plan 035);
29
+ // opt in here so this file can keep exercising alias-registration behavior.
30
+ const previousAdvertiseAliases = process.env.GSD_ADVERTISE_TOOL_ALIASES;
31
+ process.env.GSD_ADVERTISE_TOOL_ALIASES = "1";
32
+
28
33
  const pi = makeMockPi();
29
34
  registerDbTools(pi);
35
+
36
+ if (previousAdvertiseAliases === undefined) delete process.env.GSD_ADVERTISE_TOOL_ALIASES;
37
+ else process.env.GSD_ADVERTISE_TOOL_ALIASES = previousAdvertiseAliases;
30
38
  const toolByName = new Map<string, any>(pi.tools.map((tool: any) => [tool.name, tool]));
31
39
  const registeredCanonicalNames = new Set<string>(
32
40
  WORKFLOW_TOOL_CONTRACTS
@@ -46,6 +46,7 @@ import {
46
46
  insertTask,
47
47
  saveReworkBrief,
48
48
  } from "../gsd-db.ts";
49
+ import { clearGSDPreferencesCache, getProjectGSDPreferencesPath } from "../preferences.ts";
49
50
 
50
51
  // ─── Pure composer tests ──────────────────────────────────────────────────
51
52
 
@@ -418,6 +419,44 @@ test("Tool Surface composer: planning-dispatch lists allowed subagents", () => {
418
419
  assert.match(out, /\*\*planner\*\*/);
419
420
  });
420
421
 
422
+ test("Tool Surface composer: planning_subagents updates plan-milestone dispatch guidance", (t) => {
423
+ const originalGsdHome = process.env.GSD_HOME;
424
+ const tempProject = mkdtempSync(join(tmpdir(), "gsd-tool-surface-planning-subagents-"));
425
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-tool-surface-planning-subagents-home-"));
426
+
427
+ t.after(() => {
428
+ clearGSDPreferencesCache();
429
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
430
+ else process.env.GSD_HOME = originalGsdHome;
431
+ rmSync(tempProject, { recursive: true, force: true });
432
+ rmSync(tempGsdHome, { recursive: true, force: true });
433
+ });
434
+
435
+ process.env.GSD_HOME = tempGsdHome;
436
+ mkdirSync(join(tempProject, ".gsd"), { recursive: true });
437
+ clearGSDPreferencesCache();
438
+ writeFileSync(
439
+ getProjectGSDPreferencesPath(tempProject),
440
+ [
441
+ "---",
442
+ "version: 1",
443
+ "planning_subagents:",
444
+ " plan-milestone:",
445
+ " allowed: [security]",
446
+ "---",
447
+ "",
448
+ ].join("\n"),
449
+ "utf-8",
450
+ );
451
+
452
+ const out = composeToolSurfaceInstructions("plan-milestone", {
453
+ renderMode: "standalone",
454
+ basePath: tempProject,
455
+ });
456
+ assert.match(out, /Dispatch subagents only to \*\*security\*\*/);
457
+ assert.doesNotMatch(out, /Do not dispatch subagents/);
458
+ });
459
+
421
460
  test("Tool Surface composer: execute-task warns against slice/milestone closeout tools", () => {
422
461
  const out = composeToolSurfaceInstructions("execute-task", { renderMode: "nested" });
423
462
  assert.match(out, /^Tool surface: /);
@@ -352,7 +352,7 @@ test('workflow-projections: regenerateIfMissing SUMMARY is idempotent for flat-p
352
352
  keyDecisions: ['Use centralized task summary paths.'],
353
353
  });
354
354
 
355
- const summaryPath = join(phaseDir, 'T01-SUMMARY.md');
355
+ const summaryPath = join(phaseDir, 'S01-T01-SUMMARY.md');
356
356
  assert.ok(!existsSync(summaryPath), 'precondition: flat-phase task summary absent');
357
357
 
358
358
  const first = await regenerateIfMissing(base, 'M001', 'S01', 'SUMMARY');
@@ -399,7 +399,7 @@ test('workflow-projections: flat-phase SUMMARY regeneration ignores stale nested
399
399
  status: 'complete',
400
400
  });
401
401
 
402
- const rootSummaryPath = join(phaseDir, 'T01-SUMMARY.md');
402
+ const rootSummaryPath = join(phaseDir, 'S01-T01-SUMMARY.md');
403
403
  assert.ok(!existsSync(rootSummaryPath), 'precondition: canonical flat-phase task summary absent');
404
404
 
405
405
  const regenerated = await regenerateIfMissing(base, 'M001', 'S01', 'SUMMARY');
@@ -440,8 +440,8 @@ test('workflow-projections: renderSummaryProjection uses milestone title when cr
440
440
  status: 'complete',
441
441
  });
442
442
 
443
- const titleSummaryPath = join(base, '.gsd', 'phases', '01-milestone', 'T01-SUMMARY.md');
444
- const idSummaryPath = join(base, '.gsd', 'phases', '01-m001', 'T01-SUMMARY.md');
443
+ const titleSummaryPath = join(base, '.gsd', 'phases', '01-milestone', 'S01-T01-SUMMARY.md');
444
+ const idSummaryPath = join(base, '.gsd', 'phases', '01-m001', 'S01-T01-SUMMARY.md');
445
445
 
446
446
  renderSummaryProjection(base, 'M001', 'S01', 'T01');
447
447
 
@@ -13,6 +13,7 @@ import { GSD_PHASE_SCOPE_DISPLAY_REASON, GSD_SECTION_CLOSE_GATE_DISPLAY_REASON }
13
13
  import { ALLOWED_PLANNING_DISPATCH_AGENTS, shouldBlockPlanningUnit } from '../bootstrap/write-gate.ts';
14
14
  import { extractSubagentAgentClasses } from '../bootstrap/subagent-input.ts';
15
15
  import { isDeterministicPolicyError } from '../auto-tool-tracking.ts';
16
+ import { applyPlanningSubagentPreferences } from '../planning-subagent-policy.ts';
16
17
  import type { ToolsPolicy } from '../unit-context-manifest.ts';
17
18
 
18
19
  const BASE = join('/tmp', 'fake-project');
@@ -335,6 +336,39 @@ test('planning-dispatch: still allows writes inside .gsd/', () => {
335
336
  assert.strictEqual(r.block, false);
336
337
  });
337
338
 
339
+ test('planning_subagents: upgrades plan-milestone to controlled read-only dispatch', () => {
340
+ const policy = applyPlanningSubagentPreferences('plan-milestone', PLANNING, {
341
+ planning_subagents: {
342
+ 'plan-milestone': { allowed: ['security'] },
343
+ },
344
+ });
345
+ const allowed = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-milestone', policy, ['security']);
346
+ assert.strictEqual(allowed.block, false);
347
+
348
+ const blocked = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-milestone', policy, ['reviewer']);
349
+ assert.strictEqual(blocked.block, true);
350
+ assert.match(blocked.reason!, /planning_subagents/);
351
+ assert.match(blocked.reason!, /permitted agents for this unit: security/);
352
+ });
353
+
354
+ test('planning_subagents: widens plan-slice manifest allowlist without allowing unsafe agents', () => {
355
+ const policy = applyPlanningSubagentPreferences('plan-slice', {
356
+ mode: 'planning-dispatch',
357
+ allowedSubagents: ['scout', 'planner'],
358
+ }, {
359
+ planning_subagents: {
360
+ 'plan-slice': { allowed: ['security'] },
361
+ },
362
+ });
363
+
364
+ const added = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', policy, ['security']);
365
+ assert.strictEqual(added.block, false);
366
+
367
+ const unsafe = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', policy, ['worker']);
368
+ assert.strictEqual(unsafe.block, true);
369
+ assert.match(unsafe.reason!, /read-only specialists/);
370
+ });
371
+
338
372
  // ─── planning mode: pass-through tools ────────────────────────────────────
339
373
 
340
374
  test('planning-unit: allows read tool', () => {