gsd-pi 2.37.1 → 2.38.0-dev.e40f839
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 -1
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/onboarding.js +1 -0
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
- package/dist/resources/extensions/gsd/auto-loop.js +7 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto-start.js +6 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +43 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/index.js +2 -1
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
- package/dist/resources/extensions/gsd/preferences.js +4 -3
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/repo-identity.js +2 -1
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
- package/src/resources/extensions/gsd/auto-loop.ts +13 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -1
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +47 -2
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +3 -1
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
- package/src/resources/extensions/gsd/preferences.ts +5 -3
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/repo-identity.ts +3 -1
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync, unlinkSync, readdirSync, } from "node:fs";
|
|
|
13
13
|
import { join, sep as pathSep } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
16
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
16
17
|
// ─── Project Root → Worktree Sync ─────────────────────────────────────────
|
|
17
18
|
/**
|
|
18
19
|
* Sync milestone artifacts from project root INTO worktree before deriveState.
|
|
@@ -75,7 +76,7 @@ export function syncStateToProjectRoot(worktreePath, projectRoot, milestoneId) {
|
|
|
75
76
|
* doesn't falsely trigger staleness (#804).
|
|
76
77
|
*/
|
|
77
78
|
export function readResourceVersion() {
|
|
78
|
-
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(
|
|
79
|
+
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
|
|
79
80
|
const manifestPath = join(agentDir, "managed-resources.json");
|
|
80
81
|
try {
|
|
81
82
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
@@ -115,10 +116,17 @@ export function checkResourcesStale(versionOnStart) {
|
|
|
115
116
|
* Returns the corrected base path.
|
|
116
117
|
*/
|
|
117
118
|
export function escapeStaleWorktree(base) {
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Direct layout: /.gsd/worktrees/
|
|
120
|
+
const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
121
|
+
let idx = base.indexOf(directMarker);
|
|
122
|
+
if (idx === -1) {
|
|
123
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
124
|
+
const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`);
|
|
125
|
+
const match = base.match(symlinkRe);
|
|
126
|
+
if (!match || match.index === undefined)
|
|
127
|
+
return base;
|
|
128
|
+
idx = match.index;
|
|
129
|
+
}
|
|
122
130
|
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
123
131
|
const projectRoot = base.slice(0, idx);
|
|
124
132
|
try {
|
|
@@ -30,8 +30,16 @@ const VALID_CLASSIFICATIONS = [
|
|
|
30
30
|
*/
|
|
31
31
|
export function resolveCapturesPath(basePath) {
|
|
32
32
|
const resolved = resolve(basePath);
|
|
33
|
+
// Direct layout: /.gsd/worktrees/
|
|
33
34
|
const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
|
|
34
|
-
|
|
35
|
+
let idx = resolved.indexOf(worktreeMarker);
|
|
36
|
+
if (idx === -1) {
|
|
37
|
+
// Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
|
|
38
|
+
const symlinkRe = new RegExp(`\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`);
|
|
39
|
+
const match = resolved.match(symlinkRe);
|
|
40
|
+
if (match && match.index !== undefined)
|
|
41
|
+
idx = match.index;
|
|
42
|
+
}
|
|
35
43
|
if (idx !== -1) {
|
|
36
44
|
// basePath is inside a worktree — resolve to project root
|
|
37
45
|
const projectRoot = resolved.slice(0, idx);
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
12
|
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
|
12
13
|
function getRegistryPath() {
|
|
13
|
-
return join(
|
|
14
|
+
return join(gsdHome, "extensions", "registry.json");
|
|
14
15
|
}
|
|
15
16
|
function getAgentExtensionsDir() {
|
|
16
|
-
return join(
|
|
17
|
+
return join(gsdHome, "agent", "extensions");
|
|
17
18
|
}
|
|
18
19
|
function loadRegistry() {
|
|
19
20
|
const filePath = getRegistryPath();
|
|
@@ -10,7 +10,7 @@ import { deriveState } from "./state.js";
|
|
|
10
10
|
import { gsdRoot } from "./paths.js";
|
|
11
11
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
12
12
|
import { appendOverride, appendKnowledge } from "./files.js";
|
|
13
|
-
import { formatDoctorIssuesForPrompt, formatDoctorReport, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
13
|
+
import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
14
14
|
import { isAutoActive } from "./auto.js";
|
|
15
15
|
import { projectRoot } from "./commands.js";
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
@@ -28,15 +28,28 @@ export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
|
28
28
|
}
|
|
29
29
|
export async function handleDoctor(args, ctx, pi) {
|
|
30
30
|
const trimmed = args.trim();
|
|
31
|
-
|
|
31
|
+
// Extract flags before positional parsing
|
|
32
|
+
const jsonMode = trimmed.includes("--json");
|
|
33
|
+
const dryRun = trimmed.includes("--dry-run");
|
|
34
|
+
const includeBuild = trimmed.includes("--build");
|
|
35
|
+
const includeTests = trimmed.includes("--test");
|
|
36
|
+
const stripped = trimmed.replace(/--json|--dry-run|--build|--test/g, "").trim();
|
|
37
|
+
const parts = stripped ? stripped.split(/\s+/) : [];
|
|
32
38
|
const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
|
|
33
39
|
const requestedScope = mode === "doctor" ? parts[0] : parts[1];
|
|
34
40
|
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
|
35
41
|
const effectiveScope = mode === "audit" ? requestedScope : scope;
|
|
36
42
|
const report = await runGSDDoctor(projectRoot(), {
|
|
37
|
-
fix: mode === "fix" || mode === "heal",
|
|
43
|
+
fix: mode === "fix" || mode === "heal" || dryRun,
|
|
44
|
+
dryRun,
|
|
38
45
|
scope: effectiveScope,
|
|
46
|
+
includeBuild,
|
|
47
|
+
includeTests,
|
|
39
48
|
});
|
|
49
|
+
if (jsonMode) {
|
|
50
|
+
ctx.ui.notify(formatDoctorReportJson(report), "info");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
const reportText = formatDoctorReport(report, {
|
|
41
54
|
scope: effectiveScope,
|
|
42
55
|
includeWarnings: mode === "audit",
|
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { gsdRoot } from "./paths.js";
|
|
10
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
10
11
|
import { enableDebug } from "./debug-logger.js";
|
|
11
12
|
import { deriveState } from "./state.js";
|
|
12
13
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
@@ -135,7 +136,7 @@ async function guardRemoteSession(ctx, pi) {
|
|
|
135
136
|
}
|
|
136
137
|
export function registerGSDCommand(pi) {
|
|
137
138
|
pi.registerCommand("gsd", {
|
|
138
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
139
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|capture|triage|dispatch|history|undo|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update",
|
|
139
140
|
getArgumentCompletions: (prefix) => {
|
|
140
141
|
const subcommands = [
|
|
141
142
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -186,7 +187,11 @@ export function registerGSDCommand(pi) {
|
|
|
186
187
|
{ cmd: "templates", desc: "List available workflow templates" },
|
|
187
188
|
{ cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
|
|
188
189
|
];
|
|
190
|
+
const hasTrailingSpace = prefix.endsWith(" ");
|
|
189
191
|
const parts = prefix.trim().split(/\s+/);
|
|
192
|
+
if (hasTrailingSpace && parts.length >= 1) {
|
|
193
|
+
parts.push("");
|
|
194
|
+
}
|
|
190
195
|
if (parts.length <= 1) {
|
|
191
196
|
return subcommands
|
|
192
197
|
.filter((item) => item.cmd.startsWith(parts[0] ?? ""))
|
|
@@ -433,7 +438,7 @@ export function registerGSDCommand(pi) {
|
|
|
433
438
|
if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
|
|
434
439
|
const idPrefix = parts[2] ?? "";
|
|
435
440
|
try {
|
|
436
|
-
const extDir = join(
|
|
441
|
+
const extDir = join(gsdHome, "agent", "extensions");
|
|
437
442
|
const ids = [];
|
|
438
443
|
for (const entry of readdirSync(extDir, { withFileTypes: true })) {
|
|
439
444
|
if (!entry.isDirectory())
|
|
@@ -468,6 +473,10 @@ export function registerGSDCommand(pi) {
|
|
|
468
473
|
{ cmd: "fix", desc: "Auto-fix detected issues" },
|
|
469
474
|
{ cmd: "heal", desc: "AI-driven deep healing" },
|
|
470
475
|
{ cmd: "audit", desc: "Run health audit without fixing" },
|
|
476
|
+
{ cmd: "--dry-run", desc: "Show what --fix would change without applying" },
|
|
477
|
+
{ cmd: "--json", desc: "Output report as JSON (CI/tooling friendly)" },
|
|
478
|
+
{ cmd: "--build", desc: "Include slow build health check (npm run build)" },
|
|
479
|
+
{ cmd: "--test", desc: "Include slow test health check (npm test)" },
|
|
471
480
|
];
|
|
472
481
|
if (parts.length <= 2) {
|
|
473
482
|
return modes
|
|
@@ -491,6 +500,17 @@ export function registerGSDCommand(pi) {
|
|
|
491
500
|
.filter((p) => p.cmd.startsWith(phasePrefix))
|
|
492
501
|
.map((p) => ({ value: `dispatch ${p.cmd}`, label: p.cmd, description: p.desc }));
|
|
493
502
|
}
|
|
503
|
+
if (parts[0] === "rate" && parts.length <= 2) {
|
|
504
|
+
const tierPrefix = parts[1] ?? "";
|
|
505
|
+
const tiers = [
|
|
506
|
+
{ cmd: "over", desc: "Model was overqualified for this task" },
|
|
507
|
+
{ cmd: "ok", desc: "Model was appropriate for this task" },
|
|
508
|
+
{ cmd: "under", desc: "Model was underqualified for this task" },
|
|
509
|
+
];
|
|
510
|
+
return tiers
|
|
511
|
+
.filter((t) => t.cmd.startsWith(tierPrefix))
|
|
512
|
+
.map((t) => ({ value: `rate ${t.cmd}`, label: t.cmd, description: t.desc }));
|
|
513
|
+
}
|
|
494
514
|
return [];
|
|
495
515
|
},
|
|
496
516
|
async handler(args, ctx) {
|
|
@@ -9,6 +9,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { gsdRoot } from "./paths.js";
|
|
12
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
12
13
|
// ─── Project File Markers ───────────────────────────────────────────────────────
|
|
13
14
|
const PROJECT_FILES = [
|
|
14
15
|
"package.json",
|
|
@@ -309,7 +310,6 @@ function detectVerificationCommands(basePath, detectedFiles, packageManager) {
|
|
|
309
310
|
* Check if global GSD setup exists (has ~/.gsd/ with preferences).
|
|
310
311
|
*/
|
|
311
312
|
export function hasGlobalSetup() {
|
|
312
|
-
const gsdHome = join(homedir(), ".gsd");
|
|
313
313
|
return (existsSync(join(gsdHome, "preferences.md")) ||
|
|
314
314
|
existsSync(join(gsdHome, "PREFERENCES.md")));
|
|
315
315
|
}
|
|
@@ -318,7 +318,6 @@ export function hasGlobalSetup() {
|
|
|
318
318
|
* Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
|
|
319
319
|
*/
|
|
320
320
|
export function isFirstEverLaunch() {
|
|
321
|
-
const gsdHome = join(homedir(), ".gsd");
|
|
322
321
|
if (!existsSync(gsdHome))
|
|
323
322
|
return true;
|
|
324
323
|
// If we have preferences, not first launch
|
|
@@ -618,6 +618,88 @@ export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldF
|
|
|
618
618
|
catch {
|
|
619
619
|
// Non-fatal — external state check failed
|
|
620
620
|
}
|
|
621
|
+
// ── Metrics ledger integrity ───────────────────────────────────────────
|
|
622
|
+
try {
|
|
623
|
+
const metricsPath = join(root, "metrics.json");
|
|
624
|
+
if (existsSync(metricsPath)) {
|
|
625
|
+
try {
|
|
626
|
+
const raw = readFileSync(metricsPath, "utf-8");
|
|
627
|
+
const ledger = JSON.parse(raw);
|
|
628
|
+
if (ledger.version !== 1 || !Array.isArray(ledger.units)) {
|
|
629
|
+
issues.push({
|
|
630
|
+
severity: "warning",
|
|
631
|
+
code: "metrics_ledger_corrupt",
|
|
632
|
+
scope: "project",
|
|
633
|
+
unitId: "project",
|
|
634
|
+
message: "metrics.json has an unexpected structure (version !== 1 or units is not an array) — metrics data may be unreliable",
|
|
635
|
+
file: ".gsd/metrics.json",
|
|
636
|
+
fixable: false,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
issues.push({
|
|
642
|
+
severity: "warning",
|
|
643
|
+
code: "metrics_ledger_corrupt",
|
|
644
|
+
scope: "project",
|
|
645
|
+
unitId: "project",
|
|
646
|
+
message: "metrics.json is not valid JSON — metrics data may be corrupt",
|
|
647
|
+
file: ".gsd/metrics.json",
|
|
648
|
+
fixable: false,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Non-fatal — metrics check failed
|
|
655
|
+
}
|
|
656
|
+
// ── Large planning file detection ──────────────────────────────────────
|
|
657
|
+
// Files over 100KB can cause LLM context pressure. Report the worst offenders.
|
|
658
|
+
try {
|
|
659
|
+
const MAX_FILE_BYTES = 100 * 1024; // 100KB
|
|
660
|
+
const milestonesPath = milestonesDir(basePath);
|
|
661
|
+
if (existsSync(milestonesPath)) {
|
|
662
|
+
const largeFiles = [];
|
|
663
|
+
function scanForLargeFiles(dir, depth = 0) {
|
|
664
|
+
if (depth > 6)
|
|
665
|
+
return;
|
|
666
|
+
try {
|
|
667
|
+
for (const entry of readdirSync(dir)) {
|
|
668
|
+
const full = join(dir, entry);
|
|
669
|
+
try {
|
|
670
|
+
const s = statSync(full);
|
|
671
|
+
if (s.isDirectory()) {
|
|
672
|
+
scanForLargeFiles(full, depth + 1);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (entry.endsWith(".md") && s.size > MAX_FILE_BYTES) {
|
|
676
|
+
largeFiles.push({ path: full.replace(basePath + "/", ""), sizeKB: Math.round(s.size / 1024) });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
catch { /* skip entry */ }
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch { /* skip dir */ }
|
|
683
|
+
}
|
|
684
|
+
scanForLargeFiles(milestonesPath);
|
|
685
|
+
if (largeFiles.length > 0) {
|
|
686
|
+
largeFiles.sort((a, b) => b.sizeKB - a.sizeKB);
|
|
687
|
+
const worst = largeFiles[0];
|
|
688
|
+
issues.push({
|
|
689
|
+
severity: "warning",
|
|
690
|
+
code: "large_planning_file",
|
|
691
|
+
scope: "project",
|
|
692
|
+
unitId: "project",
|
|
693
|
+
message: `${largeFiles.length} planning file(s) exceed 100KB — largest: ${worst.path} (${worst.sizeKB}KB). Large files cause LLM context pressure.`,
|
|
694
|
+
file: worst.path,
|
|
695
|
+
fixable: false,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// Non-fatal — large file scan failed
|
|
702
|
+
}
|
|
621
703
|
}
|
|
622
704
|
/**
|
|
623
705
|
* Build STATE.md markdown content from derived state.
|
|
@@ -355,6 +355,63 @@ function checkGitRemote(basePath) {
|
|
|
355
355
|
}
|
|
356
356
|
return { name: "git_remote", status: "ok", message: "Git remote reachable" };
|
|
357
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Check if the project build passes (opt-in slow check, use --build flag).
|
|
360
|
+
* Runs npm run build and reports failure as env_build.
|
|
361
|
+
*/
|
|
362
|
+
function checkBuildHealth(basePath) {
|
|
363
|
+
const pkgPath = join(basePath, "package.json");
|
|
364
|
+
if (!existsSync(pkgPath))
|
|
365
|
+
return null;
|
|
366
|
+
try {
|
|
367
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
368
|
+
const buildScript = pkg.scripts?.build;
|
|
369
|
+
if (!buildScript)
|
|
370
|
+
return null;
|
|
371
|
+
const result = tryExec("npm run build 2>&1", basePath);
|
|
372
|
+
if (result === null) {
|
|
373
|
+
return {
|
|
374
|
+
name: "build",
|
|
375
|
+
status: "error",
|
|
376
|
+
message: "Build failed — npm run build exited non-zero",
|
|
377
|
+
detail: "Fix build errors before dispatching work",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { name: "build", status: "ok", message: "Build passes" };
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Check if tests pass (opt-in slow check, use --test flag).
|
|
388
|
+
* Runs npm test and reports failures as env_test.
|
|
389
|
+
*/
|
|
390
|
+
function checkTestHealth(basePath) {
|
|
391
|
+
const pkgPath = join(basePath, "package.json");
|
|
392
|
+
if (!existsSync(pkgPath))
|
|
393
|
+
return null;
|
|
394
|
+
try {
|
|
395
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
396
|
+
const testScript = pkg.scripts?.test;
|
|
397
|
+
// Skip if no test script or the default placeholder
|
|
398
|
+
if (!testScript || testScript.includes("no test specified"))
|
|
399
|
+
return null;
|
|
400
|
+
const result = tryExec("npm test 2>&1", basePath);
|
|
401
|
+
if (result === null) {
|
|
402
|
+
return {
|
|
403
|
+
name: "test",
|
|
404
|
+
status: "warning",
|
|
405
|
+
message: "Tests failing — npm test exited non-zero",
|
|
406
|
+
detail: "Fix failing tests before shipping",
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return { name: "test", status: "ok", message: "Tests pass" };
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
358
415
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
359
416
|
/**
|
|
360
417
|
* Run all environment health checks. Returns structured results for
|
|
@@ -394,6 +451,24 @@ export function runFullEnvironmentChecks(basePath) {
|
|
|
394
451
|
results.push(remoteCheck);
|
|
395
452
|
return results;
|
|
396
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Run slow opt-in checks (build and/or test).
|
|
456
|
+
* These are never run on the pre-dispatch gate — only on explicit /gsd doctor --build/--test.
|
|
457
|
+
*/
|
|
458
|
+
export function runSlowEnvironmentChecks(basePath, options) {
|
|
459
|
+
const results = [];
|
|
460
|
+
if (options?.includeBuild) {
|
|
461
|
+
const buildCheck = checkBuildHealth(basePath);
|
|
462
|
+
if (buildCheck)
|
|
463
|
+
results.push(buildCheck);
|
|
464
|
+
}
|
|
465
|
+
if (options?.includeTests) {
|
|
466
|
+
const testCheck = checkTestHealth(basePath);
|
|
467
|
+
if (testCheck)
|
|
468
|
+
results.push(testCheck);
|
|
469
|
+
}
|
|
470
|
+
return results;
|
|
471
|
+
}
|
|
397
472
|
/**
|
|
398
473
|
* Convert environment check results to DoctorIssue format for the doctor pipeline.
|
|
399
474
|
*/
|
|
@@ -417,6 +492,9 @@ export async function checkEnvironmentHealth(basePath, issues, options) {
|
|
|
417
492
|
const results = options?.includeRemote
|
|
418
493
|
? runFullEnvironmentChecks(basePath)
|
|
419
494
|
: runEnvironmentChecks(basePath);
|
|
495
|
+
if (options?.includeBuild || options?.includeTests) {
|
|
496
|
+
results.push(...runSlowEnvironmentChecks(basePath, options));
|
|
497
|
+
}
|
|
420
498
|
issues.push(...environmentResultsToDoctorIssues(results));
|
|
421
499
|
}
|
|
422
500
|
/**
|
|
@@ -69,3 +69,18 @@ export function formatDoctorIssuesForPrompt(issues) {
|
|
|
69
69
|
return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
|
|
70
70
|
}).join("\n");
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Serialize a doctor report to JSON — suitable for CI/tooling integration.
|
|
74
|
+
* Usage: /gsd doctor --json
|
|
75
|
+
*/
|
|
76
|
+
export function formatDoctorReportJson(report) {
|
|
77
|
+
return JSON.stringify({
|
|
78
|
+
ok: report.ok,
|
|
79
|
+
basePath: report.basePath,
|
|
80
|
+
generatedAt: new Date().toISOString(),
|
|
81
|
+
summary: summarizeDoctorIssues(report.issues),
|
|
82
|
+
issues: report.issues,
|
|
83
|
+
fixesApplied: report.fixesApplied,
|
|
84
|
+
...(report.timing ? { timing: report.timing } : {}),
|
|
85
|
+
}, null, 2);
|
|
86
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
15
|
+
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
15
16
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
16
17
|
import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
|
|
17
18
|
// ── Model → Provider ID mapping ───────────────────────────────────────────────
|
|
@@ -33,6 +34,7 @@ function modelToProviderId(model) {
|
|
|
33
34
|
google: "google",
|
|
34
35
|
anthropic: "anthropic",
|
|
35
36
|
openai: "openai",
|
|
37
|
+
"github-copilot": "github-copilot",
|
|
36
38
|
};
|
|
37
39
|
if (prefixMap[prefix])
|
|
38
40
|
return prefixMap[prefix];
|
|
@@ -108,13 +110,29 @@ function resolveKey(providerId) {
|
|
|
108
110
|
// auth.json malformed — fall through to env check
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
|
-
// Check environment variable
|
|
113
|
+
// Check environment variable using the authoritative env var resolution
|
|
114
|
+
// (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
|
|
115
|
+
// COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
|
|
116
|
+
if (getEnvApiKey(providerId)) {
|
|
117
|
+
return { found: true, source: "env", backedOff: false };
|
|
118
|
+
}
|
|
119
|
+
// Fall back to PROVIDER_REGISTRY env var for providers not covered by getEnvApiKey
|
|
120
|
+
// (e.g., search providers like Brave, Tavily; tool providers like Jina, Context7)
|
|
112
121
|
if (info?.envVar && process.env[info.envVar]) {
|
|
113
122
|
return { found: true, source: "env", backedOff: false };
|
|
114
123
|
}
|
|
115
124
|
return { found: false, source: "none", backedOff: false };
|
|
116
125
|
}
|
|
117
126
|
// ── Individual check groups ────────────────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Providers that can serve models normally associated with another provider.
|
|
129
|
+
* Key = the provider whose models can be served, Value = alternative providers to check.
|
|
130
|
+
* e.g. GitHub Copilot subscriptions can access Claude and GPT models.
|
|
131
|
+
*/
|
|
132
|
+
const PROVIDER_ROUTES = {
|
|
133
|
+
anthropic: ["github-copilot"],
|
|
134
|
+
openai: ["github-copilot"],
|
|
135
|
+
};
|
|
118
136
|
function checkLlmProviders() {
|
|
119
137
|
const required = collectConfiguredModelProviders();
|
|
120
138
|
const results = [];
|
|
@@ -123,6 +141,22 @@ function checkLlmProviders() {
|
|
|
123
141
|
const label = info?.label ?? providerId;
|
|
124
142
|
const lookup = resolveKey(providerId);
|
|
125
143
|
if (!lookup.found) {
|
|
144
|
+
// Check if a cross-provider can serve this provider's models
|
|
145
|
+
const routes = PROVIDER_ROUTES[providerId];
|
|
146
|
+
const routeProvider = routes?.find(routeId => resolveKey(routeId).found);
|
|
147
|
+
if (routeProvider) {
|
|
148
|
+
const routeInfo = PROVIDER_REGISTRY.find(p => p.id === routeProvider);
|
|
149
|
+
const routeLabel = routeInfo?.label ?? routeProvider;
|
|
150
|
+
results.push({
|
|
151
|
+
name: providerId,
|
|
152
|
+
label,
|
|
153
|
+
category: "llm",
|
|
154
|
+
status: "ok",
|
|
155
|
+
message: `${label} — available via ${routeLabel}`,
|
|
156
|
+
required: true,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
126
160
|
const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
|
|
127
161
|
results.push({
|
|
128
162
|
name: providerId,
|