@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
@@ -1,3 +1,5 @@
1
+ import { GSD_CONTEXT_MESSAGE_SENTINEL } from "./constants.js";
2
+
1
3
  /**
2
4
  * Observation masking for GSD auto-mode sessions.
3
5
  *
@@ -75,6 +77,26 @@ function findTurnBoundary(messages: MaskableMessage[], keepRecentTurns: number):
75
77
  return 0;
76
78
  }
77
79
 
80
+ function countRealUserTurns(messages: MaskableMessage[]): number {
81
+ let count = 0;
82
+ for (const m of messages) {
83
+ if (m.role === "user" && !isBashResultUserMessage(m)) count++;
84
+ }
85
+ return count;
86
+ }
87
+
88
+ /**
89
+ * Quantizes how many recent turns stay unmasked so the boundary only moves
90
+ * forward once per full block of `keepRecentTurns` new turns, instead of
91
+ * every turn. Keeps the masked prefix byte-stable within a block, which is
92
+ * what lets the LLM provider's prompt cache survive across turns.
93
+ */
94
+ function quantizedKeepTurns(totalTurns: number, keepRecentTurns: number): number {
95
+ if (keepRecentTurns <= 0 || totalTurns < 2 * keepRecentTurns) return totalTurns;
96
+ const maskedTurns = Math.floor((totalTurns - keepRecentTurns) / keepRecentTurns) * keepRecentTurns;
97
+ return totalTurns - maskedTurns;
98
+ }
99
+
78
100
  /**
79
101
  * Detect user messages that originated from bashExecution.
80
102
  * After convertToLlm, these are {role: "user", content: [{type:"text", text:"Ran `cmd`\n..."}]}.
@@ -95,7 +117,8 @@ function isMaskableMessage(m: MaskableMessage): boolean {
95
117
 
96
118
  export function createObservationMask(keepRecentTurns: number = 8) {
97
119
  return (messages: MaskableMessage[]): MaskableMessage[] => {
98
- const boundary = findTurnBoundary(messages, keepRecentTurns);
120
+ const totalTurns = countRealUserTurns(messages);
121
+ const boundary = findTurnBoundary(messages, quantizedKeepTurns(totalTurns, keepRecentTurns));
99
122
  if (boundary === 0) return messages;
100
123
 
101
124
  return messages.map((m, i) => {
@@ -126,6 +149,14 @@ function findResponsesTurnBoundary(items: ResponsesInputItem[], keepRecentTurns:
126
149
  return 0;
127
150
  }
128
151
 
152
+ function countResponsesRealUserTurns(items: ResponsesInputItem[]): number {
153
+ let count = 0;
154
+ for (const item of items) {
155
+ if (item.role === "user" && !isResponsesBashResultUserItem(item)) count++;
156
+ }
157
+ return count;
158
+ }
159
+
129
160
  /**
130
161
  * Observation masking for OpenAI/Codex Responses API payloads.
131
162
  *
@@ -135,7 +166,8 @@ function findResponsesTurnBoundary(items: ResponsesInputItem[], keepRecentTurns:
135
166
  */
136
167
  export function createResponsesInputObservationMask(keepRecentTurns: number = 8) {
137
168
  return (items: ResponsesInputItem[]): ResponsesInputItem[] => {
138
- const boundary = findResponsesTurnBoundary(items, keepRecentTurns);
169
+ const totalTurns = countResponsesRealUserTurns(items);
170
+ const boundary = findResponsesTurnBoundary(items, quantizedKeepTurns(totalTurns, keepRecentTurns));
139
171
  if (boundary === 0) return items;
140
172
 
141
173
  return items.map((item, i) => {
@@ -219,3 +251,67 @@ export function truncateResponsesInputResultItems(items: ResponsesInputItem[], m
219
251
  return item;
220
252
  });
221
253
  }
254
+
255
+ // GSD injects at most one context message per turn (memory/guided/forensics —
256
+ // see buildContextMessage in bootstrap/system-context.ts), marked with
257
+ // GSD_CONTEXT_MESSAGE_SENTINEL. convertToLlm strips the distinguishing
258
+ // customType before the payload reaches this hook, so detection is by content
259
+ // prefix instead. Pre-sentinel session history lacks the sentinel, so we also
260
+ // match the stable *bracketed* GSD block labels those injections begin with,
261
+ // letting resumed sessions dedupe older memory/guided blocks.
262
+ //
263
+ // Only GSD-specific bracketed markers belong here — never generic prose. A
264
+ // message is dropped from the payload when it matches, so a natural-language
265
+ // prefix (e.g. the forensics prompt "Debug GSD itself.") could silently delete
266
+ // a real user message that happens to start the same way. Legacy forensics
267
+ // injections without memory are therefore left un-deduped by design; those
268
+ // with memory still match via the "[GSD Context Metadata]" wrapper.
269
+ const LEGACY_GSD_CONTEXT_INJECTION_PREFIXES = [
270
+ "[GSD Context Metadata]",
271
+ "[MEMORY — Critical and prompt-relevant memories from the GSD memory store]",
272
+ "[GSD Guided Execute Context]",
273
+ ] as const;
274
+
275
+ function isGsdContextInjectionText(text: string | undefined): boolean {
276
+ if (typeof text !== "string") return false;
277
+ if (text.startsWith(GSD_CONTEXT_MESSAGE_SENTINEL)) return true;
278
+ return LEGACY_GSD_CONTEXT_INJECTION_PREFIXES.some((prefix) => text.startsWith(prefix));
279
+ }
280
+
281
+ function isGsdContextInjectionMessage(m: MaskableMessage): boolean {
282
+ if (m.role !== "user") return false;
283
+ return isGsdContextInjectionText(firstTextFromContent(m.content));
284
+ }
285
+
286
+ /**
287
+ * Removes every GSD context-injection user message except the latest one.
288
+ * Each turn re-injects a near-identical memory/guided/forensics block; left
289
+ * in place they duplicate verbatim across an N-turn session. Removal (not
290
+ * masking) — an empty placeholder still costs tokens and shifts message
291
+ * positions, breaking cache byte-stability. Pure function: stored history is
292
+ * never mutated, only the outgoing payload array.
293
+ */
294
+ export function filterSupersededContextInjections(messages: MaskableMessage[]): MaskableMessage[] {
295
+ let lastIndex = -1;
296
+ for (let i = 0; i < messages.length; i++) {
297
+ if (isGsdContextInjectionMessage(messages[i])) lastIndex = i;
298
+ }
299
+ if (lastIndex === -1) return messages;
300
+ return messages.filter((m, i) => i === lastIndex || !isGsdContextInjectionMessage(m));
301
+ }
302
+
303
+ function isResponsesGsdContextInjectionItem(item: ResponsesInputItem): boolean {
304
+ if (item.role !== "user") return false;
305
+ return isGsdContextInjectionText(firstTextFromContent(item.content));
306
+ }
307
+
308
+ export function filterSupersededResponsesContextInjections(
309
+ items: ResponsesInputItem[],
310
+ ): ResponsesInputItem[] {
311
+ let lastIndex = -1;
312
+ for (let i = 0; i < items.length; i++) {
313
+ if (isResponsesGsdContextInjectionItem(items[i])) lastIndex = i;
314
+ }
315
+ if (lastIndex === -1) return items;
316
+ return items.filter((item, i) => i === lastIndex || !isResponsesGsdContextInjectionItem(item));
317
+ }
@@ -357,6 +357,28 @@ In `"parent"` mode, slice/task `targetRepositories` default to the declared chil
357
357
  - `"replace"` requires `prompt`.
358
358
  - `"skip"` is valid with no additional fields.
359
359
 
360
+ - `planning_subagents`: object — project-local widening for read-only planning subagent dispatch. Supported unit keys are `plan-milestone` and `plan-slice`; each has:
361
+ - `allowed`: string[] — additional planning-safe agents for that unit.
362
+
363
+ Agents must already be classified by GSD as read-only planning specialists: `mnemo`, `scout`, `planner`, `reviewer`, `security`, or `tester`. The write gate still blocks source writes outside `.gsd/**`, unrestricted bash, stale subagent calls without agent identities, agents outside the global read-only registry, and agents not listed for the active unit.
364
+
365
+ Example:
366
+
367
+ ```yaml
368
+ planning_subagents:
369
+ plan-milestone:
370
+ allowed:
371
+ - scout
372
+ - planner
373
+ - security
374
+ plan-slice:
375
+ allowed:
376
+ - scout
377
+ - planner
378
+ - reviewer
379
+ - security
380
+ ```
381
+
360
382
  **Known unit types for `before`/`after`:** `research-milestone`, `plan-milestone`, `research-slice`, `plan-slice`, `execute-task`, `complete-slice`, `replan-slice`, `reassess-roadmap`, `run-uat`.
361
383
 
362
384
  - `experimental`: opt-in experimental features. All features here are **off by default** — you must explicitly set each one to `true` to enable it. Features in this block may change or be removed without a deprecation cycle while in experimental status. Keys:
@@ -15,13 +15,13 @@ import {
15
15
  import { MEMORIES_FTS_REBUILT_KEY } from "./db-memory-fts-schema.js";
16
16
  import { isAfter, latestExplicitReopenAt } from "./milestone-reopen-events.js";
17
17
  import {
18
- buildTaskFileName,
19
18
  gsdProjectionRoot,
20
19
  gsdRoot,
21
20
  resolveGsdPathContract,
22
21
  resolveMilestoneFile,
23
22
  resolveMilestonePath,
24
23
  resolveSliceFile,
24
+ resolveTaskFile,
25
25
  } from "./paths.js";
26
26
  import { deriveState } from "./state.js";
27
27
  import { isClosedStatus } from "./status-guards.js";
@@ -208,10 +208,8 @@ function resolveFlatPhaseArtifactDiskPath(basePath: string, row: ArtifactRow): s
208
208
  if (!artifactType) return null;
209
209
 
210
210
  if (row.slice_id && row.task_id) {
211
- const phaseDir = resolveMilestonePath(basePath, row.milestone_id);
212
- if (!phaseDir || !isFlatPhaseDiskPath(basePath, phaseDir)) return null;
213
- const candidate = join(phaseDir, buildTaskFileName(row.task_id, artifactType));
214
- return existsSync(candidate) ? candidate : null;
211
+ const candidate = resolveTaskFile(basePath, row.milestone_id, row.slice_id, row.task_id, artifactType);
212
+ return candidate && isFlatPhaseDiskPath(basePath, candidate) ? candidate : null;
215
213
  }
216
214
 
217
215
  const candidate = row.slice_id
@@ -385,10 +385,16 @@ const runtimeFilesCleanedUpRepos = new Set<string>();
385
385
  // ─── Integration Branch Metadata ───────────────────────────────────────────
386
386
 
387
387
  /**
388
- * Path to the milestone metadata file that stores the integration branch.
389
- * Format: .gsd/milestones/<MID>/<MID>-META.json
388
+ * Path to the milestone integration-branch metadata file.
389
+ * Format: .gsd/<MID>-META.json (deliberately OUTSIDE milestones/<MID>/ so it
390
+ * cannot poison isLegacyMilestonesLayout — see ADR-045).
390
391
  */
391
- function milestoneMetaPath(basePath: string, milestoneId: string): string {
392
+ export function milestoneMetaPath(basePath: string, milestoneId: string): string {
393
+ return join(gsdRoot(basePath), `${milestoneId}-META.json`);
394
+ }
395
+
396
+ /** Legacy location (pre-ADR-045). Read-only fallback for existing on-disk trees. */
397
+ function legacyMilestoneMetaPath(basePath: string, milestoneId: string): string {
392
398
  return join(gsdRoot(basePath), "milestones", milestoneId, `${milestoneId}-META.json`);
393
399
  }
394
400
 
@@ -398,8 +404,12 @@ function milestoneMetaPath(basePath: string, milestoneId: string): string {
398
404
  */
399
405
  export function readIntegrationBranch(basePath: string, milestoneId: string): string | null {
400
406
  try {
401
- const metaFile = milestoneMetaPath(basePath, milestoneId);
402
- if (!existsSync(metaFile)) return null;
407
+ let metaFile = milestoneMetaPath(basePath, milestoneId);
408
+ if (!existsSync(metaFile)) {
409
+ const legacy = legacyMilestoneMetaPath(basePath, milestoneId);
410
+ if (!existsSync(legacy)) return null;
411
+ metaFile = legacy;
412
+ }
403
413
  const data = JSON.parse(readFileSync(metaFile, "utf-8"));
404
414
  const branch = data?.integrationBranch;
405
415
  if (typeof branch === "string" && branch.trim() !== "" && VALID_BRANCH_NAME.test(branch)) {
@@ -451,7 +461,10 @@ export function writeIntegrationBranch(
451
461
  if (existingBranch === branch) return;
452
462
 
453
463
  const metaFile = milestoneMetaPath(basePath, milestoneId);
454
- mkdirSync(join(gsdRoot(basePath), "milestones", milestoneId), { recursive: true });
464
+ // Ensure the flat .gsd/ root exists (never milestones/<MID>/ that would
465
+ // poison layout detection, ADR-045). In production .gsd/ already exists; this
466
+ // guards fresh trees so the write below can't ENOENT.
467
+ mkdirSync(gsdRoot(basePath), { recursive: true });
455
468
 
456
469
  // Merge with existing metadata if present
457
470
  let existing: Record<string, unknown> = {};
@@ -26,6 +26,7 @@ import {
26
26
  buildPlanMilestonePrompt,
27
27
  buildPlanSlicePrompt,
28
28
  buildSkillActivationBlock,
29
+ capPreamble,
29
30
  } from "./auto-prompts.js";
30
31
  import { deriveState, isGhostMilestone } from "./state.js";
31
32
  import { invalidateAllCaches } from "./cache.js";
@@ -1199,7 +1200,9 @@ function resolveDiscussSliceBasePath(basePath: string, milestoneId: string): str
1199
1200
  * slice summaries so the agent can ask grounded UX/behaviour questions
1200
1201
  * without wasting a turn reading files.
1201
1202
  */
1202
- async function buildDiscussSlicePrompt(
1203
+ // Exported for tests (`prompt-budget-enforcement.test.ts`) — verifies the
1204
+ // inlined-context cap. Not part of the public flow surface otherwise.
1205
+ export async function buildDiscussSlicePrompt(
1203
1206
  mid: string,
1204
1207
  sid: string,
1205
1208
  sTitle: string,
@@ -1262,8 +1265,12 @@ async function buildDiscussSlicePrompt(
1262
1265
  }
1263
1266
  }
1264
1267
 
1268
+ // Cap the inlined block: it grows unbounded with each completed slice's full
1269
+ // SUMMARY. `capPreamble` truncates at `### ` section boundaries — roadmap and
1270
+ // milestone context come first (survive), completed-slice summaries come last
1271
+ // (dropped first) when the executor's window can't hold everything.
1265
1272
  const inlinedContext = inlined.length > 0
1266
- ? `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`
1273
+ ? capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`)
1267
1274
  : `## Inlined Context\n\n_(no context files found yet — go in blind and ask broad questions)_`;
1268
1275
 
1269
1276
  const sliceDirPath = `.gsd/milestones/${mid}/slices/${sid}`;
@@ -40,6 +40,7 @@ import {
40
40
  resolveTasksDir,
41
41
  targetMilestoneFile,
42
42
  targetSliceFile,
43
+ targetTaskFile,
43
44
  gsdProjectionRoot,
44
45
  gsdRoot,
45
46
  buildTaskFileName,
@@ -756,20 +757,7 @@ export async function renderTaskSummary(
756
757
  return false; // No summary to render — skip silently
757
758
  }
758
759
 
759
- // Resolve the tasks directory, creating path if needed
760
- const slicePath = resolveSlicePath(basePath, milestoneId, sliceId);
761
- if (!slicePath) {
762
- process.stderr.write(
763
- `markdown-renderer: cannot resolve slice path for ${milestoneId}/${sliceId}\n`,
764
- );
765
- return false;
766
- }
767
-
768
- // Use the tasks/ subdir when it exists (legacy layout), otherwise use the
769
- // slice path directly (flat-phase: task summaries live alongside plan files).
770
- const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId) ?? slicePath;
771
- const fileName = buildTaskFileName(taskId, "SUMMARY");
772
- const absPath = join(tasksDir, fileName);
760
+ const absPath = targetTaskFile(basePath, milestoneId, sliceId, taskId, "SUMMARY", getMilestone(milestoneId)?.title);
773
761
  const artifactPath = toArtifactPath(absPath, basePath);
774
762
 
775
763
  await writeAndStore(absPath, artifactPath, task.full_summary_md, {
@@ -31,6 +31,7 @@ import {
31
31
  legacyMilestonesDir,
32
32
  gsdRoot,
33
33
  resolveTaskFiles,
34
+ resolveTaskFile,
34
35
  resolveMilestonePath,
35
36
  } from './paths.js';
36
37
  import { findMilestoneIds } from './guided-flow.js';
@@ -925,8 +926,7 @@ export function migrateHierarchyToDb(
925
926
  legacyTasksDir = isLegacySlice
926
927
  ? resolveTasksDir(basePath, milestoneId, sliceEntry.id)
927
928
  : null;
928
- const summaryDir = legacyTasksDir ?? slicePath;
929
- taskSummaryFile = join(summaryDir, `${taskEntry.id}-SUMMARY.md`);
929
+ taskSummaryFile = resolveTaskFile(basePath, milestoneId, sliceEntry.id, taskEntry.id, 'SUMMARY');
930
930
  }
931
931
  const hasTaskSummary = taskSummaryFile !== null && existsSync(taskSummaryFile);
932
932
 
@@ -950,9 +950,9 @@ export function migrateHierarchyToDb(
950
950
  // first execute-task. The surviving SUMMARY wins over a stale checkbox.
951
951
  // reopen-task removes the SUMMARY, so this never re-completes an
952
952
  // intentionally reopened task.
953
- // Flat-phase summaries are phase-scoped (T01-SUMMARY.md); when a milestone
954
- // has multiple slices, require the summary parent frontmatter to match so
955
- // one slice's completion does not attest a sibling's same-named task.
953
+ // Flat-phase summaries are phase-scoped; when a milestone has multiple
954
+ // slices, require the summary parent frontmatter to match so one slice's
955
+ // completion does not attest a sibling's same-named task.
956
956
  let summaryAttestsCompletion = hasTaskSummary;
957
957
  if (summaryAttestsCompletion && !isLegacySlice && roadmap.slices.length > 1 && taskSummaryFile) {
958
958
  try {
@@ -62,6 +62,8 @@ export const BUNDLED_COST_TABLE: ModelCostEntry[] = [
62
62
  // GPT-5.5 API list price, also used for live Codex OAuth routing.
63
63
  // Source: https://openai.com/api/pricing/
64
64
  { id: "gpt-5.5", inputPer1k: 0.005, outputPer1k: 0.03, updatedAt: "2026-04-23" },
65
+ // Mirrors gpt-5.5 list pricing until OpenAI publishes official gpt-5.6 pricing.
66
+ { id: "gpt-5.6", inputPer1k: 0.005, outputPer1k: 0.03, updatedAt: "2026-07-08" },
65
67
 
66
68
  // Google
67
69
  { id: "gemini-2.0-flash", inputPer1k: 0.0001, outputPer1k: 0.0004, updatedAt: "2025-03-15" },
@@ -111,10 +111,12 @@ export const MODEL_CAPABILITY_TIER: Record<string, ComplexityTier> = {
111
111
  "gpt-5.3-codex": "heavy",
112
112
  "gpt-5.4": "heavy",
113
113
  "gpt-5.5": "heavy",
114
+ "gpt-5.6": "heavy",
114
115
  "o1": "heavy",
115
116
  "o3": "heavy",
116
117
  "o4-mini": "heavy",
117
118
  "o4-mini-deep-research": "heavy",
119
+ "grok-4.5": "heavy",
118
120
  };
119
121
 
120
122
  // ─── Cost Table (per 1K input tokens, approximate USD) ───────────────────────
@@ -149,11 +151,13 @@ const MODEL_COST_PER_1K_INPUT: Record<string, number> = {
149
151
  "gpt-5.3-codex-spark": 0.0003,
150
152
  "gpt-5.4": 0.005,
151
153
  "gpt-5.5": 0.005,
154
+ "gpt-5.6": 0.005,
152
155
  "o4-mini": 0.005,
153
156
  "o4-mini-deep-research": 0.005,
154
157
  "gemini-2.0-flash": 0.0001,
155
158
  "gemini-2.5-pro": 0.00125,
156
159
  "deepseek-chat": 0.00014,
160
+ "grok-4.5": 0.002,
157
161
  };
158
162
 
159
163
  // ─── Capability Profiles Data Table ──────────────────────────────────────────
@@ -198,6 +202,9 @@ export const MODEL_CAPABILITY_PROFILES: Record<string, ModelCapabilities> = {
198
202
  // OpenAI's 2026-04-23 published eval deltas across coding, tool use, and long context.
199
203
  // Source: https://openai.com/index/introducing-gpt-5-5/
200
204
  "gpt-5.5": { coding: 96, debugging: 93, research: 89, reasoning: 95, speed: 42, longContext: 90, instruction: 93 },
205
+ // GPT-5.6 profile is provisional: a small uniform step over gpt-5.5, to be
206
+ // recalibrated once OpenAI publishes official eval results.
207
+ "gpt-5.6": { coding: 97, debugging: 94, research: 90, reasoning: 96, speed: 42, longContext: 91, instruction: 94 },
201
208
 
202
209
  // ── OpenAI o-series (reasoning-first) ──────────────────────────────────────
203
210
  "o1": { coding: 78, debugging: 82, research: 78, reasoning: 90, speed: 20, longContext: 65, instruction: 82 },
@@ -212,6 +219,11 @@ export const MODEL_CAPABILITY_PROFILES: Record<string, ModelCapabilities> = {
212
219
 
213
220
  // ── DeepSeek ───────────────────────────────────────────────────────────────
214
221
  "deepseek-chat": { coding: 75, debugging: 65, research: 55, reasoning: 70, speed: 70, longContext: 55, instruction: 65 },
222
+
223
+ // ── xAI ────────────────────────────────────────────────────────────────────
224
+ // Grok 4.5 (2026-07): Opus-class frontier model tuned for coding and agentic
225
+ // work; notably faster and more token-efficient than peer heavy models.
226
+ "grok-4.5": { coding: 95, debugging: 90, research: 82, reasoning: 93, speed: 55, longContext: 80, instruction: 90 },
215
227
  };
216
228
 
217
229
  // ─── Base Task Requirements Data Table ───────────────────────────────────────
@@ -223,6 +223,26 @@ export function buildTaskFileName(taskId: string, suffix: string): string {
223
223
  return `${taskId}-${suffix}.md`;
224
224
  }
225
225
 
226
+ /**
227
+ * Build a flat-phase task artifact file name.
228
+ * ("S06", "T03", "SUMMARY") → "S06-T03-SUMMARY.md"
229
+ */
230
+ export function buildFlatTaskFileName(sliceId: string, taskId: string, suffix: string): string {
231
+ return `${sliceId}-${taskId}-${suffix}.md`;
232
+ }
233
+
234
+ /**
235
+ * Extract the task ID from a task artifact filename.
236
+ * Supports flat-phase S##-T##-SUFFIX.md plus legacy T##-SUFFIX.md and
237
+ * T##-DESCRIPTOR-SUFFIX.md names.
238
+ */
239
+ export function taskIdFromTaskFileName(fileName: string, suffix: string): string | null {
240
+ const flat = new RegExp(`^S\\d+-(T\\d+)-${suffix}\\.md$`, "i").exec(fileName);
241
+ if (flat?.[1]) return flat[1].toUpperCase();
242
+ const legacy = new RegExp(`^(T\\d+)(?:-.*)?-${suffix}\\.md$`, "i").exec(fileName);
243
+ return legacy?.[1]?.toUpperCase() ?? null;
244
+ }
245
+
226
246
  // ─── Resolvers ─────────────────────────────────────────────────────────────
227
247
 
228
248
  /**
@@ -284,18 +304,15 @@ export function resolveFile(dir: string, idPrefix: string, suffix: string): stri
284
304
  }
285
305
 
286
306
  /**
287
- * Find all task files matching a pattern in a tasks directory.
288
- * Returns sorted file names matching T##-SUFFIX.md or legacy T##-*-SUFFIX.md
307
+ * Find all task files matching a pattern in a tasks directory or flat phase dir.
308
+ * Returns sorted file names matching S##-T##-SUFFIX.md, T##-SUFFIX.md,
309
+ * or legacy T##-*-SUFFIX.md.
289
310
  */
290
311
  export function resolveTaskFiles(tasksDir: string, suffix: string): string[] {
291
312
  if (!existsSync(tasksDir)) return [];
292
313
  try {
293
- // Current convention: T01-PLAN.md
294
- const currentPattern = new RegExp(`^T\\d+-${suffix}\\.md$`, "i");
295
- // Legacy convention: T01-INSTALL-PACKAGES-PLAN.md
296
- const legacyPattern = new RegExp(`^T\\d+-.*-${suffix}\\.md$`, "i");
297
314
  return cachedReaddir(tasksDir)
298
- .filter(f => currentPattern.test(f) || legacyPattern.test(f))
315
+ .filter(f => taskIdFromTaskFileName(f, suffix) !== null)
299
316
  .sort();
300
317
  } catch {
301
318
  return [];
@@ -931,8 +948,10 @@ export function resolveTaskFile(
931
948
  const isLegacy = phaseDir.startsWith(legacyBase + "/") || phaseDir.startsWith(legacyBase + "\\");
932
949
 
933
950
  if (suffix !== "PLAN" && !isLegacy) {
934
- const flatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
935
- return existsSync(flatPath) ? flatPath : null;
951
+ const flatPath = join(phaseDir, buildFlatTaskFileName(sliceId, taskId, suffix));
952
+ if (existsSync(flatPath)) return flatPath;
953
+ const legacyFlatPath = join(phaseDir, buildTaskFileName(taskId, suffix));
954
+ return existsSync(legacyFlatPath) ? legacyFlatPath : null;
936
955
  }
937
956
 
938
957
  const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
@@ -1066,6 +1085,35 @@ export function targetSliceFile(
1066
1085
  return join(dir, fileName);
1067
1086
  }
1068
1087
 
1088
+ /**
1089
+ * Build the canonical absolute write target for a task file.
1090
+ * Readers that need legacy flat-phase fallback should call resolveTaskFile.
1091
+ */
1092
+ export function targetTaskFile(
1093
+ basePath: string, milestoneId: string, sliceId: string,
1094
+ taskId: string, suffix: string, milestoneTitle?: string
1095
+ ): string {
1096
+ if (suffix === "PLAN") {
1097
+ return targetSliceFile(basePath, milestoneId, sliceId, "PLAN", milestoneTitle);
1098
+ }
1099
+
1100
+ const mDir = resolveMilestonePath(basePath, milestoneId);
1101
+ const legacyBase = legacyMilestonesDir(basePath);
1102
+ const isLegacy = mDir
1103
+ ? mDir.startsWith(legacyBase + "/") || mDir.startsWith(legacyBase + "\\")
1104
+ : isLegacyMilestonesLayout(basePath);
1105
+ const milestoneDir = mDir
1106
+ ?? dirname(targetMilestoneFile(basePath, milestoneId, "ROADMAP", milestoneTitle));
1107
+
1108
+ if (isLegacy) {
1109
+ const slicesDir = join(milestoneDir, "slices");
1110
+ const sliceDir = join(slicesDir, resolveDir(slicesDir, sliceId) ?? sliceId);
1111
+ return join(sliceDir, "tasks", buildTaskFileName(taskId, suffix));
1112
+ }
1113
+
1114
+ return join(milestoneDir, buildFlatTaskFileName(sliceId, taskId, suffix));
1115
+ }
1116
+
1069
1117
  /**
1070
1118
  * Build relative .gsd/ path to a slice file.
1071
1119
  * Preserves an existing compatibility filename when present; otherwise falls
@@ -1105,5 +1153,5 @@ export function relTaskFile(
1105
1153
  return relSliceFile(basePath, milestoneId, sliceId, "PLAN");
1106
1154
  }
1107
1155
  const relS = relSlicePath(basePath, milestoneId, sliceId, milestoneTitle);
1108
- return `${relS}/${buildTaskFileName(taskId, suffix)}`;
1156
+ return `${relS}/${buildFlatTaskFileName(sliceId, taskId, suffix)}`;
1109
1157
  }
@@ -0,0 +1,58 @@
1
+ import { CONFIGURABLE_PLANNING_SUBAGENT_UNITS, type GSDPreferences } from "./preferences-types.js";
2
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
3
+ import type { ToolsPolicy } from "./unit-context-manifest.js";
4
+
5
+ const CONFIGURABLE_UNITS = new Set<string>(CONFIGURABLE_PLANNING_SUBAGENT_UNITS);
6
+
7
+ function configuredAllowedSubagents(
8
+ unitType: string,
9
+ preferences: GSDPreferences | null | undefined,
10
+ ): readonly string[] {
11
+ if (!CONFIGURABLE_UNITS.has(unitType)) return [];
12
+ return preferences?.planning_subagents?.[
13
+ unitType as keyof NonNullable<GSDPreferences["planning_subagents"]>
14
+ ]?.allowed ?? [];
15
+ }
16
+
17
+ function mergeAllowedSubagents(
18
+ current: readonly string[],
19
+ configured: readonly string[],
20
+ ): readonly string[] {
21
+ return Array.from(new Set([...current, ...configured]));
22
+ }
23
+
24
+ export function applyPlanningSubagentPreferences(
25
+ unitType: string,
26
+ policy: ToolsPolicy | null | undefined,
27
+ preferences: GSDPreferences | null | undefined,
28
+ ): ToolsPolicy | null | undefined {
29
+ if (!policy) return policy;
30
+ const configured = configuredAllowedSubagents(unitType, preferences);
31
+ if (configured.length === 0) return policy;
32
+
33
+ if (policy.mode === "planning") {
34
+ return {
35
+ mode: "planning-dispatch",
36
+ allowedSubagents: mergeAllowedSubagents([], configured),
37
+ };
38
+ }
39
+
40
+ if (policy.mode === "planning-dispatch") {
41
+ return {
42
+ ...policy,
43
+ allowedSubagents: mergeAllowedSubagents(policy.allowedSubagents, configured),
44
+ };
45
+ }
46
+
47
+ return policy;
48
+ }
49
+
50
+ export function resolveEffectivePlanningToolsPolicy(
51
+ unitType: string,
52
+ policy: ToolsPolicy | null | undefined,
53
+ basePath?: string,
54
+ ): ToolsPolicy | null | undefined {
55
+ if (!basePath) return policy;
56
+ const preferences = loadEffectiveGSDPreferences(basePath)?.preferences;
57
+ return applyPlanningSubagentPreferences(unitType, policy, preferences);
58
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Read-only planning specialists that controlled planning dispatch may use.
3
+ *
4
+ * Unit manifests and project preferences still declare per-unit subsets. This
5
+ * registry is the global safety classification checked by the write gate and
6
+ * preference validation before any configured planning allowlist takes effect.
7
+ */
8
+
9
+ const PLANNING_DISPATCH_AGENT_REGISTRY = {
10
+ mnemo: { readOnlySpecialist: true },
11
+ scout: { readOnlySpecialist: true },
12
+ planner: { readOnlySpecialist: true },
13
+ reviewer: { readOnlySpecialist: true },
14
+ security: { readOnlySpecialist: true },
15
+ tester: { readOnlySpecialist: true },
16
+ } as const satisfies Record<string, { readonly readOnlySpecialist: boolean }>;
17
+
18
+ export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set<string>(
19
+ Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
20
+ .filter(([, metadata]) => metadata.readOnlySpecialist)
21
+ .map(([agentId]) => agentId),
22
+ );
23
+
24
+ export function isReadOnlyPlanningDispatchAgent(agentId: string): boolean {
25
+ const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId as keyof typeof PLANNING_DISPATCH_AGENT_REGISTRY];
26
+ return metadata?.readOnlySpecialist === true;
27
+ }
28
+
29
+ export function allowedPlanningDispatchAgentsList(): string {
30
+ return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
31
+ }
@@ -141,6 +141,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
141
141
  "git",
142
142
  "post_unit_hooks",
143
143
  "pre_dispatch_hooks",
144
+ "planning_subagents",
144
145
  "dynamic_routing",
145
146
  "disabled_model_providers",
146
147
  "uok",
@@ -217,6 +218,20 @@ export const KNOWN_UNIT_LABELS = [
217
218
  ] as const;
218
219
  export type UnitLabel = (typeof KNOWN_UNIT_LABELS)[number];
219
220
 
221
+ export const CONFIGURABLE_PLANNING_SUBAGENT_UNITS = [
222
+ "plan-milestone",
223
+ "plan-slice",
224
+ ] as const;
225
+ export type ConfigurablePlanningSubagentUnit = (typeof CONFIGURABLE_PLANNING_SUBAGENT_UNITS)[number];
226
+
227
+ export interface PlanningSubagentUnitConfig {
228
+ allowed?: string[];
229
+ }
230
+
231
+ export type PlanningSubagentsConfig = Partial<
232
+ Record<ConfigurablePlanningSubagentUnit, PlanningSubagentUnitConfig>
233
+ >;
234
+
220
235
 
221
236
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
222
237
 
@@ -476,6 +491,7 @@ export interface GSDPreferences {
476
491
  git?: GitPreferences;
477
492
  post_unit_hooks?: PostUnitHookConfig[];
478
493
  pre_dispatch_hooks?: PreDispatchHookConfig[];
494
+ planning_subagents?: PlanningSubagentsConfig;
479
495
  dynamic_routing?: DynamicRoutingConfig;
480
496
  /** Provider IDs to exclude from /model and automatic model routing while leaving tool auth intact. */
481
497
  disabled_model_providers?: string[];