gsd-pi 2.79.0 → 2.80.0
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 +94 -47
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +61 -7
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
- package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/auto.js +159 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +41 -45
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +34 -1
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
- package/dist/resources/extensions/gsd/preferences-types.js +20 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +82 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- package/dist/resources/extensions/shared/interview-ui.js +15 -4
- 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 +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- 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 +9 -9
- 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/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +53 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +129 -2
- package/packages/mcp-server/src/workflow-tools.ts +81 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
- package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
- package/src/resources/extensions/gsd/auto/phases.ts +88 -9
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
- package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/auto.ts +167 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +49 -46
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +35 -1
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
- package/src/resources/extensions/gsd/preferences-types.ts +23 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +132 -3
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +84 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
|
@@ -304,6 +304,30 @@ describe("resolveImportPath", () => {
|
|
|
304
304
|
assert.ok(!result.exists);
|
|
305
305
|
assert.equal(result.resolvedPath, null);
|
|
306
306
|
});
|
|
307
|
+
|
|
308
|
+
test("resolves dotted TS module stem like .server via extension probing", (t) => {
|
|
309
|
+
const dir = mkdtempSync(join(tmpdir(), "post-exec-test-server-dot-"));
|
|
310
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
311
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
312
|
+
writeFileSync(join(dir, "src", "route.server.ts"), "export {};\n");
|
|
313
|
+
writeFileSync(join(dir, "src", "main.ts"), "");
|
|
314
|
+
|
|
315
|
+
const result = resolveImportPath("./route.server", "src/main.ts", dir);
|
|
316
|
+
assert.ok(result.exists);
|
|
317
|
+
assert.ok(result.resolvedPath?.endsWith("route.server.ts"));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("missing unknown explicit extension does not match code-extension shadow", (t) => {
|
|
321
|
+
const dir = mkdtempSync(join(tmpdir(), "post-exec-test-unknown-shadow-"));
|
|
322
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
323
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
324
|
+
writeFileSync(join(dir, "src", "video.mp4.ts"), "export {};\n");
|
|
325
|
+
writeFileSync(join(dir, "src", "main.ts"), "");
|
|
326
|
+
|
|
327
|
+
const result = resolveImportPath("./video.mp4", "src/main.ts", dir);
|
|
328
|
+
assert.ok(!result.exists);
|
|
329
|
+
assert.equal(result.resolvedPath, null);
|
|
330
|
+
});
|
|
307
331
|
});
|
|
308
332
|
|
|
309
333
|
// ─── Import Resolution Check Tests ───────────────────────────────────────────
|
|
@@ -334,6 +358,28 @@ describe("checkImportResolution", () => {
|
|
|
334
358
|
}
|
|
335
359
|
});
|
|
336
360
|
|
|
361
|
+
test("ignores generated React Router +types imports", () => {
|
|
362
|
+
tempDir = join(tmpdir(), `post-exec-test-${Date.now()}`);
|
|
363
|
+
mkdirSync(tempDir, { recursive: true });
|
|
364
|
+
mkdirSync(join(tempDir, "app", "routes"), { recursive: true });
|
|
365
|
+
writeFileSync(
|
|
366
|
+
join(tempDir, "app", "routes", "root.tsx"),
|
|
367
|
+
"import type { Route } from './+types/root';\nexport default function Root() { return null; }"
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const task = createTask({
|
|
372
|
+
id: "T01",
|
|
373
|
+
key_files: ["app/routes/root.tsx"],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const results = checkImportResolution(task, [], tempDir);
|
|
377
|
+
assert.deepEqual(results, []);
|
|
378
|
+
} finally {
|
|
379
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
337
383
|
test("fails when import doesn't resolve", () => {
|
|
338
384
|
tempDir = join(tmpdir(), `post-exec-test-${Date.now()}`);
|
|
339
385
|
mkdirSync(tempDir, { recursive: true });
|
|
@@ -111,6 +111,9 @@ test("#4551: buildPlanSlicePrompt injects fix section when priorPreExecFailure p
|
|
|
111
111
|
},
|
|
112
112
|
);
|
|
113
113
|
|
|
114
|
+
assert.ok(prompt.includes("## Context Mode"), "plan-slice should include standalone Context Mode guidance");
|
|
115
|
+
assert.ok(prompt.includes("planning lane"), "plan-slice should render the planning lane");
|
|
116
|
+
|
|
114
117
|
assert.ok(
|
|
115
118
|
prompt.includes("Fix these specific issues from the prior pre-exec check"),
|
|
116
119
|
"prompt must contain the fix section heading",
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
|
|
7
7
|
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
8
|
+
import { autoSession } from "../auto-runtime-state.ts";
|
|
8
9
|
import { parseContinue } from "../files.ts";
|
|
9
10
|
import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
10
11
|
import { deriveState, invalidateStateCache } from "../state.ts";
|
|
@@ -96,3 +97,87 @@ test("register-hooks writes CONTINUE checkpoint during planning phase without ac
|
|
|
96
97
|
assert.match(parsed.completedWork, /planning phase/i, "completed-work should capture non-executing phase context");
|
|
97
98
|
assert.match(parsed.nextAction, /slice S01/i, "next action should route resume to the active slice");
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
test("register-hooks writes Context Mode snapshot before active auto cancels compaction", async (t) => {
|
|
102
|
+
const base = createPlanningFixtureBase();
|
|
103
|
+
const originalCwd = process.cwd();
|
|
104
|
+
process.chdir(base);
|
|
105
|
+
invalidateStateCache();
|
|
106
|
+
closeDatabase();
|
|
107
|
+
autoSession.reset();
|
|
108
|
+
autoSession.active = true;
|
|
109
|
+
|
|
110
|
+
t.after(() => {
|
|
111
|
+
autoSession.reset();
|
|
112
|
+
invalidateStateCache();
|
|
113
|
+
closeDatabase();
|
|
114
|
+
process.chdir(originalCwd);
|
|
115
|
+
rmSync(base, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
119
|
+
const pi = {
|
|
120
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
121
|
+
const existing = handlers.get(event) ?? [];
|
|
122
|
+
existing.push(handler);
|
|
123
|
+
handlers.set(event, existing);
|
|
124
|
+
},
|
|
125
|
+
} as any;
|
|
126
|
+
|
|
127
|
+
registerHooks(pi, []);
|
|
128
|
+
|
|
129
|
+
const compactHandlers = handlers.get("session_before_compact");
|
|
130
|
+
assert.ok(compactHandlers && compactHandlers.length > 0, "session_before_compact handler should be registered");
|
|
131
|
+
|
|
132
|
+
const result = await compactHandlers;
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(result, { cancel: true }, "active auto should still cancel compaction");
|
|
135
|
+
const snapshotPath = join(base, ".gsd", "last-snapshot.md");
|
|
136
|
+
assert.ok(existsSync(snapshotPath), "active auto cancel should still leave a Context Mode snapshot");
|
|
137
|
+
assert.match(readFileSync(snapshotPath, "utf-8"), /GSD context snapshot/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("register-hooks does not write Context Mode snapshot when disabled", async (t) => {
|
|
141
|
+
const base = createPlanningFixtureBase();
|
|
142
|
+
writeFileSync(
|
|
143
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
144
|
+
"---\ncontext_mode:\n enabled: false\n---\n",
|
|
145
|
+
"utf-8",
|
|
146
|
+
);
|
|
147
|
+
const originalCwd = process.cwd();
|
|
148
|
+
process.chdir(base);
|
|
149
|
+
invalidateStateCache();
|
|
150
|
+
closeDatabase();
|
|
151
|
+
autoSession.reset();
|
|
152
|
+
|
|
153
|
+
t.after(() => {
|
|
154
|
+
autoSession.reset();
|
|
155
|
+
invalidateStateCache();
|
|
156
|
+
closeDatabase();
|
|
157
|
+
process.chdir(originalCwd);
|
|
158
|
+
rmSync(base, { recursive: true, force: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
|
|
162
|
+
const pi = {
|
|
163
|
+
on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
|
|
164
|
+
const existing = handlers.get(event) ?? [];
|
|
165
|
+
existing.push(handler);
|
|
166
|
+
handlers.set(event, existing);
|
|
167
|
+
},
|
|
168
|
+
} as any;
|
|
169
|
+
|
|
170
|
+
registerHooks(pi, []);
|
|
171
|
+
|
|
172
|
+
const compactHandlers = handlers.get("session_before_compact");
|
|
173
|
+
assert.ok(compactHandlers && compactHandlers.length > 0, "session_before_compact handler should be registered");
|
|
174
|
+
|
|
175
|
+
for (const handler of compactHandlers ?? []) {
|
|
176
|
+
await handler({});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
assert.ok(
|
|
180
|
+
!existsSync(join(base, ".gsd", "last-snapshot.md")),
|
|
181
|
+
"disabled Context Mode should not write a snapshot",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
@@ -95,6 +95,8 @@ test("#4782 phase 3: buildRunUatPrompt inlines slice UAT, slice summary, project
|
|
|
95
95
|
|
|
96
96
|
// Context wrapper present
|
|
97
97
|
assert.match(prompt, /## Inlined Context \(preloaded — do not re-read these files\)/);
|
|
98
|
+
assert.match(prompt, /## Context Mode/);
|
|
99
|
+
assert.match(prompt, /verification lane/);
|
|
98
100
|
|
|
99
101
|
// Artifacts from the manifest inline list, in declared order:
|
|
100
102
|
// slice-uat → slice-summary → project (#4925 review).
|
|
@@ -211,6 +211,14 @@ test("buildReactiveExecutePrompt: output contains model string when subagentMode
|
|
|
211
211
|
prompt.includes('model: "claude-opus-4-6"'),
|
|
212
212
|
`Prompt should contain model instruction. Got:\n${prompt.slice(0, 500)}`,
|
|
213
213
|
);
|
|
214
|
+
assert.ok(
|
|
215
|
+
prompt.includes("Context Mode (execution lane):"),
|
|
216
|
+
"embedded reactive-execute task prompt should use nested Context Mode guidance",
|
|
217
|
+
);
|
|
218
|
+
assert.ok(
|
|
219
|
+
prompt.includes("## Context Mode"),
|
|
220
|
+
"reactive-execute parent prompt should include standalone Context Mode guidance",
|
|
221
|
+
);
|
|
214
222
|
});
|
|
215
223
|
|
|
216
224
|
test("buildReactiveExecutePrompt: no model instruction when subagentModel omitted", async (t) => {
|
|
@@ -266,3 +274,54 @@ test("buildReactiveExecutePrompt: no model instruction when subagentModel omitte
|
|
|
266
274
|
"Prompt should not contain model instruction when subagentModel is omitted",
|
|
267
275
|
);
|
|
268
276
|
});
|
|
277
|
+
|
|
278
|
+
test("buildGateEvaluatePrompt: embedded gate prompts use nested Context Mode guidance", async (t) => {
|
|
279
|
+
const { buildGateEvaluatePrompt } = await import("../auto-prompts.ts");
|
|
280
|
+
const { closeDatabase, insertGateRow, insertMilestone, insertSlice, openDatabase } = await import("../gsd-db.ts");
|
|
281
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-subagent-model-gate-"));
|
|
282
|
+
t.after(() => {
|
|
283
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
284
|
+
rmSync(repo, { recursive: true, force: true });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const sliceDir = join(repo, ".gsd", "milestones", "M001", "slices", "S01");
|
|
288
|
+
mkdirSync(sliceDir, { recursive: true });
|
|
289
|
+
writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01 Plan\n\n## Verification\n- Run checks.\n");
|
|
290
|
+
openDatabase(join(repo, ".gsd", "gsd.db"));
|
|
291
|
+
insertMilestone({ id: "M001", title: "Test Milestone", status: "active", depends_on: [] });
|
|
292
|
+
insertSlice({
|
|
293
|
+
id: "S01",
|
|
294
|
+
milestoneId: "M001",
|
|
295
|
+
title: "Test Slice",
|
|
296
|
+
status: "planned",
|
|
297
|
+
risk: "low",
|
|
298
|
+
depends: [],
|
|
299
|
+
demo: "",
|
|
300
|
+
sequence: 1,
|
|
301
|
+
});
|
|
302
|
+
insertGateRow({
|
|
303
|
+
milestoneId: "M001",
|
|
304
|
+
sliceId: "S01",
|
|
305
|
+
gateId: "Q3",
|
|
306
|
+
scope: "slice",
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const prompt = await buildGateEvaluatePrompt(
|
|
310
|
+
"M001",
|
|
311
|
+
"Test Milestone",
|
|
312
|
+
"S01",
|
|
313
|
+
"Test Slice",
|
|
314
|
+
repo,
|
|
315
|
+
"claude-opus-4-6",
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
assert.ok(
|
|
319
|
+
prompt.includes("Context Mode (verification lane):"),
|
|
320
|
+
"embedded gate-evaluate prompt should use nested Context Mode guidance",
|
|
321
|
+
);
|
|
322
|
+
assert.ok(
|
|
323
|
+
prompt.includes("## Context Mode"),
|
|
324
|
+
"gate-evaluate parent prompt should include standalone Context Mode guidance",
|
|
325
|
+
);
|
|
326
|
+
assert.ok(prompt.includes('model: "claude-opus-4-6"'), "gate subagent prompt should preserve model instruction");
|
|
327
|
+
});
|
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
composeContextModeInstructions,
|
|
12
13
|
composeInlinedContext,
|
|
13
14
|
composeUnitContext,
|
|
14
15
|
manifestBudgetChars,
|
|
@@ -101,6 +102,43 @@ test("#4782 composer: manifestBudgetChars returns declared budget", () => {
|
|
|
101
102
|
assert.strictEqual(manifestBudgetChars("never-dispatched"), null);
|
|
102
103
|
});
|
|
103
104
|
|
|
105
|
+
test("Context Mode composer: disabled, unknown, and none modes return empty string", () => {
|
|
106
|
+
assert.strictEqual(
|
|
107
|
+
composeContextModeInstructions("execute-task", { enabled: false, renderMode: "standalone" }),
|
|
108
|
+
"",
|
|
109
|
+
);
|
|
110
|
+
assert.strictEqual(
|
|
111
|
+
composeContextModeInstructions("never-dispatched", { enabled: true, renderMode: "standalone" }),
|
|
112
|
+
"",
|
|
113
|
+
);
|
|
114
|
+
assert.strictEqual(
|
|
115
|
+
composeContextModeInstructions("workflow-preferences", { enabled: true, renderMode: "standalone" }),
|
|
116
|
+
"",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("Context Mode composer: standalone output starts with heading and includes required tools", () => {
|
|
121
|
+
const out = composeContextModeInstructions("execute-task", { enabled: true, renderMode: "standalone" });
|
|
122
|
+
assert.ok(out.startsWith("## Context Mode"));
|
|
123
|
+
assert.match(out, /execution lane/i);
|
|
124
|
+
assert.match(out, /`gsd_exec`/);
|
|
125
|
+
assert.match(out, /noisy scans, builds, and tests/);
|
|
126
|
+
assert.match(out, /`gsd_exec_search`/);
|
|
127
|
+
assert.match(out, /before repeating prior runs/);
|
|
128
|
+
assert.match(out, /`gsd_resume`/);
|
|
129
|
+
assert.match(out, /after compaction or resume/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("Context Mode composer: nested output is compact single sentence", () => {
|
|
133
|
+
const out = composeContextModeInstructions("gate-evaluate", { enabled: true, renderMode: "nested" });
|
|
134
|
+
assert.ok(!out.startsWith("## Context Mode"));
|
|
135
|
+
assert.match(out, /^Context Mode \(verification lane\): /);
|
|
136
|
+
assert.strictEqual(out.split(/\n/).length, 1);
|
|
137
|
+
assert.match(out, /`gsd_exec`/);
|
|
138
|
+
assert.match(out, /`gsd_exec_search`/);
|
|
139
|
+
assert.match(out, /`gsd_resume`/);
|
|
140
|
+
});
|
|
141
|
+
|
|
104
142
|
// ─── Integration: migrated buildReassessRoadmapPrompt ─────────────────────
|
|
105
143
|
|
|
106
144
|
function makeFixtureBase(): string {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
UNIT_MANIFESTS,
|
|
13
13
|
resolveManifest,
|
|
14
14
|
type ArtifactKey,
|
|
15
|
+
type ContextModePolicy,
|
|
15
16
|
type SkillsPolicy,
|
|
16
17
|
type UnitContextManifest,
|
|
17
18
|
} from "../unit-context-manifest.ts";
|
|
@@ -105,6 +106,37 @@ test("#4782 phase 1: every manifest has a positive maxSystemPromptChars", () =>
|
|
|
105
106
|
}
|
|
106
107
|
});
|
|
107
108
|
|
|
109
|
+
test("Context Mode: every manifest declares the expected contextMode lane", () => {
|
|
110
|
+
const expected: Record<string, ContextModePolicy> = {
|
|
111
|
+
"workflow-preferences": "none",
|
|
112
|
+
"research-decision": "none",
|
|
113
|
+
"discuss-project": "interview",
|
|
114
|
+
"discuss-requirements": "interview",
|
|
115
|
+
"discuss-milestone": "interview",
|
|
116
|
+
"research-project": "research",
|
|
117
|
+
"research-milestone": "research",
|
|
118
|
+
"research-slice": "research",
|
|
119
|
+
"plan-milestone": "planning",
|
|
120
|
+
"plan-slice": "planning",
|
|
121
|
+
"refine-slice": "planning",
|
|
122
|
+
"replan-slice": "planning",
|
|
123
|
+
"reassess-roadmap": "planning",
|
|
124
|
+
"execute-task": "execution",
|
|
125
|
+
"reactive-execute": "execution",
|
|
126
|
+
"run-uat": "verification",
|
|
127
|
+
"gate-evaluate": "verification",
|
|
128
|
+
"validate-milestone": "verification",
|
|
129
|
+
"complete-slice": "verification",
|
|
130
|
+
"complete-milestone": "verification",
|
|
131
|
+
"rewrite-docs": "docs",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(Object.keys(expected).sort(), [...KNOWN_UNIT_TYPES].sort());
|
|
135
|
+
for (const unitType of KNOWN_UNIT_TYPES) {
|
|
136
|
+
assert.strictEqual(UNIT_MANIFESTS[unitType].contextMode, expected[unitType]);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
108
140
|
test("#4782 phase 1: skills policy shapes are valid discriminated-union members", () => {
|
|
109
141
|
for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
|
|
110
142
|
const p = manifest.skills as SkillsPolicy;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// GSD2 UOK Contract Versioning and DB Authority Tests
|
|
2
|
+
|
|
1
3
|
import test from "node:test";
|
|
2
4
|
import assert from "node:assert/strict";
|
|
5
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
3
8
|
|
|
4
9
|
import type {
|
|
5
10
|
AuditEventEnvelope,
|
|
@@ -11,8 +16,17 @@ import type {
|
|
|
11
16
|
WriteRecord,
|
|
12
17
|
WriterToken,
|
|
13
18
|
} from "../uok/contracts.ts";
|
|
14
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CURRENT_UOK_CONTRACT_VERSION,
|
|
21
|
+
normalizeAuditEvent,
|
|
22
|
+
validateAuditEvent,
|
|
23
|
+
validateDispatchEnvelope,
|
|
24
|
+
validateTurnResult,
|
|
25
|
+
} from "../uok/contracts.ts";
|
|
26
|
+
import { buildAuditEnvelope, emitUokAuditEvent } from "../uok/audit.ts";
|
|
15
27
|
import { buildDispatchEnvelope, explainDispatch } from "../uok/dispatch-envelope.ts";
|
|
28
|
+
import { buildTurnTimeline } from "../uok/timeline.ts";
|
|
29
|
+
import { _getAdapter, closeDatabase, openDatabase } from "../gsd-db.ts";
|
|
16
30
|
|
|
17
31
|
test("uok contracts serialize/deserialize turn envelopes", () => {
|
|
18
32
|
const contract: TurnContract = {
|
|
@@ -37,6 +51,7 @@ test("uok contracts serialize/deserialize turn envelopes", () => {
|
|
|
37
51
|
};
|
|
38
52
|
|
|
39
53
|
const result: TurnResult = {
|
|
54
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
40
55
|
traceId: contract.traceId,
|
|
41
56
|
turnId: contract.turnId,
|
|
42
57
|
iteration: contract.iteration,
|
|
@@ -56,8 +71,10 @@ test("uok contracts serialize/deserialize turn envelopes", () => {
|
|
|
56
71
|
|
|
57
72
|
const roundTrip = JSON.parse(JSON.stringify(result)) as TurnResult;
|
|
58
73
|
assert.equal(roundTrip.turnId, "turn-1");
|
|
74
|
+
assert.equal(roundTrip.version, CURRENT_UOK_CONTRACT_VERSION);
|
|
59
75
|
assert.equal(roundTrip.gateResults?.[0]?.gateId, "Q3");
|
|
60
76
|
assert.equal(roundTrip.phaseResults.length, 3);
|
|
77
|
+
assert.equal(validateTurnResult(roundTrip).ok, true);
|
|
61
78
|
});
|
|
62
79
|
|
|
63
80
|
test("uok contracts include required DAG node kinds", () => {
|
|
@@ -84,9 +101,11 @@ test("uok audit envelope includes trace/turn/causality fields", () => {
|
|
|
84
101
|
});
|
|
85
102
|
|
|
86
103
|
assert.equal(event.traceId, "trace-xyz");
|
|
104
|
+
assert.equal(event.version, CURRENT_UOK_CONTRACT_VERSION);
|
|
87
105
|
assert.equal(event.turnId, "turn-xyz");
|
|
88
106
|
assert.equal(event.causedBy, "turn-start");
|
|
89
107
|
assert.equal(event.payload.status, "completed");
|
|
108
|
+
assert.equal(validateAuditEvent(event).ok, true);
|
|
90
109
|
});
|
|
91
110
|
|
|
92
111
|
test("uok dispatch envelope carries scheduler reason and constraints", () => {
|
|
@@ -107,9 +126,98 @@ test("uok dispatch envelope carries scheduler reason and constraints", () => {
|
|
|
107
126
|
});
|
|
108
127
|
|
|
109
128
|
assert.equal(envelope.nodeKind, "unit");
|
|
129
|
+
assert.equal(envelope.version, CURRENT_UOK_CONTRACT_VERSION);
|
|
110
130
|
assert.equal(envelope.reason.reasonCode, "dependency");
|
|
111
131
|
assert.deepEqual(envelope.constraints?.dependsOn, ["plan-gate"]);
|
|
112
132
|
assert.ok(explainDispatch(envelope).includes("execute-task M001/S01/T01"));
|
|
133
|
+
assert.equal(validateDispatchEnvelope(envelope).ok, true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("uok contracts normalize legacy records without losing payload fields", () => {
|
|
137
|
+
const legacy = {
|
|
138
|
+
eventId: "event-legacy",
|
|
139
|
+
traceId: "trace-legacy",
|
|
140
|
+
category: "orchestration",
|
|
141
|
+
type: "turn-result",
|
|
142
|
+
ts: new Date().toISOString(),
|
|
143
|
+
payload: { status: "completed", extra: "preserved" },
|
|
144
|
+
} as AuditEventEnvelope;
|
|
145
|
+
|
|
146
|
+
const normalized = normalizeAuditEvent(legacy);
|
|
147
|
+
assert.equal(normalized.version, "0");
|
|
148
|
+
assert.equal(normalized.payload.extra, "preserved");
|
|
149
|
+
assert.equal(validateAuditEvent(legacy).ok, true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("uok audit emission writes DB as authoritative before jsonl projection", (t) => {
|
|
153
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-db-audit-"));
|
|
154
|
+
mkdirSync(join(basePath, ".gsd"), { recursive: true });
|
|
155
|
+
t.after(() => {
|
|
156
|
+
closeDatabase();
|
|
157
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
assert.equal(openDatabase(join(basePath, ".gsd", "gsd.db")), true);
|
|
161
|
+
emitUokAuditEvent(
|
|
162
|
+
basePath,
|
|
163
|
+
buildAuditEnvelope({
|
|
164
|
+
traceId: "trace-db",
|
|
165
|
+
turnId: "turn-db",
|
|
166
|
+
category: "orchestration",
|
|
167
|
+
type: "turn-start",
|
|
168
|
+
payload: { unitType: "execute-task" },
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const row = _getAdapter()!.prepare(
|
|
173
|
+
"SELECT payload_json FROM audit_events WHERE trace_id = 'trace-db' AND turn_id = 'turn-db'",
|
|
174
|
+
).get() as { payload_json: string } | undefined;
|
|
175
|
+
assert.ok(row, "DB audit row should be written");
|
|
176
|
+
assert.equal(JSON.parse(row.payload_json).contractVersion, CURRENT_UOK_CONTRACT_VERSION);
|
|
177
|
+
|
|
178
|
+
const projection = readFileSync(join(basePath, ".gsd", "audit", "events.jsonl"), "utf-8");
|
|
179
|
+
assert.ok(projection.includes("trace-db"), "jsonl projection should still be written");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("uok timeline prefers DB records over jsonl projection when DB is available", (t) => {
|
|
183
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-timeline-"));
|
|
184
|
+
const auditDir = join(basePath, ".gsd", "audit");
|
|
185
|
+
mkdirSync(auditDir, { recursive: true });
|
|
186
|
+
writeFileSync(
|
|
187
|
+
join(auditDir, "events.jsonl"),
|
|
188
|
+
`${JSON.stringify({
|
|
189
|
+
version: CURRENT_UOK_CONTRACT_VERSION,
|
|
190
|
+
eventId: "jsonl-only",
|
|
191
|
+
traceId: "trace-timeline",
|
|
192
|
+
turnId: "turn-timeline",
|
|
193
|
+
category: "orchestration",
|
|
194
|
+
type: "jsonl-projection",
|
|
195
|
+
ts: "2026-01-01T00:00:00.000Z",
|
|
196
|
+
payload: {},
|
|
197
|
+
})}\n`,
|
|
198
|
+
);
|
|
199
|
+
t.after(() => {
|
|
200
|
+
closeDatabase();
|
|
201
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
assert.equal(openDatabase(join(basePath, ".gsd", "gsd.db")), true);
|
|
205
|
+
emitUokAuditEvent(
|
|
206
|
+
basePath,
|
|
207
|
+
buildAuditEnvelope({
|
|
208
|
+
traceId: "trace-timeline",
|
|
209
|
+
turnId: "turn-timeline",
|
|
210
|
+
category: "orchestration",
|
|
211
|
+
type: "db-authoritative",
|
|
212
|
+
payload: {},
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const timeline = buildTurnTimeline(basePath, { traceId: "trace-timeline", turnId: "turn-timeline" });
|
|
217
|
+
assert.equal(timeline.authoritative, "db");
|
|
218
|
+
assert.equal(timeline.degraded, false);
|
|
219
|
+
assert.ok(timeline.entries.some((entry) => entry.type === "db-authoritative"));
|
|
220
|
+
assert.equal(timeline.entries.some((entry) => entry.type === "jsonl-projection"), false);
|
|
113
221
|
});
|
|
114
222
|
|
|
115
223
|
test("uok writer records serialize sequence metadata", () => {
|
|
@@ -63,3 +63,101 @@ test("uok turn observer adds writer sequence metadata to audit events", (t) => {
|
|
|
63
63
|
assert.equal(payloads[1]?.writeSequence, 2);
|
|
64
64
|
assert.equal(typeof payloads[0]?.writerTokenId, "string");
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
test("uok turn observer releases writer token when validation throws", (t) => {
|
|
68
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-loop-writer-throw-"));
|
|
69
|
+
resetWriterTokensForTests();
|
|
70
|
+
t.after(() => {
|
|
71
|
+
resetWriterTokensForTests();
|
|
72
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const observer = createTurnObserver({
|
|
76
|
+
basePath,
|
|
77
|
+
gitAction: "status-only",
|
|
78
|
+
gitPush: false,
|
|
79
|
+
enableAudit: false,
|
|
80
|
+
enableGitops: false,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
observer.onTurnStart({
|
|
84
|
+
basePath,
|
|
85
|
+
traceId: "trace-throw",
|
|
86
|
+
turnId: "turn-throw",
|
|
87
|
+
iteration: 1,
|
|
88
|
+
unitType: "execute-task",
|
|
89
|
+
unitId: "M001/S01/T01",
|
|
90
|
+
startedAt: new Date().toISOString(),
|
|
91
|
+
});
|
|
92
|
+
assert.equal(hasActiveWriterToken(basePath, "turn-throw"), true);
|
|
93
|
+
|
|
94
|
+
// Invalid payload (missing required fields like status/finishedAt) should
|
|
95
|
+
// trigger validateTurnResult to fail and throw.
|
|
96
|
+
assert.throws(() => {
|
|
97
|
+
observer.onTurnResult({
|
|
98
|
+
traceId: "trace-throw",
|
|
99
|
+
turnId: "turn-throw",
|
|
100
|
+
// @ts-expect-error intentionally invalid for test
|
|
101
|
+
iteration: "not-a-number",
|
|
102
|
+
unitType: "execute-task",
|
|
103
|
+
unitId: "M001/S01/T01",
|
|
104
|
+
status: "completed",
|
|
105
|
+
failureClass: "none",
|
|
106
|
+
phaseResults: [],
|
|
107
|
+
startedAt: new Date().toISOString(),
|
|
108
|
+
finishedAt: new Date().toISOString(),
|
|
109
|
+
});
|
|
110
|
+
}, /Invalid UOK turn result/);
|
|
111
|
+
|
|
112
|
+
// Cleanup must run in finally — token released, no leaked state.
|
|
113
|
+
assert.equal(hasActiveWriterToken(basePath, "turn-throw"), false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("uok turn observer falls back to cached phaseResults when result.phaseResults is missing", (t) => {
|
|
117
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-loop-writer-missing-"));
|
|
118
|
+
resetWriterTokensForTests();
|
|
119
|
+
t.after(() => {
|
|
120
|
+
resetWriterTokensForTests();
|
|
121
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const observer = createTurnObserver({
|
|
125
|
+
basePath,
|
|
126
|
+
gitAction: "status-only",
|
|
127
|
+
gitPush: false,
|
|
128
|
+
enableAudit: false,
|
|
129
|
+
enableGitops: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
observer.onTurnStart({
|
|
133
|
+
basePath,
|
|
134
|
+
traceId: "trace-missing",
|
|
135
|
+
turnId: "turn-missing",
|
|
136
|
+
iteration: 1,
|
|
137
|
+
unitType: "execute-task",
|
|
138
|
+
unitId: "M001/S01/T01",
|
|
139
|
+
startedAt: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Without the Array.isArray guard, accessing result.phaseResults.length on a
|
|
143
|
+
// payload where phaseResults is undefined would throw TypeError before
|
|
144
|
+
// validateTurnResult could surface a structured error. The guard must defer
|
|
145
|
+
// to the cached phaseResults fallback so the turn completes cleanly.
|
|
146
|
+
assert.doesNotThrow(() => {
|
|
147
|
+
observer.onTurnResult({
|
|
148
|
+
traceId: "trace-missing",
|
|
149
|
+
turnId: "turn-missing",
|
|
150
|
+
iteration: 1,
|
|
151
|
+
unitType: "execute-task",
|
|
152
|
+
unitId: "M001/S01/T01",
|
|
153
|
+
status: "completed",
|
|
154
|
+
failureClass: "none",
|
|
155
|
+
// @ts-expect-error intentionally missing for test
|
|
156
|
+
phaseResults: undefined,
|
|
157
|
+
startedAt: new Date().toISOString(),
|
|
158
|
+
finishedAt: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
assert.equal(hasActiveWriterToken(basePath, "turn-missing"), false);
|
|
163
|
+
});
|