gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.9d08d820b
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/dist/bundled-resource-path.d.ts +7 -0
- package/dist/bundled-resource-path.js +34 -2
- package/dist/claude-cli-check.js +18 -6
- package/dist/headless-query.js +21 -6
- package/dist/loader.js +2 -3
- package/dist/resource-loader.js +2 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +19 -7
- package/dist/resources/extensions/gsd/auto/phases.js +3 -11
- package/dist/resources/extensions/gsd/auto/session.js +2 -6
- package/dist/resources/extensions/gsd/auto-dashboard.js +3 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +18 -6
- package/dist/resources/extensions/gsd/auto-prompts.js +63 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -13
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -1
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
- package/dist/resources/extensions/gsd/commands-config.js +3 -2
- package/dist/resources/extensions/gsd/commands-extensions.js +46 -3
- package/dist/resources/extensions/gsd/commands-handlers.js +3 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +6 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +2 -1
- package/dist/resources/extensions/gsd/forensics.js +8 -6
- package/dist/resources/extensions/gsd/guided-flow.js +2 -1
- package/dist/resources/extensions/gsd/home-dir.js +16 -0
- package/dist/resources/extensions/gsd/key-manager.js +2 -1
- package/dist/resources/extensions/gsd/migrate/command.js +3 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +10 -0
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +10 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +29 -4
- package/dist/resources/extensions/gsd/worktree-manager.js +20 -1
- package/dist/resources/extensions/gsd/worktree-resolver.js +4 -13
- package/dist/resources/extensions/gsd/worktree-root.js +124 -0
- package/dist/resources/extensions/gsd/worktree.js +4 -115
- package/dist/resources/extensions/ollama/index.js +15 -2
- package/dist/resources/extensions/ollama/model-capabilities.js +31 -0
- package/dist/resources/extensions/ollama/ollama-client.js +40 -4
- package/dist/resources/extensions/subagent/index.js +324 -178
- 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/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 +9 -9
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/worktree-cli.d.ts +1 -0
- package/dist/worktree-cli.js +9 -3
- package/package.json +1 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +52 -0
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +20 -7
- package/src/resources/extensions/gsd/auto/phases.ts +3 -11
- package/src/resources/extensions/gsd/auto/session.ts +2 -6
- package/src/resources/extensions/gsd/auto-dashboard.ts +3 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -6
- package/src/resources/extensions/gsd/auto-prompts.ts +60 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +44 -12
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
- package/src/resources/extensions/gsd/commands-config.ts +3 -2
- package/src/resources/extensions/gsd/commands-extensions.ts +43 -3
- package/src/resources/extensions/gsd/commands-handlers.ts +3 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +6 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +2 -1
- package/src/resources/extensions/gsd/forensics.ts +10 -5
- package/src/resources/extensions/gsd/guided-flow.ts +2 -1
- package/src/resources/extensions/gsd/home-dir.ts +19 -0
- package/src/resources/extensions/gsd/journal.ts +4 -1
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/migrate/command.ts +3 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +10 -0
- package/src/resources/extensions/gsd/prompts/refine-slice.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/home-dir.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +18 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +17 -1
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +38 -3
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +34 -33
- package/src/resources/extensions/gsd/tests/worktree.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +116 -1
- package/src/resources/extensions/gsd/unit-context-manifest.ts +36 -4
- package/src/resources/extensions/gsd/worktree-manager.ts +40 -1
- package/src/resources/extensions/gsd/worktree-resolver.ts +4 -14
- package/src/resources/extensions/gsd/worktree-root.ts +144 -0
- package/src/resources/extensions/gsd/worktree.ts +8 -119
- package/src/resources/extensions/ollama/index.ts +16 -2
- package/src/resources/extensions/ollama/model-capabilities.ts +34 -0
- package/src/resources/extensions/ollama/ollama-client.ts +41 -4
- package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +96 -0
- package/src/resources/extensions/ollama/tests/ollama-client-timeout-env.test.ts +147 -0
- package/src/resources/extensions/subagent/index.ts +165 -7
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → -Ukk6_YxRd4GY4iUOnRUE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → -Ukk6_YxRd4GY4iUOnRUE}/_ssgManifest.js +0 -0
|
@@ -20,6 +20,16 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
|
|
|
20
20
|
|
|
21
21
|
## Your Role in the Pipeline
|
|
22
22
|
|
|
23
|
+
### Delegate Recon When Useful
|
|
24
|
+
|
|
25
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate recon and sub-decomposition. Prefer delegation over inline work when:
|
|
26
|
+
|
|
27
|
+
- You'd otherwise read more than ~3 files to understand a subsystem touched by the sketch → dispatch the **scout** agent and work from its compressed report.
|
|
28
|
+
- A specific area of the refinement needs deeper architectural analysis → dispatch the **planner** agent for a focused sub-plan, then integrate.
|
|
29
|
+
- You need current external information (library docs, API behavior) → dispatch the **researcher** agent.
|
|
30
|
+
|
|
31
|
+
**Do not** dispatch implementation-tier agents (`worker`, `refactorer`, `tester`) — they would write user source and bypass write isolation. Implementation belongs in `execute-task`.
|
|
32
|
+
|
|
23
33
|
### Respect the Sketch Scope
|
|
24
34
|
|
|
25
35
|
The sketch scope inlined above is a **hard constraint**. Plan within it. If, after exploring the codebase, the scope is too narrow to deliver the goal, surface this as a deviation in the plan's narrative and still produce the plan — do not silently expand the scope.
|
|
@@ -14,6 +14,7 @@ import assert from "node:assert/strict";
|
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { AutoSession } from "../auto/session.ts";
|
|
17
18
|
|
|
18
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const AUTO_TS_PATH = join(__dirname, "..", "auto.ts");
|
|
@@ -32,6 +33,20 @@ function getRuntimeStateTsSource(): string {
|
|
|
32
33
|
return readFileSync(RUNTIME_STATE_TS_PATH, "utf-8");
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
test("AutoSession.lockBasePath uses GSD_PROJECT_ROOT for symlink-resolved worktrees", () => {
|
|
37
|
+
const savedProjectRoot = process.env.GSD_PROJECT_ROOT;
|
|
38
|
+
process.env.GSD_PROJECT_ROOT = "/real/project";
|
|
39
|
+
try {
|
|
40
|
+
const session = new AutoSession();
|
|
41
|
+
session.basePath = "/Users/dev/.gsd/projects/abc123/worktrees/M001/slices/S01";
|
|
42
|
+
|
|
43
|
+
assert.equal(session.lockBasePath, "/real/project");
|
|
44
|
+
} finally {
|
|
45
|
+
if (savedProjectRoot === undefined) delete process.env.GSD_PROJECT_ROOT;
|
|
46
|
+
else process.env.GSD_PROJECT_ROOT = savedProjectRoot;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
35
50
|
// ── Invariant 1: No module-level mutable variables in auto.ts ────────────────
|
|
36
51
|
|
|
37
52
|
test("auto.ts has no module-level let declarations", () => {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// GSD2 — Regression test for Issue #4946 isVersionGreater (commands-extensions.ts)
|
|
2
|
+
//
|
|
3
|
+
// Covers the inline npm-version comparator that replaced the `semver` import in
|
|
4
|
+
// commands-extensions.ts. The original import broke `tsc -p tsconfig.json` whenever
|
|
5
|
+
// `@types/semver` failed to install — the file is type-checked transitively despite
|
|
6
|
+
// being under the `src/resources` exclude. The replacement keeps the same comparison
|
|
7
|
+
// semantics for the realistic input space (npm extension version strings) without
|
|
8
|
+
// pulling in the full semver type surface.
|
|
9
|
+
|
|
10
|
+
import test, { describe } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { isVersionGreater } from "../commands-extensions.ts";
|
|
13
|
+
|
|
14
|
+
describe("isVersionGreater — npm extension version comparison (#4946)", () => {
|
|
15
|
+
test("strictly greater patch beats lesser patch", () => {
|
|
16
|
+
assert.equal(isVersionGreater("1.2.4", "1.2.3"), true);
|
|
17
|
+
assert.equal(isVersionGreater("1.2.3", "1.2.4"), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("equal versions are not strictly greater", () => {
|
|
21
|
+
assert.equal(isVersionGreater("1.2.3", "1.2.3"), false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("numeric (not lexicographic) component comparison", () => {
|
|
25
|
+
// Lexicographic compare would say "1.10.0" < "1.9.0".
|
|
26
|
+
assert.equal(isVersionGreater("1.10.0", "1.9.0"), true);
|
|
27
|
+
assert.equal(isVersionGreater("1.9.0", "1.10.0"), false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("major bump beats minor and patch differences", () => {
|
|
31
|
+
assert.equal(isVersionGreater("2.0.0", "1.99.99"), true);
|
|
32
|
+
assert.equal(isVersionGreater("1.99.99", "2.0.0"), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("missing trailing components default to zero", () => {
|
|
36
|
+
assert.equal(isVersionGreater("1.2", "1.1.9"), true);
|
|
37
|
+
assert.equal(isVersionGreater("1", "0.9.9"), true);
|
|
38
|
+
assert.equal(isVersionGreater("1.2", "1.2.0"), false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("release version beats prerelease at the same release number", () => {
|
|
42
|
+
assert.equal(isVersionGreater("1.0.0", "1.0.0-beta.1"), true);
|
|
43
|
+
assert.equal(isVersionGreater("1.0.0-beta.1", "1.0.0"), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("prerelease ordering: beta.2 > beta.1, rc.1 > beta.9", () => {
|
|
47
|
+
assert.equal(isVersionGreater("1.0.0-beta.2", "1.0.0-beta.1"), true);
|
|
48
|
+
assert.equal(isVersionGreater("1.0.0-rc.1", "1.0.0-beta.9"), true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("non-numeric junk in components doesn't crash — coerces to 0", () => {
|
|
52
|
+
// Defensive: we don't throw on garbage input; we treat unparseable
|
|
53
|
+
// components as 0. This matches the behaviour the extension installer
|
|
54
|
+
// can rely on without surfacing an error to the user.
|
|
55
|
+
assert.equal(isVersionGreater("1.x.0", "1.0.0"), false);
|
|
56
|
+
assert.equal(isVersionGreater("abc", "0.0.1"), false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for getHomeDir() — cross-platform home directory resolution.
|
|
3
|
+
*
|
|
4
|
+
* @see https://github.com/gsd-build/gsd-2/issues/5015
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
|
|
10
|
+
describe("getHomeDir", () => {
|
|
11
|
+
let savedHome: string | undefined;
|
|
12
|
+
let savedUserProfile: string | undefined;
|
|
13
|
+
let getHomeDir: () => string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
savedHome = process.env.HOME;
|
|
17
|
+
savedUserProfile = process.env.USERPROFILE;
|
|
18
|
+
const mod = await import("../home-dir.ts");
|
|
19
|
+
getHomeDir = mod.getHomeDir;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (savedHome !== undefined) {
|
|
24
|
+
process.env.HOME = savedHome;
|
|
25
|
+
} else {
|
|
26
|
+
delete process.env.HOME;
|
|
27
|
+
}
|
|
28
|
+
if (savedUserProfile !== undefined) {
|
|
29
|
+
process.env.USERPROFILE = savedUserProfile;
|
|
30
|
+
} else {
|
|
31
|
+
delete process.env.USERPROFILE;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns HOME when set", () => {
|
|
36
|
+
process.env.HOME = "/test/home";
|
|
37
|
+
delete process.env.USERPROFILE;
|
|
38
|
+
assert.equal(getHomeDir(), "/test/home");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("falls back to USERPROFILE when HOME is unset", () => {
|
|
42
|
+
delete process.env.HOME;
|
|
43
|
+
process.env.USERPROFILE = String.raw`C:\Users\test`;
|
|
44
|
+
assert.equal(getHomeDir(), String.raw`C:\Users\test`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("falls back to os.homedir() when both HOME and USERPROFILE are unset", () => {
|
|
48
|
+
delete process.env.HOME;
|
|
49
|
+
delete process.env.USERPROFILE;
|
|
50
|
+
assert.equal(getHomeDir(), homedir());
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { describe, test, afterEach } from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from "node:fs";
|
|
10
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync, symlinkSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { execSync } from "node:child_process";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getAutoWorktreeOriginalBase,
|
|
22
22
|
getActiveAutoWorktreeContext,
|
|
23
23
|
syncGsdStateToWorktree,
|
|
24
|
+
_resetAutoWorktreeOriginalBaseForTests,
|
|
24
25
|
} from "../../auto-worktree.ts";
|
|
25
26
|
|
|
26
27
|
// Note: execSync is used intentionally in tests for git operations with
|
|
@@ -142,6 +143,54 @@ describe("auto-worktree lifecycle", () => {
|
|
|
142
143
|
teardownAutoWorktree(tempDir, "M003");
|
|
143
144
|
});
|
|
144
145
|
|
|
146
|
+
test("symlink-resolved auto worktree is detected after module state reset", () => {
|
|
147
|
+
tempDir = createTempRepo();
|
|
148
|
+
const savedGsdHome = process.env.GSD_HOME;
|
|
149
|
+
const fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "auto-wt-home-")));
|
|
150
|
+
const storage = join(fakeHome, ".gsd", "projects", "abc123def456");
|
|
151
|
+
mkdirSync(join(storage, "milestones", "M001"), { recursive: true });
|
|
152
|
+
writeFileSync(join(storage, "milestones", "M001", "CONTEXT.md"), "# M001\n");
|
|
153
|
+
symlinkSync(storage, join(tempDir, ".gsd"));
|
|
154
|
+
process.env.GSD_HOME = join(fakeHome, ".gsd");
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const wtPath = createAutoWorktree(tempDir, "M001");
|
|
158
|
+
const realWtPath = realpathSync(wtPath);
|
|
159
|
+
assert.ok(realWtPath.startsWith(storage), "git registered the symlink-resolved worktree path");
|
|
160
|
+
|
|
161
|
+
_resetAutoWorktreeOriginalBaseForTests();
|
|
162
|
+
process.chdir(join(realWtPath, ".gsd", "milestones", "M001"));
|
|
163
|
+
|
|
164
|
+
assert.ok(isInAutoWorktree(tempDir), "structural detection works without module originalBase");
|
|
165
|
+
const resolved = getAutoWorktreePath(realWtPath, "M001");
|
|
166
|
+
assert.ok(resolved, "existing worktree is found when basePath is the worktree path");
|
|
167
|
+
assert.equal(realpathSync(resolved!), realWtPath);
|
|
168
|
+
assert.equal(existsSync(join(realWtPath, ".gsd", "worktrees", "M001")), false);
|
|
169
|
+
|
|
170
|
+
enterAutoWorktree(tempDir, "M001");
|
|
171
|
+
process.chdir(join(realWtPath, ".gsd", "milestones", "M001"));
|
|
172
|
+
assert.deepStrictEqual(
|
|
173
|
+
getActiveAutoWorktreeContext(),
|
|
174
|
+
{
|
|
175
|
+
originalBase: tempDir,
|
|
176
|
+
worktreeName: "M001",
|
|
177
|
+
branch: "milestone/M001",
|
|
178
|
+
},
|
|
179
|
+
"active context is detected from a symlink-resolved worktree cwd",
|
|
180
|
+
);
|
|
181
|
+
} finally {
|
|
182
|
+
process.chdir(tempDir);
|
|
183
|
+
try {
|
|
184
|
+
teardownAutoWorktree(tempDir, "M001");
|
|
185
|
+
} catch {
|
|
186
|
+
// Best-effort cleanup for partially-created temp worktrees.
|
|
187
|
+
}
|
|
188
|
+
if (savedGsdHome === undefined) delete process.env.GSD_HOME;
|
|
189
|
+
else process.env.GSD_HOME = savedGsdHome;
|
|
190
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
145
194
|
test("coexistence with manual worktree", async () => {
|
|
146
195
|
tempDir = createTempRepo();
|
|
147
196
|
const msDir = join(tempDir, ".gsd", "milestones", "M003");
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { describe, test } from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
|
|
13
|
-
import { _resolveReportBasePath } from "../auto/phases.ts";
|
|
13
|
+
import { _resolveDispatchGuardBasePath, _resolveReportBasePath } from "../auto/phases.ts";
|
|
14
14
|
|
|
15
15
|
describe("_resolveReportBasePath", () => {
|
|
16
16
|
test("uses originalBasePath when set (worktree scenario)", () => {
|
|
@@ -48,4 +48,21 @@ describe("_resolveReportBasePath", () => {
|
|
|
48
48
|
|
|
49
49
|
assert.equal(_resolveReportBasePath(session), "/home/user/repo");
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test("uses GSD_PROJECT_ROOT for symlink-resolved worktree paths", () => {
|
|
53
|
+
const savedProjectRoot = process.env.GSD_PROJECT_ROOT;
|
|
54
|
+
process.env.GSD_PROJECT_ROOT = "/real/project";
|
|
55
|
+
try {
|
|
56
|
+
const session = {
|
|
57
|
+
originalBasePath: "",
|
|
58
|
+
basePath: "/Users/dev/.gsd/projects/abc123/worktrees/M001/slices/S01",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
assert.equal(_resolveReportBasePath(session), "/real/project");
|
|
62
|
+
assert.equal(_resolveDispatchGuardBasePath(session), "/real/project");
|
|
63
|
+
} finally {
|
|
64
|
+
if (savedProjectRoot === undefined) delete process.env.GSD_PROJECT_ROOT;
|
|
65
|
+
else process.env.GSD_PROJECT_ROOT = savedProjectRoot;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
51
68
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdtempSync, mkdirSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { mkdtempSync, mkdirSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { appendOverride, loadActiveOverrides } from "../files.ts";
|
|
@@ -73,6 +73,22 @@ describe("steer worktree path resolution (#3476)", () => {
|
|
|
73
73
|
assert.equal(result, null, "returns null for worktree without .git file");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
test("getAutoWorktreePath returns null when .git is a directory", () => {
|
|
77
|
+
mkdirSync(join(worktreePath, ".git"), { recursive: true });
|
|
78
|
+
|
|
79
|
+
const result = getAutoWorktreePath(projectRoot, "M001");
|
|
80
|
+
|
|
81
|
+
assert.equal(result, null, "returns null for standalone .git directories");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("getAutoWorktreePath returns null when .git file is not a gitdir pointer", () => {
|
|
85
|
+
writeFileSync(join(worktreePath, ".git"), "not-a-gitdir\n", "utf-8");
|
|
86
|
+
|
|
87
|
+
const result = getAutoWorktreePath(projectRoot, "M001");
|
|
88
|
+
|
|
89
|
+
assert.equal(result, null, "returns null for invalid .git files");
|
|
90
|
+
});
|
|
91
|
+
|
|
76
92
|
test("override routing: inactive worktree directory should not receive overrides", async () => {
|
|
77
93
|
// Simulate the handleSteer path-resolution logic:
|
|
78
94
|
// When no auto-mode is running, even if a worktree dir exists,
|
|
@@ -199,8 +199,8 @@ test("#4934: every manifest declares a tools policy", () => {
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
test("#4934: tools.mode is one of the
|
|
203
|
-
const validModes = new Set(["all", "read-only", "planning", "docs"]);
|
|
202
|
+
test("#4934: tools.mode is one of the declared policies", () => {
|
|
203
|
+
const validModes = new Set(["all", "read-only", "planning", "planning-dispatch", "docs"]);
|
|
204
204
|
for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
|
|
205
205
|
const mode = (manifest as { tools: { mode: string } }).tools.mode;
|
|
206
206
|
assert.ok(
|
|
@@ -219,7 +219,42 @@ test('#4934: only execute-task and reactive-execute may use tools.mode "all" (fu
|
|
|
219
219
|
allowedAllUnits.has(unitType),
|
|
220
220
|
`manifest "${unitType}" declares tools.mode = "all" but is not on the execute-track. ` +
|
|
221
221
|
'Only execute-task and reactive-execute should have full source write access; ' +
|
|
222
|
-
'planning/discuss/research units must use "planning" (or "docs" for rewrite-docs).',
|
|
222
|
+
'planning/discuss/research units must use "planning" or "planning-dispatch" (or "docs" for rewrite-docs).',
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('planning-dispatch mode is reserved for slice-level decomposition and completion units', () => {
|
|
229
|
+
const allowedDispatchUnits = new Set([
|
|
230
|
+
"plan-slice",
|
|
231
|
+
"refine-slice",
|
|
232
|
+
"complete-slice",
|
|
233
|
+
"complete-milestone",
|
|
234
|
+
]);
|
|
235
|
+
for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
|
|
236
|
+
const mode = (manifest as { tools: { mode: string } }).tools.mode;
|
|
237
|
+
if (mode === "planning-dispatch") {
|
|
238
|
+
assert.ok(
|
|
239
|
+
allowedDispatchUnits.has(unitType),
|
|
240
|
+
`manifest "${unitType}" declares tools.mode = "planning-dispatch" but is not on the dispatch-allowed allowlist. ` +
|
|
241
|
+
'planning-dispatch is intentionally narrow — extend the allowlist consciously when a new unit type genuinely benefits from subagent delegation.',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('planning-dispatch manifests declare non-empty allowedSubagents lists', () => {
|
|
248
|
+
for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
|
|
249
|
+
if (manifest.tools.mode !== "planning-dispatch") continue;
|
|
250
|
+
assert.ok(
|
|
251
|
+
Array.isArray(manifest.tools.allowedSubagents) && manifest.tools.allowedSubagents.length > 0,
|
|
252
|
+
`manifest "${unitType}" has planning-dispatch policy but no allowedSubagents — explicit allowlist is required for runtime dispatch gating`,
|
|
253
|
+
);
|
|
254
|
+
for (const agent of manifest.tools.allowedSubagents) {
|
|
255
|
+
assert.ok(
|
|
256
|
+
typeof agent === "string" && agent.length > 0,
|
|
257
|
+
`manifest "${unitType}" has empty/invalid allowedSubagents entry: ${JSON.stringify(agent)}`,
|
|
223
258
|
);
|
|
224
259
|
}
|
|
225
260
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
listWorktrees,
|
|
21
21
|
worktreePath,
|
|
22
22
|
} from "../worktree-manager.ts";
|
|
23
|
-
import {
|
|
23
|
+
import { test } from 'node:test';
|
|
24
24
|
import assert from 'node:assert/strict';
|
|
25
25
|
|
|
26
26
|
|
|
@@ -28,37 +28,41 @@ function run(command: string, cwd: string): string {
|
|
|
28
28
|
return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Resolve tmpdir to handle macOS /tmp -> /private/var/... symlink.
|
|
34
|
-
const realTmp = realpathSync(tmpdir());
|
|
35
|
-
const base = mkdtempSync(join(realTmp, "gsd-wt-symlink-test-"));
|
|
36
|
-
const externalState = mkdtempSync(join(realTmp, "gsd-wt-symlink-ext-"));
|
|
37
|
-
|
|
38
|
-
run("git init -b main", base);
|
|
39
|
-
run('git config user.name "Test"', base);
|
|
40
|
-
run('git config user.email "test@example.com"', base);
|
|
41
|
-
|
|
42
|
-
// Create external state directory structure
|
|
43
|
-
mkdirSync(join(externalState, "worktrees"), { recursive: true });
|
|
44
|
-
|
|
45
|
-
// Create .gsd as a symlink to the external state directory
|
|
46
|
-
symlinkSync(externalState, join(base, ".gsd"));
|
|
47
|
-
|
|
48
|
-
// Verify the symlink is in place
|
|
49
|
-
assert.ok(existsSync(join(base, ".gsd")), ".gsd symlink exists");
|
|
50
|
-
assert.ok(
|
|
51
|
-
realpathSync(join(base, ".gsd")) === externalState,
|
|
52
|
-
".gsd resolves to external state dir",
|
|
53
|
-
);
|
|
31
|
+
test('worktree-symlink-removal removes the git-registered symlink target safely', (t) => {
|
|
32
|
+
console.log("\n=== #1852: removeWorktree with symlinked .gsd/ ===");
|
|
54
33
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
// Set up a test repo with .gsd/ as a symlink to an external directory,
|
|
35
|
+
// mimicking the external state directory layout (~/.gsd/projects/<hash>/).
|
|
36
|
+
// Resolve tmpdir to handle macOS /tmp -> /private/var/... symlink.
|
|
37
|
+
const realTmp = realpathSync(tmpdir());
|
|
38
|
+
const base = mkdtempSync(join(realTmp, "gsd-wt-symlink-test-"));
|
|
39
|
+
const externalState = mkdtempSync(join(realTmp, "gsd-wt-symlink-ext-"));
|
|
40
|
+
t.after(() => {
|
|
41
|
+
rmSync(base, { recursive: true, force: true });
|
|
42
|
+
rmSync(externalState, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
run("git init -b main", base);
|
|
46
|
+
run('git config user.name "Test"', base);
|
|
47
|
+
run('git config user.email "test@example.com"', base);
|
|
48
|
+
|
|
49
|
+
// Create external state directory structure
|
|
50
|
+
mkdirSync(join(externalState, "worktrees"), { recursive: true });
|
|
51
|
+
|
|
52
|
+
// Create .gsd as a symlink to the external state directory
|
|
53
|
+
symlinkSync(externalState, join(base, ".gsd"));
|
|
54
|
+
|
|
55
|
+
// Verify the symlink is in place
|
|
56
|
+
assert.ok(existsSync(join(base, ".gsd")), ".gsd symlink exists");
|
|
57
|
+
assert.ok(
|
|
58
|
+
realpathSync(join(base, ".gsd")) === externalState,
|
|
59
|
+
".gsd resolves to external state dir",
|
|
60
|
+
);
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
// Create initial commit so we have a valid repo
|
|
63
|
+
writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
|
|
64
|
+
run("git add .", base);
|
|
65
|
+
run('git commit -m "init"', base);
|
|
62
66
|
|
|
63
67
|
// Create a worktree — git will resolve the symlink and register
|
|
64
68
|
// the worktree at the external path
|
|
@@ -127,7 +131,4 @@ describe('worktree-symlink-removal', async () => {
|
|
|
127
131
|
const listed = listWorktrees(base);
|
|
128
132
|
assert.deepStrictEqual(listed.length, 0, "no worktrees listed after removal");
|
|
129
133
|
|
|
130
|
-
// Cleanup
|
|
131
|
-
rmSync(base, { recursive: true, force: true });
|
|
132
|
-
rmSync(externalState, { recursive: true, force: true });
|
|
133
134
|
});
|
|
@@ -255,6 +255,14 @@ describe('worktree', async () => {
|
|
|
255
255
|
"returns unchanged for non-worktree path",
|
|
256
256
|
);
|
|
257
257
|
|
|
258
|
+
const nestedRepoDir = join(base, "packages", "demo");
|
|
259
|
+
mkdirSync(nestedRepoDir, { recursive: true });
|
|
260
|
+
assert.deepStrictEqual(
|
|
261
|
+
normalizePath(resolveProjectRoot(nestedRepoDir)),
|
|
262
|
+
normalizePath(base),
|
|
263
|
+
"resolves normal repo subdirectories to the project root",
|
|
264
|
+
);
|
|
265
|
+
|
|
258
266
|
// Without GSD_PROJECT_ROOT, direct layout with nested subdirs
|
|
259
267
|
assert.deepStrictEqual(
|
|
260
268
|
resolveProjectRoot("/data/.gsd/worktrees/M003/nested"),
|
|
@@ -9,12 +9,21 @@ import test from 'node:test';
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { join, sep } from 'node:path';
|
|
11
11
|
|
|
12
|
-
import { shouldBlockPlanningUnit } from '../bootstrap/write-gate.ts';
|
|
12
|
+
import { ALLOWED_PLANNING_DISPATCH_AGENTS, shouldBlockPlanningUnit } from '../bootstrap/write-gate.ts';
|
|
13
|
+
import { extractSubagentAgentClasses } from '../bootstrap/subagent-input.ts';
|
|
13
14
|
import { isDeterministicPolicyError } from '../auto-tool-tracking.ts';
|
|
14
15
|
import type { ToolsPolicy } from '../unit-context-manifest.ts';
|
|
15
16
|
|
|
16
17
|
const BASE = join('/tmp', 'fake-project');
|
|
17
18
|
const PLANNING: ToolsPolicy = { mode: 'planning' };
|
|
19
|
+
const PLANNING_DISPATCH: ToolsPolicy = {
|
|
20
|
+
mode: 'planning-dispatch',
|
|
21
|
+
allowedSubagents: [...ALLOWED_PLANNING_DISPATCH_AGENTS],
|
|
22
|
+
};
|
|
23
|
+
const PLANNING_DISPATCH_REVIEW: ToolsPolicy = {
|
|
24
|
+
mode: 'planning-dispatch',
|
|
25
|
+
allowedSubagents: ['reviewer', 'security', 'tester'],
|
|
26
|
+
};
|
|
18
27
|
const READ_ONLY: ToolsPolicy = { mode: 'read-only' };
|
|
19
28
|
const ALL: ToolsPolicy = { mode: 'all' };
|
|
20
29
|
const DOCS: ToolsPolicy = {
|
|
@@ -143,6 +152,112 @@ test('planning-unit: blocks task tool (alt subagent name)', () => {
|
|
|
143
152
|
assert.strictEqual(r.block, true);
|
|
144
153
|
});
|
|
145
154
|
|
|
155
|
+
test('planning-dispatch: allows subagent dispatch (delegated recon/planner during slice planning)', () => {
|
|
156
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', PLANNING_DISPATCH, ['scout']);
|
|
157
|
+
assert.strictEqual(r.block, false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('planning-dispatch: allows task dispatch (delegated recon/planner during slice planning)', () => {
|
|
161
|
+
const r = shouldBlockPlanningUnit('task', '', BASE, 'plan-slice', PLANNING_DISPATCH, ['planner']);
|
|
162
|
+
assert.strictEqual(r.block, false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('planning-dispatch: extracts subagent classes from single, parallel, and chain inputs', () => {
|
|
166
|
+
assert.deepEqual(extractSubagentAgentClasses({ agent: ' scout ' }), ['scout']);
|
|
167
|
+
assert.deepEqual(
|
|
168
|
+
extractSubagentAgentClasses({ tasks: [{ agent: 'planner' }, { agent: ' tester ' }] }),
|
|
169
|
+
['planner', 'tester'],
|
|
170
|
+
);
|
|
171
|
+
assert.deepEqual(
|
|
172
|
+
extractSubagentAgentClasses({ chain: [{ agent: 'reviewer' }, { agent: 'security' }] }),
|
|
173
|
+
['reviewer', 'security'],
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('planning-dispatch: blocks subagent dispatch when agentClasses is undefined (stale caller shim)', () => {
|
|
178
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', PLANNING_DISPATCH, undefined);
|
|
179
|
+
assert.strictEqual(r.block, true);
|
|
180
|
+
assert.match(r.reason!, /stale caller/);
|
|
181
|
+
assert.match(r.reason!, /tools-policy "planning-dispatch"/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('planning-dispatch: allows explicitly empty agent classes for downstream validation', () => {
|
|
185
|
+
const emptyClasses = extractSubagentAgentClasses({});
|
|
186
|
+
assert.deepEqual(emptyClasses, []);
|
|
187
|
+
const empty = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', PLANNING_DISPATCH, emptyClasses);
|
|
188
|
+
assert.strictEqual(empty.block, false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('planning-dispatch: allows all globally allowed specialists when listed by policy', () => {
|
|
192
|
+
const policy: ToolsPolicy = {
|
|
193
|
+
mode: 'planning-dispatch',
|
|
194
|
+
allowedSubagents: [...ALLOWED_PLANNING_DISPATCH_AGENTS],
|
|
195
|
+
};
|
|
196
|
+
const r = shouldBlockPlanningUnit(
|
|
197
|
+
'subagent',
|
|
198
|
+
'',
|
|
199
|
+
BASE,
|
|
200
|
+
'complete-milestone',
|
|
201
|
+
policy,
|
|
202
|
+
[...ALLOWED_PLANNING_DISPATCH_AGENTS],
|
|
203
|
+
);
|
|
204
|
+
assert.strictEqual(r.block, false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('planning-dispatch: blocks implementation-tier agent', () => {
|
|
208
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', PLANNING_DISPATCH, ['worker']);
|
|
209
|
+
assert.strictEqual(r.block, true);
|
|
210
|
+
assert.match(r.reason!, /"worker"/);
|
|
211
|
+
assert.match(r.reason!, /read-only specialists/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('planning-dispatch: blocks globally disallowed agent even if listed by policy', () => {
|
|
215
|
+
const policy: ToolsPolicy = {
|
|
216
|
+
mode: 'planning-dispatch',
|
|
217
|
+
allowedSubagents: ['refactorer'],
|
|
218
|
+
};
|
|
219
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'refine-slice', policy, ['refactorer']);
|
|
220
|
+
assert.strictEqual(r.block, true);
|
|
221
|
+
assert.match(r.reason!, /"refactorer"/);
|
|
222
|
+
assert.match(r.reason!, /read-only specialists/);
|
|
223
|
+
assert.doesNotMatch(r.reason!, /ToolsPolicy\.allowedSubagents|permitted agents for this unit/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('planning-dispatch: blocks mixed batch containing a disallowed agent', () => {
|
|
227
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'plan-slice', PLANNING_DISPATCH, ['scout', 'worker']);
|
|
228
|
+
assert.strictEqual(r.block, true);
|
|
229
|
+
assert.match(r.reason!, /"worker"/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('planning-dispatch: allows review-tier agent under closeout policy', () => {
|
|
233
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'complete-slice', PLANNING_DISPATCH_REVIEW, ['reviewer']);
|
|
234
|
+
assert.strictEqual(r.block, false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('planning-dispatch: blocks recon agent under closeout policy', () => {
|
|
238
|
+
const r = shouldBlockPlanningUnit('subagent', '', BASE, 'complete-slice', PLANNING_DISPATCH_REVIEW, ['scout']);
|
|
239
|
+
assert.strictEqual(r.block, true);
|
|
240
|
+
assert.match(r.reason!, /"scout"/);
|
|
241
|
+
assert.match(r.reason!, /ToolsPolicy\.allowedSubagents|permitted agents for this unit/);
|
|
242
|
+
assert.doesNotMatch(r.reason!, /read-only specialists/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('planning-dispatch: still blocks writes to user source (write isolation preserved)', () => {
|
|
246
|
+
const r = shouldBlockPlanningUnit('write', join(BASE, 'src', 'main.ts'), BASE, 'plan-slice', PLANNING_DISPATCH);
|
|
247
|
+
assert.strictEqual(r.block, true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('planning-dispatch: still allows writes inside .gsd/', () => {
|
|
251
|
+
const r = shouldBlockPlanningUnit(
|
|
252
|
+
'write',
|
|
253
|
+
join(BASE, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'PLAN.md'),
|
|
254
|
+
BASE,
|
|
255
|
+
'plan-slice',
|
|
256
|
+
PLANNING_DISPATCH,
|
|
257
|
+
);
|
|
258
|
+
assert.strictEqual(r.block, false);
|
|
259
|
+
});
|
|
260
|
+
|
|
146
261
|
// ─── planning mode: pass-through tools ────────────────────────────────────
|
|
147
262
|
|
|
148
263
|
test('planning-unit: allows read tool', () => {
|