@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
|
@@ -4,9 +4,12 @@ import assert from "node:assert/strict";
|
|
|
4
4
|
import {
|
|
5
5
|
createObservationMask,
|
|
6
6
|
createResponsesInputObservationMask,
|
|
7
|
+
filterSupersededContextInjections,
|
|
8
|
+
filterSupersededResponsesContextInjections,
|
|
7
9
|
truncateContextResultMessages,
|
|
8
10
|
truncateResponsesInputResultItems,
|
|
9
11
|
} from "../context-masker.js";
|
|
12
|
+
import { GSD_CONTEXT_MESSAGE_SENTINEL } from "../bootstrap/system-context.js";
|
|
10
13
|
|
|
11
14
|
// These helpers produce messages in the pi-ai LLM payload format
|
|
12
15
|
// (post-convertToLlm, pre-provider), which is what before_provider_request sees.
|
|
@@ -43,25 +46,35 @@ test("masks nothing when message count is within keepRecentTurns", () => {
|
|
|
43
46
|
assert.deepEqual((result[2].content as any)[0].text, "file contents");
|
|
44
47
|
});
|
|
45
48
|
|
|
46
|
-
test("masks tool results older than
|
|
49
|
+
test("masks tool results older than a full quantization block", () => {
|
|
50
|
+
// With keepRecentTurns=2, masking is quantized in blocks of 2 turns: nothing
|
|
51
|
+
// is masked until a full block (2 turns) of excess history accumulates.
|
|
47
52
|
const mask = createObservationMask(2);
|
|
48
53
|
const messages = [
|
|
49
54
|
userMsg("turn 1"),
|
|
50
|
-
toolResult("
|
|
55
|
+
toolResult("turn 1 tool output"),
|
|
51
56
|
assistantMsg("response 1"),
|
|
52
57
|
userMsg("turn 2"),
|
|
53
|
-
toolResult("
|
|
58
|
+
toolResult("turn 2 tool output"),
|
|
54
59
|
assistantMsg("response 2"),
|
|
55
60
|
userMsg("turn 3"),
|
|
56
|
-
toolResult("
|
|
61
|
+
toolResult("turn 3 tool output"),
|
|
57
62
|
assistantMsg("response 3"),
|
|
63
|
+
userMsg("turn 4"),
|
|
64
|
+
toolResult("turn 4 tool output"),
|
|
65
|
+
assistantMsg("response 4"),
|
|
66
|
+
userMsg("turn 5"),
|
|
67
|
+
toolResult("turn 5 tool output"),
|
|
68
|
+
assistantMsg("response 5"),
|
|
58
69
|
];
|
|
59
70
|
const result = mask(messages as any);
|
|
60
|
-
//
|
|
71
|
+
// First block (turns 1-2) is masked
|
|
61
72
|
assert.equal((result[1].content as any)[0].text, MASK_TEXT);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assert.equal((result[7].content as any)[0].text, "
|
|
73
|
+
assert.equal((result[4].content as any)[0].text, MASK_TEXT);
|
|
74
|
+
// Remaining turns (3-5) are within the unmasked window
|
|
75
|
+
assert.equal((result[7].content as any)[0].text, "turn 3 tool output");
|
|
76
|
+
assert.equal((result[10].content as any)[0].text, "turn 4 tool output");
|
|
77
|
+
assert.equal((result[13].content as any)[0].text, "turn 5 tool output");
|
|
65
78
|
});
|
|
66
79
|
|
|
67
80
|
test("never masks assistant messages", () => {
|
|
@@ -126,6 +139,67 @@ test("masks toolResult by role, not by type field", () => {
|
|
|
126
139
|
assert.equal((result[1].content as any)[0].text, MASK_TEXT);
|
|
127
140
|
});
|
|
128
141
|
|
|
142
|
+
function buildTurns(count: number) {
|
|
143
|
+
const messages: unknown[] = [];
|
|
144
|
+
for (let i = 1; i <= count; i++) {
|
|
145
|
+
messages.push(userMsg(`turn ${i}`));
|
|
146
|
+
messages.push(toolResult(`turn ${i} tool output`));
|
|
147
|
+
messages.push(assistantMsg(`response ${i}`));
|
|
148
|
+
}
|
|
149
|
+
return messages;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function maskedTextsByTurn(mask: (m: any) => any[], messages: unknown[], turnCount: number): (string | undefined)[] {
|
|
153
|
+
const result = mask(messages as any);
|
|
154
|
+
const texts: (string | undefined)[] = [];
|
|
155
|
+
for (let i = 0; i < turnCount; i++) {
|
|
156
|
+
const toolResultIndex = i * 3 + 1;
|
|
157
|
+
texts.push((result[toolResultIndex].content as any)[0].text);
|
|
158
|
+
}
|
|
159
|
+
return texts;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test("quantized mask invariant: at least keepRecentTurns most recent turns are always unmasked", () => {
|
|
163
|
+
const N = 3;
|
|
164
|
+
const mask = createObservationMask(N);
|
|
165
|
+
for (let totalTurns = 1; totalTurns <= 20; totalTurns++) {
|
|
166
|
+
const messages = buildTurns(totalTurns);
|
|
167
|
+
const texts = maskedTextsByTurn(mask, messages, totalTurns);
|
|
168
|
+
for (let i = Math.max(0, totalTurns - N); i < totalTurns; i++) {
|
|
169
|
+
assert.notEqual(texts[i], MASK_TEXT, `turn ${i + 1} of ${totalTurns} should be unmasked`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("quantized mask invariant: at most 2*keepRecentTurns-1 turns are unmasked", () => {
|
|
175
|
+
const N = 3;
|
|
176
|
+
const mask = createObservationMask(N);
|
|
177
|
+
for (let totalTurns = 1; totalTurns <= 20; totalTurns++) {
|
|
178
|
+
const messages = buildTurns(totalTurns);
|
|
179
|
+
const texts = maskedTextsByTurn(mask, messages, totalTurns);
|
|
180
|
+
const unmaskedCount = texts.filter((t) => t !== MASK_TEXT).length;
|
|
181
|
+
assert.ok(unmaskedCount <= 2 * N - 1, `expected <= ${2 * N - 1} unmasked turns at total=${totalTurns}, got ${unmaskedCount}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("quantized mask invariant: prefix is byte-stable except at block rollover", () => {
|
|
186
|
+
const N = 3;
|
|
187
|
+
const mask = createObservationMask(N);
|
|
188
|
+
let previousTexts: (string | undefined)[] = [];
|
|
189
|
+
for (let totalTurns = 1; totalTurns <= 20; totalTurns++) {
|
|
190
|
+
const messages = buildTurns(totalTurns);
|
|
191
|
+
const texts = maskedTextsByTurn(mask, messages, totalTurns);
|
|
192
|
+
// Compare the shared prefix (turns 1..totalTurns-1) against the previous turn's run.
|
|
193
|
+
let changedCount = 0;
|
|
194
|
+
for (let i = 0; i < previousTexts.length; i++) {
|
|
195
|
+
if (previousTexts[i] !== texts[i]) changedCount++;
|
|
196
|
+
}
|
|
197
|
+
// A block rollover flips at most N turns from unmasked to masked; otherwise 0 should change.
|
|
198
|
+
assert.ok(changedCount === 0 || changedCount <= N, `turn ${totalTurns}: ${changedCount} prefix turns changed status`);
|
|
199
|
+
previousTexts = texts;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
129
203
|
test("truncates recent bash result user messages", () => {
|
|
130
204
|
const messages = [
|
|
131
205
|
userMsg("turn 1"),
|
|
@@ -175,3 +249,125 @@ test("truncates Responses API function outputs and recent bash result items", ()
|
|
|
175
249
|
assert.ok((result[1].output as string).length < (items[1].output as string).length);
|
|
176
250
|
assert.ok((result[2].content as any)[0].text.length < (items[2].content as any)[0].text.length);
|
|
177
251
|
});
|
|
252
|
+
|
|
253
|
+
function contextInjectionMsg(label: string) {
|
|
254
|
+
return userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[MEMORY] turn=${label}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
test("filterSupersededContextInjections keeps only the latest of three same-kind injections", () => {
|
|
258
|
+
const messages = [
|
|
259
|
+
userMsg("turn 1 prompt"),
|
|
260
|
+
contextInjectionMsg("1"),
|
|
261
|
+
assistantMsg("response 1"),
|
|
262
|
+
userMsg("turn 2 prompt"),
|
|
263
|
+
contextInjectionMsg("2"),
|
|
264
|
+
assistantMsg("response 2"),
|
|
265
|
+
userMsg("turn 3 prompt"),
|
|
266
|
+
contextInjectionMsg("3"),
|
|
267
|
+
];
|
|
268
|
+
const original = JSON.parse(JSON.stringify(messages));
|
|
269
|
+
|
|
270
|
+
const result = filterSupersededContextInjections(messages as any);
|
|
271
|
+
|
|
272
|
+
const survivors = result.filter((m: any) => (m.content?.[0]?.text ?? "").startsWith(GSD_CONTEXT_MESSAGE_SENTINEL));
|
|
273
|
+
assert.equal(survivors.length, 1);
|
|
274
|
+
assert.match((survivors[0] as any).content[0].text, /turn=3/);
|
|
275
|
+
assert.equal(result.length, messages.length - 2);
|
|
276
|
+
// Pure function — input array is untouched.
|
|
277
|
+
assert.deepEqual(messages, original);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("filterSupersededContextInjections keeps the latest injection overall across mixed kinds", () => {
|
|
281
|
+
// buildContextMessage emits one message per turn (guided > forensics > memory);
|
|
282
|
+
// the latest one already embeds the memory block when present, so "keep the
|
|
283
|
+
// latest overall" is correct — no separate per-kind tracking needed.
|
|
284
|
+
const memoryOnly = userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]`);
|
|
285
|
+
const guided = userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[GSD Context Metadata]\n- Memory supplied: yes\n\n[GUIDED EXECUTE]`);
|
|
286
|
+
const messages = [userMsg("turn 1"), memoryOnly, assistantMsg("r1"), userMsg("turn 2"), guided];
|
|
287
|
+
|
|
288
|
+
const result = filterSupersededContextInjections(messages as any);
|
|
289
|
+
|
|
290
|
+
const survivors = result.filter((m: any) => (m.content?.[0]?.text ?? "").startsWith(GSD_CONTEXT_MESSAGE_SENTINEL));
|
|
291
|
+
assert.equal(survivors.length, 1);
|
|
292
|
+
assert.match((survivors[0] as any).content[0].text, /GUIDED EXECUTE/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("filterSupersededContextInjections dedupes legacy pre-sentinel injections via bracketed GSD markers", () => {
|
|
296
|
+
// Sessions created before the sentinel existed carry the stable
|
|
297
|
+
// "[GSD Context Metadata]" / "[GSD Guided Execute Context]" labels but no
|
|
298
|
+
// sentinel prefix; resuming them must still collapse to the latest injection.
|
|
299
|
+
const legacyMemory1 = userMsg("[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY] turn=1");
|
|
300
|
+
const legacyMemory2 = userMsg("[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY] turn=2");
|
|
301
|
+
const messages = [userMsg("turn 1"), legacyMemory1, assistantMsg("r1"), userMsg("turn 2"), legacyMemory2];
|
|
302
|
+
|
|
303
|
+
const result = filterSupersededContextInjections(messages as any);
|
|
304
|
+
|
|
305
|
+
const survivors = result.filter((m: any) => (m.content?.[0]?.text ?? "").startsWith("[GSD Context Metadata]"));
|
|
306
|
+
assert.equal(survivors.length, 1);
|
|
307
|
+
assert.match((survivors[0] as any).content[0].text, /turn=2/);
|
|
308
|
+
assert.equal(result.length, messages.length - 1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("filterSupersededContextInjections never drops a real user message that starts with forensics prose", () => {
|
|
312
|
+
// "Debug GSD itself." is the forensics prompt text — generic prose, not a
|
|
313
|
+
// bracketed GSD marker. A user can legitimately type it; treating it as an
|
|
314
|
+
// injection would silently delete real user input. Only sentinel/bracketed
|
|
315
|
+
// injections may be superseded.
|
|
316
|
+
const userAsk = userMsg("Debug GSD itself. Why does resume hang after a crash?");
|
|
317
|
+
const injection = userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n[GSD Context Metadata]\n- Memory supplied: yes\n\n[MEMORY]`);
|
|
318
|
+
const messages = [userAsk, assistantMsg("r1"), userMsg("turn 2"), injection];
|
|
319
|
+
|
|
320
|
+
const result = filterSupersededContextInjections(messages as any);
|
|
321
|
+
|
|
322
|
+
assert.ok(result.some((m: any) => (m.content?.[0]?.text ?? "").startsWith("Debug GSD itself.")));
|
|
323
|
+
assert.equal(result.length, messages.length);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("filterSupersededContextInjections returns the array unchanged when no injections present", () => {
|
|
327
|
+
const messages = [userMsg("hi"), assistantMsg("hello"), toolResult("data")];
|
|
328
|
+
const result = filterSupersededContextInjections(messages as any);
|
|
329
|
+
assert.equal(result, messages);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("filterSupersededContextInjections preserves forensics opening prompt across follow-up reinjection", () => {
|
|
333
|
+
const forensicsOpening =
|
|
334
|
+
"Debug GSD itself. Trace the symptom to root cause in current source and produce a filing-ready GitHub issue.";
|
|
335
|
+
const messages = [
|
|
336
|
+
userMsg(forensicsOpening),
|
|
337
|
+
assistantMsg("I found the root cause in auto-loop.ts."),
|
|
338
|
+
userMsg("yes, create the issue"),
|
|
339
|
+
userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n${forensicsOpening}`),
|
|
340
|
+
assistantMsg("Creating the issue now."),
|
|
341
|
+
userMsg("also add the stack trace"),
|
|
342
|
+
userMsg(`${GSD_CONTEXT_MESSAGE_SENTINEL}\n${forensicsOpening}\n\n## Follow-up\nalso add the stack trace`),
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
const result = filterSupersededContextInjections(messages as any);
|
|
346
|
+
|
|
347
|
+
assert.equal(result.length, messages.length - 1);
|
|
348
|
+
assert.equal((result[0] as any).content[0].text, forensicsOpening);
|
|
349
|
+
assert.match((result[5] as any).content[0].text, /Follow-up/);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("filterSupersededResponsesContextInjections keeps only the latest injection in payload.input", () => {
|
|
353
|
+
const items = [
|
|
354
|
+
{ role: "user", content: [{ type: "input_text", text: "turn 1" }] },
|
|
355
|
+
{ role: "user", content: [{ type: "input_text", text: `${GSD_CONTEXT_MESSAGE_SENTINEL}\n[MEMORY] turn=1` }] },
|
|
356
|
+
{ type: "function_call_output", call_id: "call_1", output: "ok" },
|
|
357
|
+
{ role: "user", content: [{ type: "input_text", text: `${GSD_CONTEXT_MESSAGE_SENTINEL}\n[MEMORY] turn=2` }] },
|
|
358
|
+
];
|
|
359
|
+
const original = JSON.parse(JSON.stringify(items));
|
|
360
|
+
|
|
361
|
+
const result = filterSupersededResponsesContextInjections(items as any);
|
|
362
|
+
|
|
363
|
+
const survivors = result.filter((i: any) => (i.content?.[0]?.text ?? "").startsWith(GSD_CONTEXT_MESSAGE_SENTINEL));
|
|
364
|
+
assert.equal(survivors.length, 1);
|
|
365
|
+
assert.match((survivors[0] as any).content[0].text, /turn=2/);
|
|
366
|
+
assert.deepEqual(items, original);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("filterSupersededResponsesContextInjections returns the array unchanged when no injections present", () => {
|
|
370
|
+
const items = [{ role: "user", content: [{ type: "input_text", text: "hi" }] }];
|
|
371
|
+
const result = filterSupersededResponsesContextInjections(items as any);
|
|
372
|
+
assert.equal(result, items);
|
|
373
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Verifies GSD_ADVERTISE_TOOL_ALIASES gates alias registration on the native tool path (plan 035).
|
|
3
|
+
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { registerDbTools } from '../bootstrap/db-tools.ts';
|
|
7
|
+
import { WORKFLOW_TOOL_ALIAS_NAMES, WORKFLOW_TOOL_CONTRACTS } from '../workflow-tool-surface.ts';
|
|
8
|
+
|
|
9
|
+
function makeMockPi() {
|
|
10
|
+
const tools: any[] = [];
|
|
11
|
+
return {
|
|
12
|
+
registerTool: (tool: any) => tools.push(tool),
|
|
13
|
+
tools,
|
|
14
|
+
} as any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function withEnv(name: string, value: string | undefined, fn: () => void): void {
|
|
18
|
+
const previous = process.env[name];
|
|
19
|
+
if (value === undefined) delete process.env[name];
|
|
20
|
+
else process.env[name] = value;
|
|
21
|
+
try {
|
|
22
|
+
fn();
|
|
23
|
+
} finally {
|
|
24
|
+
if (previous === undefined) delete process.env[name];
|
|
25
|
+
else process.env[name] = previous;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('default: only canonical names are registered, no aliases', () => {
|
|
30
|
+
withEnv('GSD_ADVERTISE_TOOL_ALIASES', undefined, () => {
|
|
31
|
+
const pi = makeMockPi();
|
|
32
|
+
registerDbTools(pi);
|
|
33
|
+
const names = new Set(pi.tools.map((tool: any) => tool.name));
|
|
34
|
+
|
|
35
|
+
for (const alias of WORKFLOW_TOOL_ALIAS_NAMES) {
|
|
36
|
+
assert.ok(!names.has(alias), `alias "${alias}" should not be registered by default`);
|
|
37
|
+
}
|
|
38
|
+
const registeredCanonical = WORKFLOW_TOOL_CONTRACTS
|
|
39
|
+
.map((tool) => tool.canonicalName)
|
|
40
|
+
.filter((name) => names.has(name));
|
|
41
|
+
assert.ok(registeredCanonical.length > 0, 'canonical tools should still be registered');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('GSD_ADVERTISE_TOOL_ALIASES=1: aliases are registered with the "(alias for ...)" description suffix', () => {
|
|
46
|
+
withEnv('GSD_ADVERTISE_TOOL_ALIASES', '1', () => {
|
|
47
|
+
const pi = makeMockPi();
|
|
48
|
+
registerDbTools(pi);
|
|
49
|
+
const toolByName = new Map<string, any>(pi.tools.map((tool: any) => [tool.name, tool]));
|
|
50
|
+
|
|
51
|
+
const registeredAliases = WORKFLOW_TOOL_ALIAS_NAMES.filter((alias) => toolByName.has(alias));
|
|
52
|
+
assert.ok(registeredAliases.length > 0, 'at least one alias should be registered when opted in');
|
|
53
|
+
|
|
54
|
+
for (const alias of registeredAliases) {
|
|
55
|
+
const aliasTool = toolByName.get(alias);
|
|
56
|
+
assert.ok(
|
|
57
|
+
aliasTool.description.includes('(alias for '),
|
|
58
|
+
`alias "${alias}" description should include "(alias for ...)"`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -18,6 +18,7 @@ import test from "node:test";
|
|
|
18
18
|
import assert from "node:assert/strict";
|
|
19
19
|
|
|
20
20
|
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
21
|
+
import { autoSession } from "../auto-runtime-state.ts";
|
|
21
22
|
import { classifyCommand } from "../safety/destructive-guard.ts";
|
|
22
23
|
import {
|
|
23
24
|
DESTRUCTIVE_CONFIRM_GATE_MARKER,
|
|
@@ -232,6 +233,38 @@ test("integration: real hooks block a force push, then allow it once after confi
|
|
|
232
233
|
assert.equal(thirdAttempt?.block, true, "second run of the same command re-blocks (one-shot)");
|
|
233
234
|
});
|
|
234
235
|
|
|
236
|
+
test("integration: auto-mode destructive block does not pause before confirmation can be asked", async (t) => {
|
|
237
|
+
const dir = "/tmp/gsd-destructive-confirm-int-auto-defer";
|
|
238
|
+
const notifications: string[] = [];
|
|
239
|
+
const ctx = {
|
|
240
|
+
cwd: dir,
|
|
241
|
+
isIdle: () => true,
|
|
242
|
+
sessionManager: { getSessionFile: () => null },
|
|
243
|
+
ui: { notify: (message: string) => notifications.push(message) },
|
|
244
|
+
} as any;
|
|
245
|
+
resetDestructiveConfirmation(dir);
|
|
246
|
+
autoSession.reset();
|
|
247
|
+
autoSession.active = true;
|
|
248
|
+
autoSession.basePath = dir;
|
|
249
|
+
t.after(() => {
|
|
250
|
+
resetDestructiveConfirmation(dir);
|
|
251
|
+
autoSession.reset();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const { handlers, pi } = makeHookHarness();
|
|
255
|
+
registerHooks(pi, []);
|
|
256
|
+
|
|
257
|
+
const block = await runBashGuard(handlers, FORCE_PUSH, ctx);
|
|
258
|
+
|
|
259
|
+
assert.equal(block?.block, true, "real guard blocks the first destructive attempt");
|
|
260
|
+
assert.equal(autoSession.active, true, "auto-mode must remain active so ask_user_questions can run");
|
|
261
|
+
assert.deepEqual(
|
|
262
|
+
notifications.filter((message) => message.includes("Destructive-command confirmation")),
|
|
263
|
+
[],
|
|
264
|
+
"destructive confirmation pause must not fire from the bash tool_call hook",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
235
268
|
test("integration: declining the confirm gate leaves the command blocked", async (t) => {
|
|
236
269
|
const dir = "/tmp/gsd-destructive-confirm-int-decline";
|
|
237
270
|
const ctx = { cwd: dir, ui: { notify: () => undefined } } as any;
|
|
@@ -29,6 +29,7 @@ test("destructive guard still detects direct and wrapper-prefixed recursive dele
|
|
|
29
29
|
"find . -exec rm -rf {} \\;",
|
|
30
30
|
"cat doomed.txt | xargs rm -rf /tmp/x",
|
|
31
31
|
'rm -rf "/tmp/my build"',
|
|
32
|
+
"rm --recursive build",
|
|
32
33
|
];
|
|
33
34
|
|
|
34
35
|
for (const command of destructiveCommands) {
|
|
@@ -39,6 +40,19 @@ test("destructive guard still detects direct and wrapper-prefixed recursive dele
|
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
test("destructive guard does not classify force-only rm as recursive delete", () => {
|
|
44
|
+
const nonRecursiveCommands = [
|
|
45
|
+
"rm -f single-file.md",
|
|
46
|
+
"rm -fv single-file.md",
|
|
47
|
+
"rm single-file.md -f",
|
|
48
|
+
"rm --force single-file.md",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
for (const command of nonRecursiveCommands) {
|
|
52
|
+
assert.deepEqual(classifyCommand(command), { destructive: false, labels: [] }, command);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
42
56
|
test("destructive guard treats quoted interpreter payloads as literals", () => {
|
|
43
57
|
assert.deepEqual(classifyCommand('bash -c "rm -rf /"'), { destructive: false, labels: [] });
|
|
44
58
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtempSync, mkdirSync, rmSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
resolveGsdPathContract,
|
|
13
13
|
resolveSliceFile,
|
|
14
14
|
resolveTaskFile,
|
|
15
|
+
resolveTaskFiles,
|
|
16
|
+
taskIdFromTaskFileName,
|
|
15
17
|
_clearGsdRootCache,
|
|
16
18
|
} from "../../paths.ts";
|
|
17
19
|
/** Create a tmp dir and resolve symlinks + 8.3 short names (macOS /var→/private/var, Windows RUNNER~1→runneradmin). */
|
|
@@ -171,21 +173,37 @@ describe('paths', () => {
|
|
|
171
173
|
test('Case 9: flat-phase task SUMMARY resolves at phase root despite a stray slices/SID/ dir', () => {
|
|
172
174
|
const root = tmp();
|
|
173
175
|
try {
|
|
174
|
-
// Hybrid flat-phase milestone: phase dir holds flat
|
|
176
|
+
// Hybrid flat-phase milestone: phase dir holds flat task summaries, but a
|
|
175
177
|
// stray slices/S01/ folder also exists on disk. resolveSlicePath then points
|
|
176
178
|
// under slices/, so layout (phases/ vs milestones/) — not slicePath===phaseDir
|
|
177
179
|
// — must decide the flat-phase task summary location.
|
|
178
180
|
const phaseDir = join(root, ".gsd", "phases", "01-foo");
|
|
179
181
|
mkdirSync(join(phaseDir, "slices", "S01"), { recursive: true });
|
|
180
|
-
const flatSummary = join(phaseDir, "T01-SUMMARY.md");
|
|
182
|
+
const flatSummary = join(phaseDir, "S01-T01-SUMMARY.md");
|
|
183
|
+
const legacyFlatSummary = join(phaseDir, "T01-SUMMARY.md");
|
|
181
184
|
writeFileSync(flatSummary, "# task summary\n");
|
|
185
|
+
writeFileSync(legacyFlatSummary, "# legacy task summary\n");
|
|
182
186
|
|
|
183
187
|
_clearGsdRootCache();
|
|
184
188
|
|
|
189
|
+
assert.deepStrictEqual(resolveTaskFiles(phaseDir, "SUMMARY").sort(), [
|
|
190
|
+
"S01-T01-SUMMARY.md",
|
|
191
|
+
"T01-SUMMARY.md",
|
|
192
|
+
]);
|
|
193
|
+
assert.equal(taskIdFromTaskFileName("S01-T01-SUMMARY.md", "SUMMARY"), "T01");
|
|
194
|
+
assert.equal(taskIdFromTaskFileName("T01-SUMMARY.md", "SUMMARY"), "T01");
|
|
195
|
+
|
|
185
196
|
assert.deepStrictEqual(
|
|
186
197
|
resolveTaskFile(root, "M001", "S01", "T01", "SUMMARY"),
|
|
187
198
|
flatSummary,
|
|
188
|
-
"flat-phase
|
|
199
|
+
"flat-phase S01-T01-SUMMARY.md at phase root is preferred even when slices/S01/ exists",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
unlinkSync(flatSummary);
|
|
203
|
+
assert.deepStrictEqual(
|
|
204
|
+
resolveTaskFile(root, "M001", "S01", "T01", "SUMMARY"),
|
|
205
|
+
legacyFlatSummary,
|
|
206
|
+
"legacy flat T01-SUMMARY.md is still found as a fallback",
|
|
189
207
|
);
|
|
190
208
|
} finally { cleanup(root); }
|
|
191
209
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// gsd-pi — Integration-branch META lives OUTSIDE milestones/<MID>/ (ADR-045).
|
|
2
|
+
// Verifies the relocation from milestones/<MID>/<MID>-META.json to the flat
|
|
3
|
+
// .gsd/<MID>-META.json so it can never poison isLegacyMilestonesLayout.
|
|
4
|
+
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { afterEach, beforeEach, test } from "node:test";
|
|
7
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
|
|
11
|
+
import { readIntegrationBranch, writeIntegrationBranch } from "../git-service.js";
|
|
12
|
+
import { gsdRoot, isLegacyMilestonesLayout } from "../paths.js";
|
|
13
|
+
|
|
14
|
+
let repo: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
repo = mkdtempSync(join(tmpdir(), "gsd-meta-loc-"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(repo, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("write goes to the new flat .gsd/<MID>-META.json, not milestones/<MID>/", () => {
|
|
25
|
+
writeIntegrationBranch(repo, "M001", "main");
|
|
26
|
+
|
|
27
|
+
assert.ok(
|
|
28
|
+
existsSync(join(gsdRoot(repo), "M001-META.json")),
|
|
29
|
+
"META written to flat .gsd/<MID>-META.json",
|
|
30
|
+
);
|
|
31
|
+
assert.ok(
|
|
32
|
+
!existsSync(join(gsdRoot(repo), "milestones", "M001", "M001-META.json")),
|
|
33
|
+
"no META in legacy milestones/<MID>/ location",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("round-trip: read returns what write recorded", () => {
|
|
38
|
+
writeIntegrationBranch(repo, "M001", "main");
|
|
39
|
+
assert.equal(readIntegrationBranch(repo, "M001"), "main");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("read falls back to the legacy milestones/<MID>/ location", () => {
|
|
43
|
+
const legacyDir = join(gsdRoot(repo), "milestones", "M001");
|
|
44
|
+
mkdirSync(legacyDir, { recursive: true });
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(legacyDir, "M001-META.json"),
|
|
47
|
+
JSON.stringify({ integrationBranch: "develop" }) + "\n",
|
|
48
|
+
"utf-8",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
assert.equal(readIntegrationBranch(repo, "M001"), "develop");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("writing META does not create milestones/<MID>/ or flip layout detection", () => {
|
|
55
|
+
assert.equal(isLegacyMilestonesLayout(repo), false, "clean tree is not legacy layout");
|
|
56
|
+
writeIntegrationBranch(repo, "M001", "main");
|
|
57
|
+
assert.equal(
|
|
58
|
+
isLegacyMilestonesLayout(repo),
|
|
59
|
+
false,
|
|
60
|
+
"writing integration-branch META must not poison layout detection",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
@@ -824,9 +824,9 @@ test('── markdown-renderer: renderTaskSummary round-trip ──', async () =
|
|
|
824
824
|
|
|
825
825
|
// Verify file exists on disk
|
|
826
826
|
const summaryPath = path.join(
|
|
827
|
-
tmpDir, '.gsd', 'phases', '01-test', 'T01-SUMMARY.md',
|
|
827
|
+
tmpDir, '.gsd', 'phases', '01-test', 'S01-T01-SUMMARY.md',
|
|
828
828
|
);
|
|
829
|
-
assert.ok(fs.existsSync(summaryPath), 'T01-SUMMARY.md written to disk');
|
|
829
|
+
assert.ok(fs.existsSync(summaryPath), 'S01-T01-SUMMARY.md written to disk');
|
|
830
830
|
|
|
831
831
|
// Parse and verify
|
|
832
832
|
const rendered = fs.readFileSync(summaryPath, 'utf-8');
|
|
@@ -1020,7 +1020,7 @@ test('── markdown-renderer: renderAllFromDb produces all files ──', asyn
|
|
|
1020
1020
|
|
|
1021
1021
|
// Verify task summary written
|
|
1022
1022
|
const taskSummaryPath = path.join(
|
|
1023
|
-
tmpDir, '.gsd', 'phases', '01-test', 'T01-SUMMARY.md',
|
|
1023
|
+
tmpDir, '.gsd', 'phases', '01-test', 'S01-T01-SUMMARY.md',
|
|
1024
1024
|
);
|
|
1025
1025
|
assert.ok(fs.existsSync(taskSummaryPath), 'T01 summary written by renderAll');
|
|
1026
1026
|
} finally {
|
|
@@ -1546,9 +1546,9 @@ test('── markdown-renderer: repairStaleRenders writes missing task summary
|
|
|
1546
1546
|
|
|
1547
1547
|
// Verify file written
|
|
1548
1548
|
const summaryPath = path.join(
|
|
1549
|
-
tmpDir, '.gsd', 'phases', '01-test', 'T01-SUMMARY.md',
|
|
1549
|
+
tmpDir, '.gsd', 'phases', '01-test', 'S01-T01-SUMMARY.md',
|
|
1550
1550
|
);
|
|
1551
|
-
assert.ok(fs.existsSync(summaryPath), 'T01-SUMMARY.md should exist after repair');
|
|
1551
|
+
assert.ok(fs.existsSync(summaryPath), 'S01-T01-SUMMARY.md should exist after repair');
|
|
1552
1552
|
|
|
1553
1553
|
// Second detect should be empty
|
|
1554
1554
|
clearAllCaches();
|
|
@@ -74,7 +74,7 @@ test("#2885: cost table includes openai-codex provider models", () => {
|
|
|
74
74
|
const ids = BUNDLED_COST_TABLE.map(e => e.id);
|
|
75
75
|
const codexModels = [
|
|
76
76
|
"gpt-5.1", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
|
|
77
|
-
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
|
|
77
|
+
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6",
|
|
78
78
|
];
|
|
79
79
|
for (const model of codexModels) {
|
|
80
80
|
assert.ok(ids.includes(model), `cost table should include openai-codex model "${model}"`);
|
|
@@ -109,3 +109,11 @@ test("gpt-5.5 uses official OpenAI list pricing", () => {
|
|
|
109
109
|
assert.equal(entry.outputPer1k, 0.03);
|
|
110
110
|
assert.equal(entry.updatedAt, "2026-04-23");
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
test("gpt-5.6 mirrors gpt-5.5 pricing until official pricing is published", () => {
|
|
114
|
+
const entry = lookupModelCost("gpt-5.6");
|
|
115
|
+
assert.ok(entry, "lookupModelCost should find gpt-5.6");
|
|
116
|
+
assert.equal(entry.inputPer1k, 0.005);
|
|
117
|
+
assert.equal(entry.outputPer1k, 0.03);
|
|
118
|
+
assert.equal(entry.updatedAt, "2026-07-08");
|
|
119
|
+
});
|
|
@@ -900,7 +900,7 @@ test("#2885: openai-codex standard-tier models are recognized", () => {
|
|
|
900
900
|
|
|
901
901
|
test("#2885: openai-codex heavy-tier models are recognized", () => {
|
|
902
902
|
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
903
|
-
const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "o4-mini", "o4-mini-deep-research"];
|
|
903
|
+
const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-5.6", "o4-mini", "o4-mini-deep-research"];
|
|
904
904
|
for (const model of heavyModels) {
|
|
905
905
|
const result = resolveModelForComplexity(
|
|
906
906
|
makeClassification("heavy"),
|
|
@@ -29,12 +29,14 @@ import {
|
|
|
29
29
|
|
|
30
30
|
const SKILL_NAME = "testskill";
|
|
31
31
|
const COMPLETE_SLICE_SKILL_NAME = "complete-slice-policies";
|
|
32
|
-
const SKILL_ACTIVATION_SUBSTRING = `Read the installed '${SKILL_NAME}' skill file from <available_skills>`;
|
|
33
|
-
const COMPLETE_SLICE_SKILL_ACTIVATION_SUBSTRING = `Read the installed '${COMPLETE_SLICE_SKILL_NAME}' skill file from <available_skills>`;
|
|
34
32
|
|
|
35
33
|
const tmpDirs: string[] = [];
|
|
36
34
|
let savedCwd: string | undefined;
|
|
37
35
|
|
|
36
|
+
function expectedSkillRead(base: string, name: string): string {
|
|
37
|
+
return `Read the skill file at \`${join(base, ".agents", "skills", name, "SKILL.md")}\``;
|
|
38
|
+
}
|
|
39
|
+
|
|
38
40
|
function setupProjectWithSkill(options: {
|
|
39
41
|
skillName?: string;
|
|
40
42
|
preferencesLines?: string[];
|
|
@@ -139,7 +141,7 @@ test("worker prompt (buildResearchSlicePrompt) includes <skill_activation> from
|
|
|
139
141
|
"research-slice prompt should contain a <skill_activation> block",
|
|
140
142
|
);
|
|
141
143
|
assert.ok(
|
|
142
|
-
prompt.includes(
|
|
144
|
+
prompt.includes(expectedSkillRead(base, SKILL_NAME)),
|
|
143
145
|
`research-slice prompt should reference the always-used skill '${SKILL_NAME}'`,
|
|
144
146
|
);
|
|
145
147
|
});
|
|
@@ -164,7 +166,7 @@ test("complete-slice prompt includes <skill_activation> from unit-specific skill
|
|
|
164
166
|
"complete-slice prompt should contain a <skill_activation> block",
|
|
165
167
|
);
|
|
166
168
|
assert.ok(
|
|
167
|
-
prompt.includes(
|
|
169
|
+
prompt.includes(expectedSkillRead(base, COMPLETE_SLICE_SKILL_NAME)),
|
|
168
170
|
`complete-slice prompt should reference the skill-rule skill '${COMPLETE_SLICE_SKILL_NAME}'`,
|
|
169
171
|
);
|
|
170
172
|
});
|
|
@@ -192,7 +194,7 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
|
|
|
192
194
|
`expected at least 2 <skill_activation> blocks (one per slice), got ${blockCount}`,
|
|
193
195
|
);
|
|
194
196
|
assert.ok(
|
|
195
|
-
prompt.includes(
|
|
197
|
+
prompt.includes(expectedSkillRead(base, SKILL_NAME)),
|
|
196
198
|
`parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
|
|
197
199
|
);
|
|
198
200
|
assert.ok(
|