gsd-pi 2.37.1-dev.d3ace49 → 2.38.0-dev.29edcdc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/remote-questions-config.js +2 -2
- package/dist/resource-loader.js +34 -1
- 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 +8 -9
- package/dist/resources/extensions/gsd/auto-loop.js +597 -588
- package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
- package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
- 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-worktree.js +3 -3
- 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 +24 -3
- 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 +27 -11
- package/dist/resources/extensions/gsd/doctor.js +204 -12
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +6 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +15 -12
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/index.js +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -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 +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +59 -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/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- 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/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- 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 +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/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.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/extensions/loader.ts +223 -7
- 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 +7 -9
- package/src/resources/extensions/gsd/auto-loop.ts +484 -546
- package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
- package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
- 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-worktree.ts +3 -3
- 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 +26 -4
- 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 +26 -9
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +199 -14
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +5 -3
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +20 -10
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/index.ts +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +4 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +51 -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/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- 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/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
- 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 +86 -3
- 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/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 +0 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- 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
|
@@ -407,6 +407,63 @@ function checkGitRemote(basePath: string): EnvironmentCheckResult | null {
|
|
|
407
407
|
return { name: "git_remote", status: "ok", message: "Git remote reachable" };
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Check if the project build passes (opt-in slow check, use --build flag).
|
|
412
|
+
* Runs npm run build and reports failure as env_build.
|
|
413
|
+
*/
|
|
414
|
+
function checkBuildHealth(basePath: string): EnvironmentCheckResult | null {
|
|
415
|
+
const pkgPath = join(basePath, "package.json");
|
|
416
|
+
if (!existsSync(pkgPath)) return null;
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
420
|
+
const buildScript = pkg.scripts?.build;
|
|
421
|
+
if (!buildScript) return null;
|
|
422
|
+
|
|
423
|
+
const result = tryExec("npm run build 2>&1", basePath);
|
|
424
|
+
if (result === null) {
|
|
425
|
+
return {
|
|
426
|
+
name: "build",
|
|
427
|
+
status: "error",
|
|
428
|
+
message: "Build failed — npm run build exited non-zero",
|
|
429
|
+
detail: "Fix build errors before dispatching work",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return { name: "build", status: "ok", message: "Build passes" };
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Check if tests pass (opt-in slow check, use --test flag).
|
|
440
|
+
* Runs npm test and reports failures as env_test.
|
|
441
|
+
*/
|
|
442
|
+
function checkTestHealth(basePath: string): EnvironmentCheckResult | null {
|
|
443
|
+
const pkgPath = join(basePath, "package.json");
|
|
444
|
+
if (!existsSync(pkgPath)) return null;
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
448
|
+
const testScript = pkg.scripts?.test;
|
|
449
|
+
// Skip if no test script or the default placeholder
|
|
450
|
+
if (!testScript || testScript.includes("no test specified")) return null;
|
|
451
|
+
|
|
452
|
+
const result = tryExec("npm test 2>&1", basePath);
|
|
453
|
+
if (result === null) {
|
|
454
|
+
return {
|
|
455
|
+
name: "test",
|
|
456
|
+
status: "warning",
|
|
457
|
+
message: "Tests failing — npm test exited non-zero",
|
|
458
|
+
detail: "Fix failing tests before shipping",
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return { name: "test", status: "ok", message: "Tests pass" };
|
|
462
|
+
} catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
410
467
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
411
468
|
|
|
412
469
|
/**
|
|
@@ -454,6 +511,26 @@ export function runFullEnvironmentChecks(basePath: string): EnvironmentCheckResu
|
|
|
454
511
|
return results;
|
|
455
512
|
}
|
|
456
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Run slow opt-in checks (build and/or test).
|
|
516
|
+
* These are never run on the pre-dispatch gate — only on explicit /gsd doctor --build/--test.
|
|
517
|
+
*/
|
|
518
|
+
export function runSlowEnvironmentChecks(
|
|
519
|
+
basePath: string,
|
|
520
|
+
options?: { includeBuild?: boolean; includeTests?: boolean },
|
|
521
|
+
): EnvironmentCheckResult[] {
|
|
522
|
+
const results: EnvironmentCheckResult[] = [];
|
|
523
|
+
if (options?.includeBuild) {
|
|
524
|
+
const buildCheck = checkBuildHealth(basePath);
|
|
525
|
+
if (buildCheck) results.push(buildCheck);
|
|
526
|
+
}
|
|
527
|
+
if (options?.includeTests) {
|
|
528
|
+
const testCheck = checkTestHealth(basePath);
|
|
529
|
+
if (testCheck) results.push(testCheck);
|
|
530
|
+
}
|
|
531
|
+
return results;
|
|
532
|
+
}
|
|
533
|
+
|
|
457
534
|
/**
|
|
458
535
|
* Convert environment check results to DoctorIssue format for the doctor pipeline.
|
|
459
536
|
*/
|
|
@@ -477,12 +554,16 @@ export function environmentResultsToDoctorIssues(results: EnvironmentCheckResult
|
|
|
477
554
|
export async function checkEnvironmentHealth(
|
|
478
555
|
basePath: string,
|
|
479
556
|
issues: DoctorIssue[],
|
|
480
|
-
options?: { includeRemote?: boolean },
|
|
557
|
+
options?: { includeRemote?: boolean; includeBuild?: boolean; includeTests?: boolean },
|
|
481
558
|
): Promise<void> {
|
|
482
559
|
const results = options?.includeRemote
|
|
483
560
|
? runFullEnvironmentChecks(basePath)
|
|
484
561
|
: runEnvironmentChecks(basePath);
|
|
485
562
|
|
|
563
|
+
if (options?.includeBuild || options?.includeTests) {
|
|
564
|
+
results.push(...runSlowEnvironmentChecks(basePath, options));
|
|
565
|
+
}
|
|
566
|
+
|
|
486
567
|
issues.push(...environmentResultsToDoctorIssues(results));
|
|
487
568
|
}
|
|
488
569
|
|
|
@@ -76,3 +76,23 @@ export function formatDoctorIssuesForPrompt(issues: DoctorIssue[]): string {
|
|
|
76
76
|
return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
|
|
77
77
|
}).join("\n");
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Serialize a doctor report to JSON — suitable for CI/tooling integration.
|
|
82
|
+
* Usage: /gsd doctor --json
|
|
83
|
+
*/
|
|
84
|
+
export function formatDoctorReportJson(report: DoctorReport): string {
|
|
85
|
+
return JSON.stringify(
|
|
86
|
+
{
|
|
87
|
+
ok: report.ok,
|
|
88
|
+
basePath: report.basePath,
|
|
89
|
+
generatedAt: new Date().toISOString(),
|
|
90
|
+
summary: summarizeDoctorIssues(report.issues),
|
|
91
|
+
issues: report.issues,
|
|
92
|
+
fixesApplied: report.fixesApplied,
|
|
93
|
+
...(report.timing ? { timing: report.timing } : {}),
|
|
94
|
+
},
|
|
95
|
+
null,
|
|
96
|
+
2,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -51,10 +51,12 @@ function modelToProviderId(model: string): string | null {
|
|
|
51
51
|
const prefix = model.split("/")[0].toLowerCase();
|
|
52
52
|
// Map known prefixes to registry IDs
|
|
53
53
|
const prefixMap: Record<string, string> = {
|
|
54
|
+
"anthropic-vertex": "anthropic-vertex",
|
|
54
55
|
openrouter: "openrouter",
|
|
55
56
|
groq: "groq",
|
|
56
57
|
mistral: "mistral",
|
|
57
58
|
google: "google",
|
|
59
|
+
"google-vertex": "google-vertex",
|
|
58
60
|
anthropic: "anthropic",
|
|
59
61
|
openai: "openai",
|
|
60
62
|
"github-copilot": "github-copilot",
|
|
@@ -88,11 +90,20 @@ function collectConfiguredModelProviders(): Set<string> {
|
|
|
88
90
|
|
|
89
91
|
const modelEntries = typeof models === "object" ? Object.values(models) : [];
|
|
90
92
|
for (const entry of modelEntries) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
if (typeof entry === "string") {
|
|
94
|
+
const pid = modelToProviderId(entry);
|
|
95
|
+
if (pid) providers.add(pid);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof entry === "object" && entry !== null && "model" in entry) {
|
|
100
|
+
const configuredProvider = "provider" in entry ? (entry as { provider?: unknown }).provider : undefined;
|
|
101
|
+
if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) {
|
|
102
|
+
providers.add(configuredProvider);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const modelId = String((entry as { model: unknown }).model);
|
|
96
107
|
const pid = modelToProviderId(modelId);
|
|
97
108
|
if (pid) providers.add(pid);
|
|
98
109
|
}
|
|
@@ -175,7 +186,9 @@ function checkLlmProviders(): ProviderCheckResult[] {
|
|
|
175
186
|
|
|
176
187
|
for (const providerId of required) {
|
|
177
188
|
const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
|
|
178
|
-
const label =
|
|
189
|
+
const label = providerId === "anthropic-vertex"
|
|
190
|
+
? "Anthropic Vertex"
|
|
191
|
+
: info?.label ?? providerId;
|
|
179
192
|
const lookup = resolveKey(providerId);
|
|
180
193
|
|
|
181
194
|
if (!lookup.found) {
|
|
@@ -196,14 +209,18 @@ function checkLlmProviders(): ProviderCheckResult[] {
|
|
|
196
209
|
continue;
|
|
197
210
|
}
|
|
198
211
|
|
|
199
|
-
const envVar =
|
|
212
|
+
const envVar = providerId === "anthropic-vertex"
|
|
213
|
+
? "ANTHROPIC_VERTEX_PROJECT_ID"
|
|
214
|
+
: info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
|
|
200
215
|
results.push({
|
|
201
216
|
name: providerId,
|
|
202
217
|
label,
|
|
203
218
|
category: "llm",
|
|
204
219
|
status: "error",
|
|
205
|
-
message: `${label} —
|
|
206
|
-
detail:
|
|
220
|
+
message: `${label} — not configured`,
|
|
221
|
+
detail: providerId === "anthropic-vertex"
|
|
222
|
+
? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
|
|
223
|
+
: info?.hasOAuth
|
|
207
224
|
? `Run /gsd keys to authenticate`
|
|
208
225
|
: `Set ${envVar} or run /gsd keys`,
|
|
209
226
|
required: true,
|
|
@@ -53,7 +53,20 @@ export type DoctorIssueCode =
|
|
|
53
53
|
| "stranded_lock_directory"
|
|
54
54
|
// Git / worktree integrity checks
|
|
55
55
|
| "integration_branch_missing"
|
|
56
|
-
| "worktree_directory_orphaned"
|
|
56
|
+
| "worktree_directory_orphaned"
|
|
57
|
+
// GSD state structural checks
|
|
58
|
+
| "circular_slice_dependency"
|
|
59
|
+
| "orphaned_slice_directory"
|
|
60
|
+
| "duplicate_task_id"
|
|
61
|
+
| "task_file_not_in_plan"
|
|
62
|
+
| "stale_replan_file"
|
|
63
|
+
| "future_timestamp"
|
|
64
|
+
// Runtime data integrity
|
|
65
|
+
| "metrics_ledger_corrupt"
|
|
66
|
+
| "large_planning_file"
|
|
67
|
+
// Slow environment checks (opt-in via --build / --test flags)
|
|
68
|
+
| "env_build"
|
|
69
|
+
| "env_test";
|
|
57
70
|
|
|
58
71
|
/**
|
|
59
72
|
* Issue codes that represent expected completion-transition states.
|
|
@@ -83,6 +96,8 @@ export interface DoctorReport {
|
|
|
83
96
|
basePath: string;
|
|
84
97
|
issues: DoctorIssue[];
|
|
85
98
|
fixesApplied: string[];
|
|
99
|
+
/** Per-domain check durations in milliseconds. Present on explicit /gsd doctor runs. */
|
|
100
|
+
timing?: { git: number; runtime: number; environment: number; gsdState: number };
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
export interface DoctorSummary {
|
|
@@ -1,14 +1,15 @@
|
|
|
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
|
|
|
4
4
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
|
-
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
5
|
+
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
|
|
6
6
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
7
7
|
import { invalidateAllCaches } from "./cache.js";
|
|
8
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
9
9
|
|
|
10
|
-
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
10
|
+
import type { DoctorIssue, DoctorIssueCode, DoctorReport } from "./doctor-types.js";
|
|
11
11
|
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
12
|
+
import type { RoadmapSliceEntry } from "./types.js";
|
|
12
13
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
13
14
|
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
|
14
15
|
import { runProviderChecks } from "./doctor-providers.js";
|
|
@@ -17,7 +18,7 @@ import { runProviderChecks } from "./doctor-providers.js";
|
|
|
17
18
|
// All public types and functions from extracted modules are re-exported here
|
|
18
19
|
// so that existing imports from "./doctor.js" continue to work unchanged.
|
|
19
20
|
export type { DoctorSeverity, DoctorIssueCode, DoctorIssue, DoctorReport, DoctorSummary } from "./doctor-types.js";
|
|
20
|
-
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt } from "./doctor-format.js";
|
|
21
|
+
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
|
|
21
22
|
export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport, type EnvironmentCheckResult } from "./doctor-environment.js";
|
|
22
23
|
export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport, type ProgressScore, type ProgressLevel } from "./progress-score.js";
|
|
23
24
|
|
|
@@ -279,9 +280,24 @@ async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sli
|
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
async function markSliceUndoneInRoadmap(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
|
|
284
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
285
|
+
if (!roadmapPath) return;
|
|
286
|
+
const content = await loadFile(roadmapPath);
|
|
287
|
+
if (!content) return;
|
|
288
|
+
const updated = content.replace(
|
|
289
|
+
new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"),
|
|
290
|
+
`$1[ ] **${sliceId}:`,
|
|
291
|
+
);
|
|
292
|
+
if (updated !== content) {
|
|
293
|
+
await saveFile(roadmapPath, updated);
|
|
294
|
+
fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
282
298
|
function matchesScope(unitId: string, scope?: string): boolean {
|
|
283
299
|
if (!scope) return true;
|
|
284
|
-
return unitId === scope || unitId.startsWith(`${scope}/`)
|
|
300
|
+
return unitId === scope || unitId.startsWith(`${scope}/`);
|
|
285
301
|
}
|
|
286
302
|
|
|
287
303
|
function auditRequirements(content: string | null): DoctorIssue[] {
|
|
@@ -350,10 +366,60 @@ export async function selectDoctorScope(basePath: string, requestedScope?: strin
|
|
|
350
366
|
return state.registry[0]?.id;
|
|
351
367
|
}
|
|
352
368
|
|
|
353
|
-
|
|
369
|
+
// ── Helper: circular dependency detection ──────────────────────────────────
|
|
370
|
+
function detectCircularDependencies(slices: RoadmapSliceEntry[]): string[][] {
|
|
371
|
+
const known = new Set(slices.map(s => s.id));
|
|
372
|
+
const adj = new Map<string, string[]>();
|
|
373
|
+
for (const s of slices) adj.set(s.id, s.depends.filter(d => known.has(d)));
|
|
374
|
+
const state = new Map<string, "unvisited" | "visiting" | "done">();
|
|
375
|
+
for (const s of slices) state.set(s.id, "unvisited");
|
|
376
|
+
const cycles: string[][] = [];
|
|
377
|
+
function dfs(id: string, path: string[]): void {
|
|
378
|
+
const st = state.get(id);
|
|
379
|
+
if (st === "done") return;
|
|
380
|
+
if (st === "visiting") { cycles.push([...path.slice(path.indexOf(id)), id]); return; }
|
|
381
|
+
state.set(id, "visiting");
|
|
382
|
+
for (const dep of adj.get(id) ?? []) dfs(dep, [...path, id]);
|
|
383
|
+
state.set(id, "done");
|
|
384
|
+
}
|
|
385
|
+
for (const s of slices) if (state.get(s.id) === "unvisited") dfs(s.id, []);
|
|
386
|
+
return cycles;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Helper: doctor run history ──────────────────────────────────────────────
|
|
390
|
+
interface DoctorHistoryEntry { ts: string; ok: boolean; errors: number; warnings: number; fixes: number; codes: string[] }
|
|
391
|
+
|
|
392
|
+
async function appendDoctorHistory(basePath: string, report: DoctorReport): Promise<void> {
|
|
393
|
+
try {
|
|
394
|
+
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
395
|
+
const entry = JSON.stringify({
|
|
396
|
+
ts: new Date().toISOString(),
|
|
397
|
+
ok: report.ok,
|
|
398
|
+
errors: report.issues.filter(i => i.severity === "error").length,
|
|
399
|
+
warnings: report.issues.filter(i => i.severity === "warning").length,
|
|
400
|
+
fixes: report.fixesApplied.length,
|
|
401
|
+
codes: [...new Set(report.issues.map(i => i.code))],
|
|
402
|
+
} satisfies DoctorHistoryEntry);
|
|
403
|
+
const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
|
|
404
|
+
await saveFile(historyPath, existing + entry + "\n");
|
|
405
|
+
} catch { /* non-fatal */ }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Read the last N doctor history entries. Returns most-recent-first. */
|
|
409
|
+
export async function readDoctorHistory(basePath: string, lastN = 50): Promise<DoctorHistoryEntry[]> {
|
|
410
|
+
try {
|
|
411
|
+
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
412
|
+
if (!existsSync(historyPath)) return [];
|
|
413
|
+
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
|
|
414
|
+
return lines.slice(-lastN).reverse().map(l => JSON.parse(l) as DoctorHistoryEntry);
|
|
415
|
+
} catch { return []; }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; dryRun?: boolean; scope?: string; fixLevel?: "task" | "all"; isolationMode?: "none" | "worktree" | "branch"; includeBuild?: boolean; includeTests?: boolean }): Promise<DoctorReport> {
|
|
354
419
|
const issues: DoctorIssue[] = [];
|
|
355
420
|
const fixesApplied: string[] = [];
|
|
356
421
|
const fix = options?.fix === true;
|
|
422
|
+
const dryRun = options?.dryRun === true;
|
|
357
423
|
const fixLevel = options?.fixLevel ?? "all";
|
|
358
424
|
|
|
359
425
|
// Issue codes that represent completion state transitions — creating summary
|
|
@@ -364,11 +430,18 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
364
430
|
|
|
365
431
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
366
432
|
const shouldFix = (code: DoctorIssueCode): boolean => {
|
|
367
|
-
if (!fix) return false;
|
|
433
|
+
if (!fix || dryRun) return false;
|
|
368
434
|
if (fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code)) return false;
|
|
369
435
|
return true;
|
|
370
436
|
};
|
|
371
437
|
|
|
438
|
+
/** Log a dry-run "would fix" entry when fix=true but dryRun=true. */
|
|
439
|
+
const dryRunCanFix = (code: DoctorIssueCode, message: string): void => {
|
|
440
|
+
if (dryRun && fix && !(fixLevel === "task" && COMPLETION_TRANSITION_CODES.has(code))) {
|
|
441
|
+
fixesApplied.push(`[dry-run] would fix: ${message}`);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
372
445
|
const prefs = loadEffectiveGSDPreferences();
|
|
373
446
|
if (prefs) {
|
|
374
447
|
const prefIssues = validatePreferenceShape(prefs.preferences);
|
|
@@ -385,21 +458,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
385
458
|
}
|
|
386
459
|
}
|
|
387
460
|
|
|
388
|
-
// Git health checks
|
|
461
|
+
// Git health checks — timed
|
|
462
|
+
const t0git = Date.now();
|
|
389
463
|
const isolationMode: "none" | "worktree" | "branch" = options?.isolationMode ??
|
|
390
464
|
(prefs?.preferences?.git?.isolation === "none" ? "none" :
|
|
391
465
|
prefs?.preferences?.git?.isolation === "branch" ? "branch" : "worktree");
|
|
392
466
|
await checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode);
|
|
467
|
+
const gitMs = Date.now() - t0git;
|
|
393
468
|
|
|
394
|
-
// Runtime health checks
|
|
469
|
+
// Runtime health checks — timed
|
|
470
|
+
const t0runtime = Date.now();
|
|
395
471
|
await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
|
|
472
|
+
const runtimeMs = Date.now() - t0runtime;
|
|
396
473
|
|
|
397
|
-
// Environment health checks
|
|
398
|
-
|
|
474
|
+
// Environment health checks — timed
|
|
475
|
+
const t0env = Date.now();
|
|
476
|
+
await checkEnvironmentHealth(basePath, issues, {
|
|
477
|
+
includeRemote: !options?.scope,
|
|
478
|
+
includeBuild: options?.includeBuild,
|
|
479
|
+
includeTests: options?.includeTests,
|
|
480
|
+
});
|
|
481
|
+
const envMs = Date.now() - t0env;
|
|
399
482
|
|
|
400
483
|
const milestonesPath = milestonesDir(basePath);
|
|
401
484
|
if (!existsSync(milestonesPath)) {
|
|
402
|
-
|
|
485
|
+
const report: DoctorReport = { ok: issues.every(i => i.severity !== "error"), basePath, issues, fixesApplied, timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: 0 } };
|
|
486
|
+
await appendDoctorHistory(basePath, report);
|
|
487
|
+
return report;
|
|
403
488
|
}
|
|
404
489
|
|
|
405
490
|
const requirementsPath = resolveGsdRootFile(basePath, "REQUIREMENTS");
|
|
@@ -465,6 +550,43 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
465
550
|
if (!roadmapContent) continue;
|
|
466
551
|
const roadmap = parseRoadmap(roadmapContent);
|
|
467
552
|
|
|
553
|
+
// ── Circular dependency detection ──────────────────────────────────────
|
|
554
|
+
for (const cycle of detectCircularDependencies(roadmap.slices)) {
|
|
555
|
+
issues.push({
|
|
556
|
+
severity: "error",
|
|
557
|
+
code: "circular_slice_dependency",
|
|
558
|
+
scope: "milestone",
|
|
559
|
+
unitId: milestoneId,
|
|
560
|
+
message: `Circular dependency detected: ${cycle.join(" → ")}`,
|
|
561
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
562
|
+
fixable: false,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── Orphaned slice directories ─────────────────────────────────────────
|
|
567
|
+
try {
|
|
568
|
+
const slicesDir = join(milestonePath, "slices");
|
|
569
|
+
if (existsSync(slicesDir)) {
|
|
570
|
+
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
571
|
+
for (const entry of readdirSync(slicesDir)) {
|
|
572
|
+
try {
|
|
573
|
+
if (!lstatSync(join(slicesDir, entry)).isDirectory()) continue;
|
|
574
|
+
} catch { continue; }
|
|
575
|
+
if (!knownSliceIds.has(entry)) {
|
|
576
|
+
issues.push({
|
|
577
|
+
severity: "warning",
|
|
578
|
+
code: "orphaned_slice_directory",
|
|
579
|
+
scope: "milestone",
|
|
580
|
+
unitId: milestoneId,
|
|
581
|
+
message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap`,
|
|
582
|
+
file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
|
|
583
|
+
fixable: false,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} catch { /* non-fatal */ }
|
|
589
|
+
|
|
468
590
|
for (const slice of roadmap.slices) {
|
|
469
591
|
const unitId = `${milestoneId}/${slice.id}`;
|
|
470
592
|
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
|
|
@@ -539,6 +661,33 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
539
661
|
continue;
|
|
540
662
|
}
|
|
541
663
|
|
|
664
|
+
// ── Duplicate task IDs ───────────────────────────────────────────────
|
|
665
|
+
const taskIdCounts = new Map<string, number>();
|
|
666
|
+
for (const task of plan.tasks) taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
|
|
667
|
+
for (const [taskId, count] of taskIdCounts) {
|
|
668
|
+
if (count > 1) {
|
|
669
|
+
issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
|
|
670
|
+
message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
|
|
671
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ── Task files on disk not in plan ────────────────────────────────────
|
|
676
|
+
try {
|
|
677
|
+
if (tasksDir) {
|
|
678
|
+
const planTaskIds = new Set(plan.tasks.map(t => t.id));
|
|
679
|
+
for (const f of readdirSync(tasksDir)) {
|
|
680
|
+
if (!f.endsWith("-SUMMARY.md")) continue;
|
|
681
|
+
const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
|
|
682
|
+
if (!planTaskIds.has(diskTaskId)) {
|
|
683
|
+
issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
|
|
684
|
+
message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
|
|
685
|
+
file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch { /* non-fatal */ }
|
|
690
|
+
|
|
542
691
|
let allTasksDone = plan.tasks.length > 0;
|
|
543
692
|
for (const task of plan.tasks) {
|
|
544
693
|
const taskUnitId = `${unitId}/${task.id}`;
|
|
@@ -555,6 +704,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
555
704
|
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
556
705
|
fixable: true,
|
|
557
706
|
});
|
|
707
|
+
dryRunCanFix("task_done_missing_summary", `create stub summary for ${taskUnitId}`);
|
|
558
708
|
if (shouldFix("task_done_missing_summary")) {
|
|
559
709
|
const stubPath = join(
|
|
560
710
|
basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
|
|
@@ -618,6 +768,22 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
618
768
|
}
|
|
619
769
|
}
|
|
620
770
|
|
|
771
|
+
// ── Future timestamp check ─────────────────────────────────────
|
|
772
|
+
if (task.done && hasSummary && summaryPath) {
|
|
773
|
+
try {
|
|
774
|
+
const rawSummary = await loadFile(summaryPath);
|
|
775
|
+
const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
|
|
776
|
+
if (m) {
|
|
777
|
+
const ts = new Date(m[1].trim());
|
|
778
|
+
if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
|
|
779
|
+
issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
|
|
780
|
+
message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
|
|
781
|
+
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} catch { /* non-fatal */ }
|
|
785
|
+
}
|
|
786
|
+
|
|
621
787
|
allTasksDone = allTasksDone && task.done;
|
|
622
788
|
}
|
|
623
789
|
|
|
@@ -646,6 +812,13 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
646
812
|
}
|
|
647
813
|
}
|
|
648
814
|
|
|
815
|
+
// ── Stale REPLAN: exists but all tasks done ────────────────────────
|
|
816
|
+
if (replanPath && allTasksDone) {
|
|
817
|
+
issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
|
|
818
|
+
message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
|
|
819
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
|
|
820
|
+
}
|
|
821
|
+
|
|
649
822
|
const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
|
|
650
823
|
const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
|
|
651
824
|
const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
|
|
@@ -661,6 +834,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
661
834
|
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
662
835
|
fixable: true,
|
|
663
836
|
});
|
|
837
|
+
dryRunCanFix("all_tasks_done_missing_slice_summary", `create placeholder summary for ${unitId}`);
|
|
664
838
|
if (shouldFix("all_tasks_done_missing_slice_summary")) await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
665
839
|
}
|
|
666
840
|
|
|
@@ -674,6 +848,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
674
848
|
file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
|
|
675
849
|
fixable: true,
|
|
676
850
|
});
|
|
851
|
+
dryRunCanFix("all_tasks_done_missing_slice_uat", `create placeholder UAT for ${unitId}`);
|
|
677
852
|
if (shouldFix("all_tasks_done_missing_slice_uat")) await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
678
853
|
}
|
|
679
854
|
|
|
@@ -687,6 +862,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
687
862
|
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
688
863
|
fixable: true,
|
|
689
864
|
});
|
|
865
|
+
dryRunCanFix("all_tasks_done_roadmap_not_checked", `mark ${slice.id} done in roadmap`);
|
|
690
866
|
if (shouldFix("all_tasks_done_roadmap_not_checked") && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
|
|
691
867
|
await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
692
868
|
}
|
|
@@ -702,6 +878,12 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
702
878
|
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
703
879
|
fixable: true,
|
|
704
880
|
});
|
|
881
|
+
if (!allTasksDone) {
|
|
882
|
+
dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
|
|
883
|
+
if (shouldFix("slice_checked_missing_summary")) {
|
|
884
|
+
await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
705
887
|
}
|
|
706
888
|
|
|
707
889
|
if (slice.done && !hasSliceUat) {
|
|
@@ -744,14 +926,17 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
744
926
|
}
|
|
745
927
|
}
|
|
746
928
|
|
|
747
|
-
if (fix && fixesApplied.length > 0) {
|
|
929
|
+
if (fix && !dryRun && fixesApplied.length > 0) {
|
|
748
930
|
await updateStateFile(basePath, fixesApplied);
|
|
749
931
|
}
|
|
750
932
|
|
|
751
|
-
|
|
933
|
+
const report: DoctorReport = {
|
|
752
934
|
ok: issues.every(issue => issue.severity !== "error"),
|
|
753
935
|
basePath,
|
|
754
936
|
issues,
|
|
755
937
|
fixesApplied,
|
|
938
|
+
timing: { git: gitMs, runtime: runtimeMs, environment: envMs, gsdState: Math.max(0, Date.now() - t0env - envMs) },
|
|
756
939
|
};
|
|
940
|
+
await appendDoctorHistory(basePath, report);
|
|
941
|
+
return report;
|
|
757
942
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise<void>;
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ export function registerExitCommand(
|
|
|
10
10
|
description: "Exit GSD gracefully",
|
|
11
11
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
12
12
|
// Stop auto-mode first so locks and activity state are cleaned up before shutdown.
|
|
13
|
-
const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
|
|
13
|
+
const stopAuto = deps.stopAuto ?? (await importExtensionModule<typeof import("./auto.js")>(import.meta.url, "./auto.js")).stopAuto;
|
|
14
14
|
await stopAuto(ctx, pi, "Graceful exit");
|
|
15
15
|
ctx.shutdown();
|
|
16
16
|
},
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./metrics.js";
|
|
12
12
|
import type { UnitMetrics } from "./metrics.js";
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
|
-
import { formatDuration, fileLink } from "../shared/
|
|
14
|
+
import { formatDuration, fileLink } from "../shared/format-utils.js";
|
|
15
15
|
import { getErrorMessage } from "./error-utils.js";
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs';
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { atomicWriteAsync } from './atomic-write.js';
|
|
9
9
|
import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
|
|
10
|
-
import { milestoneIdSort, findMilestoneIds } from './
|
|
10
|
+
import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
Roadmap, BoundaryMapEntry,
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
ManifestStatus,
|
|
21
21
|
} from './types.js';
|
|
22
22
|
|
|
23
|
-
import { checkExistingEnvKeys } from '../
|
|
23
|
+
import { checkExistingEnvKeys } from '../env-utils.js';
|
|
24
24
|
import { parseRoadmapSlices } from './roadmap-slices.js';
|
|
25
25
|
import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
|
|
26
26
|
import { debugTime, debugCount } from './debug-logger.js';
|
|
@@ -775,7 +775,7 @@ export function parseTaskPlanIO(content: string): { inputFiles: string[]; output
|
|
|
775
775
|
* The four UAT classification types recognised by GSD auto-mode.
|
|
776
776
|
* `undefined` is returned (not this union) when no type can be determined.
|
|
777
777
|
*/
|
|
778
|
-
export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed';
|
|
778
|
+
export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed' | 'browser-executable' | 'runtime-executable';
|
|
779
779
|
|
|
780
780
|
/**
|
|
781
781
|
* Extract the UAT type from a UAT file's raw content.
|
|
@@ -799,6 +799,8 @@ export function extractUatType(content: string): UatType | undefined {
|
|
|
799
799
|
const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
|
|
800
800
|
|
|
801
801
|
if (rawValue.startsWith('artifact-driven')) return 'artifact-driven';
|
|
802
|
+
if (rawValue.startsWith('browser-executable')) return 'browser-executable';
|
|
803
|
+
if (rawValue.startsWith('runtime-executable')) return 'runtime-executable';
|
|
802
804
|
if (rawValue.startsWith('live-runtime')) return 'live-runtime';
|
|
803
805
|
if (rawValue.startsWith('human-experience')) return 'human-experience';
|
|
804
806
|
if (rawValue.startsWith('mixed')) return 'mixed';
|
|
@@ -27,7 +27,7 @@ import { deriveState } from "./state.js";
|
|
|
27
27
|
import { isAutoActive } from "./auto.js";
|
|
28
28
|
import { loadPrompt } from "./prompt-loader.js";
|
|
29
29
|
import { gsdRoot } from "./paths.js";
|
|
30
|
-
import { formatDuration } from "../shared/
|
|
30
|
+
import { formatDuration } from "../shared/format-utils.js";
|
|
31
31
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
32
32
|
|
|
33
33
|
// ─── Types ────────────────────────────────────────────────────────────────────
|