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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { minimatch } from "minimatch";
|
|
4
|
+
import { logWarning } from "../workflow-logger.js";
|
|
4
5
|
/**
|
|
5
6
|
* Regex matching milestone CONTEXT.md file names in both legacy M001
|
|
6
7
|
* and unique M001-abc123 formats. Exported so regex-hardening tests
|
|
@@ -458,6 +459,42 @@ export function shouldBlockQueueExecutionInSnapshot(snapshot, toolName, input, q
|
|
|
458
459
|
// guards.
|
|
459
460
|
const PLANNING_WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "notebook_edit"]);
|
|
460
461
|
const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
|
|
462
|
+
/**
|
|
463
|
+
* Canonical registry for agents that planning-dispatch may consider. Unit
|
|
464
|
+
* manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
|
|
465
|
+
*/
|
|
466
|
+
const PLANNING_DISPATCH_AGENT_REGISTRY = {
|
|
467
|
+
scout: { readOnlySpecialist: true },
|
|
468
|
+
planner: { readOnlySpecialist: true },
|
|
469
|
+
reviewer: { readOnlySpecialist: true },
|
|
470
|
+
security: { readOnlySpecialist: true },
|
|
471
|
+
tester: { readOnlySpecialist: true },
|
|
472
|
+
};
|
|
473
|
+
export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set(Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
|
|
474
|
+
.filter(([, metadata]) => metadata.readOnlySpecialist)
|
|
475
|
+
.map(([agentId]) => agentId));
|
|
476
|
+
let warnedMissingPlanningDispatchAgentClasses = false;
|
|
477
|
+
function isReadOnlySpecialist(agentId) {
|
|
478
|
+
const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId];
|
|
479
|
+
return metadata?.readOnlySpecialist === true;
|
|
480
|
+
}
|
|
481
|
+
function allowedPlanningDispatchAgentsList() {
|
|
482
|
+
return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
|
|
483
|
+
}
|
|
484
|
+
function warnMissingPlanningDispatchAgentClasses(unitType, mode, toolName) {
|
|
485
|
+
if (warnedMissingPlanningDispatchAgentClasses)
|
|
486
|
+
return;
|
|
487
|
+
warnedMissingPlanningDispatchAgentClasses = true;
|
|
488
|
+
// TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
|
|
489
|
+
const message = `[write-gate] planning-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
|
|
490
|
+
`on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
|
|
491
|
+
console.warn(message);
|
|
492
|
+
logWarning("intercept", message, {
|
|
493
|
+
unitType,
|
|
494
|
+
mode,
|
|
495
|
+
toolName,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
461
498
|
/**
|
|
462
499
|
* Read-only / planning-safe tools that any non-"all" mode allows. Mirrors
|
|
463
500
|
* QUEUE_SAFE_TOOLS / GATE_SAFE_TOOLS but is the inclusive default for
|
|
@@ -501,6 +538,10 @@ function blockReason(unitType, mode, what) {
|
|
|
501
538
|
* - "read-only" → blocks all writes, bash, and subagent dispatch.
|
|
502
539
|
* - "planning" → blocks writes to paths outside <basePath>/.gsd/,
|
|
503
540
|
* bash that isn't read-only, and subagent dispatch.
|
|
541
|
+
* - "planning-dispatch"
|
|
542
|
+
* → like "planning", but permits subagent dispatch only
|
|
543
|
+
* when every forwarded agent class is globally allowed
|
|
544
|
+
* and listed in the policy's allowedSubagents.
|
|
504
545
|
* - "docs" → like "planning" but also allows writes to paths
|
|
505
546
|
* matching `allowedPathGlobs` relative to basePath.
|
|
506
547
|
*
|
|
@@ -510,8 +551,13 @@ function blockReason(unitType, mode, what) {
|
|
|
510
551
|
* `policy` of null means "no manifest resolved" — pass-through. Callers
|
|
511
552
|
* that have no active unit (interactive sessions) pass null and this
|
|
512
553
|
* predicate is a no-op.
|
|
554
|
+
*
|
|
555
|
+
* `agentClasses` is supplied by the tool hook for subagent-shaped calls. If
|
|
556
|
+
* absent, planning-dispatch fails closed so stale callers cannot silently
|
|
557
|
+
* bypass the agent allowlists. An explicitly supplied-but-empty list is
|
|
558
|
+
* allowed through so the downstream tool call can reject the malformed input.
|
|
513
559
|
*/
|
|
514
|
-
export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitType, policy) {
|
|
560
|
+
export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitType, policy, agentClasses) {
|
|
515
561
|
if (!policy)
|
|
516
562
|
return { block: false };
|
|
517
563
|
if (policy.mode === "all")
|
|
@@ -529,12 +575,48 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
529
575
|
// Unknown tool in read-only mode — block by default.
|
|
530
576
|
return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
|
|
531
577
|
}
|
|
532
|
-
// planning / docs modes share the same surface for safe tools, bash, and subagent.
|
|
578
|
+
// planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
|
|
533
579
|
if (PLANNING_SAFE_TOOLS.has(tool))
|
|
534
580
|
return { block: false };
|
|
535
581
|
if (tool.startsWith("gsd_"))
|
|
536
582
|
return { block: false };
|
|
537
583
|
if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
|
|
584
|
+
if (policy.mode === "planning-dispatch") {
|
|
585
|
+
const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
|
|
586
|
+
const allowedSubagents = Array.isArray(policy.allowedSubagents) ? policy.allowedSubagents : [];
|
|
587
|
+
const allowed = new Set(allowedSubagents);
|
|
588
|
+
// When agentClasses is undefined, the caller has not been updated to extract
|
|
589
|
+
// agent identities yet. Block and warn so stale callers surface in telemetry
|
|
590
|
+
// instead of silently bypassing the gate.
|
|
591
|
+
if (agentClasses === undefined) {
|
|
592
|
+
warnMissingPlanningDispatchAgentClasses(unitType, policy.mode, tool);
|
|
593
|
+
return {
|
|
594
|
+
block: true,
|
|
595
|
+
reason: blockReason(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
// agentClasses was explicitly provided but resolved to an empty list (for
|
|
599
|
+
// example, a bare tool call with no agent field). Pass through; no agents
|
|
600
|
+
// to validate means the downstream tool call itself will fail.
|
|
601
|
+
if (requested.length === 0) {
|
|
602
|
+
return { block: false };
|
|
603
|
+
}
|
|
604
|
+
const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
|
|
605
|
+
if (globallyDisallowed) {
|
|
606
|
+
return {
|
|
607
|
+
block: true,
|
|
608
|
+
reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from planning-dispatch units`),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const disallowedByPolicy = requested.find(a => !allowed.has(a));
|
|
612
|
+
if (disallowedByPolicy) {
|
|
613
|
+
return {
|
|
614
|
+
block: true,
|
|
615
|
+
reason: blockReason(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
return { block: false };
|
|
619
|
+
}
|
|
538
620
|
return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
|
|
539
621
|
}
|
|
540
622
|
if (tool === "bash") {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
7
7
|
import { existsSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { join, dirname } from "node:path";
|
|
9
|
+
import { getHomeDir } from "./home-dir.js";
|
|
9
10
|
/**
|
|
10
11
|
* Tool API key configurations.
|
|
11
12
|
* This is the source of truth for tool credentials - used by both the config wizard
|
|
@@ -29,7 +30,7 @@ function getStoredToolKey(auth, providerId) {
|
|
|
29
30
|
*/
|
|
30
31
|
export function loadToolApiKeys() {
|
|
31
32
|
try {
|
|
32
|
-
const authPath = join(
|
|
33
|
+
const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
33
34
|
if (!existsSync(authPath))
|
|
34
35
|
return;
|
|
35
36
|
const auth = AuthStorage.create(authPath);
|
|
@@ -45,7 +46,7 @@ export function loadToolApiKeys() {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
export function getConfigAuthStorage() {
|
|
48
|
-
const authPath = join(
|
|
49
|
+
const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
49
50
|
mkdirSync(dirname(authPath), { recursive: true });
|
|
50
51
|
return AuthStorage.create(authPath);
|
|
51
52
|
}
|
|
@@ -10,7 +10,50 @@ import { dirname, join, resolve } from "node:path";
|
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
12
|
import { lockSync, unlockSync } from "proper-lockfile";
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Strict numeric comparison of two npm-style version strings.
|
|
15
|
+
*
|
|
16
|
+
* Returns true when `a` is strictly greater than `b`. Compares the dotted
|
|
17
|
+
* release components numerically (so `1.10.0` > `1.9.0`) and treats any
|
|
18
|
+
* prerelease suffix (`-beta.1`, `-rc.2`) as less than the equivalent
|
|
19
|
+
* release version (`1.0.0` > `1.0.0-beta.1`). Sufficient for npm package
|
|
20
|
+
* version comparison in the extension installer; we don't need the full
|
|
21
|
+
* semver range/intersect machinery here.
|
|
22
|
+
*
|
|
23
|
+
* Replaces the earlier `import semver from "semver"` — that import broke
|
|
24
|
+
* `tsc -p tsconfig.json` whenever `@types/semver` failed to install
|
|
25
|
+
* (Issue #4946) because the file is pulled in transitively despite being
|
|
26
|
+
* under the `src/resources` exclude.
|
|
27
|
+
*/
|
|
28
|
+
export function isVersionGreater(a, b) {
|
|
29
|
+
const split = (v) => {
|
|
30
|
+
const dash = v.indexOf("-");
|
|
31
|
+
const release = (dash === -1 ? v : v.slice(0, dash))
|
|
32
|
+
.split(".")
|
|
33
|
+
.map(part => Number.parseInt(part, 10) || 0);
|
|
34
|
+
const pre = dash === -1 ? null : v.slice(dash + 1);
|
|
35
|
+
return { release, pre };
|
|
36
|
+
};
|
|
37
|
+
const sa = split(a);
|
|
38
|
+
const sb = split(b);
|
|
39
|
+
const len = Math.max(sa.release.length, sb.release.length);
|
|
40
|
+
for (let i = 0; i < len; i++) {
|
|
41
|
+
const ai = sa.release[i] ?? 0;
|
|
42
|
+
const bi = sb.release[i] ?? 0;
|
|
43
|
+
if (ai !== bi)
|
|
44
|
+
return ai > bi;
|
|
45
|
+
}
|
|
46
|
+
// Release components equal — a release version beats any prerelease,
|
|
47
|
+
// and prerelease strings are compared lexicographically (good enough
|
|
48
|
+
// for `beta.1` vs `beta.2`, the only realistic case here).
|
|
49
|
+
if (sa.pre === null && sb.pre !== null)
|
|
50
|
+
return true;
|
|
51
|
+
if (sa.pre !== null && sb.pre === null)
|
|
52
|
+
return false;
|
|
53
|
+
if (sa.pre !== null && sb.pre !== null)
|
|
54
|
+
return sa.pre > sb.pre;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
14
57
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
15
58
|
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
|
16
59
|
function getRegistryPath() {
|
|
@@ -413,7 +456,7 @@ async function updateSingleExtension(id, registry, ctx) {
|
|
|
413
456
|
ctx.ui.notify(`Could not fetch latest version for "${id}".`, "warning");
|
|
414
457
|
return;
|
|
415
458
|
}
|
|
416
|
-
if (
|
|
459
|
+
if (isVersionGreater(latest, current)) {
|
|
417
460
|
ctx.ui.notify(`Updating "${id}": v${current} → v${latest}...`, "info");
|
|
418
461
|
await handleInstall(packageName, ctx);
|
|
419
462
|
}
|
|
@@ -464,7 +507,7 @@ async function updateAllExtensions(registry, ctx) {
|
|
|
464
507
|
skipped++;
|
|
465
508
|
continue;
|
|
466
509
|
}
|
|
467
|
-
if (
|
|
510
|
+
if (isVersionGreater(latest, current)) {
|
|
468
511
|
ctx.ui.notify(` ${entry.id}: v${current} → v${latest} (updating)`, "info");
|
|
469
512
|
await handleInstall(packageName, ctx);
|
|
470
513
|
updated++;
|
|
@@ -9,6 +9,7 @@ import { join, resolve as resolvePath, sep } from "node:path";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { deriveState } from "./state.js";
|
|
11
11
|
import { gsdRoot } from "./paths.js";
|
|
12
|
+
import { getHomeDir } from "./home-dir.js";
|
|
12
13
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
13
14
|
import { appendOverride, appendKnowledge } from "./files.js";
|
|
14
15
|
import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
@@ -60,7 +61,7 @@ async function fetchLatestVersionForCommand() {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
63
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
64
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
64
65
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
65
66
|
const prompt = loadPrompt("doctor-heal", {
|
|
66
67
|
doctorSummary: reportText,
|
|
@@ -210,7 +211,7 @@ export async function handleTriage(ctx, pi, basePath) {
|
|
|
210
211
|
currentPlan: currentPlan || "(no active slice plan)",
|
|
211
212
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
212
213
|
});
|
|
213
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
214
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
214
215
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
215
216
|
pi.sendMessage({
|
|
216
217
|
customType: "gsd-triage",
|
|
@@ -165,6 +165,12 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
165
165
|
- `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
|
|
166
166
|
- `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
|
|
167
167
|
|
|
168
|
+
- `reactive_execution`: controls automatic parallel task dispatch inside a slice. Reactive execution is enabled by default when omitted; set `enabled: false` to opt out. With default-on behavior, GSD only attempts a reactive batch when at least three ready tasks are available and the task-plan IO graph is non-ambiguous. If you set `enabled: true` explicitly, GSD uses the earlier opt-in threshold of two ready tasks. Keys:
|
|
169
|
+
- `enabled`: boolean — set `false` to force sequential task execution. Default: `true`.
|
|
170
|
+
- `max_parallel`: number — maximum tasks to dispatch in one batch, range `1`-`8`. Default: `2`.
|
|
171
|
+
- `isolation_mode`: `"same-tree"` — currently the only supported value.
|
|
172
|
+
- `subagent_model`: string — optional model override for reactive task subagents. Falls back to the `models.subagent` routing when omitted.
|
|
173
|
+
|
|
168
174
|
- `remote_questions`: route interactive questions to Slack/Discord for headless auto-mode. Keys:
|
|
169
175
|
- `channel`: `"slack"` or `"discord"` — channel type.
|
|
170
176
|
- `channel_id`: string or number — channel ID.
|
|
@@ -16,6 +16,7 @@ import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
|
16
16
|
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
17
17
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
18
18
|
import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
|
|
19
|
+
import { getHomeDir } from "./home-dir.js";
|
|
19
20
|
// ── Provider routing constants ────────────────────────────────────────────────
|
|
20
21
|
/**
|
|
21
22
|
* Providers that use external CLI authentication (not API keys).
|
|
@@ -158,7 +159,7 @@ function isCliBinaryInPath(providerId) {
|
|
|
158
159
|
return pathDirs.some(dir => executableNames.some(name => existsSync(join(dir, name))));
|
|
159
160
|
}
|
|
160
161
|
function modelsJsonPaths() {
|
|
161
|
-
const home =
|
|
162
|
+
const home = getHomeDir();
|
|
162
163
|
return [
|
|
163
164
|
join(home, ".gsd", "agent", "models.json"),
|
|
164
165
|
// Keep parity with custom-provider discovery during auto bootstrap.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join, dirname, relative } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import {
|
|
13
|
+
import { getHomeDir } from "./home-dir.js";
|
|
14
14
|
import { extractTrace } from "./session-forensics.js";
|
|
15
15
|
import { nativeParseJsonlTail } from "./native-parser-bridge.js";
|
|
16
16
|
import { MAX_JSONL_BYTES, parseJSONL } from "./jsonl-utils.js";
|
|
@@ -135,7 +135,7 @@ export async function handleForensics(args, ctx, pi) {
|
|
|
135
135
|
// when import.meta.url resolves to the npm-global install path (Windows).
|
|
136
136
|
let gsdSourceDir = dirname(fileURLToPath(import.meta.url));
|
|
137
137
|
if (!existsSync(join(gsdSourceDir, "prompts"))) {
|
|
138
|
-
const gsdHome = process.env.GSD_HOME || join(
|
|
138
|
+
const gsdHome = process.env.GSD_HOME || join(getHomeDir(), ".gsd");
|
|
139
139
|
const fallback = join(gsdHome, "agent", "extensions", "gsd");
|
|
140
140
|
if (existsSync(join(fallback, "prompts")))
|
|
141
141
|
gsdSourceDir = fallback;
|
|
@@ -1109,11 +1109,13 @@ function formatReportForPrompt(report) {
|
|
|
1109
1109
|
// ─── Redaction ────────────────────────────────────────────────────────────────
|
|
1110
1110
|
function redactForGitHub(text, basePath) {
|
|
1111
1111
|
let result = text;
|
|
1112
|
+
// Build regex that matches both / and \ separator variants (Windows)
|
|
1113
|
+
// Normalize to / first, escape for regex, then replace each / with [/\\]
|
|
1114
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1115
|
+
const pathRe = (p) => new RegExp(esc(p.replace(/\\/g, "/")).replace(/\//g, "[/\\\\]"), "gi");
|
|
1112
1116
|
// Replace absolute paths
|
|
1113
|
-
result = result.
|
|
1114
|
-
|
|
1115
|
-
if (home)
|
|
1116
|
-
result = result.replaceAll(home, "~");
|
|
1117
|
+
result = result.replace(pathRe(basePath), ".");
|
|
1118
|
+
result = result.replace(pathRe(getHomeDir()), "~");
|
|
1117
1119
|
// Strip API key patterns
|
|
1118
1120
|
result = result.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***");
|
|
1119
1121
|
result = result.replace(/Bearer\s+\S+/g, "Bearer ***");
|
|
@@ -18,6 +18,7 @@ import { clearLock } from "./crash-recovery.js";
|
|
|
18
18
|
import { assessInterruptedSession, formatInterruptedSessionRunningMessage, formatInterruptedSessionSummary, } from "./interrupted-session.js";
|
|
19
19
|
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
20
20
|
import { resolveExpectedArtifactPath } from "./auto.js";
|
|
21
|
+
import { getHomeDir } from "./home-dir.js";
|
|
21
22
|
import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
|
|
22
23
|
import { join } from "node:path";
|
|
23
24
|
import { readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
|
|
@@ -495,7 +496,7 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType)
|
|
|
495
496
|
removed: currentTools.length - scopedTools.length,
|
|
496
497
|
});
|
|
497
498
|
}
|
|
498
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
499
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
499
500
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
500
501
|
pi.sendMessage({
|
|
501
502
|
customType,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform home directory resolution.
|
|
3
|
+
*
|
|
4
|
+
* `process.env.HOME` is not set on Windows (CMD/PowerShell).
|
|
5
|
+
* Falls back to USERPROFILE, then os.homedir(), then throws.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/gsd-build/gsd-2/issues/5015
|
|
8
|
+
*/
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
export function getHomeDir() {
|
|
11
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
12
|
+
if (!home) {
|
|
13
|
+
throw new Error("Cannot resolve home directory. Set HOME or USERPROFILE environment variable.");
|
|
14
|
+
}
|
|
15
|
+
return home;
|
|
16
|
+
}
|
|
@@ -9,6 +9,7 @@ import { existsSync, statSync, chmodSync } from "node:fs";
|
|
|
9
9
|
import { join, dirname } from "node:path";
|
|
10
10
|
import { mkdirSync } from "node:fs";
|
|
11
11
|
import { getErrorMessage } from "./error-utils.js";
|
|
12
|
+
import { getHomeDir } from "./home-dir.js";
|
|
12
13
|
export const PROVIDER_REGISTRY = [
|
|
13
14
|
// LLM Providers
|
|
14
15
|
{ id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
|
|
@@ -98,7 +99,7 @@ export function describeCredential(cred) {
|
|
|
98
99
|
* Get the auth.json path.
|
|
99
100
|
*/
|
|
100
101
|
export function getAuthPath() {
|
|
101
|
-
return join(
|
|
102
|
+
return join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
105
|
* Create an AuthStorage instance for key management.
|
|
@@ -13,6 +13,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
13
13
|
import { gsdRoot } from "../paths.js";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { showNextAction } from "../../shared/tui.js";
|
|
16
|
+
import { getHomeDir } from "../home-dir.js";
|
|
16
17
|
import { validatePlanningDirectory, parsePlanningDirectory, transformToGSD, generatePreview, writeGSDDirectory, } from "./index.js";
|
|
17
18
|
/** Format preview stats for embedding in the review prompt. */
|
|
18
19
|
function formatPreviewStats(preview) {
|
|
@@ -50,10 +51,10 @@ export async function handleMigrate(args, ctx, pi) {
|
|
|
50
51
|
// Default to cwd when no args given; expand ~ to HOME
|
|
51
52
|
let rawPath = args.trim() || ".";
|
|
52
53
|
if (rawPath.startsWith("~/")) {
|
|
53
|
-
rawPath = join(
|
|
54
|
+
rawPath = join(getHomeDir(), rawPath.slice(2));
|
|
54
55
|
}
|
|
55
56
|
else if (rawPath === "~") {
|
|
56
|
-
rawPath =
|
|
57
|
+
rawPath = getHomeDir();
|
|
57
58
|
}
|
|
58
59
|
let sourcePath = resolve(process.cwd(), rawPath);
|
|
59
60
|
if (!sourcePath.endsWith(".planning")) {
|
|
@@ -16,6 +16,16 @@ Start with what the excerpts give you. Read full files when the section heads si
|
|
|
16
16
|
|
|
17
17
|
**On-demand Read ordering:** Complete all slice SUMMARY Reads you need for cross-slice synthesis, the Decision Re-evaluation table, and LEARNINGS **before** calling `gsd_complete_milestone` (step 10). Once that tool runs, the milestone is marked complete in the DB — running out of tool budget between step 10 and the LEARNINGS write (step 12) leaves the milestone committed without its LEARNINGS artifact.
|
|
18
18
|
|
|
19
|
+
### Delegate Review Work
|
|
20
|
+
|
|
21
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate review work that benefits from a fresh context window. For non-trivial milestones, delegate before drafting LEARNINGS:
|
|
22
|
+
|
|
23
|
+
- **Cross-slice integrations or new public APIs** → dispatch the **reviewer** agent with the milestone diff and roadmap; treat its findings as input to your Decision Re-evaluation and LEARNINGS sections.
|
|
24
|
+
- **Touched auth, network, parsing, file IO, shell exec, or crypto** → dispatch the **security** agent for an OWASP-style audit across the merged slices.
|
|
25
|
+
- **Significant test surface added or changed** → dispatch the **tester** agent to assess coverage gaps relative to the milestone success criteria.
|
|
26
|
+
|
|
27
|
+
Subagents read the diff and report findings — they do **not** write user source. Apply their feedback into the milestone summary and any captured decisions before calling `gsd_complete_milestone`.
|
|
28
|
+
|
|
19
29
|
{{inlinedContext}}
|
|
20
30
|
|
|
21
31
|
Then:
|
|
@@ -20,6 +20,16 @@ All relevant context has been preloaded below — the slice plan, all task summa
|
|
|
20
20
|
|
|
21
21
|
**Match effort to complexity.** A simple slice with 1-2 tasks needs a brief summary and lightweight verification. A complex slice with 5 tasks across multiple subsystems needs thorough verification and a detailed summary. Scale the work below accordingly.
|
|
22
22
|
|
|
23
|
+
### Delegate Review Work
|
|
24
|
+
|
|
25
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate review work that benefits from a fresh context window. Strongly consider delegating when the slice is non-trivial:
|
|
26
|
+
|
|
27
|
+
- **Cross-cutting code or new abstractions** → dispatch the **reviewer** agent with the slice diff and plan; apply High/Critical findings before completing.
|
|
28
|
+
- **Touched auth, network, parsing, file IO, shell exec, or crypto** → dispatch the **security** agent for an OWASP-style audit.
|
|
29
|
+
- **Added or modified tests** → dispatch the **tester** agent to assess coverage gaps relative to the slice plan.
|
|
30
|
+
|
|
31
|
+
Subagents read the diff and report findings — they do **not** write user source. You remain responsible for acting on their feedback before calling `gsd_complete_slice` with `milestoneId` and `sliceId`.
|
|
32
|
+
|
|
23
33
|
Then:
|
|
24
34
|
1. Use the **Slice Summary** and **UAT** output templates from the inlined context above
|
|
25
35
|
2. {{skillActivation}}
|
|
@@ -20,6 +20,16 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
|
|
|
20
20
|
|
|
21
21
|
You have full tool access. Before decomposing, explore the relevant code to ground your plan in reality.
|
|
22
22
|
|
|
23
|
+
### Delegate Recon and Sub-Decomposition When Useful
|
|
24
|
+
|
|
25
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate work that benefits from an isolated context window. Prefer delegation over inline work when:
|
|
26
|
+
|
|
27
|
+
- You'd otherwise read more than ~3 files to understand a subsystem → dispatch the **scout** agent for codebase recon and work from its compressed report.
|
|
28
|
+
- The slice spans multiple subsystems and the decomposition isn't obvious → dispatch the **planner** agent or use the **decompose-into-slices** skill on a focused sub-area, then integrate.
|
|
29
|
+
- You need current external information (library docs, API behavior, recent changes) → dispatch the **researcher** agent.
|
|
30
|
+
|
|
31
|
+
**Do not** dispatch implementation-tier agents (`worker`, `refactorer`, `tester`) from this unit — they would write user source and bypass this unit's write isolation. Implementation belongs in `execute-task`.
|
|
32
|
+
|
|
23
33
|
### Verify Roadmap Assumptions (JIT Reassessment — ADR-003 §4)
|
|
24
34
|
|
|
25
35
|
Before planning this slice, verify that the roadmap's assumptions still hold given prior slice summaries. Check inlined dependency summaries (below) for discovered constraints, changed approaches, or flagged fragility.
|
|
@@ -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.
|
|
@@ -73,6 +73,18 @@ const COMMON_BUDGET_SMALL = 250_000; // ~65K tokens
|
|
|
73
73
|
// allowed-path set for the docs policy lives in one reviewable place.
|
|
74
74
|
const TOOLS_ALL = { mode: "all" };
|
|
75
75
|
const TOOLS_PLANNING = { mode: "planning" };
|
|
76
|
+
// Like TOOLS_PLANNING but permits dispatch to read-only recon/planning
|
|
77
|
+
// specialists. Runtime-enforced by write-gate.ts before the subagent tool runs.
|
|
78
|
+
const TOOLS_PLANNING_DISPATCH_RECON = {
|
|
79
|
+
mode: "planning-dispatch",
|
|
80
|
+
allowedSubagents: ["scout", "planner"],
|
|
81
|
+
};
|
|
82
|
+
// Like TOOLS_PLANNING_DISPATCH_RECON, but for closeout units that fan out
|
|
83
|
+
// verification work to review-tier specialists.
|
|
84
|
+
const TOOLS_PLANNING_DISPATCH_REVIEW = {
|
|
85
|
+
mode: "planning-dispatch",
|
|
86
|
+
allowedSubagents: ["reviewer", "security", "tester"],
|
|
87
|
+
};
|
|
76
88
|
const TOOLS_DOCS = {
|
|
77
89
|
mode: "docs",
|
|
78
90
|
// Globs are resolved relative to project basePath. The set is intentionally
|
|
@@ -177,7 +189,11 @@ export const UNIT_MANIFESTS = {
|
|
|
177
189
|
memory: "prompt-relevant",
|
|
178
190
|
codebaseMap: false,
|
|
179
191
|
preferences: "active-only",
|
|
180
|
-
|
|
192
|
+
// planning-dispatch: completion is a high-leverage place to fan out to
|
|
193
|
+
// reviewer / security / tester subagents. They read the diff and report
|
|
194
|
+
// findings; they do not write user source. Write isolation to .gsd/ is
|
|
195
|
+
// preserved.
|
|
196
|
+
tools: TOOLS_PLANNING_DISPATCH_REVIEW,
|
|
181
197
|
artifacts: {
|
|
182
198
|
// #4780 landed slice-summary as excerpt for this unit; phase 2 of
|
|
183
199
|
// the architecture will read this manifest as the source of truth
|
|
@@ -209,7 +225,10 @@ export const UNIT_MANIFESTS = {
|
|
|
209
225
|
memory: "prompt-relevant",
|
|
210
226
|
codebaseMap: true,
|
|
211
227
|
preferences: "active-only",
|
|
212
|
-
|
|
228
|
+
// planning-dispatch: allows subagent dispatch so the planner can fan out
|
|
229
|
+
// to scout for codebase recon and to planner/decompose-style specialists
|
|
230
|
+
// for sub-decomposition. Write-isolation to .gsd/ is preserved.
|
|
231
|
+
tools: TOOLS_PLANNING_DISPATCH_RECON,
|
|
213
232
|
artifacts: {
|
|
214
233
|
inline: ["roadmap", "slice-research", "dependency-summaries", "requirements", "decisions", "templates"],
|
|
215
234
|
excerpt: [],
|
|
@@ -223,7 +242,10 @@ export const UNIT_MANIFESTS = {
|
|
|
223
242
|
memory: "prompt-relevant",
|
|
224
243
|
codebaseMap: true,
|
|
225
244
|
preferences: "active-only",
|
|
226
|
-
|
|
245
|
+
// See plan-slice — same rationale: dispatch to scout/planner-style
|
|
246
|
+
// specialists during refinement is materially better than re-doing recon
|
|
247
|
+
// inline.
|
|
248
|
+
tools: TOOLS_PLANNING_DISPATCH_RECON,
|
|
227
249
|
artifacts: {
|
|
228
250
|
inline: ["slice-plan", "slice-research", "dependency-summaries", "templates"],
|
|
229
251
|
excerpt: [],
|
|
@@ -251,7 +273,10 @@ export const UNIT_MANIFESTS = {
|
|
|
251
273
|
memory: "prompt-relevant",
|
|
252
274
|
codebaseMap: false,
|
|
253
275
|
preferences: "active-only",
|
|
254
|
-
|
|
276
|
+
// See complete-milestone — same rationale: dispatch to reviewer / security /
|
|
277
|
+
// tester subagents to fan out review work without bloating this unit's
|
|
278
|
+
// context.
|
|
279
|
+
tools: TOOLS_PLANNING_DISPATCH_REVIEW,
|
|
255
280
|
artifacts: {
|
|
256
281
|
// Phase 3 migration (#4782): matches today's actual
|
|
257
282
|
// buildCompleteSlicePrompt inlining order. Overrides prepend +
|
|
@@ -21,6 +21,7 @@ import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERRO
|
|
|
21
21
|
import { logWarning } from "./workflow-logger.js";
|
|
22
22
|
import { nativeBranchDelete, nativeBranchExists, nativeBranchForceReset, nativeCommit, nativeDetectMainBranch, nativeDiffContent, nativeDiffNameStatus, nativeDiffNumstat, nativeGetCurrentBranch, nativeIsAncestor, nativeLogOneline, nativeMergeSquash, nativeWorktreeAdd, nativeWorktreeList, nativeWorktreePrune, nativeWorktreeRemove, } from "./native-git-bridge.js";
|
|
23
23
|
import { emitCanonicalRootRedirect } from "./worktree-telemetry.js";
|
|
24
|
+
import { isGsdWorktreePath, normalizeWorktreePathForCompare, resolveWorktreeProjectRoot, } from "./worktree-root.js";
|
|
24
25
|
// ─── Path Helpers ──────────────────────────────────────────────────────────
|
|
25
26
|
function normalizePathForComparison(path) {
|
|
26
27
|
const normalized = path
|
|
@@ -29,6 +30,14 @@ function normalizePathForComparison(path) {
|
|
|
29
30
|
.replace(/\/+$/, "");
|
|
30
31
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
31
32
|
}
|
|
33
|
+
function normalizeBasePathForWorktreeOps(basePath) {
|
|
34
|
+
const resolved = resolveWorktreeProjectRoot(basePath);
|
|
35
|
+
if (isGsdWorktreePath(basePath) &&
|
|
36
|
+
normalizeWorktreePathForCompare(resolved) === normalizeWorktreePathForCompare(basePath)) {
|
|
37
|
+
throw new GSDError(GSD_GIT_ERROR, `Cannot resolve project root from worktree path: ${basePath}. Run the command from the project root or set GSD_PROJECT_ROOT.`);
|
|
38
|
+
}
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
32
41
|
// ─── resolveGitDir ─────────────────────────────────────────────────────────
|
|
33
42
|
/**
|
|
34
43
|
* Resolve the actual git directory for a given repository path.
|
|
@@ -61,7 +70,7 @@ export function resolveGitDir(basePath) {
|
|
|
61
70
|
return gitPath;
|
|
62
71
|
}
|
|
63
72
|
export function worktreesDir(basePath) {
|
|
64
|
-
return join(basePath, ".gsd", "worktrees");
|
|
73
|
+
return join(resolveWorktreeProjectRoot(basePath), ".gsd", "worktrees");
|
|
65
74
|
}
|
|
66
75
|
export function worktreePath(basePath, name) {
|
|
67
76
|
return join(worktreesDir(basePath), name);
|
|
@@ -143,6 +152,7 @@ export function resolveCanonicalMilestoneRoot(basePath, milestoneId) {
|
|
|
143
152
|
* @param opts.branch — override the default `worktree/<name>` branch name
|
|
144
153
|
*/
|
|
145
154
|
export function createWorktree(basePath, name, opts = {}) {
|
|
155
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
146
156
|
// Validate name: alphanumeric, hyphens, underscores only
|
|
147
157
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
148
158
|
throw new GSDError(GSD_PARSE_ERROR, `Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
|
@@ -227,6 +237,7 @@ export function createWorktree(basePath, name, opts = {}) {
|
|
|
227
237
|
* Uses native worktree list and filters to those under .gsd/worktrees/.
|
|
228
238
|
*/
|
|
229
239
|
export function listWorktrees(basePath) {
|
|
240
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
230
241
|
const baseVariants = [resolve(basePath)];
|
|
231
242
|
if (existsSync(basePath)) {
|
|
232
243
|
baseVariants.push(realpathSync(basePath));
|
|
@@ -366,6 +377,7 @@ export function findNestedGitDirs(rootPath) {
|
|
|
366
377
|
* If the process is currently inside the worktree, chdir out first.
|
|
367
378
|
*/
|
|
368
379
|
export function removeWorktree(basePath, name, opts = {}) {
|
|
380
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
369
381
|
let wtPath = worktreePath(basePath, name);
|
|
370
382
|
const branch = opts.branch ?? worktreeBranchName(name);
|
|
371
383
|
const { deleteBranch = true, force = true } = opts;
|
|
@@ -614,6 +626,7 @@ function parseDiffNameStatus(entries) {
|
|
|
614
626
|
* Returns a summary of added, modified, and removed GSD artifacts.
|
|
615
627
|
*/
|
|
616
628
|
export function diffWorktreeGSD(basePath, name) {
|
|
629
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
617
630
|
const branch = worktreeBranchName(name);
|
|
618
631
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
619
632
|
const entries = nativeDiffNameStatus(basePath, mainBranch, branch, ".gsd/", true);
|
|
@@ -626,6 +639,7 @@ export function diffWorktreeGSD(basePath, name) {
|
|
|
626
639
|
* content, this correctly returns an empty diff.
|
|
627
640
|
*/
|
|
628
641
|
export function diffWorktreeAll(basePath, name) {
|
|
642
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
629
643
|
const branch = worktreeBranchName(name);
|
|
630
644
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
631
645
|
const entries = nativeDiffNameStatus(basePath, mainBranch, branch);
|
|
@@ -636,6 +650,7 @@ export function diffWorktreeAll(basePath, name) {
|
|
|
636
650
|
* Uses direct diff (not merge-base) so the preview matches the actual merge outcome.
|
|
637
651
|
*/
|
|
638
652
|
export function diffWorktreeNumstat(basePath, name) {
|
|
653
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
639
654
|
const branch = worktreeBranchName(name);
|
|
640
655
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
641
656
|
const rawStats = nativeDiffNumstat(basePath, mainBranch, branch);
|
|
@@ -652,6 +667,7 @@ export function diffWorktreeNumstat(basePath, name) {
|
|
|
652
667
|
* Returns the raw unified diff for LLM consumption.
|
|
653
668
|
*/
|
|
654
669
|
export function getWorktreeGSDDiff(basePath, name) {
|
|
670
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
655
671
|
const branch = worktreeBranchName(name);
|
|
656
672
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
657
673
|
return nativeDiffContent(basePath, mainBranch, branch, ".gsd/", undefined, true);
|
|
@@ -661,6 +677,7 @@ export function getWorktreeGSDDiff(basePath, name) {
|
|
|
661
677
|
* Returns the raw unified diff for LLM consumption.
|
|
662
678
|
*/
|
|
663
679
|
export function getWorktreeCodeDiff(basePath, name) {
|
|
680
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
664
681
|
const branch = worktreeBranchName(name);
|
|
665
682
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
666
683
|
return nativeDiffContent(basePath, mainBranch, branch, undefined, ".gsd/", true);
|
|
@@ -669,6 +686,7 @@ export function getWorktreeCodeDiff(basePath, name) {
|
|
|
669
686
|
* Get commit log for the worktree branch since it diverged from main.
|
|
670
687
|
*/
|
|
671
688
|
export function getWorktreeLog(basePath, name) {
|
|
689
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
672
690
|
const branch = worktreeBranchName(name);
|
|
673
691
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
674
692
|
const entries = nativeLogOneline(basePath, mainBranch, branch);
|
|
@@ -680,6 +698,7 @@ export function getWorktreeLog(basePath, name) {
|
|
|
680
698
|
* Returns the merge commit message.
|
|
681
699
|
*/
|
|
682
700
|
export function mergeWorktreeToMain(basePath, name, commitMessage) {
|
|
701
|
+
basePath = normalizeBasePathForWorktreeOps(basePath);
|
|
683
702
|
const branch = worktreeBranchName(name);
|
|
684
703
|
const mainBranch = nativeDetectMainBranch(basePath);
|
|
685
704
|
const current = nativeGetCurrentBranch(basePath);
|