gsd-pi 2.78.1-dev.eccf86e27 → 2.79.0-dev.579e14e9b
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-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +33 -37
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- 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 +41 -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/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- 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/required-server-files.json +1 -1
- 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 +10 -10
- 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/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
- package/dist/web/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/dist/web/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
- package/dist/web/standalone/server.js +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 +116 -0
- 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-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +38 -38
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- 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-recovery.test.ts +68 -1
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -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/workflow-tool-executors.test.ts +100 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +41 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Y5UeGFkXTYM9WIQOWHkot → X6D0ObmOxuQCMG5piZpbE}/_ssgManifest.js +0 -0
|
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
|
|
8
8
|
import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
|
|
9
9
|
import { resolveMilestoneFile } from "../paths.ts";
|
|
10
|
-
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow } from "../gsd-db.ts";
|
|
10
|
+
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask } from "../gsd-db.ts";
|
|
11
11
|
import { clearParseCache } from "../files.ts";
|
|
12
12
|
import { parseRoadmap } from "../parsers-legacy.ts";
|
|
13
13
|
import { invalidateAllCaches } from "../cache.ts";
|
|
@@ -764,6 +764,73 @@ test("hasImplementationArtifacts finds implementation commits when .gsd/ is giti
|
|
|
764
764
|
}
|
|
765
765
|
});
|
|
766
766
|
|
|
767
|
+
test("hasImplementationArtifacts binds GSD-Task trailer to milestone via DB state when .gsd/ is gitignored", () => {
|
|
768
|
+
const base = makeGitBase();
|
|
769
|
+
try {
|
|
770
|
+
writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
|
|
771
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
772
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
773
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
774
|
+
insertSlice({
|
|
775
|
+
id: "S01",
|
|
776
|
+
milestoneId: "M001",
|
|
777
|
+
title: "Slice One",
|
|
778
|
+
status: "complete",
|
|
779
|
+
risk: "low",
|
|
780
|
+
depends: [],
|
|
781
|
+
});
|
|
782
|
+
insertTask({
|
|
783
|
+
id: "T01",
|
|
784
|
+
sliceId: "S01",
|
|
785
|
+
milestoneId: "M001",
|
|
786
|
+
title: "Task One",
|
|
787
|
+
status: "complete",
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
mkdirSync(join(base, "src"), { recursive: true });
|
|
791
|
+
writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
|
|
792
|
+
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
793
|
+
execFileSync(
|
|
794
|
+
"git",
|
|
795
|
+
["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
|
|
796
|
+
{ cwd: base, stdio: "ignore" },
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
const result = hasImplementationArtifacts(base, "M001");
|
|
800
|
+
assert.equal(
|
|
801
|
+
result,
|
|
802
|
+
"present",
|
|
803
|
+
"DB task ownership should bind S01/T01 implementation commits to M001 without explicit M001 text",
|
|
804
|
+
);
|
|
805
|
+
} finally {
|
|
806
|
+
cleanup(base);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("hasImplementationArtifacts does not bind GSD-Task trailer without milestone ownership evidence", () => {
|
|
811
|
+
const base = makeGitBase();
|
|
812
|
+
try {
|
|
813
|
+
writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
|
|
814
|
+
mkdirSync(join(base, "src"), { recursive: true });
|
|
815
|
+
writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
|
|
816
|
+
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
817
|
+
execFileSync(
|
|
818
|
+
"git",
|
|
819
|
+
["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
|
|
820
|
+
{ cwd: base, stdio: "ignore" },
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const result = hasImplementationArtifacts(base, "M001");
|
|
824
|
+
assert.equal(
|
|
825
|
+
result,
|
|
826
|
+
"absent",
|
|
827
|
+
"S01/T01 shape alone must not bind an implementation commit to M001",
|
|
828
|
+
);
|
|
829
|
+
} finally {
|
|
830
|
+
cleanup(base);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
767
834
|
test("hasImplementationArtifacts ignores malformed milestone IDs in commit-message fallback", () => {
|
|
768
835
|
const base = makeGitBase();
|
|
769
836
|
try {
|
|
@@ -31,9 +31,9 @@ describe("bootstrap deriveState DB guards (#3844)", () => {
|
|
|
31
31
|
const compactIdx = registerHooksSrc.indexOf('pi.on("session_before_compact"');
|
|
32
32
|
assert.ok(compactIdx > -1, "register-hooks should define session_before_compact");
|
|
33
33
|
const compactSection = extractSourceRegion(registerHooksSrc, 'pi.on("session_before_compact"');
|
|
34
|
-
const ensureIdx = compactSection.indexOf("ensureDbOpen()");
|
|
34
|
+
const ensureIdx = compactSection.indexOf("ensureDbOpen(basePath)");
|
|
35
35
|
const deriveIdx = compactSection.indexOf("deriveGsdState(basePath)");
|
|
36
|
-
assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen()");
|
|
36
|
+
assert.ok(ensureIdx > -1, "session_before_compact should call ensureDbOpen(basePath)");
|
|
37
37
|
assert.ok(deriveIdx > -1, "session_before_compact should derive state");
|
|
38
38
|
assert.ok(ensureIdx < deriveIdx, "session_before_compact should open DB before deriveState");
|
|
39
39
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test — currentDirectoryRoot() uses os.homedir() as fallback
|
|
3
|
+
* when process.cwd() throws (e.g. worktree teardown deletes the cwd).
|
|
4
|
+
*
|
|
5
|
+
* Before the fix, the catch block used `process.env.HOME ?? "/"`. On
|
|
6
|
+
* Windows, HOME is typically unset so this resolved to "/", an invalid
|
|
7
|
+
* path. After the fix, os.homedir() is used — it checks USERPROFILE,
|
|
8
|
+
* HOMEDRIVE+HOMEPATH, etc., returning a valid path on all platforms.
|
|
9
|
+
*
|
|
10
|
+
* The test monkey-patches process.cwd() to throw ENOENT, simulating a
|
|
11
|
+
* deleted cwd. currentDirectoryRoot() must NOT propagate the raw error;
|
|
12
|
+
* instead it falls back to homedir(), which validateDirectory correctly
|
|
13
|
+
* rejects as "blocked", yielding GSDNoProjectError — the same controlled
|
|
14
|
+
* error path handlers already know how to catch.
|
|
15
|
+
*
|
|
16
|
+
* The error message is also asserted to match validateDirectory(homedir()),
|
|
17
|
+
* confirming the fallback resolved to homedir() specifically (not "/" or
|
|
18
|
+
* any other path).
|
|
19
|
+
*/
|
|
20
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
|
|
24
|
+
import { currentDirectoryRoot, GSDNoProjectError } from "../commands/context.ts";
|
|
25
|
+
import { validateDirectory } from "../validate-directory.ts";
|
|
26
|
+
|
|
27
|
+
describe("currentDirectoryRoot() homedir() fallback on deleted cwd", () => {
|
|
28
|
+
const originalCwd = process.cwd.bind(process);
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
process.cwd = () => {
|
|
32
|
+
const err = new Error("ENOENT: no such file or directory, uv_cwd");
|
|
33
|
+
(err as NodeJS.ErrnoException).code = "ENOENT";
|
|
34
|
+
throw err;
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.cwd = originalCwd;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("does not propagate ENOENT — throws GSDNoProjectError via homedir() fallback", () => {
|
|
43
|
+
const expected = validateDirectory(homedir());
|
|
44
|
+
assert.equal(expected.severity, "blocked", "homedir() itself should be blocked");
|
|
45
|
+
|
|
46
|
+
assert.throws(
|
|
47
|
+
() => currentDirectoryRoot(),
|
|
48
|
+
(err: unknown) => {
|
|
49
|
+
assert.ok(
|
|
50
|
+
err instanceof GSDNoProjectError,
|
|
51
|
+
`expected GSDNoProjectError, got: ${err}`,
|
|
52
|
+
);
|
|
53
|
+
assert.equal(
|
|
54
|
+
(err as Error).message,
|
|
55
|
+
expected.reason ?? "GSD must be run inside a project directory.",
|
|
56
|
+
"error message must match validateDirectory(homedir()), confirming homedir() was the fallback",
|
|
57
|
+
);
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
"should throw GSDNoProjectError (homedir fallback validated), not raw ENOENT",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -101,6 +101,20 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
|
|
|
101
101
|
prompt.includes("Implement the thing"),
|
|
102
102
|
"must include task plan body content from disk",
|
|
103
103
|
);
|
|
104
|
+
assert.ok(prompt.includes("## Context Mode"), "execute-task should include standalone Context Mode guidance");
|
|
105
|
+
assert.ok(prompt.includes("execution lane"), "execute-task should render the execution lane");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("buildExecuteTaskPrompt omits Context Mode when disabled", async () => {
|
|
109
|
+
writeFileSync(
|
|
110
|
+
join(base, ".gsd", "PREFERENCES.md"),
|
|
111
|
+
["---", "context_mode:", " enabled: false", "---", ""].join("\n"),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const prompt = await buildExecuteTaskPrompt(MID, SID, S_TITLE, TID, T_TITLE, base);
|
|
115
|
+
|
|
116
|
+
assert.ok(!prompt.includes("## Context Mode"));
|
|
117
|
+
assert.ok(!prompt.includes("Context Mode (execution lane)"));
|
|
104
118
|
});
|
|
105
119
|
|
|
106
120
|
test("buildCompleteSlicePrompt carries the complete-slice contract", async () => {
|
|
@@ -147,4 +147,12 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
|
|
|
147
147
|
prompt.includes(SKILL_ACTIVATION_SUBSTRING),
|
|
148
148
|
`parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
|
|
149
149
|
);
|
|
150
|
+
assert.ok(
|
|
151
|
+
prompt.includes("Context Mode (research lane):"),
|
|
152
|
+
"embedded parallel research subagent prompts should use nested Context Mode guidance",
|
|
153
|
+
);
|
|
154
|
+
assert.ok(
|
|
155
|
+
!prompt.includes("## Context Mode\n\nLane: **research lane**."),
|
|
156
|
+
"embedded parallel research subagent prompts should not use standalone Context Mode heading",
|
|
157
|
+
);
|
|
150
158
|
});
|
|
@@ -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;
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
_getAdapter,
|
|
12
12
|
insertGateRow,
|
|
13
13
|
upsertRequirement,
|
|
14
|
+
getAllMilestones,
|
|
14
15
|
} from "../gsd-db.ts";
|
|
16
|
+
import { deriveState, invalidateStateCache } from "../state.ts";
|
|
15
17
|
import { markApprovalGateVerified, markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot, setPendingGate } from "../bootstrap/write-gate.ts";
|
|
16
18
|
import {
|
|
17
19
|
executeCompleteMilestone,
|
|
@@ -737,6 +739,104 @@ test("executeSummarySave supports root-level deep planning artifacts", async ()
|
|
|
737
739
|
}
|
|
738
740
|
});
|
|
739
741
|
|
|
742
|
+
test("executeSummarySave registers PROJECT milestone sequence for the next run", async () => {
|
|
743
|
+
const base = makeTmpBase();
|
|
744
|
+
try {
|
|
745
|
+
openTestDb(base);
|
|
746
|
+
|
|
747
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
748
|
+
artifact_type: "PROJECT",
|
|
749
|
+
content: [
|
|
750
|
+
"# Project",
|
|
751
|
+
"",
|
|
752
|
+
"## What This Is",
|
|
753
|
+
"",
|
|
754
|
+
"Deep project setup output.",
|
|
755
|
+
"",
|
|
756
|
+
"## Project Shape",
|
|
757
|
+
"",
|
|
758
|
+
"**Complexity:** complex",
|
|
759
|
+
"**Why:** It spans multiple delivery steps.",
|
|
760
|
+
"",
|
|
761
|
+
"## Capability Contract",
|
|
762
|
+
"",
|
|
763
|
+
"See .gsd/REQUIREMENTS.md.",
|
|
764
|
+
"",
|
|
765
|
+
"## Milestone Sequence",
|
|
766
|
+
"",
|
|
767
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
768
|
+
"- [ ] M002: Polish - Follow-up experience work.",
|
|
769
|
+
"",
|
|
770
|
+
].join("\n"),
|
|
771
|
+
}, base));
|
|
772
|
+
|
|
773
|
+
assert.equal(result.isError, undefined);
|
|
774
|
+
assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
|
|
775
|
+
|
|
776
|
+
const milestones = getAllMilestones();
|
|
777
|
+
assert.deepEqual(
|
|
778
|
+
milestones.map((m) => [m.id, m.title, m.status]),
|
|
779
|
+
[
|
|
780
|
+
["M001", "Foundation", "queued"],
|
|
781
|
+
["M002", "Polish", "queued"],
|
|
782
|
+
],
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
invalidateStateCache();
|
|
786
|
+
const state = await deriveState(base);
|
|
787
|
+
assert.equal(state.activeMilestone?.id, "M001");
|
|
788
|
+
assert.equal(state.phase, "pre-planning");
|
|
789
|
+
assert.equal(state.registry[0]?.status, "active");
|
|
790
|
+
assert.equal(state.registry[1]?.status, "pending");
|
|
791
|
+
} finally {
|
|
792
|
+
closeDatabase();
|
|
793
|
+
cleanup(base);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("executeSummarySave keeps PROJECT artifact save successful if milestone registration fails", async () => {
|
|
798
|
+
const base = makeTmpBase();
|
|
799
|
+
try {
|
|
800
|
+
openTestDb(base);
|
|
801
|
+
const db = _getAdapter();
|
|
802
|
+
assert.ok(db, "DB should be open");
|
|
803
|
+
const originalPrepare = db.prepare.bind(db);
|
|
804
|
+
(db as any).prepare = (sql: string) => {
|
|
805
|
+
if (sql.includes("INSERT OR IGNORE INTO milestones")) {
|
|
806
|
+
throw new Error("simulated milestone registration failure");
|
|
807
|
+
}
|
|
808
|
+
return originalPrepare(sql);
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const result = await inProjectDir(base, () => executeSummarySave({
|
|
812
|
+
artifact_type: "PROJECT",
|
|
813
|
+
content: [
|
|
814
|
+
"# Project",
|
|
815
|
+
"",
|
|
816
|
+
"## What This Is",
|
|
817
|
+
"",
|
|
818
|
+
"Deep project setup output.",
|
|
819
|
+
"",
|
|
820
|
+
"## Milestone Sequence",
|
|
821
|
+
"",
|
|
822
|
+
"- [ ] M001: Foundation - Establish the first runnable slice.",
|
|
823
|
+
"",
|
|
824
|
+
].join("\n"),
|
|
825
|
+
}, base));
|
|
826
|
+
|
|
827
|
+
assert.equal(result.isError, undefined);
|
|
828
|
+
assert.equal(result.details.path, "PROJECT.md");
|
|
829
|
+
assert.equal(result.details.registeredMilestones, undefined);
|
|
830
|
+
assert.match(String(result.details.warning), /milestone registration failed/);
|
|
831
|
+
assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
|
|
832
|
+
const artifact = originalPrepare("SELECT path FROM artifacts WHERE path = ?").get("PROJECT.md");
|
|
833
|
+
assert.equal(artifact?.path, "PROJECT.md");
|
|
834
|
+
} finally {
|
|
835
|
+
closeDatabase();
|
|
836
|
+
cleanup(base);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
740
840
|
test("executeSummarySave blocks final root artifacts while approval gate is pending", async () => {
|
|
741
841
|
const base = makeTmpBase();
|
|
742
842
|
try {
|
|
@@ -229,6 +229,9 @@ test("worktree-aware prompt builders include the explicit working directory", as
|
|
|
229
229
|
),
|
|
230
230
|
]);
|
|
231
231
|
|
|
232
|
+
assert.ok(prompts[0].includes("## Context Mode"), "discuss-milestone should include standalone Context Mode guidance");
|
|
233
|
+
assert.ok(prompts[0].includes("interview lane"), "discuss-milestone should render the interview lane");
|
|
234
|
+
|
|
232
235
|
for (const prompt of prompts) {
|
|
233
236
|
assert.match(prompt, /working directory/i);
|
|
234
237
|
assert.ok(prompt.includes(base), "prompt should include the provided working directory");
|