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
|
@@ -46,6 +46,11 @@ import {
|
|
|
46
46
|
resolveGitHeadPath,
|
|
47
47
|
nudgeGitBranchCache,
|
|
48
48
|
} from "./worktree.js";
|
|
49
|
+
import {
|
|
50
|
+
isGsdWorktreePath,
|
|
51
|
+
normalizeWorktreePathForCompare,
|
|
52
|
+
resolveWorktreeProjectRoot,
|
|
53
|
+
} from "./worktree-root.js";
|
|
49
54
|
import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
|
|
50
55
|
import { debugLog } from "./debug-logger.js";
|
|
51
56
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
@@ -1202,6 +1207,8 @@ export function createAutoWorktree(
|
|
|
1202
1207
|
basePath: string,
|
|
1203
1208
|
milestoneId: string,
|
|
1204
1209
|
): string {
|
|
1210
|
+
basePath = resolveWorktreeProjectRoot(basePath);
|
|
1211
|
+
|
|
1205
1212
|
// Check if repo has commits — git worktree requires a valid HEAD
|
|
1206
1213
|
try {
|
|
1207
1214
|
execFileSync("git", ["rev-parse", "--verify", "HEAD"], { cwd: basePath, stdio: "pipe" });
|
|
@@ -1361,6 +1368,8 @@ export function teardownAutoWorktree(
|
|
|
1361
1368
|
milestoneId: string,
|
|
1362
1369
|
opts: { preserveBranch?: boolean } = {},
|
|
1363
1370
|
): void {
|
|
1371
|
+
originalBasePath = resolveWorktreeProjectRoot(originalBasePath);
|
|
1372
|
+
|
|
1364
1373
|
const branch = autoWorktreeBranch(milestoneId);
|
|
1365
1374
|
const { preserveBranch = false } = opts;
|
|
1366
1375
|
const previousCwd = process.cwd();
|
|
@@ -1412,16 +1421,28 @@ export function teardownAutoWorktree(
|
|
|
1412
1421
|
|
|
1413
1422
|
/**
|
|
1414
1423
|
* Detect if the process is currently inside an auto-worktree.
|
|
1415
|
-
*
|
|
1424
|
+
* Uses the current directory structure plus git branch prefix so detection
|
|
1425
|
+
* still works after process restart when module state has been reset.
|
|
1416
1426
|
*/
|
|
1417
1427
|
export function isInAutoWorktree(basePath: string): boolean {
|
|
1418
|
-
if (!originalBase) return false;
|
|
1419
1428
|
const cwd = process.cwd();
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1429
|
+
if (!isGsdWorktreePath(cwd)) return false;
|
|
1430
|
+
|
|
1431
|
+
const projectRoot = resolveWorktreeProjectRoot(basePath, originalBase);
|
|
1432
|
+
const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
|
|
1433
|
+
if (
|
|
1434
|
+
normalizeWorktreePathForCompare(projectRoot) !==
|
|
1435
|
+
normalizeWorktreePathForCompare(cwdProjectRoot)
|
|
1436
|
+
) {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
try {
|
|
1441
|
+
const branch = nativeGetCurrentBranch(cwd);
|
|
1442
|
+
return branch.startsWith("milestone/");
|
|
1443
|
+
} catch {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1425
1446
|
}
|
|
1426
1447
|
|
|
1427
1448
|
/**
|
|
@@ -1436,6 +1457,8 @@ export function getAutoWorktreePath(
|
|
|
1436
1457
|
basePath: string,
|
|
1437
1458
|
milestoneId: string,
|
|
1438
1459
|
): string | null {
|
|
1460
|
+
basePath = resolveWorktreeProjectRoot(basePath);
|
|
1461
|
+
|
|
1439
1462
|
const p = worktreePath(basePath, milestoneId);
|
|
1440
1463
|
if (!existsSync(p)) return null;
|
|
1441
1464
|
|
|
@@ -1464,6 +1487,8 @@ export function enterAutoWorktree(
|
|
|
1464
1487
|
basePath: string,
|
|
1465
1488
|
milestoneId: string,
|
|
1466
1489
|
): string {
|
|
1490
|
+
basePath = resolveWorktreeProjectRoot(basePath);
|
|
1491
|
+
|
|
1467
1492
|
const p = worktreePath(basePath, milestoneId);
|
|
1468
1493
|
if (!existsSync(p)) {
|
|
1469
1494
|
throw new GSDError(
|
|
@@ -1520,6 +1545,10 @@ export function getAutoWorktreeOriginalBase(): string | null {
|
|
|
1520
1545
|
return originalBase;
|
|
1521
1546
|
}
|
|
1522
1547
|
|
|
1548
|
+
export function _resetAutoWorktreeOriginalBaseForTests(): void {
|
|
1549
|
+
originalBase = null;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1523
1552
|
export function getActiveAutoWorktreeContext(): {
|
|
1524
1553
|
originalBase: string;
|
|
1525
1554
|
worktreeName: string;
|
|
@@ -1527,11 +1556,14 @@ export function getActiveAutoWorktreeContext(): {
|
|
|
1527
1556
|
} | null {
|
|
1528
1557
|
if (!originalBase) return null;
|
|
1529
1558
|
const cwd = process.cwd();
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1559
|
+
if (!isGsdWorktreePath(cwd)) return null;
|
|
1560
|
+
const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
|
|
1561
|
+
if (
|
|
1562
|
+
normalizeWorktreePathForCompare(cwdProjectRoot) !==
|
|
1563
|
+
normalizeWorktreePathForCompare(originalBase)
|
|
1564
|
+
) {
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1535
1567
|
const worktreeName = detectWorktreeName(cwd);
|
|
1536
1568
|
if (!worktreeName) return null;
|
|
1537
1569
|
const branch = nativeGetCurrentBranch(cwd);
|
|
@@ -22,6 +22,7 @@ import { logWarning as safetyLogWarning } from "../workflow-logger.js";
|
|
|
22
22
|
import { installNotifyInterceptor } from "./notify-interceptor.js";
|
|
23
23
|
import { initNotificationStore } from "../notification-store.js";
|
|
24
24
|
import { initNotificationWidget } from "../notification-widget.js";
|
|
25
|
+
import { extractSubagentAgentClasses } from "./subagent-input.js";
|
|
25
26
|
|
|
26
27
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
27
28
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
@@ -390,12 +391,16 @@ export function registerHooks(
|
|
|
390
391
|
const manifest = resolveManifest(activeUnitType);
|
|
391
392
|
if (manifest) {
|
|
392
393
|
let planningInput = "";
|
|
394
|
+
let agentClasses: string[] | undefined;
|
|
393
395
|
if (isToolCallEventType("write", event)) {
|
|
394
396
|
planningInput = event.input.path;
|
|
395
397
|
} else if (isToolCallEventType("edit", event)) {
|
|
396
398
|
planningInput = event.input.path;
|
|
397
399
|
} else if (isToolCallEventType("bash", event)) {
|
|
398
400
|
planningInput = event.input.command;
|
|
401
|
+
} else if (event.toolName === "subagent" || event.toolName === "task") {
|
|
402
|
+
// Subagent inputs use { agent }, { tasks: [{ agent }] }, or { chain: [{ agent }] }.
|
|
403
|
+
agentClasses = extractSubagentAgentClasses((event as { input?: unknown }).input);
|
|
399
404
|
}
|
|
400
405
|
const planningGuard = shouldBlockPlanningUnit(
|
|
401
406
|
event.toolName,
|
|
@@ -403,6 +408,7 @@ export function registerHooks(
|
|
|
403
408
|
dash.basePath || discussionBasePath,
|
|
404
409
|
activeUnitType,
|
|
405
410
|
manifest.tools,
|
|
411
|
+
agentClasses,
|
|
406
412
|
);
|
|
407
413
|
if (planningGuard.block) return planningGuard;
|
|
408
414
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function extractSubagentAgentClasses(input: unknown): string[] {
|
|
2
|
+
if (!input || typeof input !== "object") return [];
|
|
3
|
+
|
|
4
|
+
const record = input as Record<string, unknown>;
|
|
5
|
+
const agentClasses: string[] = [];
|
|
6
|
+
const addAgentClass = (value: unknown): void => {
|
|
7
|
+
if (typeof value === "string" && value.trim().length > 0) agentClasses.push(value.trim());
|
|
8
|
+
};
|
|
9
|
+
const addFromItems = (value: unknown): void => {
|
|
10
|
+
if (!Array.isArray(value)) return;
|
|
11
|
+
for (const item of value) {
|
|
12
|
+
if (item && typeof item === "object") addAgentClass((item as Record<string, unknown>).agent);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
addAgentClass(record.agent);
|
|
17
|
+
addFromItems(record.tasks);
|
|
18
|
+
addFromItems(record.chain);
|
|
19
|
+
return agentClasses;
|
|
20
|
+
}
|
|
@@ -4,6 +4,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
|
|
6
6
|
import type { ToolsPolicy } from "../unit-context-manifest.js";
|
|
7
|
+
import { logWarning } from "../workflow-logger.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Regex matching milestone CONTEXT.md file names in both legacy M001
|
|
@@ -546,6 +547,49 @@ export function shouldBlockQueueExecutionInSnapshot(
|
|
|
546
547
|
const PLANNING_WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "notebook_edit"]);
|
|
547
548
|
const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
|
|
548
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Canonical registry for agents that planning-dispatch may consider. Unit
|
|
552
|
+
* manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
|
|
553
|
+
*/
|
|
554
|
+
const PLANNING_DISPATCH_AGENT_REGISTRY = {
|
|
555
|
+
scout: { readOnlySpecialist: true },
|
|
556
|
+
planner: { readOnlySpecialist: true },
|
|
557
|
+
reviewer: { readOnlySpecialist: true },
|
|
558
|
+
security: { readOnlySpecialist: true },
|
|
559
|
+
tester: { readOnlySpecialist: true },
|
|
560
|
+
} as const satisfies Record<string, { readonly readOnlySpecialist: boolean }>;
|
|
561
|
+
|
|
562
|
+
export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set<string>(
|
|
563
|
+
Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
|
|
564
|
+
.filter(([, metadata]) => metadata.readOnlySpecialist)
|
|
565
|
+
.map(([agentId]) => agentId),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
let warnedMissingPlanningDispatchAgentClasses = false;
|
|
569
|
+
|
|
570
|
+
function isReadOnlySpecialist(agentId: string): boolean {
|
|
571
|
+
const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId as keyof typeof PLANNING_DISPATCH_AGENT_REGISTRY];
|
|
572
|
+
return metadata?.readOnlySpecialist === true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function allowedPlanningDispatchAgentsList(): string {
|
|
576
|
+
return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function warnMissingPlanningDispatchAgentClasses(unitType: string, mode: string, toolName: string): void {
|
|
580
|
+
if (warnedMissingPlanningDispatchAgentClasses) return;
|
|
581
|
+
warnedMissingPlanningDispatchAgentClasses = true;
|
|
582
|
+
// TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
|
|
583
|
+
const message = `[write-gate] planning-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
|
|
584
|
+
`on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
|
|
585
|
+
console.warn(message);
|
|
586
|
+
logWarning("intercept", message, {
|
|
587
|
+
unitType,
|
|
588
|
+
mode,
|
|
589
|
+
toolName,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
549
593
|
/**
|
|
550
594
|
* Read-only / planning-safe tools that any non-"all" mode allows. Mirrors
|
|
551
595
|
* QUEUE_SAFE_TOOLS / GATE_SAFE_TOOLS but is the inclusive default for
|
|
@@ -592,6 +636,10 @@ function blockReason(unitType: string, mode: string, what: string): string {
|
|
|
592
636
|
* - "read-only" → blocks all writes, bash, and subagent dispatch.
|
|
593
637
|
* - "planning" → blocks writes to paths outside <basePath>/.gsd/,
|
|
594
638
|
* bash that isn't read-only, and subagent dispatch.
|
|
639
|
+
* - "planning-dispatch"
|
|
640
|
+
* → like "planning", but permits subagent dispatch only
|
|
641
|
+
* when every forwarded agent class is globally allowed
|
|
642
|
+
* and listed in the policy's allowedSubagents.
|
|
595
643
|
* - "docs" → like "planning" but also allows writes to paths
|
|
596
644
|
* matching `allowedPathGlobs` relative to basePath.
|
|
597
645
|
*
|
|
@@ -601,6 +649,11 @@ function blockReason(unitType: string, mode: string, what: string): string {
|
|
|
601
649
|
* `policy` of null means "no manifest resolved" — pass-through. Callers
|
|
602
650
|
* that have no active unit (interactive sessions) pass null and this
|
|
603
651
|
* predicate is a no-op.
|
|
652
|
+
*
|
|
653
|
+
* `agentClasses` is supplied by the tool hook for subagent-shaped calls. If
|
|
654
|
+
* absent, planning-dispatch fails closed so stale callers cannot silently
|
|
655
|
+
* bypass the agent allowlists. An explicitly supplied-but-empty list is
|
|
656
|
+
* allowed through so the downstream tool call can reject the malformed input.
|
|
604
657
|
*/
|
|
605
658
|
export function shouldBlockPlanningUnit(
|
|
606
659
|
toolName: string,
|
|
@@ -608,6 +661,7 @@ export function shouldBlockPlanningUnit(
|
|
|
608
661
|
basePath: string,
|
|
609
662
|
unitType: string,
|
|
610
663
|
policy: ToolsPolicy | null | undefined,
|
|
664
|
+
agentClasses?: readonly string[],
|
|
611
665
|
): { block: boolean; reason?: string } {
|
|
612
666
|
if (!policy) return { block: false };
|
|
613
667
|
if (policy.mode === "all") return { block: false };
|
|
@@ -625,11 +679,59 @@ export function shouldBlockPlanningUnit(
|
|
|
625
679
|
return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
|
|
626
680
|
}
|
|
627
681
|
|
|
628
|
-
// planning / docs modes share the same surface for safe tools, bash, and subagent.
|
|
682
|
+
// planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
|
|
629
683
|
if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
|
|
630
684
|
if (tool.startsWith("gsd_")) return { block: false };
|
|
631
685
|
|
|
632
686
|
if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
|
|
687
|
+
if (policy.mode === "planning-dispatch") {
|
|
688
|
+
const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
|
|
689
|
+
const allowedSubagents = Array.isArray(policy.allowedSubagents) ? policy.allowedSubagents : [];
|
|
690
|
+
const allowed = new Set(allowedSubagents);
|
|
691
|
+
// When agentClasses is undefined, the caller has not been updated to extract
|
|
692
|
+
// agent identities yet. Block and warn so stale callers surface in telemetry
|
|
693
|
+
// instead of silently bypassing the gate.
|
|
694
|
+
if (agentClasses === undefined) {
|
|
695
|
+
warnMissingPlanningDispatchAgentClasses(unitType, policy.mode, tool);
|
|
696
|
+
return {
|
|
697
|
+
block: true,
|
|
698
|
+
reason: blockReason(
|
|
699
|
+
unitType,
|
|
700
|
+
policy.mode,
|
|
701
|
+
`subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`,
|
|
702
|
+
),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
// agentClasses was explicitly provided but resolved to an empty list (for
|
|
706
|
+
// example, a bare tool call with no agent field). Pass through; no agents
|
|
707
|
+
// to validate means the downstream tool call itself will fail.
|
|
708
|
+
if (requested.length === 0) {
|
|
709
|
+
return { block: false };
|
|
710
|
+
}
|
|
711
|
+
const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
|
|
712
|
+
if (globallyDisallowed) {
|
|
713
|
+
return {
|
|
714
|
+
block: true,
|
|
715
|
+
reason: blockReason(
|
|
716
|
+
unitType,
|
|
717
|
+
policy.mode,
|
|
718
|
+
`subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from planning-dispatch units`,
|
|
719
|
+
),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const disallowedByPolicy = requested.find(a => !allowed.has(a));
|
|
723
|
+
if (disallowedByPolicy) {
|
|
724
|
+
return {
|
|
725
|
+
block: true,
|
|
726
|
+
reason: blockReason(
|
|
727
|
+
unitType,
|
|
728
|
+
policy.mode,
|
|
729
|
+
`subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`,
|
|
730
|
+
),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
return { block: false };
|
|
734
|
+
}
|
|
633
735
|
return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
|
|
634
736
|
}
|
|
635
737
|
|
|
@@ -8,6 +8,7 @@ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
|
8
8
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
9
9
|
import { existsSync, mkdirSync } from "node:fs";
|
|
10
10
|
import { join, dirname } from "node:path";
|
|
11
|
+
import { getHomeDir } from "./home-dir.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Tool API key configurations.
|
|
@@ -34,7 +35,7 @@ function getStoredToolKey(auth: AuthStorage, providerId: string): string | undef
|
|
|
34
35
|
*/
|
|
35
36
|
export function loadToolApiKeys(): void {
|
|
36
37
|
try {
|
|
37
|
-
const authPath = join(
|
|
38
|
+
const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
38
39
|
if (!existsSync(authPath)) return;
|
|
39
40
|
|
|
40
41
|
const auth = AuthStorage.create(authPath);
|
|
@@ -50,7 +51,7 @@ export function loadToolApiKeys(): void {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function getConfigAuthStorage(): AuthStorage {
|
|
53
|
-
const authPath = join(
|
|
54
|
+
const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
54
55
|
mkdirSync(dirname(authPath), { recursive: true });
|
|
55
56
|
return AuthStorage.create(authPath);
|
|
56
57
|
}
|
|
@@ -12,7 +12,47 @@ import { dirname, join, resolve } from "node:path";
|
|
|
12
12
|
import { homedir, tmpdir } from "node:os";
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { lockSync, unlockSync } from "proper-lockfile";
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Strict numeric comparison of two npm-style version strings.
|
|
18
|
+
*
|
|
19
|
+
* Returns true when `a` is strictly greater than `b`. Compares the dotted
|
|
20
|
+
* release components numerically (so `1.10.0` > `1.9.0`) and treats any
|
|
21
|
+
* prerelease suffix (`-beta.1`, `-rc.2`) as less than the equivalent
|
|
22
|
+
* release version (`1.0.0` > `1.0.0-beta.1`). Sufficient for npm package
|
|
23
|
+
* version comparison in the extension installer; we don't need the full
|
|
24
|
+
* semver range/intersect machinery here.
|
|
25
|
+
*
|
|
26
|
+
* Replaces the earlier `import semver from "semver"` — that import broke
|
|
27
|
+
* `tsc -p tsconfig.json` whenever `@types/semver` failed to install
|
|
28
|
+
* (Issue #4946) because the file is pulled in transitively despite being
|
|
29
|
+
* under the `src/resources` exclude.
|
|
30
|
+
*/
|
|
31
|
+
export function isVersionGreater(a: string, b: string): boolean {
|
|
32
|
+
const split = (v: string): { release: number[]; pre: string | null } => {
|
|
33
|
+
const dash = v.indexOf("-");
|
|
34
|
+
const release = (dash === -1 ? v : v.slice(0, dash))
|
|
35
|
+
.split(".")
|
|
36
|
+
.map(part => Number.parseInt(part, 10) || 0);
|
|
37
|
+
const pre = dash === -1 ? null : v.slice(dash + 1);
|
|
38
|
+
return { release, pre };
|
|
39
|
+
};
|
|
40
|
+
const sa = split(a);
|
|
41
|
+
const sb = split(b);
|
|
42
|
+
const len = Math.max(sa.release.length, sb.release.length);
|
|
43
|
+
for (let i = 0; i < len; i++) {
|
|
44
|
+
const ai = sa.release[i] ?? 0;
|
|
45
|
+
const bi = sb.release[i] ?? 0;
|
|
46
|
+
if (ai !== bi) return ai > bi;
|
|
47
|
+
}
|
|
48
|
+
// Release components equal — a release version beats any prerelease,
|
|
49
|
+
// and prerelease strings are compared lexicographically (good enough
|
|
50
|
+
// for `beta.1` vs `beta.2`, the only realistic case here).
|
|
51
|
+
if (sa.pre === null && sb.pre !== null) return true;
|
|
52
|
+
if (sa.pre !== null && sb.pre === null) return false;
|
|
53
|
+
if (sa.pre !== null && sb.pre !== null) return sa.pre > sb.pre;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
16
56
|
|
|
17
57
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
18
58
|
|
|
@@ -538,7 +578,7 @@ async function updateSingleExtension(
|
|
|
538
578
|
return;
|
|
539
579
|
}
|
|
540
580
|
|
|
541
|
-
if (
|
|
581
|
+
if (isVersionGreater(latest, current)) {
|
|
542
582
|
ctx.ui.notify(`Updating "${id}": v${current} → v${latest}...`, "info");
|
|
543
583
|
await handleInstall(packageName, ctx);
|
|
544
584
|
} else {
|
|
@@ -599,7 +639,7 @@ async function updateAllExtensions(
|
|
|
599
639
|
continue;
|
|
600
640
|
}
|
|
601
641
|
|
|
602
|
-
if (
|
|
642
|
+
if (isVersionGreater(latest, current)) {
|
|
603
643
|
ctx.ui.notify(` ${entry.id}: v${current} → v${latest} (updating)`, "info");
|
|
604
644
|
await handleInstall(packageName, ctx);
|
|
605
645
|
updated++;
|
|
@@ -11,6 +11,7 @@ import { join, resolve as resolvePath, sep } from "node:path";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { deriveState } from "./state.js";
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
|
+
import { getHomeDir } from "./home-dir.js";
|
|
14
15
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
15
16
|
import { appendOverride, appendKnowledge } from "./files.js";
|
|
16
17
|
import {
|
|
@@ -68,7 +69,7 @@ async function fetchLatestVersionForCommand(): Promise<string | null> {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
|
71
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
72
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
72
73
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
73
74
|
const prompt = loadPrompt("doctor-heal", {
|
|
74
75
|
doctorSummary: reportText,
|
|
@@ -255,7 +256,7 @@ export async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAP
|
|
|
255
256
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
256
257
|
});
|
|
257
258
|
|
|
258
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
259
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
259
260
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
260
261
|
|
|
261
262
|
pi.sendMessage(
|
|
@@ -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.
|
|
@@ -17,6 +17,7 @@ import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
|
17
17
|
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
18
18
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
19
19
|
import { getAuthPath, PROVIDER_REGISTRY, type ProviderCategory } from "./key-manager.js";
|
|
20
|
+
import { getHomeDir } from "./home-dir.js";
|
|
20
21
|
|
|
21
22
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -194,7 +195,7 @@ function isCliBinaryInPath(providerId: string): boolean {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
function modelsJsonPaths(): string[] {
|
|
197
|
-
const home =
|
|
198
|
+
const home = getHomeDir();
|
|
198
199
|
return [
|
|
199
200
|
join(home, ".gsd", "agent", "models.json"),
|
|
200
201
|
// Keep parity with custom-provider discovery during auto bootstrap.
|
|
@@ -12,7 +12,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join, dirname, relative } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import {
|
|
15
|
+
import { getHomeDir } from "./home-dir.js";
|
|
16
16
|
|
|
17
17
|
import { extractTrace, type ExecutionTrace } from "./session-forensics.js";
|
|
18
18
|
import { nativeParseJsonlTail } from "./native-parser-bridge.js";
|
|
@@ -244,7 +244,7 @@ export async function handleForensics(
|
|
|
244
244
|
// when import.meta.url resolves to the npm-global install path (Windows).
|
|
245
245
|
let gsdSourceDir = dirname(fileURLToPath(import.meta.url));
|
|
246
246
|
if (!existsSync(join(gsdSourceDir, "prompts"))) {
|
|
247
|
-
const gsdHome = process.env.GSD_HOME || join(
|
|
247
|
+
const gsdHome = process.env.GSD_HOME || join(getHomeDir(), ".gsd");
|
|
248
248
|
const fallback = join(gsdHome, "agent", "extensions", "gsd");
|
|
249
249
|
if (existsSync(join(fallback, "prompts"))) gsdSourceDir = fallback;
|
|
250
250
|
}
|
|
@@ -1299,10 +1299,15 @@ function formatReportForPrompt(report: ForensicReport): string {
|
|
|
1299
1299
|
function redactForGitHub(text: string, basePath: string): string {
|
|
1300
1300
|
let result = text;
|
|
1301
1301
|
|
|
1302
|
+
// Build regex that matches both / and \ separator variants (Windows)
|
|
1303
|
+
// Normalize to / first, escape for regex, then replace each / with [/\\]
|
|
1304
|
+
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1305
|
+
const pathRe = (p: string) =>
|
|
1306
|
+
new RegExp(esc(p.replace(/\\/g, "/")).replace(/\//g, "[/\\\\]"), "gi");
|
|
1307
|
+
|
|
1302
1308
|
// Replace absolute paths
|
|
1303
|
-
result = result.
|
|
1304
|
-
|
|
1305
|
-
if (home) result = result.replaceAll(home, "~");
|
|
1309
|
+
result = result.replace(pathRe(basePath), ".");
|
|
1310
|
+
result = result.replace(pathRe(getHomeDir()), "~");
|
|
1306
1311
|
|
|
1307
1312
|
// Strip API key patterns
|
|
1308
1313
|
result = result.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***");
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./interrupted-session.js";
|
|
26
26
|
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
27
27
|
import { resolveExpectedArtifactPath } from "./auto.js";
|
|
28
|
+
import { getHomeDir } from "./home-dir.js";
|
|
28
29
|
import {
|
|
29
30
|
gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath,
|
|
30
31
|
resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile,
|
|
@@ -607,7 +608,7 @@ async function dispatchWorkflow(
|
|
|
607
608
|
});
|
|
608
609
|
}
|
|
609
610
|
|
|
610
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
611
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
611
612
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
612
613
|
|
|
613
614
|
pi.sendMessage(
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
|
|
11
|
+
export function getHomeDir(): string {
|
|
12
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
13
|
+
if (!home) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Cannot resolve home directory. Set HOME or USERPROFILE environment variable.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return home;
|
|
19
|
+
}
|
|
@@ -60,7 +60,10 @@ export type JournalEventType =
|
|
|
60
60
|
| "canonical-root-redirect"
|
|
61
61
|
// #4765 — slice-cadence collapse
|
|
62
62
|
| "slice-merged"
|
|
63
|
-
| "milestone-resquash"
|
|
63
|
+
| "milestone-resquash"
|
|
64
|
+
// dispatch telemetry — measure agent/subagent invocation frequency and shape
|
|
65
|
+
| "subagent-invoked"
|
|
66
|
+
| "subagent-completed";
|
|
64
67
|
|
|
65
68
|
/** A single structured event in the journal. */
|
|
66
69
|
export interface JournalEntry {
|
|
@@ -17,6 +17,7 @@ import { existsSync, statSync, chmodSync } from "node:fs";
|
|
|
17
17
|
import { join, dirname } from "node:path";
|
|
18
18
|
import { mkdirSync } from "node:fs";
|
|
19
19
|
import { getErrorMessage } from "./error-utils.js";
|
|
20
|
+
import { getHomeDir } from "./home-dir.js";
|
|
20
21
|
|
|
21
22
|
// ─── Provider Registry ─────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -122,7 +123,7 @@ export function describeCredential(cred: AuthCredential): string {
|
|
|
122
123
|
* Get the auth.json path.
|
|
123
124
|
*/
|
|
124
125
|
export function getAuthPath(): string {
|
|
125
|
-
return join(
|
|
126
|
+
return join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
@@ -15,6 +15,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
15
15
|
import { gsdRoot } from "../paths.js";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { showNextAction } from "../../shared/tui.js";
|
|
18
|
+
import { getHomeDir } from "../home-dir.js";
|
|
18
19
|
import {
|
|
19
20
|
validatePlanningDirectory,
|
|
20
21
|
parsePlanningDirectory,
|
|
@@ -85,9 +86,9 @@ export async function handleMigrate(
|
|
|
85
86
|
// Default to cwd when no args given; expand ~ to HOME
|
|
86
87
|
let rawPath = args.trim() || ".";
|
|
87
88
|
if (rawPath.startsWith("~/")) {
|
|
88
|
-
rawPath = join(
|
|
89
|
+
rawPath = join(getHomeDir(), rawPath.slice(2));
|
|
89
90
|
} else if (rawPath === "~") {
|
|
90
|
-
rawPath =
|
|
91
|
+
rawPath = getHomeDir();
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
let sourcePath = resolve(process.cwd(), rawPath);
|
|
@@ -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.
|