gsd-pi 2.37.1 → 2.38.0-dev.4d4d14a
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/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
- package/dist/resources/extensions/gsd/auto-loop.js +149 -170
- package/dist/resources/extensions/gsd/auto-post-unit.js +105 -68
- package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto.js +143 -96
- 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-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -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 +62 -12
- 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/git-service.js +8 -1
- package/dist/resources/extensions/gsd/index.js +24 -20
- 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-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +3 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
- package/dist/resources/extensions/gsd/preferences.js +8 -5
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- 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/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.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-ai/src/utils/oauth/anthropic.ts +2 -2
- 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/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
- package/src/resources/extensions/gsd/auto-loop.ts +207 -252
- package/src/resources/extensions/gsd/auto-post-unit.ts +82 -39
- package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto.ts +139 -101
- 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-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -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 +64 -10
- 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/git-service.ts +13 -1
- package/src/resources/extensions/gsd/index.ts +24 -17
- 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-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +9 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
- package/src/resources/extensions/gsd/preferences.ts +8 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- 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/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +16 -37
- 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 +191 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -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/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -1
- 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
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -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 ───────────────────────────────────────────────
|
|
@@ -27,12 +28,15 @@ function modelToProviderId(model) {
|
|
|
27
28
|
const prefix = model.split("/")[0].toLowerCase();
|
|
28
29
|
// Map known prefixes to registry IDs
|
|
29
30
|
const prefixMap = {
|
|
31
|
+
"anthropic-vertex": "anthropic-vertex",
|
|
30
32
|
openrouter: "openrouter",
|
|
31
33
|
groq: "groq",
|
|
32
34
|
mistral: "mistral",
|
|
33
35
|
google: "google",
|
|
36
|
+
"google-vertex": "google-vertex",
|
|
34
37
|
anthropic: "anthropic",
|
|
35
38
|
openai: "openai",
|
|
39
|
+
"github-copilot": "github-copilot",
|
|
36
40
|
};
|
|
37
41
|
if (prefixMap[prefix])
|
|
38
42
|
return prefixMap[prefix];
|
|
@@ -65,11 +69,19 @@ function collectConfiguredModelProviders() {
|
|
|
65
69
|
}
|
|
66
70
|
const modelEntries = typeof models === "object" ? Object.values(models) : [];
|
|
67
71
|
for (const entry of modelEntries) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
if (typeof entry === "string") {
|
|
73
|
+
const pid = modelToProviderId(entry);
|
|
74
|
+
if (pid)
|
|
75
|
+
providers.add(pid);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (typeof entry === "object" && entry !== null && "model" in entry) {
|
|
79
|
+
const configuredProvider = "provider" in entry ? entry.provider : undefined;
|
|
80
|
+
if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) {
|
|
81
|
+
providers.add(configuredProvider);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const modelId = String(entry.model);
|
|
73
85
|
const pid = modelToProviderId(modelId);
|
|
74
86
|
if (pid)
|
|
75
87
|
providers.add(pid);
|
|
@@ -108,31 +120,69 @@ function resolveKey(providerId) {
|
|
|
108
120
|
// auth.json malformed — fall through to env check
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
|
-
// Check environment variable
|
|
123
|
+
// Check environment variable using the authoritative env var resolution
|
|
124
|
+
// (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
|
|
125
|
+
// COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
|
|
126
|
+
if (getEnvApiKey(providerId)) {
|
|
127
|
+
return { found: true, source: "env", backedOff: false };
|
|
128
|
+
}
|
|
129
|
+
// Fall back to PROVIDER_REGISTRY env var for providers not covered by getEnvApiKey
|
|
130
|
+
// (e.g., search providers like Brave, Tavily; tool providers like Jina, Context7)
|
|
112
131
|
if (info?.envVar && process.env[info.envVar]) {
|
|
113
132
|
return { found: true, source: "env", backedOff: false };
|
|
114
133
|
}
|
|
115
134
|
return { found: false, source: "none", backedOff: false };
|
|
116
135
|
}
|
|
117
136
|
// ── Individual check groups ────────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Providers that can serve models normally associated with another provider.
|
|
139
|
+
* Key = the provider whose models can be served, Value = alternative providers to check.
|
|
140
|
+
* e.g. GitHub Copilot subscriptions can access Claude and GPT models.
|
|
141
|
+
*/
|
|
142
|
+
const PROVIDER_ROUTES = {
|
|
143
|
+
anthropic: ["github-copilot"],
|
|
144
|
+
openai: ["github-copilot"],
|
|
145
|
+
};
|
|
118
146
|
function checkLlmProviders() {
|
|
119
147
|
const required = collectConfiguredModelProviders();
|
|
120
148
|
const results = [];
|
|
121
149
|
for (const providerId of required) {
|
|
122
150
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
123
|
-
const label =
|
|
151
|
+
const label = providerId === "anthropic-vertex"
|
|
152
|
+
? "Anthropic Vertex"
|
|
153
|
+
: info?.label ?? providerId;
|
|
124
154
|
const lookup = resolveKey(providerId);
|
|
125
155
|
if (!lookup.found) {
|
|
126
|
-
|
|
156
|
+
// Check if a cross-provider can serve this provider's models
|
|
157
|
+
const routes = PROVIDER_ROUTES[providerId];
|
|
158
|
+
const routeProvider = routes?.find(routeId => resolveKey(routeId).found);
|
|
159
|
+
if (routeProvider) {
|
|
160
|
+
const routeInfo = PROVIDER_REGISTRY.find(p => p.id === routeProvider);
|
|
161
|
+
const routeLabel = routeInfo?.label ?? routeProvider;
|
|
162
|
+
results.push({
|
|
163
|
+
name: providerId,
|
|
164
|
+
label,
|
|
165
|
+
category: "llm",
|
|
166
|
+
status: "ok",
|
|
167
|
+
message: `${label} — available via ${routeLabel}`,
|
|
168
|
+
required: true,
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const envVar = providerId === "anthropic-vertex"
|
|
173
|
+
? "ANTHROPIC_VERTEX_PROJECT_ID"
|
|
174
|
+
: info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
|
|
127
175
|
results.push({
|
|
128
176
|
name: providerId,
|
|
129
177
|
label,
|
|
130
178
|
category: "llm",
|
|
131
179
|
status: "error",
|
|
132
|
-
message: `${label} —
|
|
133
|
-
detail:
|
|
134
|
-
?
|
|
135
|
-
:
|
|
180
|
+
message: `${label} — not configured`,
|
|
181
|
+
detail: providerId === "anthropic-vertex"
|
|
182
|
+
? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
|
|
183
|
+
: info?.hasOAuth
|
|
184
|
+
? `Run /gsd keys to authenticate`
|
|
185
|
+
: `Set ${envVar} or run /gsd keys`,
|
|
136
186
|
required: true,
|
|
137
187
|
});
|
|
138
188
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
4
|
-
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
4
|
+
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
|
|
5
5
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
6
6
|
import { invalidateAllCaches } from "./cache.js";
|
|
7
7
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -9,7 +9,7 @@ import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
|
9
9
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
10
10
|
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
|
11
11
|
import { runProviderChecks } from "./doctor-providers.js";
|
|
12
|
-
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt } from "./doctor-format.js";
|
|
12
|
+
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
|
|
13
13
|
export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport } from "./doctor-environment.js";
|
|
14
14
|
export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport } from "./progress-score.js";
|
|
15
15
|
/**
|
|
@@ -324,10 +324,68 @@ export async function selectDoctorScope(basePath, requestedScope) {
|
|
|
324
324
|
}
|
|
325
325
|
return state.registry[0]?.id;
|
|
326
326
|
}
|
|
327
|
+
// ── Helper: circular dependency detection ──────────────────────────────────
|
|
328
|
+
function detectCircularDependencies(slices) {
|
|
329
|
+
const known = new Set(slices.map(s => s.id));
|
|
330
|
+
const adj = new Map();
|
|
331
|
+
for (const s of slices)
|
|
332
|
+
adj.set(s.id, s.depends.filter(d => known.has(d)));
|
|
333
|
+
const state = new Map();
|
|
334
|
+
for (const s of slices)
|
|
335
|
+
state.set(s.id, "unvisited");
|
|
336
|
+
const cycles = [];
|
|
337
|
+
function dfs(id, path) {
|
|
338
|
+
const st = state.get(id);
|
|
339
|
+
if (st === "done")
|
|
340
|
+
return;
|
|
341
|
+
if (st === "visiting") {
|
|
342
|
+
cycles.push([...path.slice(path.indexOf(id)), id]);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
state.set(id, "visiting");
|
|
346
|
+
for (const dep of adj.get(id) ?? [])
|
|
347
|
+
dfs(dep, [...path, id]);
|
|
348
|
+
state.set(id, "done");
|
|
349
|
+
}
|
|
350
|
+
for (const s of slices)
|
|
351
|
+
if (state.get(s.id) === "unvisited")
|
|
352
|
+
dfs(s.id, []);
|
|
353
|
+
return cycles;
|
|
354
|
+
}
|
|
355
|
+
async function appendDoctorHistory(basePath, report) {
|
|
356
|
+
try {
|
|
357
|
+
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
358
|
+
const entry = JSON.stringify({
|
|
359
|
+
ts: new Date().toISOString(),
|
|
360
|
+
ok: report.ok,
|
|
361
|
+
errors: report.issues.filter(i => i.severity === "error").length,
|
|
362
|
+
warnings: report.issues.filter(i => i.severity === "warning").length,
|
|
363
|
+
fixes: report.fixesApplied.length,
|
|
364
|
+
codes: [...new Set(report.issues.map(i => i.code))],
|
|
365
|
+
});
|
|
366
|
+
const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
|
|
367
|
+
await saveFile(historyPath, existing + entry + "\n");
|
|
368
|
+
}
|
|
369
|
+
catch { /* non-fatal */ }
|
|
370
|
+
}
|
|
371
|
+
/** Read the last N doctor history entries. Returns most-recent-first. */
|
|
372
|
+
export async function readDoctorHistory(basePath, lastN = 50) {
|
|
373
|
+
try {
|
|
374
|
+
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
375
|
+
if (!existsSync(historyPath))
|
|
376
|
+
return [];
|
|
377
|
+
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
|
|
378
|
+
return lines.slice(-lastN).reverse().map(l => JSON.parse(l));
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
327
384
|
export async function runGSDDoctor(basePath, options) {
|
|
328
385
|
const issues = [];
|
|
329
386
|
const fixesApplied = [];
|
|
330
387
|
const fix = options?.fix === true;
|
|
388
|
+
const dryRun = options?.dryRun === true;
|
|
331
389
|
const fixLevel = options?.fixLevel ?? "all";
|
|
332
390
|
// Issue codes that represent completion state transitions — creating summary
|
|
333
391
|
// stubs, marking slices/milestones done in the roadmap. These belong to the
|
|
@@ -336,12 +394,18 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
336
394
|
// detected and reported but never auto-fixed.
|
|
337
395
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
338
396
|
const shouldFix = (code) => {
|
|
339
|
-
if (!fix)
|
|
397
|
+
if (!fix || dryRun)
|
|
340
398
|
return false;
|
|
341
399
|
if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))
|
|
342
400
|
return false;
|
|
343
401
|
return true;
|
|
344
402
|
};
|
|
403
|
+
/** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
|
|
404
|
+
const dryRunCanFix = (code, message) => {
|
|
405
|
+
if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
|
|
406
|
+
fixesApplied.push(`[dry-run] would fix: ${message}`);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
345
409
|
const prefs = loadEffectiveGSDPreferences();
|
|
346
410
|
if (prefs) {
|
|
347
411
|
const prefIssues = validatePreferenceShape(prefs.preferences);
|
|
@@ -357,18 +421,30 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
357
421
|
});
|
|
358
422
|
}
|
|
359
423
|
}
|
|
360
|
-
// Git health checks
|
|
424
|
+
// Git health checks — timed
|
|
425
|
+
const t0git = Date.now();
|
|
361
426
|
const isolationMode = options?.isolationMode ??
|
|
362
427
|
(prefs?.preferences?.git?.isolation === "none" ? "none" :
|
|
363
428
|
prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
|
|
364
429
|
await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
|
|
365
|
-
|
|
430
|
+
const gitMs = Date.now() - t0git;
|
|
431
|
+
// Runtime health checks — timed
|
|
432
|
+
const t0runtime = Date.now();
|
|
366
433
|
await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
|
|
367
|
-
|
|
368
|
-
|
|
434
|
+
const runtimeMs = Date.now() - t0runtime;
|
|
435
|
+
// Environment health checks — timed
|
|
436
|
+
const t0env = Date.now();
|
|
437
|
+
await checkEnvironmentHealth(basePath, issues, {
|
|
438
|
+
includeRemote: !options?.scope,
|
|
439
|
+
includeBuild: options?.includeBuild,
|
|
440
|
+
includeTests: options?.includeTests,
|
|
441
|
+
});
|
|
442
|
+
const envMs = Date.now() - t0env;
|
|
369
443
|
const milestonesPath = milestonesDir(basePath);
|
|
370
444
|
if (!existsSync(milestonesPath)) {
|
|
371
|
-
|
|
445
|
+
const report = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
|
|
446
|
+
await appendDoctorHistory(basePath, report);
|
|
447
|
+
return report;
|
|
372
448
|
}
|
|
373
449
|
const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
|
|
374
450
|
const requirementsContent = await loadFile(requirementsPath);
|
|
@@ -432,6 +508,46 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
432
508
|
if (!roadmapContent)
|
|
433
509
|
continue;
|
|
434
510
|
const roadmap = parseRoadmap(roadmapContent);
|
|
511
|
+
// ── Circular dependency detection ──────────────────────────────────────
|
|
512
|
+
for (const cycle of detectCircularDependencies(roadmap.slices)) {
|
|
513
|
+
issues.push({
|
|
514
|
+
severity: "error",
|
|
515
|
+
code: "circular_slice_dependency",
|
|
516
|
+
scope: "milestone",
|
|
517
|
+
unitId: milestoneId,
|
|
518
|
+
message: `Circular dependency detected: ${cycle.join(" → ")}`,
|
|
519
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
520
|
+
fixable: false,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// ── Orphaned slice directories ─────────────────────────────────────────
|
|
524
|
+
try {
|
|
525
|
+
const slicesDir = join(milestonePath, "slices");
|
|
526
|
+
if (existsSync(slicesDir)) {
|
|
527
|
+
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
528
|
+
for (const entry of readdirSync(slicesDir)) {
|
|
529
|
+
try {
|
|
530
|
+
if (!lstatSync(join(slicesDir, entry)).isDirectory())
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (!knownSliceIds.has(entry)) {
|
|
537
|
+
issues.push({
|
|
538
|
+
severity: "warning",
|
|
539
|
+
code: "orphaned_slice_directory",
|
|
540
|
+
scope: "milestone",
|
|
541
|
+
unitId: milestoneId,
|
|
542
|
+
message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
|
|
543
|
+
file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
|
|
544
|
+
fixable: false,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch { /* non-fatal */ }
|
|
435
551
|
for (const slice of roadmap.slices) {
|
|
436
552
|
const unitId = `${milestoneId}/${slice.id}`;
|
|
437
553
|
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId)
|
|
@@ -502,6 +618,34 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
502
618
|
}
|
|
503
619
|
continue;
|
|
504
620
|
}
|
|
621
|
+
// ── Duplicate task IDs ───────────────────────────────────────────────
|
|
622
|
+
const taskIdCounts = new Map();
|
|
623
|
+
for (const task of plan.tasks)
|
|
624
|
+
taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
|
|
625
|
+
for (const [taskId, count] of taskIdCounts) {
|
|
626
|
+
if (count > 1) {
|
|
627
|
+
issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
|
|
628
|
+
message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
|
|
629
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// ── Task files on disk not in plan ────────────────────────────────────
|
|
633
|
+
try {
|
|
634
|
+
if (tasksDir) {
|
|
635
|
+
const planTaskIds = new Set(plan.tasks.map(t => t.id));
|
|
636
|
+
for (const f of readdirSync(tasksDir)) {
|
|
637
|
+
if (!f.endsWith("-SUMMARY.md"))
|
|
638
|
+
continue;
|
|
639
|
+
const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
|
|
640
|
+
if (!planTaskIds.has(diskTaskId)) {
|
|
641
|
+
issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
|
|
642
|
+
message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
|
|
643
|
+
file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch { /* non-fatal */ }
|
|
505
649
|
let allTasksDone = plan.tasks.length > 0;
|
|
506
650
|
for (const task of plan.tasks) {
|
|
507
651
|
const taskUnitId = `${unitId}/${task.id}`;
|
|
@@ -517,6 +661,7 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
517
661
|
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
518
662
|
fixable: true,
|
|
519
663
|
});
|
|
664
|
+
dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
|
|
520
665
|
if (shouldFix("task_done_missing_summary")) {
|
|
521
666
|
const stubPath = join(basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks", `${task.id}-SUMMARY.md`);
|
|
522
667
|
const stubContent = [
|
|
@@ -575,6 +720,22 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
575
720
|
}
|
|
576
721
|
}
|
|
577
722
|
}
|
|
723
|
+
// ── Future timestamp check ─────────────────────────────────────
|
|
724
|
+
if (task.done && hasSummary && summaryPath) {
|
|
725
|
+
try {
|
|
726
|
+
const rawSummary = await loadFile(summaryPath);
|
|
727
|
+
const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
|
|
728
|
+
if (m) {
|
|
729
|
+
const ts = new Date(m[1].trim());
|
|
730
|
+
if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
|
|
731
|
+
issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
|
|
732
|
+
message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
|
|
733
|
+
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
catch { /* non-fatal */ }
|
|
738
|
+
}
|
|
578
739
|
allTasksDone = allTasksDone && task.done;
|
|
579
740
|
}
|
|
580
741
|
// Blocker-without-replan detection
|
|
@@ -604,6 +765,12 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
604
765
|
}
|
|
605
766
|
}
|
|
606
767
|
}
|
|
768
|
+
// ── Stale REPLAN: exists but all tasks done ────────────────────────
|
|
769
|
+
if (replanPath && allTasksDone) {
|
|
770
|
+
issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
|
|
771
|
+
message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
|
|
772
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
|
|
773
|
+
}
|
|
607
774
|
const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
|
|
608
775
|
const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
|
|
609
776
|
const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
|
|
@@ -618,6 +785,7 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
618
785
|
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
619
786
|
fixable: true,
|
|
620
787
|
});
|
|
788
|
+
dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
|
|
621
789
|
if (shouldFix("all_tasks_done_missing_slice_summary"))
|
|
622
790
|
await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
623
791
|
}
|
|
@@ -631,6 +799,7 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
631
799
|
file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
|
|
632
800
|
fixable: true,
|
|
633
801
|
});
|
|
802
|
+
dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
|
|
634
803
|
if (shouldFix("all_tasks_done_missing_slice_uat"))
|
|
635
804
|
await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
636
805
|
}
|
|
@@ -644,6 +813,7 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
644
813
|
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
645
814
|
fixable: true,
|
|
646
815
|
});
|
|
816
|
+
dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
|
|
647
817
|
if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
|
|
648
818
|
await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
649
819
|
}
|
|
@@ -696,13 +866,16 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
696
866
|
});
|
|
697
867
|
}
|
|
698
868
|
}
|
|
699
|
-
if (fix && fixesApplied.length > 0) {
|
|
869
|
+
if (fix && !dryRun && fixesApplied.length > 0) {
|
|
700
870
|
await updateStateFile(basePath, fixesApplied);
|
|
701
871
|
}
|
|
702
|
-
|
|
872
|
+
const report = {
|
|
703
873
|
ok: issues.every(issue => issue.severity !== "error"),
|
|
704
874
|
basePath,
|
|
705
875
|
issues,
|
|
706
876
|
fixesApplied,
|
|
877
|
+
timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
|
|
707
878
|
};
|
|
879
|
+
await appendDoctorHistory(basePath, report);
|
|
880
|
+
return report;
|
|
708
881
|
}
|
|
@@ -5,7 +5,7 @@ import { join, basename } from "node:path";
|
|
|
5
5
|
import { exec } from "node:child_process";
|
|
6
6
|
import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk, } from "./metrics.js";
|
|
7
7
|
import { gsdRoot } from "./paths.js";
|
|
8
|
-
import { formatDuration, fileLink } from "../shared/
|
|
8
|
+
import { formatDuration, fileLink } from "../shared/format-utils.js";
|
|
9
9
|
import { getErrorMessage } from "./error-utils.js";
|
|
10
10
|
/**
|
|
11
11
|
* Open a file in the user's default browser.
|
|
@@ -6,8 +6,8 @@ import { promises as fs } from 'node:fs';
|
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { atomicWriteAsync } from './atomic-write.js';
|
|
8
8
|
import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
|
|
9
|
-
import { findMilestoneIds } from './
|
|
10
|
-
import { checkExistingEnvKeys } from '../
|
|
9
|
+
import { findMilestoneIds } from './milestone-ids.js';
|
|
10
|
+
import { checkExistingEnvKeys } from '../env-utils.js';
|
|
11
11
|
import { parseRoadmapSlices } from './roadmap-slices.js';
|
|
12
12
|
import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
|
|
13
13
|
import { debugTime, debugCount } from './debug-logger.js';
|
|
@@ -629,6 +629,47 @@ export function countMustHavesMentionedInSummary(mustHaves, summaryContent) {
|
|
|
629
629
|
}
|
|
630
630
|
return count;
|
|
631
631
|
}
|
|
632
|
+
// ─── Task Plan IO Extractor ────────────────────────────────────────────────
|
|
633
|
+
/**
|
|
634
|
+
* Extract input and output file paths from a task plan's `## Inputs` and
|
|
635
|
+
* `## Expected Output` sections. Looks for backtick-wrapped file paths on
|
|
636
|
+
* each line (e.g. `` `src/foo.ts` ``).
|
|
637
|
+
*
|
|
638
|
+
* Returns empty arrays for missing/empty sections — callers should treat
|
|
639
|
+
* tasks with no IO as ambiguous (sequential fallback trigger).
|
|
640
|
+
*/
|
|
641
|
+
export function parseTaskPlanIO(content) {
|
|
642
|
+
const backtickPathRegex = /`([^`]+)`/g;
|
|
643
|
+
function extractPaths(sectionText) {
|
|
644
|
+
if (!sectionText)
|
|
645
|
+
return [];
|
|
646
|
+
const paths = [];
|
|
647
|
+
for (const line of sectionText.split("\n")) {
|
|
648
|
+
const trimmed = line.trim();
|
|
649
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
650
|
+
continue;
|
|
651
|
+
let match;
|
|
652
|
+
backtickPathRegex.lastIndex = 0;
|
|
653
|
+
while ((match = backtickPathRegex.exec(trimmed)) !== null) {
|
|
654
|
+
const candidate = match[1];
|
|
655
|
+
// Filter out things that look like code tokens rather than file paths
|
|
656
|
+
// (e.g. `true`, `false`, `npm run test`). A file path has at least one
|
|
657
|
+
// dot or slash.
|
|
658
|
+
if (candidate.includes("/") || candidate.includes(".")) {
|
|
659
|
+
paths.push(candidate);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return paths;
|
|
664
|
+
}
|
|
665
|
+
const [, body] = splitFrontmatter(content);
|
|
666
|
+
const inputSection = extractSection(body, "Inputs");
|
|
667
|
+
const outputSection = extractSection(body, "Expected Output");
|
|
668
|
+
return {
|
|
669
|
+
inputFiles: extractPaths(inputSection),
|
|
670
|
+
outputFiles: extractPaths(outputSection),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
632
673
|
/**
|
|
633
674
|
* Extract the UAT type from a UAT file's raw content.
|
|
634
675
|
*
|
|
@@ -21,7 +21,7 @@ import { deriveState } from "./state.js";
|
|
|
21
21
|
import { isAutoActive } from "./auto.js";
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
import { gsdRoot } from "./paths.js";
|
|
24
|
-
import { formatDuration } from "../shared/
|
|
24
|
+
import { formatDuration } from "../shared/format-utils.js";
|
|
25
25
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
26
26
|
// ─── Entry Point ──────────────────────────────────────────────────────────────
|
|
27
27
|
export async function handleForensics(args, ctx, pi) {
|
|
@@ -36,12 +36,19 @@ export function buildTaskCommitMessage(ctx) {
|
|
|
36
36
|
: description;
|
|
37
37
|
const subject = `${type}(${scope}): ${truncated}`;
|
|
38
38
|
// Build body with key files if available
|
|
39
|
+
const bodyParts = [];
|
|
39
40
|
if (ctx.keyFiles && ctx.keyFiles.length > 0) {
|
|
40
41
|
const fileLines = ctx.keyFiles
|
|
41
42
|
.slice(0, 8) // cap at 8 files to keep commit concise
|
|
42
43
|
.map(f => `- ${f}`)
|
|
43
44
|
.join("\n");
|
|
44
|
-
|
|
45
|
+
bodyParts.push(fileLines);
|
|
46
|
+
}
|
|
47
|
+
if (ctx.issueNumber) {
|
|
48
|
+
bodyParts.push(`Resolves #${ctx.issueNumber}`);
|
|
49
|
+
}
|
|
50
|
+
if (bodyParts.length > 0) {
|
|
51
|
+
return `${subject}\n\n${bodyParts.join("\n\n")}`;
|
|
45
52
|
}
|
|
46
53
|
return subject;
|
|
47
54
|
}
|