@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.
- package/README.md +10 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +2 -2
- package/dist/resources/extensions/gsd/artifact-verification.js +3 -4
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -8
- package/dist/resources/extensions/gsd/auto-prompts.js +90 -17
- package/dist/resources/extensions/gsd/auto-recovery.js +10 -5
- package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +4 -0
- package/dist/resources/extensions/gsd/auto-worktree-teardown.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -5
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -24
- package/dist/resources/extensions/gsd/constants.js +12 -13
- package/dist/resources/extensions/gsd/context-masker.js +97 -21
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +22 -0
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +3 -6
- package/dist/resources/extensions/gsd/git-service.js +19 -7
- package/dist/resources/extensions/gsd/guided-flow.js +9 -3
- package/dist/resources/extensions/gsd/markdown-renderer.js +2 -12
- package/dist/resources/extensions/gsd/md-importer.js +5 -6
- package/dist/resources/extensions/gsd/model-cost-table.js +2 -0
- package/dist/resources/extensions/gsd/model-router.js +11 -0
- package/dist/resources/extensions/gsd/paths.js +51 -10
- package/dist/resources/extensions/gsd/planning-subagent-policy.js +37 -0
- package/dist/resources/extensions/gsd/planning-subagent-registry.js +25 -0
- package/dist/resources/extensions/gsd/preferences-types.js +5 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +48 -1
- package/dist/resources/extensions/gsd/preferences.js +17 -0
- package/dist/resources/extensions/gsd/provider-payload-policy.js +16 -5
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +1 -1
- package/dist/resources/extensions/gsd/skill-activation.js +17 -3
- package/dist/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
- package/dist/resources/extensions/gsd/tools/complete-task.js +12 -5
- package/dist/resources/extensions/gsd/tools/reopen-milestone.js +14 -5
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +15 -5
- package/dist/resources/extensions/gsd/tools/reopen-task.js +16 -10
- package/dist/resources/extensions/gsd/unit-context-composer.js +28 -5
- package/dist/resources/extensions/gsd/workflow-projections.js +2 -3
- package/dist/resources/extensions/gsd/workspace.js +2 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +6 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/compaction/compaction.js +36 -4
- package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
- package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/sdk.js +1 -0
- package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/package.json +1 -1
- package/packages/mcp-server/README.md +7 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +5 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +4 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +5 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +59 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +53 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +6 -1
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.js +1 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +11 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/GSD-WORKFLOW.md +2 -2
- package/src/resources/extensions/gsd/artifact-verification.ts +3 -8
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -7
- package/src/resources/extensions/gsd/auto-prompts.ts +104 -15
- package/src/resources/extensions/gsd/auto-recovery.ts +10 -8
- package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +4 -0
- package/src/resources/extensions/gsd/auto-worktree-teardown.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +39 -10
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -3
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +7 -29
- package/src/resources/extensions/gsd/constants.ts +14 -13
- package/src/resources/extensions/gsd/context-masker.ts +98 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +22 -0
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +3 -5
- package/src/resources/extensions/gsd/git-service.ts +19 -6
- package/src/resources/extensions/gsd/guided-flow.ts +9 -2
- package/src/resources/extensions/gsd/markdown-renderer.ts +2 -14
- package/src/resources/extensions/gsd/md-importer.ts +5 -5
- package/src/resources/extensions/gsd/model-cost-table.ts +2 -0
- package/src/resources/extensions/gsd/model-router.ts +12 -0
- package/src/resources/extensions/gsd/paths.ts +58 -10
- package/src/resources/extensions/gsd/planning-subagent-policy.ts +58 -0
- package/src/resources/extensions/gsd/planning-subagent-registry.ts +31 -0
- package/src/resources/extensions/gsd/preferences-types.ts +16 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +62 -0
- package/src/resources/extensions/gsd/preferences.ts +25 -0
- package/src/resources/extensions/gsd/provider-payload-policy.ts +18 -4
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +1 -1
- package/src/resources/extensions/gsd/skill-activation.ts +17 -3
- package/src/resources/extensions/gsd/templates/PREFERENCES.md +5 -0
- package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +76 -1
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +204 -8
- package/src/resources/extensions/gsd/tests/db-tools-alias-suppression.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/destructive-confirmation.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration/paths.test.ts +22 -4
- package/src/resources/extensions/gsd/tests/integration-branch-meta-location.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
- package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/preferences.test.ts +84 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +33 -7
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -16
- package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +13 -7
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +34 -0
- package/src/resources/extensions/gsd/tools/complete-task.ts +20 -5
- package/src/resources/extensions/gsd/tools/reopen-milestone.ts +24 -5
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +25 -5
- package/src/resources/extensions/gsd/tools/reopen-task.ts +23 -9
- package/src/resources/extensions/gsd/unit-context-composer.ts +34 -8
- package/src/resources/extensions/gsd/workflow-projections.ts +2 -3
- package/src/resources/extensions/gsd/workspace.ts +2 -1
- /package/dist/web/standalone/.next/static/{TezjdjSHBVRRr54XjAvte → 6YWV7VAPg-jqjfPswrKZb}/_buildManifest.js +0 -0
- /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
|
|
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
|
|
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
|
|
212
|
-
|
|
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
|
|
389
|
-
* Format: .gsd
|
|
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
|
-
|
|
402
|
-
if (!existsSync(metaFile))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
954
|
-
//
|
|
955
|
-
//
|
|
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
|
|
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 =>
|
|
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,
|
|
935
|
-
|
|
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}/${
|
|
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[];
|