gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.8a893322c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- 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/google-search/index.js +2 -6
- 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 +19 -1
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -0
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +8 -0
- 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/commands-worktree.js +309 -0
- 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/mcp-client/index.js +0 -6
- 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 +17 -17
- 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 +17 -17
- 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/welcome-screen.js +27 -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/google-search/index.ts +2 -9
- 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 +19 -0
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
- 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/commands-worktree.ts +383 -0
- 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/bundled-skill-triggers.test.ts +50 -27
- package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +25 -65
- 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/safety-harness-false-positives.test.ts +34 -0
- 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/mcp-client/index.ts +0 -7
- 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 → QK8fABiGPmonfTgboN0Y9}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_ssgManifest.js +0 -0
|
@@ -16,6 +16,7 @@ import { logWarning as safetyLogWarning } from "../workflow-logger.js";
|
|
|
16
16
|
import { installNotifyInterceptor } from "./notify-interceptor.js";
|
|
17
17
|
import { initNotificationStore } from "../notification-store.js";
|
|
18
18
|
import { initNotificationWidget } from "../notification-widget.js";
|
|
19
|
+
import { extractSubagentAgentClasses } from "./subagent-input.js";
|
|
19
20
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
20
21
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
21
22
|
let isFirstSession = true;
|
|
@@ -358,6 +359,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
358
359
|
const manifest = resolveManifest(activeUnitType);
|
|
359
360
|
if (manifest) {
|
|
360
361
|
let planningInput = "";
|
|
362
|
+
let agentClasses;
|
|
361
363
|
if (isToolCallEventType("write", event)) {
|
|
362
364
|
planningInput = event.input.path;
|
|
363
365
|
}
|
|
@@ -367,7 +369,11 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
367
369
|
else if (isToolCallEventType("bash", event)) {
|
|
368
370
|
planningInput = event.input.command;
|
|
369
371
|
}
|
|
370
|
-
|
|
372
|
+
else if (event.toolName === "subagent" || event.toolName === "task") {
|
|
373
|
+
// Subagent inputs use { agent }, { tasks: [{ agent }] }, or { chain: [{ agent }] }.
|
|
374
|
+
agentClasses = extractSubagentAgentClasses(event.input);
|
|
375
|
+
}
|
|
376
|
+
const planningGuard = shouldBlockPlanningUnit(event.toolName, planningInput, dash.basePath || discussionBasePath, activeUnitType, manifest.tools, agentClasses);
|
|
371
377
|
if (planningGuard.block)
|
|
372
378
|
return planningGuard;
|
|
373
379
|
}
|
|
@@ -401,6 +407,18 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
401
407
|
return;
|
|
402
408
|
markToolStart(event.toolCallId, event.toolName);
|
|
403
409
|
safetyRecordToolCall(event.toolCallId, event.toolName, event.input);
|
|
410
|
+
// Persist immediately at dispatch so a mid-unit re-dispatch — which calls
|
|
411
|
+
// resetEvidence() + loadEvidenceFromDisk() in runUnitPhase — cannot wipe
|
|
412
|
+
// the entry between tool_call and tool_execution_end. Without this, the
|
|
413
|
+
// race window equals the tool's runtime, producing the "no bash calls"
|
|
414
|
+
// false positive when the LLM clearly ran a verification command.
|
|
415
|
+
const callDash = getAutoRuntimeSnapshot();
|
|
416
|
+
if (callDash.basePath && callDash.currentUnit?.type === "execute-task") {
|
|
417
|
+
const { milestone: cMid, slice: cSid, task: cTid } = parseUnitId(callDash.currentUnit.id);
|
|
418
|
+
if (cMid && cSid && cTid) {
|
|
419
|
+
saveEvidenceToDisk(callDash.basePath, cMid, cSid, cTid);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
404
422
|
// Destructive command classification (warn only, never block)
|
|
405
423
|
if (isToolCallEventType("bash", event)) {
|
|
406
424
|
const classification = classifyCommand(event.input.command);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function extractSubagentAgentClasses(input) {
|
|
2
|
+
if (!input || typeof input !== "object")
|
|
3
|
+
return [];
|
|
4
|
+
const record = input;
|
|
5
|
+
const agentClasses = [];
|
|
6
|
+
const addAgentClass = (value) => {
|
|
7
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
8
|
+
agentClasses.push(value.trim());
|
|
9
|
+
};
|
|
10
|
+
const addFromItems = (value) => {
|
|
11
|
+
if (!Array.isArray(value))
|
|
12
|
+
return;
|
|
13
|
+
for (const item of value) {
|
|
14
|
+
if (item && typeof item === "object")
|
|
15
|
+
addAgentClass(item.agent);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
addAgentClass(record.agent);
|
|
19
|
+
addFromItems(record.tasks);
|
|
20
|
+
addFromItems(record.chain);
|
|
21
|
+
return agentClasses;
|
|
22
|
+
}
|
|
@@ -47,6 +47,17 @@ export const BUNDLED_SKILL_TRIGGERS = [
|
|
|
47
47
|
{ trigger: "HTTP/REST/GraphQL API design — verbs, status codes, pagination, errors, idempotency, versioning", skill: "api-design" },
|
|
48
48
|
{ trigger: "Dependency upgrades — risk-batched, verified between batches, one major per commit", skill: "dependency-upgrade" },
|
|
49
49
|
{ trigger: "Agent-first observability — structured logs, persisted failure state, health surfaces, explicit failure modes", skill: "observability" },
|
|
50
|
+
{ trigger: "React/Next.js performance — components, data fetching, bundle optimization, rendering patterns from Vercel Engineering", skill: "react-best-practices" },
|
|
51
|
+
{ trigger: "Core Web Vitals — fix LCP, CLS, INP; layout shifts; page experience optimization", skill: "core-web-vitals" },
|
|
52
|
+
{ trigger: "GitHub Actions CI/CD — write, run, and debug workflow files; live syntax and run monitoring", skill: "github-workflows" },
|
|
53
|
+
{ trigger: "Comprehensive web quality audit — performance, accessibility, SEO, and best-practices (Lighthouse-style)", skill: "web-quality-audit" },
|
|
54
|
+
{ trigger: "Browser automation — open sites, fill forms, click, screenshot, scrape, or test web apps programmatically", skill: "agent-browser" },
|
|
55
|
+
{ trigger: "Review UI code for Web Interface Guidelines compliance — UX, design, and accessibility patterns", skill: "web-design-guidelines" },
|
|
56
|
+
{ trigger: "UI/UX patterns reference — animations, CSS, typography, prefetching, icons (file:line findings)", skill: "userinterface-wiki" },
|
|
57
|
+
{ trigger: "Author or refine a GSD skill — SKILL.md structure, frontmatter, and best practices", skill: "create-skill" },
|
|
58
|
+
{ trigger: "Create or debug a GSD extension — tools, commands, event hooks, custom TUI, providers", skill: "create-gsd-extension" },
|
|
59
|
+
{ trigger: "Author a YAML workflow definition — steps, triggers, and templates", skill: "create-workflow" },
|
|
60
|
+
{ trigger: "Deep code optimization audit — perf anti-patterns, memory leaks, algorithmic complexity, bundle size, I/O, caching, dead code (parallel pattern-based hunt)", skill: "code-optimizer" },
|
|
50
61
|
];
|
|
51
62
|
function buildBundledSkillsTable() {
|
|
52
63
|
const cwd = process.cwd();
|
|
@@ -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") {
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { loadRegistry } from "../workflow-templates.js";
|
|
5
5
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
6
|
-
export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language";
|
|
6
|
+
export const GSD_COMMAND_DESCRIPTION = "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language|worktree";
|
|
7
7
|
export const TOP_LEVEL_SUBCOMMANDS = [
|
|
8
8
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
9
9
|
{ cmd: "next", desc: "Explicit step mode (same as /gsd)" },
|
|
@@ -71,6 +71,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
|
|
|
71
71
|
{ cmd: "add-tests", desc: "Generate tests for completed slices" },
|
|
72
72
|
{ cmd: "scan", desc: "Rapid codebase assessment — lightweight alternative to full map (--focus tech|arch|quality|concerns|tech+arch)" },
|
|
73
73
|
{ cmd: "language", desc: "Set or clear the global response language (e.g. /gsd language Chinese)" },
|
|
74
|
+
{ cmd: "worktree", desc: "Manage worktrees from the TUI (list, merge, clean, remove)" },
|
|
74
75
|
];
|
|
75
76
|
const NESTED_COMPLETIONS = {
|
|
76
77
|
auto: [
|
|
@@ -286,6 +287,12 @@ const NESTED_COMPLETIONS = {
|
|
|
286
287
|
{ cmd: "off", desc: "Clear the language preference (revert to default)" },
|
|
287
288
|
{ cmd: "clear", desc: "Alias for off — clear the language preference" },
|
|
288
289
|
],
|
|
290
|
+
worktree: [
|
|
291
|
+
{ cmd: "list", desc: "Show all worktrees with status" },
|
|
292
|
+
{ cmd: "merge", desc: "Merge a worktree into main and clean up" },
|
|
293
|
+
{ cmd: "clean", desc: "Remove all merged/empty worktrees" },
|
|
294
|
+
{ cmd: "remove", desc: "Remove a worktree (--force to skip safety checks)" },
|
|
295
|
+
],
|
|
289
296
|
};
|
|
290
297
|
function filterOptions(partial, options, prefix = "") {
|
|
291
298
|
const normalizedPrefix = prefix ? `${prefix} ` : "";
|
|
@@ -124,6 +124,7 @@ export function showHelp(ctx, args = "") {
|
|
|
124
124
|
" /gsd forensics Examine execution logs and post-mortem analysis",
|
|
125
125
|
" /gsd export Export milestone/slice results [--json|--markdown|--html] [--all]",
|
|
126
126
|
" /gsd cleanup Remove merged branches or snapshots [branches|snapshots]",
|
|
127
|
+
" /gsd worktree Manage worktrees from the TUI [list|merge|clean|remove]",
|
|
127
128
|
" /gsd migrate Migrate .planning/ (v1) to .gsd/ (v2) format",
|
|
128
129
|
" /gsd remote Control remote auto-mode [slack|discord|status|disconnect]",
|
|
129
130
|
" /gsd inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
|
|
@@ -259,5 +259,13 @@ Examples:
|
|
|
259
259
|
await handleScan(trimmed.replace(/^scan\s*/, "").trim(), ctx, pi);
|
|
260
260
|
return true;
|
|
261
261
|
}
|
|
262
|
+
if (trimmed === "worktree" ||
|
|
263
|
+
trimmed.startsWith("worktree ") ||
|
|
264
|
+
trimmed === "wt" ||
|
|
265
|
+
trimmed.startsWith("wt ")) {
|
|
266
|
+
const { handleWorktree } = await import("../../commands-worktree.js");
|
|
267
|
+
await handleWorktree(trimmed.replace(/^(worktree|wt)\s*/, "").trim(), ctx);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
262
270
|
return false;
|
|
263
271
|
}
|
|
@@ -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",
|