gsd-pi 2.19.0 → 2.20.0
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 +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Doctor — Proactive Healing Layer
|
|
3
|
+
*
|
|
4
|
+
* Three mechanisms for automatic health monitoring during auto-mode:
|
|
5
|
+
*
|
|
6
|
+
* 1. Pre-dispatch health gate: lightweight check before each unit dispatch.
|
|
7
|
+
* Returns blocking issues that should pause auto-mode rather than
|
|
8
|
+
* dispatching into a broken state.
|
|
9
|
+
*
|
|
10
|
+
* 2. Health score tracking: tracks issue counts over time to detect
|
|
11
|
+
* degradation trends. If health is declining, surfaces a warning.
|
|
12
|
+
*
|
|
13
|
+
* 3. Auto-heal escalation: if deterministic fix can't resolve issues
|
|
14
|
+
* after N units, escalates to LLM-assisted heal dispatch.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { gsdRoot, resolveGsdRootFile } from "./paths.js";
|
|
20
|
+
import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
|
|
21
|
+
import { abortAndReset } from "./git-self-heal.js";
|
|
22
|
+
|
|
23
|
+
// ── Health Score Tracking ──────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface HealthSnapshot {
|
|
26
|
+
timestamp: number;
|
|
27
|
+
errors: number;
|
|
28
|
+
warnings: number;
|
|
29
|
+
fixesApplied: number;
|
|
30
|
+
unitIndex: number; // which unit dispatch triggered this snapshot
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** In-memory health history for the current auto-mode session. */
|
|
34
|
+
let healthHistory: HealthSnapshot[] = [];
|
|
35
|
+
|
|
36
|
+
/** Count of consecutive units with unresolved errors. */
|
|
37
|
+
let consecutiveErrorUnits = 0;
|
|
38
|
+
|
|
39
|
+
/** Unit index counter for health tracking. */
|
|
40
|
+
let healthUnitIndex = 0;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Record a health snapshot after a doctor run.
|
|
44
|
+
* Called from the post-unit hook in auto.ts.
|
|
45
|
+
*/
|
|
46
|
+
export function recordHealthSnapshot(errors: number, warnings: number, fixesApplied: number): void {
|
|
47
|
+
healthUnitIndex++;
|
|
48
|
+
healthHistory.push({
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
errors,
|
|
51
|
+
warnings,
|
|
52
|
+
fixesApplied,
|
|
53
|
+
unitIndex: healthUnitIndex,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Keep only the last 50 snapshots to bound memory
|
|
57
|
+
if (healthHistory.length > 50) {
|
|
58
|
+
healthHistory = healthHistory.slice(-50);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (errors > 0) {
|
|
62
|
+
consecutiveErrorUnits++;
|
|
63
|
+
} else {
|
|
64
|
+
consecutiveErrorUnits = 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the current health trend.
|
|
70
|
+
* Returns "improving", "stable", "degrading", or "unknown" (not enough data).
|
|
71
|
+
*/
|
|
72
|
+
export function getHealthTrend(): "improving" | "stable" | "degrading" | "unknown" {
|
|
73
|
+
if (healthHistory.length < 3) return "unknown";
|
|
74
|
+
|
|
75
|
+
const recent = healthHistory.slice(-5);
|
|
76
|
+
const older = healthHistory.slice(-10, -5);
|
|
77
|
+
|
|
78
|
+
if (older.length === 0) return "unknown";
|
|
79
|
+
|
|
80
|
+
const recentAvg = recent.reduce((sum, s) => sum + s.errors + s.warnings, 0) / recent.length;
|
|
81
|
+
const olderAvg = older.reduce((sum, s) => sum + s.errors + s.warnings, 0) / older.length;
|
|
82
|
+
|
|
83
|
+
const delta = recentAvg - olderAvg;
|
|
84
|
+
if (delta > 1) return "degrading";
|
|
85
|
+
if (delta < -1) return "improving";
|
|
86
|
+
return "stable";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the number of consecutive units with unresolved errors.
|
|
91
|
+
*/
|
|
92
|
+
export function getConsecutiveErrorUnits(): number {
|
|
93
|
+
return consecutiveErrorUnits;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get health history for display (e.g., dashboard overlay).
|
|
98
|
+
*/
|
|
99
|
+
export function getHealthHistory(): readonly HealthSnapshot[] {
|
|
100
|
+
return healthHistory;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reset health tracking state. Called on auto-mode start/stop.
|
|
105
|
+
*/
|
|
106
|
+
export function resetHealthTracking(): void {
|
|
107
|
+
healthHistory = [];
|
|
108
|
+
consecutiveErrorUnits = 0;
|
|
109
|
+
healthUnitIndex = 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Pre-Dispatch Health Gate ───────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export interface PreDispatchHealthResult {
|
|
115
|
+
/** Whether the dispatch should proceed. */
|
|
116
|
+
proceed: boolean;
|
|
117
|
+
/** If blocked, the reason to show the user. */
|
|
118
|
+
reason?: string;
|
|
119
|
+
/** Issues found (for logging). */
|
|
120
|
+
issues: string[];
|
|
121
|
+
/** Whether fix was applied. */
|
|
122
|
+
fixesApplied: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Lightweight pre-dispatch health check. Runs fast checks that should
|
|
127
|
+
* block dispatch if they fail — avoids dispatching into a broken state.
|
|
128
|
+
*
|
|
129
|
+
* This is NOT a full doctor run — it only checks critical, fast-to-evaluate
|
|
130
|
+
* conditions that would cause the next unit to fail or corrupt state.
|
|
131
|
+
*
|
|
132
|
+
* Returns { proceed: true } if dispatch should continue.
|
|
133
|
+
*/
|
|
134
|
+
export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult {
|
|
135
|
+
const issues: string[] = [];
|
|
136
|
+
const fixesApplied: string[] = [];
|
|
137
|
+
|
|
138
|
+
// ── Stale crash lock blocks dispatch ──
|
|
139
|
+
// If a stale lock exists, the crash recovery path should handle it,
|
|
140
|
+
// not a new dispatch. This prevents double-dispatch after crashes.
|
|
141
|
+
try {
|
|
142
|
+
const lock = readCrashLock(basePath);
|
|
143
|
+
if (lock && !isLockProcessAlive(lock)) {
|
|
144
|
+
// Auto-clear it since we're about to dispatch anyway
|
|
145
|
+
clearLock(basePath);
|
|
146
|
+
fixesApplied.push("cleared stale auto.lock before dispatch");
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// Non-fatal
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Corrupt merge/rebase state blocks dispatch ──
|
|
153
|
+
// Dispatching a unit with MERGE_HEAD present will cause git operations to fail.
|
|
154
|
+
try {
|
|
155
|
+
const gitDir = join(basePath, ".git");
|
|
156
|
+
if (existsSync(gitDir)) {
|
|
157
|
+
const blockers = ["MERGE_HEAD", "rebase-apply", "rebase-merge"].filter(
|
|
158
|
+
f => existsSync(join(gitDir, f)),
|
|
159
|
+
);
|
|
160
|
+
if (blockers.length > 0) {
|
|
161
|
+
// Try to auto-heal
|
|
162
|
+
try {
|
|
163
|
+
const result = abortAndReset(basePath);
|
|
164
|
+
fixesApplied.push(`pre-dispatch: cleaned merge state (${result.cleaned.join(", ")})`);
|
|
165
|
+
} catch {
|
|
166
|
+
issues.push(`Corrupt git state: ${blockers.join(", ")}. Run /gsd doctor fix.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Non-fatal
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── STATE.md existence check ──
|
|
175
|
+
// If STATE.md is missing, deriveState will still work but the LLM
|
|
176
|
+
// may get confused. Rebuild it silently.
|
|
177
|
+
try {
|
|
178
|
+
const stateFile = resolveGsdRootFile(basePath, "STATE");
|
|
179
|
+
const milestonesDir = join(gsdRoot(basePath), "milestones");
|
|
180
|
+
if (existsSync(milestonesDir) && !existsSync(stateFile)) {
|
|
181
|
+
issues.push("STATE.md missing — will rebuild after this unit");
|
|
182
|
+
// Don't block dispatch — rebuilding happens in post-hook
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// Non-fatal
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If we had critical issues that couldn't be auto-healed, block dispatch
|
|
189
|
+
if (issues.length > 0) {
|
|
190
|
+
return {
|
|
191
|
+
proceed: false,
|
|
192
|
+
reason: `Pre-dispatch health check failed:\n${issues.map(i => ` - ${i}`).join("\n")}\nRun /gsd doctor fix to resolve.`,
|
|
193
|
+
issues,
|
|
194
|
+
fixesApplied,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { proceed: true, issues, fixesApplied };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Auto-Heal Escalation ──────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/** Threshold: escalate to LLM heal after this many consecutive error units. */
|
|
204
|
+
const ESCALATION_THRESHOLD = 5;
|
|
205
|
+
|
|
206
|
+
/** Whether an escalation has already been triggered this session (prevent spam). */
|
|
207
|
+
let escalationTriggered = false;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check whether auto-heal should escalate from deterministic fix to
|
|
211
|
+
* LLM-assisted heal. Called after each post-unit doctor run.
|
|
212
|
+
*
|
|
213
|
+
* Returns the structured issue text for LLM dispatch, or null if
|
|
214
|
+
* escalation is not needed.
|
|
215
|
+
*/
|
|
216
|
+
export function checkHealEscalation(
|
|
217
|
+
errors: number,
|
|
218
|
+
unresolvedIssues: Array<{ code: string; message: string; unitId: string }>,
|
|
219
|
+
): { shouldEscalate: boolean; reason: string; issues: typeof unresolvedIssues } {
|
|
220
|
+
if (escalationTriggered) {
|
|
221
|
+
return { shouldEscalate: false, reason: "already escalated this session", issues: [] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (consecutiveErrorUnits < ESCALATION_THRESHOLD) {
|
|
225
|
+
return {
|
|
226
|
+
shouldEscalate: false,
|
|
227
|
+
reason: `${consecutiveErrorUnits}/${ESCALATION_THRESHOLD} consecutive error units`,
|
|
228
|
+
issues: [],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (errors === 0) {
|
|
233
|
+
return { shouldEscalate: false, reason: "no errors to escalate", issues: [] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const trend = getHealthTrend();
|
|
237
|
+
if (trend === "improving") {
|
|
238
|
+
return { shouldEscalate: false, reason: "health is improving — deferring escalation", issues: [] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
escalationTriggered = true;
|
|
242
|
+
return {
|
|
243
|
+
shouldEscalate: true,
|
|
244
|
+
reason: `${consecutiveErrorUnits} consecutive units with unresolved errors (trend: ${trend})`,
|
|
245
|
+
issues: unresolvedIssues,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reset escalation state. Called on auto-mode start/stop.
|
|
251
|
+
*/
|
|
252
|
+
export function resetEscalation(): void {
|
|
253
|
+
escalationTriggered = false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format a health summary for display in the auto-mode dashboard.
|
|
258
|
+
*/
|
|
259
|
+
export function formatHealthSummary(): string {
|
|
260
|
+
if (healthHistory.length === 0) return "No health data yet.";
|
|
261
|
+
|
|
262
|
+
const latest = healthHistory[healthHistory.length - 1]!;
|
|
263
|
+
const trend = getHealthTrend();
|
|
264
|
+
const trendIcon = trend === "improving" ? "+" : trend === "degrading" ? "-" : "=";
|
|
265
|
+
const totalFixes = healthHistory.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
266
|
+
|
|
267
|
+
const parts = [
|
|
268
|
+
`Health: ${latest.errors}E/${latest.warnings}W`,
|
|
269
|
+
`trend:${trendIcon}`,
|
|
270
|
+
`fixes:${totalFixes}`,
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
if (consecutiveErrorUnits > 0) {
|
|
274
|
+
parts.push(`streak:${consecutiveErrorUnits}/${ESCALATION_THRESHOLD}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return parts.join(" | ");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Reset all proactive healing state. Called on auto-mode start/stop.
|
|
282
|
+
*/
|
|
283
|
+
export function resetProactiveHealing(): void {
|
|
284
|
+
resetHealthTracking();
|
|
285
|
+
resetEscalation();
|
|
286
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
@@ -9,6 +9,8 @@ import { listWorktrees } from "./worktree-manager.js";
|
|
|
9
9
|
import { abortAndReset } from "./git-self-heal.js";
|
|
10
10
|
import { RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
|
|
11
11
|
import { nativeIsRepo, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
|
|
12
|
+
import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
|
|
13
|
+
import { ensureGitignore } from "./gitignore.js";
|
|
12
14
|
|
|
13
15
|
export type DoctorSeverity = "info" | "warning" | "error";
|
|
14
16
|
export type DoctorIssueCode =
|
|
@@ -32,7 +34,14 @@ export type DoctorIssueCode =
|
|
|
32
34
|
| "stale_milestone_branch"
|
|
33
35
|
| "corrupt_merge_state"
|
|
34
36
|
| "tracked_runtime_files"
|
|
35
|
-
| "legacy_slice_branches"
|
|
37
|
+
| "legacy_slice_branches"
|
|
38
|
+
| "stale_crash_lock"
|
|
39
|
+
| "orphaned_completed_units"
|
|
40
|
+
| "stale_hook_state"
|
|
41
|
+
| "activity_log_bloat"
|
|
42
|
+
| "state_file_stale"
|
|
43
|
+
| "state_file_missing"
|
|
44
|
+
| "gitignore_missing_patterns";
|
|
36
45
|
|
|
37
46
|
export interface DoctorIssue {
|
|
38
47
|
severity: DoctorSeverity;
|
|
@@ -657,6 +666,275 @@ async function checkGitHealth(
|
|
|
657
666
|
}
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
// ── Runtime Health Checks ──────────────────────────────────────────────────
|
|
670
|
+
// Checks for stale crash locks, orphaned completed-units, stale hook state,
|
|
671
|
+
// activity log bloat, STATE.md drift, and gitignore drift.
|
|
672
|
+
|
|
673
|
+
async function checkRuntimeHealth(
|
|
674
|
+
basePath: string,
|
|
675
|
+
issues: DoctorIssue[],
|
|
676
|
+
fixesApplied: string[],
|
|
677
|
+
shouldFix: (code: DoctorIssueCode) => boolean,
|
|
678
|
+
): Promise<void> {
|
|
679
|
+
const root = gsdRoot(basePath);
|
|
680
|
+
|
|
681
|
+
// ── Stale crash lock ──────────────────────────────────────────────────
|
|
682
|
+
try {
|
|
683
|
+
const lock = readCrashLock(basePath);
|
|
684
|
+
if (lock) {
|
|
685
|
+
const alive = isLockProcessAlive(lock);
|
|
686
|
+
if (!alive) {
|
|
687
|
+
issues.push({
|
|
688
|
+
severity: "error",
|
|
689
|
+
code: "stale_crash_lock",
|
|
690
|
+
scope: "project",
|
|
691
|
+
unitId: "project",
|
|
692
|
+
message: `Stale auto.lock from PID ${lock.pid} (started ${lock.startedAt}, was executing ${lock.unitType} ${lock.unitId}) — process is no longer running`,
|
|
693
|
+
file: ".gsd/auto.lock",
|
|
694
|
+
fixable: true,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
if (shouldFix("stale_crash_lock")) {
|
|
698
|
+
clearLock(basePath);
|
|
699
|
+
fixesApplied.push("cleared stale auto.lock");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} catch {
|
|
704
|
+
// Non-fatal — crash lock check failed
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── Orphaned completed-units keys ─────────────────────────────────────
|
|
708
|
+
try {
|
|
709
|
+
const completedKeysFile = join(root, "completed-units.json");
|
|
710
|
+
if (existsSync(completedKeysFile)) {
|
|
711
|
+
const raw = readFileSync(completedKeysFile, "utf-8");
|
|
712
|
+
const keys: string[] = JSON.parse(raw);
|
|
713
|
+
const orphaned: string[] = [];
|
|
714
|
+
|
|
715
|
+
for (const key of keys) {
|
|
716
|
+
// Key format: "unitType/unitId" e.g. "execute-task/M001/S01/T01"
|
|
717
|
+
const slashIdx = key.indexOf("/");
|
|
718
|
+
if (slashIdx === -1) continue;
|
|
719
|
+
const unitType = key.slice(0, slashIdx);
|
|
720
|
+
const unitId = key.slice(slashIdx + 1);
|
|
721
|
+
|
|
722
|
+
// Only validate artifact-producing unit types
|
|
723
|
+
const { verifyExpectedArtifact } = await import("./auto-recovery.js");
|
|
724
|
+
if (!verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
725
|
+
orphaned.push(key);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (orphaned.length > 0) {
|
|
730
|
+
issues.push({
|
|
731
|
+
severity: "warning",
|
|
732
|
+
code: "orphaned_completed_units",
|
|
733
|
+
scope: "project",
|
|
734
|
+
unitId: "project",
|
|
735
|
+
message: `${orphaned.length} completed-unit key(s) reference missing artifacts: ${orphaned.slice(0, 3).join(", ")}${orphaned.length > 3 ? "..." : ""}`,
|
|
736
|
+
file: ".gsd/completed-units.json",
|
|
737
|
+
fixable: true,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
if (shouldFix("orphaned_completed_units")) {
|
|
741
|
+
const { removePersistedKey } = await import("./auto-recovery.js");
|
|
742
|
+
for (const key of orphaned) {
|
|
743
|
+
removePersistedKey(basePath, key);
|
|
744
|
+
}
|
|
745
|
+
fixesApplied.push(`removed ${orphaned.length} orphaned completed-unit key(s)`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
// Non-fatal — completed-units check failed
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ── Stale hook state ──────────────────────────────────────────────────
|
|
754
|
+
try {
|
|
755
|
+
const hookStateFile = join(root, "hook-state.json");
|
|
756
|
+
if (existsSync(hookStateFile)) {
|
|
757
|
+
const raw = readFileSync(hookStateFile, "utf-8");
|
|
758
|
+
const state = JSON.parse(raw);
|
|
759
|
+
const hasCycleCounts = state.cycleCounts && typeof state.cycleCounts === "object"
|
|
760
|
+
&& Object.keys(state.cycleCounts).length > 0;
|
|
761
|
+
|
|
762
|
+
// Only flag if there are actual cycle counts AND no auto-mode is running
|
|
763
|
+
if (hasCycleCounts) {
|
|
764
|
+
const lock = readCrashLock(basePath);
|
|
765
|
+
const autoRunning = lock ? isLockProcessAlive(lock) : false;
|
|
766
|
+
|
|
767
|
+
if (!autoRunning) {
|
|
768
|
+
issues.push({
|
|
769
|
+
severity: "info",
|
|
770
|
+
code: "stale_hook_state",
|
|
771
|
+
scope: "project",
|
|
772
|
+
unitId: "project",
|
|
773
|
+
message: `hook-state.json has ${Object.keys(state.cycleCounts).length} residual cycle count(s) from a previous session`,
|
|
774
|
+
file: ".gsd/hook-state.json",
|
|
775
|
+
fixable: true,
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
if (shouldFix("stale_hook_state")) {
|
|
779
|
+
const { clearPersistedHookState } = await import("./post-unit-hooks.js");
|
|
780
|
+
clearPersistedHookState(basePath);
|
|
781
|
+
fixesApplied.push("cleared stale hook-state.json");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
} catch {
|
|
787
|
+
// Non-fatal — hook state check failed
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ── Activity log bloat ────────────────────────────────────────────────
|
|
791
|
+
try {
|
|
792
|
+
const activityDir = join(root, "activity");
|
|
793
|
+
if (existsSync(activityDir)) {
|
|
794
|
+
const files = readdirSync(activityDir);
|
|
795
|
+
let totalSize = 0;
|
|
796
|
+
for (const f of files) {
|
|
797
|
+
try {
|
|
798
|
+
totalSize += statSync(join(activityDir, f)).size;
|
|
799
|
+
} catch {
|
|
800
|
+
// stat failed — skip
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const totalMB = totalSize / (1024 * 1024);
|
|
805
|
+
const BLOAT_FILE_THRESHOLD = 500;
|
|
806
|
+
const BLOAT_SIZE_MB = 100;
|
|
807
|
+
|
|
808
|
+
if (files.length > BLOAT_FILE_THRESHOLD || totalMB > BLOAT_SIZE_MB) {
|
|
809
|
+
issues.push({
|
|
810
|
+
severity: "warning",
|
|
811
|
+
code: "activity_log_bloat",
|
|
812
|
+
scope: "project",
|
|
813
|
+
unitId: "project",
|
|
814
|
+
message: `Activity logs: ${files.length} files, ${totalMB.toFixed(1)}MB (thresholds: ${BLOAT_FILE_THRESHOLD} files / ${BLOAT_SIZE_MB}MB)`,
|
|
815
|
+
file: ".gsd/activity/",
|
|
816
|
+
fixable: true,
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
if (shouldFix("activity_log_bloat")) {
|
|
820
|
+
const { pruneActivityLogs } = await import("./activity-log.js");
|
|
821
|
+
pruneActivityLogs(activityDir, 7); // 7-day retention
|
|
822
|
+
fixesApplied.push("pruned activity logs (7-day retention)");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
} catch {
|
|
827
|
+
// Non-fatal — activity log check failed
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ── STATE.md health ───────────────────────────────────────────────────
|
|
831
|
+
try {
|
|
832
|
+
const stateFilePath = resolveGsdRootFile(basePath, "STATE");
|
|
833
|
+
const milestonesPath = milestonesDir(basePath);
|
|
834
|
+
|
|
835
|
+
if (existsSync(milestonesPath)) {
|
|
836
|
+
if (!existsSync(stateFilePath)) {
|
|
837
|
+
issues.push({
|
|
838
|
+
severity: "warning",
|
|
839
|
+
code: "state_file_missing",
|
|
840
|
+
scope: "project",
|
|
841
|
+
unitId: "project",
|
|
842
|
+
message: "STATE.md is missing — state display will not work",
|
|
843
|
+
file: ".gsd/STATE.md",
|
|
844
|
+
fixable: true,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
if (shouldFix("state_file_missing")) {
|
|
848
|
+
const state = await deriveState(basePath);
|
|
849
|
+
await saveFile(stateFilePath, buildStateMarkdown(state));
|
|
850
|
+
fixesApplied.push("created STATE.md from derived state");
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
// Check if STATE.md is stale by comparing active milestone/slice/phase
|
|
854
|
+
const currentContent = readFileSync(stateFilePath, "utf-8");
|
|
855
|
+
const state = await deriveState(basePath);
|
|
856
|
+
const freshContent = buildStateMarkdown(state);
|
|
857
|
+
|
|
858
|
+
// Extract key fields for comparison — don't compare full content
|
|
859
|
+
// since timestamp/formatting differences are normal
|
|
860
|
+
const extractFields = (content: string) => {
|
|
861
|
+
const milestone = content.match(/\*\*Active Milestone:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
862
|
+
const slice = content.match(/\*\*Active Slice:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
863
|
+
const phase = content.match(/\*\*Phase:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
864
|
+
return { milestone, slice, phase };
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const current = extractFields(currentContent);
|
|
868
|
+
const fresh = extractFields(freshContent);
|
|
869
|
+
|
|
870
|
+
if (current.milestone !== fresh.milestone || current.slice !== fresh.slice || current.phase !== fresh.phase) {
|
|
871
|
+
issues.push({
|
|
872
|
+
severity: "warning",
|
|
873
|
+
code: "state_file_stale",
|
|
874
|
+
scope: "project",
|
|
875
|
+
unitId: "project",
|
|
876
|
+
message: `STATE.md is stale — shows "${current.phase}" but derived state is "${fresh.phase}"`,
|
|
877
|
+
file: ".gsd/STATE.md",
|
|
878
|
+
fixable: true,
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
if (shouldFix("state_file_stale")) {
|
|
882
|
+
await saveFile(stateFilePath, freshContent);
|
|
883
|
+
fixesApplied.push("rebuilt STATE.md from derived state");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
// Non-fatal — STATE.md check failed
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ── Gitignore drift ───────────────────────────────────────────────────
|
|
893
|
+
try {
|
|
894
|
+
const gitignorePath = join(basePath, ".gitignore");
|
|
895
|
+
if (existsSync(gitignorePath) && nativeIsRepo(basePath)) {
|
|
896
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
897
|
+
const existingLines = new Set(
|
|
898
|
+
content.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#")),
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
// Check for critical runtime patterns that must be present
|
|
902
|
+
const criticalPatterns = [
|
|
903
|
+
".gsd/activity/",
|
|
904
|
+
".gsd/runtime/",
|
|
905
|
+
".gsd/auto.lock",
|
|
906
|
+
".gsd/gsd.db",
|
|
907
|
+
".gsd/completed-units.json",
|
|
908
|
+
];
|
|
909
|
+
|
|
910
|
+
// If blanket .gsd/ or .gsd is present, all patterns are covered
|
|
911
|
+
const hasBlanketIgnore = existingLines.has(".gsd/") || existingLines.has(".gsd");
|
|
912
|
+
|
|
913
|
+
if (!hasBlanketIgnore) {
|
|
914
|
+
const missing = criticalPatterns.filter(p => !existingLines.has(p));
|
|
915
|
+
if (missing.length > 0) {
|
|
916
|
+
issues.push({
|
|
917
|
+
severity: "warning",
|
|
918
|
+
code: "gitignore_missing_patterns",
|
|
919
|
+
scope: "project",
|
|
920
|
+
unitId: "project",
|
|
921
|
+
message: `${missing.length} critical GSD runtime pattern(s) missing from .gitignore: ${missing.join(", ")}`,
|
|
922
|
+
file: ".gitignore",
|
|
923
|
+
fixable: true,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
if (shouldFix("gitignore_missing_patterns")) {
|
|
927
|
+
ensureGitignore(basePath);
|
|
928
|
+
fixesApplied.push("added missing GSD runtime patterns to .gitignore");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
// Non-fatal — gitignore check failed
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
660
938
|
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; scope?: string; fixLevel?: "task" | "all" }): Promise<DoctorReport> {
|
|
661
939
|
const issues: DoctorIssue[] = [];
|
|
662
940
|
const fixesApplied: string[] = [];
|
|
@@ -700,6 +978,9 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
700
978
|
// Git health checks (orphaned worktrees, stale branches, corrupt merge state, tracked runtime files)
|
|
701
979
|
await checkGitHealth(basePath, issues, fixesApplied, shouldFix);
|
|
702
980
|
|
|
981
|
+
// Runtime health checks (crash locks, completed-units, hook state, activity logs, STATE.md, gitignore)
|
|
982
|
+
await checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix);
|
|
983
|
+
|
|
703
984
|
const milestonesPath = milestonesDir(basePath);
|
|
704
985
|
if (!existsSync(milestonesPath)) {
|
|
705
986
|
return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
|
|
@@ -1,18 +1,97 @@
|
|
|
1
1
|
// GSD Extension — Session/Milestone Export
|
|
2
2
|
// Generate shareable reports of milestone work in JSON or markdown format.
|
|
3
|
-
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
4
3
|
|
|
5
4
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
6
5
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
7
6
|
import { join, basename } from "node:path";
|
|
8
7
|
import {
|
|
9
8
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
10
|
-
aggregateByModel, formatCost, formatTokenCount,
|
|
9
|
+
aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
|
|
11
10
|
} from "./metrics.js";
|
|
12
11
|
import type { UnitMetrics } from "./metrics.js";
|
|
13
12
|
import { gsdRoot } from "./paths.js";
|
|
14
13
|
import { formatDuration } from "./history.js";
|
|
15
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Write an export file directly, without requiring an ExtensionCommandContext.
|
|
17
|
+
* Used by the visualizer overlay export tab.
|
|
18
|
+
* Returns the output file path, or null on failure.
|
|
19
|
+
*/
|
|
20
|
+
export function writeExportFile(
|
|
21
|
+
basePath: string,
|
|
22
|
+
format: "markdown" | "json",
|
|
23
|
+
visualizerData?: { totals: any; byPhase: any[]; bySlice: any[]; byModel: any[]; units: any[]; criticalPath?: any; remainingSliceCount?: number },
|
|
24
|
+
): string | null {
|
|
25
|
+
const ledger = getLedger();
|
|
26
|
+
let units: UnitMetrics[];
|
|
27
|
+
|
|
28
|
+
if (visualizerData && visualizerData.units.length > 0) {
|
|
29
|
+
units = visualizerData.units;
|
|
30
|
+
} else if (ledger && ledger.units.length > 0) {
|
|
31
|
+
units = ledger.units;
|
|
32
|
+
} else {
|
|
33
|
+
const diskLedger = loadLedgerFromDisk(basePath);
|
|
34
|
+
if (!diskLedger || diskLedger.units.length === 0) return null;
|
|
35
|
+
units = diskLedger.units;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectName = basename(basePath);
|
|
39
|
+
const exportDir = gsdRoot(basePath);
|
|
40
|
+
mkdirSync(exportDir, { recursive: true });
|
|
41
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
42
|
+
|
|
43
|
+
if (format === "json") {
|
|
44
|
+
const report = {
|
|
45
|
+
exportedAt: new Date().toISOString(),
|
|
46
|
+
project: projectName,
|
|
47
|
+
totals: visualizerData?.totals ?? getProjectTotals(units),
|
|
48
|
+
byPhase: visualizerData?.byPhase ?? aggregateByPhase(units),
|
|
49
|
+
bySlice: visualizerData?.bySlice ?? aggregateBySlice(units),
|
|
50
|
+
byModel: visualizerData?.byModel ?? aggregateByModel(units),
|
|
51
|
+
units,
|
|
52
|
+
};
|
|
53
|
+
const outPath = join(exportDir, `export-${timestamp}.json`);
|
|
54
|
+
writeFileSync(outPath, JSON.stringify(report, null, 2) + "\n", "utf-8");
|
|
55
|
+
return outPath;
|
|
56
|
+
} else {
|
|
57
|
+
const totals = visualizerData?.totals ?? getProjectTotals(units);
|
|
58
|
+
const phases = visualizerData?.byPhase ?? aggregateByPhase(units);
|
|
59
|
+
const slices = visualizerData?.bySlice ?? aggregateBySlice(units);
|
|
60
|
+
|
|
61
|
+
const md = [
|
|
62
|
+
`# GSD Session Report — ${projectName}`,
|
|
63
|
+
``,
|
|
64
|
+
`**Generated**: ${new Date().toISOString()}`,
|
|
65
|
+
`**Units completed**: ${totals.units}`,
|
|
66
|
+
`**Total cost**: ${formatCost(totals.cost)}`,
|
|
67
|
+
`**Total tokens**: ${formatTokenCount(totals.tokens.total)}`,
|
|
68
|
+
`**Total duration**: ${formatDuration(totals.duration)}`,
|
|
69
|
+
`**Tool calls**: ${totals.toolCalls}`,
|
|
70
|
+
``,
|
|
71
|
+
`## Cost by Phase`,
|
|
72
|
+
``,
|
|
73
|
+
`| Phase | Units | Cost | Tokens | Duration |`,
|
|
74
|
+
`|-------|-------|------|--------|----------|`,
|
|
75
|
+
...phases.map((p: any) =>
|
|
76
|
+
`| ${p.phase} | ${p.units} | ${formatCost(p.cost)} | ${formatTokenCount(p.tokens.total)} | ${formatDuration(p.duration)} |`,
|
|
77
|
+
),
|
|
78
|
+
``,
|
|
79
|
+
`## Cost by Slice`,
|
|
80
|
+
``,
|
|
81
|
+
`| Slice | Units | Cost | Tokens | Duration |`,
|
|
82
|
+
`|-------|-------|------|--------|----------|`,
|
|
83
|
+
...slices.map((s: any) =>
|
|
84
|
+
`| ${s.sliceId} | ${s.units} | ${formatCost(s.cost)} | ${formatTokenCount(s.tokens.total)} | ${formatDuration(s.duration)} |`,
|
|
85
|
+
),
|
|
86
|
+
``,
|
|
87
|
+
].join("\n");
|
|
88
|
+
|
|
89
|
+
const outPath = join(exportDir, `export-${timestamp}.md`);
|
|
90
|
+
writeFileSync(outPath, md, "utf-8");
|
|
91
|
+
return outPath;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
16
95
|
/**
|
|
17
96
|
* Export session/milestone data to JSON or markdown.
|
|
18
97
|
*/
|