gsd-pi 2.35.0-dev.55dcc60 → 2.35.0-dev.6179610
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 +3 -1
- package/dist/cli.js +7 -2
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +13 -1
- package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
- package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
- package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
- package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
- package/dist/resources/extensions/bg-shell/types.js +0 -2
- package/dist/resources/extensions/context7/index.js +5 -0
- package/dist/resources/extensions/get-secrets-from-user.js +2 -30
- package/dist/resources/extensions/google-search/index.js +5 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
- package/dist/resources/extensions/gsd/auto-loop.js +10 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
- package/dist/resources/extensions/gsd/auto-start.js +35 -2
- package/dist/resources/extensions/gsd/auto.js +59 -4
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/commands-rate.js +31 -0
- package/dist/resources/extensions/gsd/commands.js +6 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
- package/dist/resources/extensions/gsd/files.js +11 -2
- package/dist/resources/extensions/gsd/gitignore.js +54 -7
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
- package/dist/resources/extensions/gsd/health-widget.js +97 -46
- package/dist/resources/extensions/gsd/index.js +26 -33
- package/dist/resources/extensions/gsd/migrate-external.js +55 -2
- package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
- package/dist/resources/extensions/gsd/paths.js +74 -7
- package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +16 -1
- package/dist/resources/extensions/gsd/preferences.js +12 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
- package/dist/resources/extensions/gsd/session-lock.js +53 -2
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/templates/plan.md +8 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
- package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +30 -0
- package/dist/resources/extensions/subagent/index.js +6 -14
- package/package.json +2 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
- package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
- package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
- package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
- package/src/resources/extensions/bg-shell/types.ts +0 -12
- package/src/resources/extensions/context7/index.ts +7 -0
- package/src/resources/extensions/get-secrets-from-user.ts +2 -35
- package/src/resources/extensions/google-search/index.ts +7 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
- package/src/resources/extensions/gsd/auto-loop.ts +11 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
- package/src/resources/extensions/gsd/auto-start.ts +42 -2
- package/src/resources/extensions/gsd/auto.ts +61 -3
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/commands-rate.ts +55 -0
- package/src/resources/extensions/gsd/commands.ts +7 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
- package/src/resources/extensions/gsd/files.ts +12 -2
- package/src/resources/extensions/gsd/gitignore.ts +54 -7
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
- package/src/resources/extensions/gsd/health-widget.ts +103 -59
- package/src/resources/extensions/gsd/index.ts +30 -33
- package/src/resources/extensions/gsd/migrate-external.ts +47 -2
- package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
- package/src/resources/extensions/gsd/paths.ts +73 -7
- package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +16 -1
- package/src/resources/extensions/gsd/preferences.ts +14 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
- package/src/resources/extensions/gsd/session-lock.ts +59 -2
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/templates/plan.md +8 -0
- package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +36 -0
- package/src/resources/extensions/subagent/index.ts +6 -12
- package/dist/resources/extensions/shared/wizard-ui.js +0 -478
- package/src/resources/extensions/shared/wizard-ui.ts +0 -551
|
@@ -148,27 +148,36 @@ function checkPortConflicts(basePath) {
|
|
|
148
148
|
// Try to detect ports from package.json scripts
|
|
149
149
|
const portsToCheck = new Set();
|
|
150
150
|
const pkgPath = join(basePath, "package.json");
|
|
151
|
-
if (existsSync(pkgPath)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
if (!existsSync(pkgPath)) {
|
|
152
|
+
// No package.json — this isn't a Node.js project. Skip port checks
|
|
153
|
+
// entirely to avoid false positives from system services (e.g., macOS
|
|
154
|
+
// AirPlay Receiver on port 5000). (#1381)
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
159
|
+
const scripts = pkg.scripts ?? {};
|
|
160
|
+
const scriptText = Object.values(scripts).join(" ");
|
|
161
|
+
// Look for --port NNNN, -p NNNN, PORT=NNNN, :NNNN patterns
|
|
162
|
+
const portMatches = scriptText.matchAll(/(?:--port\s+|(?:^|[^a-z])PORT[=:]\s*|-p\s+|:)(\d{4,5})\b/gi);
|
|
163
|
+
for (const m of portMatches) {
|
|
164
|
+
const port = parseInt(m[1], 10);
|
|
165
|
+
if (port >= 1024 && port <= 65535)
|
|
166
|
+
portsToCheck.add(port);
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
+
catch {
|
|
170
|
+
// parse failed — skip port checks rather than using defaults
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
// If no ports found in scripts, check common defaults.
|
|
174
|
+
// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
|
|
169
175
|
if (portsToCheck.size === 0) {
|
|
170
|
-
for (const p of DEFAULT_DEV_PORTS)
|
|
176
|
+
for (const p of DEFAULT_DEV_PORTS) {
|
|
177
|
+
if (p === 5000 && process.platform === "darwin")
|
|
178
|
+
continue;
|
|
171
179
|
portsToCheck.add(p);
|
|
180
|
+
}
|
|
172
181
|
}
|
|
173
182
|
for (const port of portsToCheck) {
|
|
174
183
|
const result = tryExec(`lsof -i :${port} -sTCP:LISTEN -t`, basePath);
|
|
@@ -509,7 +509,8 @@ export async function loadFile(path) {
|
|
|
509
509
|
return await fs.readFile(path, 'utf-8');
|
|
510
510
|
}
|
|
511
511
|
catch (err) {
|
|
512
|
-
|
|
512
|
+
const code = err.code;
|
|
513
|
+
if (code === 'ENOENT' || code === 'EISDIR')
|
|
513
514
|
return null;
|
|
514
515
|
throw err;
|
|
515
516
|
}
|
|
@@ -704,7 +705,7 @@ export async function inlinePriorMilestoneSummary(mid, base) {
|
|
|
704
705
|
* Returns `null` when no manifest file exists (path resolution failure or
|
|
705
706
|
* file not on disk) - callers can distinguish "no manifest" from "empty manifest".
|
|
706
707
|
*/
|
|
707
|
-
export async function getManifestStatus(base, milestoneId) {
|
|
708
|
+
export async function getManifestStatus(base, milestoneId, projectRoot) {
|
|
708
709
|
const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
|
|
709
710
|
if (!resolvedPath)
|
|
710
711
|
return null;
|
|
@@ -713,8 +714,16 @@ export async function getManifestStatus(base, milestoneId) {
|
|
|
713
714
|
return null;
|
|
714
715
|
const manifest = parseSecretsManifest(content);
|
|
715
716
|
const keys = manifest.entries.map(e => e.key);
|
|
717
|
+
// Check both the base path .env AND the project root .env (#1387).
|
|
718
|
+
// In worktree mode, base is the worktree path which may not have .env.
|
|
719
|
+
// The project root's .env is where the user actually defined their keys.
|
|
716
720
|
const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
|
|
717
721
|
const existingSet = new Set(existingKeys);
|
|
722
|
+
if (projectRoot && projectRoot !== base) {
|
|
723
|
+
const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env'));
|
|
724
|
+
for (const k of rootKeys)
|
|
725
|
+
existingSet.add(k);
|
|
726
|
+
}
|
|
718
727
|
const result = {
|
|
719
728
|
pending: [],
|
|
720
729
|
collected: [],
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Both idempotent — non-destructive if already present.
|
|
7
7
|
*/
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { nativeRmCached } from "./native-git-bridge.js";
|
|
9
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { nativeRmCached, nativeLsFiles } from "./native-git-bridge.js";
|
|
11
11
|
import { gsdRoot } from "./paths.js";
|
|
12
12
|
/**
|
|
13
13
|
* GSD runtime patterns for git index cleanup.
|
|
@@ -67,12 +67,48 @@ const BASELINE_PATTERNS = [
|
|
|
67
67
|
"tmp/",
|
|
68
68
|
];
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* Check whether `.gsd/` contains files tracked by git.
|
|
71
|
+
* If so, the project intentionally keeps `.gsd/` in version control
|
|
72
|
+
* and we must NOT add `.gsd` to `.gitignore` or attempt migration.
|
|
73
|
+
*
|
|
74
|
+
* Returns true if git tracks at least one file under `.gsd/`.
|
|
75
|
+
* Returns false (safe to ignore) if:
|
|
76
|
+
* - Not a git repo
|
|
77
|
+
* - `.gsd/` is a symlink (external state, should be ignored)
|
|
78
|
+
* - `.gsd/` doesn't exist
|
|
79
|
+
* - No tracked files found under `.gsd/`
|
|
80
|
+
*/
|
|
81
|
+
export function hasGitTrackedGsdFiles(basePath) {
|
|
82
|
+
const localGsd = join(basePath, ".gsd");
|
|
83
|
+
// If .gsd doesn't exist or is already a symlink, no tracked files concern
|
|
84
|
+
if (!existsSync(localGsd))
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
if (lstatSync(localGsd).isSymbolicLink())
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Check if git tracks any files under .gsd/
|
|
94
|
+
try {
|
|
95
|
+
const tracked = nativeLsFiles(basePath, ".gsd");
|
|
96
|
+
return tracked.length > 0;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Not a git repo or git not available — safe to proceed
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Ensure basePath/.gitignore contains baseline ignore patterns.
|
|
105
|
+
* Creates the file if missing; appends missing patterns.
|
|
72
106
|
* Returns true if the file was created or modified, false if already complete.
|
|
73
107
|
*
|
|
74
|
-
* `.gsd/`
|
|
75
|
-
*
|
|
108
|
+
* **Safety check:** If `.gsd/` contains git-tracked files (i.e., the project
|
|
109
|
+
* intentionally keeps `.gsd/` in version control), the `.gsd` ignore pattern
|
|
110
|
+
* is excluded to prevent data loss. Only the `.gsd` pattern is affected —
|
|
111
|
+
* all other baseline patterns are still applied normally.
|
|
76
112
|
*/
|
|
77
113
|
export function ensureGitignore(basePath, options) {
|
|
78
114
|
// If manage_gitignore is explicitly false, do not touch .gitignore at all
|
|
@@ -88,8 +124,14 @@ export function ensureGitignore(basePath, options) {
|
|
|
88
124
|
.split("\n")
|
|
89
125
|
.map((l) => l.trim())
|
|
90
126
|
.filter((l) => l && !l.startsWith("#")));
|
|
127
|
+
// Determine which patterns to apply. If .gsd/ has tracked files,
|
|
128
|
+
// exclude the ".gsd" pattern to prevent deleting tracked state.
|
|
129
|
+
const gsdIsTracked = hasGitTrackedGsdFiles(basePath);
|
|
130
|
+
const patternsToApply = gsdIsTracked
|
|
131
|
+
? BASELINE_PATTERNS.filter((p) => p !== ".gsd")
|
|
132
|
+
: BASELINE_PATTERNS;
|
|
91
133
|
// Find patterns not yet present
|
|
92
|
-
const missing =
|
|
134
|
+
const missing = patternsToApply.filter((p) => !existingLines.has(p));
|
|
93
135
|
if (missing.length === 0)
|
|
94
136
|
return false;
|
|
95
137
|
// Build the block to append
|
|
@@ -111,6 +153,11 @@ export function ensureGitignore(basePath, options) {
|
|
|
111
153
|
* already in the index even after .gitignore is updated.
|
|
112
154
|
*
|
|
113
155
|
* Only removes from the index (`--cached`), never from disk. Idempotent.
|
|
156
|
+
*
|
|
157
|
+
* Note: These are strictly runtime/ephemeral paths (activity logs, lock files,
|
|
158
|
+
* metrics, STATE.md). They are always safe to untrack, even when the project
|
|
159
|
+
* intentionally keeps other `.gsd/` files (like PROJECT.md, milestones/) in
|
|
160
|
+
* version control.
|
|
114
161
|
*/
|
|
115
162
|
export function untrackRuntimeFiles(basePath) {
|
|
116
163
|
const runtimePaths = GSD_RUNTIME_PATTERNS;
|
|
@@ -154,7 +154,7 @@ function parseMilestoneSequenceFromProject(content) {
|
|
|
154
154
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
155
155
|
*/
|
|
156
156
|
function dispatchWorkflow(pi, note, customType = "gsd-run") {
|
|
157
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
157
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
158
158
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
159
159
|
pi.sendMessage({
|
|
160
160
|
customType,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure GSD health widget logic.
|
|
3
|
+
*
|
|
4
|
+
* Separates project-state detection and line rendering from the widget's
|
|
5
|
+
* runtime integrations so the regressions can be tested directly.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
8
|
+
import { gsdRoot } from "./paths.js";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
export function detectHealthWidgetProjectState(basePath) {
|
|
11
|
+
const root = gsdRoot(basePath);
|
|
12
|
+
if (!existsSync(root))
|
|
13
|
+
return "none";
|
|
14
|
+
// Lightweight milestone count — avoids the full detectProjectState() scan
|
|
15
|
+
// (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
|
|
16
|
+
try {
|
|
17
|
+
const milestonesDir = join(root, "milestones");
|
|
18
|
+
if (existsSync(milestonesDir)) {
|
|
19
|
+
const entries = readdirSync(milestonesDir, { withFileTypes: true });
|
|
20
|
+
if (entries.some(e => e.isDirectory()))
|
|
21
|
+
return "active";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch { /* non-fatal */ }
|
|
25
|
+
return "initialized";
|
|
26
|
+
}
|
|
27
|
+
function formatCost(n) {
|
|
28
|
+
return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
|
|
29
|
+
}
|
|
30
|
+
function formatProgress(progress) {
|
|
31
|
+
if (!progress)
|
|
32
|
+
return null;
|
|
33
|
+
const parts = [];
|
|
34
|
+
parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
|
|
35
|
+
if (progress.slices)
|
|
36
|
+
parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
|
|
37
|
+
if (progress.tasks)
|
|
38
|
+
parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
|
|
39
|
+
return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
|
|
40
|
+
}
|
|
41
|
+
function formatEnvironmentSummary(errorCount, warningCount) {
|
|
42
|
+
if (errorCount <= 0 && warningCount <= 0)
|
|
43
|
+
return null;
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (errorCount > 0)
|
|
46
|
+
parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
47
|
+
if (warningCount > 0)
|
|
48
|
+
parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
49
|
+
return `Env: ${parts.join(", ")}`;
|
|
50
|
+
}
|
|
51
|
+
function formatBudgetSummary(data) {
|
|
52
|
+
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
53
|
+
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
54
|
+
return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
|
|
55
|
+
}
|
|
56
|
+
if (data.budgetSpent > 0) {
|
|
57
|
+
return `Spent: ${formatCost(data.budgetSpent)}`;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function buildExecutionHeadline(data) {
|
|
62
|
+
const status = data.executionStatus ?? "Active project";
|
|
63
|
+
const target = data.executionTarget ?? data.blocker ?? "loading status…";
|
|
64
|
+
return ` GSD ${status}${target ? ` - ${target}` : ""}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build compact health lines for the widget.
|
|
68
|
+
* Returns a string array suitable for setWidget().
|
|
69
|
+
*/
|
|
70
|
+
export function buildHealthLines(data) {
|
|
71
|
+
if (data.projectState === "none") {
|
|
72
|
+
return [" GSD No project loaded — run /gsd to start"];
|
|
73
|
+
}
|
|
74
|
+
if (data.projectState === "initialized") {
|
|
75
|
+
return [" GSD Project initialized — run /gsd to continue setup"];
|
|
76
|
+
}
|
|
77
|
+
const lines = [buildExecutionHeadline(data)];
|
|
78
|
+
const details = [];
|
|
79
|
+
const progress = formatProgress(data.progress);
|
|
80
|
+
if (progress)
|
|
81
|
+
details.push(progress);
|
|
82
|
+
if (data.providerIssue)
|
|
83
|
+
details.push(data.providerIssue);
|
|
84
|
+
const environment = formatEnvironmentSummary(data.environmentErrorCount, data.environmentWarningCount);
|
|
85
|
+
if (environment)
|
|
86
|
+
details.push(environment);
|
|
87
|
+
const budget = formatBudgetSummary(data);
|
|
88
|
+
if (budget)
|
|
89
|
+
details.push(budget);
|
|
90
|
+
if (data.eta)
|
|
91
|
+
details.push(data.eta);
|
|
92
|
+
if (details.length > 0) {
|
|
93
|
+
lines.push(` ${details.join(" │ ")}`);
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
@@ -11,21 +11,23 @@ import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.j
|
|
|
11
11
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
12
12
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
13
13
|
import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
|
|
14
|
+
import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
|
|
14
15
|
import { projectRoot } from "./commands.js";
|
|
16
|
+
import { deriveState, invalidateStateCache } from "./state.js";
|
|
17
|
+
import { buildHealthLines, detectHealthWidgetProjectState, } from "./health-widget-core.js";
|
|
15
18
|
// ── Data loader ────────────────────────────────────────────────────────────────
|
|
16
|
-
function
|
|
17
|
-
let hasProject = false;
|
|
19
|
+
function loadBaseHealthWidgetData(basePath) {
|
|
18
20
|
let budgetCeiling;
|
|
19
21
|
let budgetSpent = 0;
|
|
20
22
|
let providerIssue = null;
|
|
21
23
|
let environmentErrorCount = 0;
|
|
22
24
|
let environmentWarningCount = 0;
|
|
25
|
+
const projectState = detectHealthWidgetProjectState(basePath);
|
|
23
26
|
try {
|
|
24
27
|
const prefs = loadEffectiveGSDPreferences();
|
|
25
28
|
budgetCeiling = prefs?.preferences?.budget_ceiling;
|
|
26
29
|
const ledger = loadLedgerFromDisk(basePath);
|
|
27
30
|
if (ledger) {
|
|
28
|
-
hasProject = true;
|
|
29
31
|
const totals = getProjectTotals(ledger.units ?? []);
|
|
30
32
|
budgetSpent = totals.cost;
|
|
31
33
|
}
|
|
@@ -47,7 +49,7 @@ function loadHealthWidgetData(basePath) {
|
|
|
47
49
|
}
|
|
48
50
|
catch { /* non-fatal */ }
|
|
49
51
|
return {
|
|
50
|
-
|
|
52
|
+
projectState,
|
|
51
53
|
budgetCeiling,
|
|
52
54
|
budgetSpent,
|
|
53
55
|
providerIssue,
|
|
@@ -56,50 +58,85 @@ function loadHealthWidgetData(basePath) {
|
|
|
56
58
|
lastRefreshed: Date.now(),
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
function compactText(text, max = 64) {
|
|
62
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
63
|
+
if (trimmed.length <= max)
|
|
64
|
+
return trimmed;
|
|
65
|
+
return `${trimmed.slice(0, max - 1).trimEnd()}…`;
|
|
62
66
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
else if (data.environmentErrorCount > 0 || data.providerIssue?.includes("✗")) {
|
|
78
|
-
parts.push(`✗ ${totalIssues} issue${totalIssues > 1 ? "s" : ""}`);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
parts.push(`⚠ ${totalIssues} warning${totalIssues > 1 ? "s" : ""}`);
|
|
67
|
+
function summarizeExecutionStatus(state) {
|
|
68
|
+
switch (state.phase) {
|
|
69
|
+
case "blocked": return "Blocked";
|
|
70
|
+
case "paused": return "Paused";
|
|
71
|
+
case "complete": return "Complete";
|
|
72
|
+
case "executing": return "Executing";
|
|
73
|
+
case "planning": return "Planning";
|
|
74
|
+
case "pre-planning": return "Pre-planning";
|
|
75
|
+
case "summarizing": return "Summarizing";
|
|
76
|
+
case "validating-milestone": return "Validating";
|
|
77
|
+
case "completing-milestone": return "Completing";
|
|
78
|
+
case "needs-discussion": return "Needs discussion";
|
|
79
|
+
case "replanning-slice": return "Replanning";
|
|
80
|
+
default: return "Active";
|
|
82
81
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
}
|
|
83
|
+
function summarizeExecutionTarget(state) {
|
|
84
|
+
switch (state.phase) {
|
|
85
|
+
case "needs-discussion":
|
|
86
|
+
return state.activeMilestone ? `Discuss ${state.activeMilestone.id}` : "Discuss milestone draft";
|
|
87
|
+
case "pre-planning":
|
|
88
|
+
return state.activeMilestone ? `Plan ${state.activeMilestone.id}` : "Research & plan milestone";
|
|
89
|
+
case "planning":
|
|
90
|
+
return state.activeSlice ? `Plan ${state.activeSlice.id}` : "Plan next slice";
|
|
91
|
+
case "executing":
|
|
92
|
+
return state.activeTask ? `Execute ${state.activeTask.id}` : "Execute next task";
|
|
93
|
+
case "summarizing":
|
|
94
|
+
return state.activeSlice ? `Complete ${state.activeSlice.id}` : "Complete current slice";
|
|
95
|
+
case "validating-milestone":
|
|
96
|
+
return state.activeMilestone ? `Validate ${state.activeMilestone.id}` : "Validate milestone";
|
|
97
|
+
case "completing-milestone":
|
|
98
|
+
return state.activeMilestone ? `Complete ${state.activeMilestone.id}` : "Complete milestone";
|
|
99
|
+
case "replanning-slice":
|
|
100
|
+
return state.activeSlice ? `Replan ${state.activeSlice.id}` : "Replan current slice";
|
|
101
|
+
case "blocked":
|
|
102
|
+
return `waiting on ${compactText(state.blockers[0] ?? state.nextAction, 56)}`;
|
|
103
|
+
case "paused":
|
|
104
|
+
return compactText(state.nextAction || "waiting to resume", 56);
|
|
105
|
+
case "complete":
|
|
106
|
+
return "All milestones complete";
|
|
107
|
+
default:
|
|
108
|
+
return compactText(describeNextUnit(state).label, 56);
|
|
94
109
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
}
|
|
111
|
+
async function enrichHealthWidgetData(basePath, baseData) {
|
|
112
|
+
if (baseData.projectState !== "active")
|
|
113
|
+
return baseData;
|
|
114
|
+
try {
|
|
115
|
+
invalidateStateCache();
|
|
116
|
+
const state = await deriveState(basePath);
|
|
117
|
+
if (state.activeMilestone) {
|
|
118
|
+
// Warm the slice-progress cache so estimateTimeRemaining() has data
|
|
119
|
+
updateSliceProgressCache(basePath, state.activeMilestone.id, state.activeSlice?.id);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
...baseData,
|
|
123
|
+
executionPhase: state.phase,
|
|
124
|
+
executionStatus: summarizeExecutionStatus(state),
|
|
125
|
+
executionTarget: summarizeExecutionTarget(state),
|
|
126
|
+
nextAction: state.nextAction,
|
|
127
|
+
blocker: state.blockers[0] ?? null,
|
|
128
|
+
activeMilestoneId: state.activeMilestone?.id,
|
|
129
|
+
activeSliceId: state.activeSlice?.id,
|
|
130
|
+
activeTaskId: state.activeTask?.id,
|
|
131
|
+
progress: state.progress,
|
|
132
|
+
eta: state.phase === "blocked" || state.phase === "paused" || state.phase === "complete"
|
|
133
|
+
? null
|
|
134
|
+
: estimateTimeRemaining(),
|
|
135
|
+
};
|
|
98
136
|
}
|
|
99
|
-
|
|
100
|
-
|
|
137
|
+
catch {
|
|
138
|
+
return baseData;
|
|
101
139
|
}
|
|
102
|
-
return [` ${parts.join(" │ ")}`];
|
|
103
140
|
}
|
|
104
141
|
// ── Widget init ────────────────────────────────────────────────────────────────
|
|
105
142
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
@@ -112,19 +149,33 @@ export function initHealthWidget(ctx) {
|
|
|
112
149
|
return;
|
|
113
150
|
const basePath = projectRoot();
|
|
114
151
|
// String-array fallback — used in RPC mode (factory is a no-op there)
|
|
115
|
-
const initialData =
|
|
152
|
+
const initialData = loadBaseHealthWidgetData(basePath);
|
|
116
153
|
ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
|
|
117
154
|
// Factory-based widget for TUI mode — replaces the string-array above
|
|
118
155
|
ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
|
|
119
156
|
let data = initialData;
|
|
120
157
|
let cachedLines;
|
|
121
|
-
|
|
158
|
+
let refreshInFlight = false;
|
|
159
|
+
const refresh = async () => {
|
|
160
|
+
if (refreshInFlight)
|
|
161
|
+
return;
|
|
162
|
+
refreshInFlight = true;
|
|
122
163
|
try {
|
|
123
|
-
|
|
164
|
+
const baseData = loadBaseHealthWidgetData(basePath);
|
|
165
|
+
data = await enrichHealthWidgetData(basePath, baseData);
|
|
124
166
|
cachedLines = undefined;
|
|
125
167
|
_tui.requestRender();
|
|
126
168
|
}
|
|
127
169
|
catch { /* non-fatal */ }
|
|
170
|
+
finally {
|
|
171
|
+
refreshInFlight = false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
// Fire first enrichment immediately. requestRender() inside is a no-op
|
|
175
|
+
// if the widget has not yet rendered, so this is safe before factory return.
|
|
176
|
+
void refresh();
|
|
177
|
+
const refreshTimer = setInterval(() => {
|
|
178
|
+
void refresh();
|
|
128
179
|
}, REFRESH_INTERVAL_MS);
|
|
129
180
|
return {
|
|
130
181
|
render(_width) {
|
|
@@ -46,33 +46,21 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
46
46
|
import { toPosixPath } from "../shared/mod.js";
|
|
47
47
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
48
48
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
49
|
-
// ── Agent Instructions
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
catch { /* non-fatal — skip unreadable file */ }
|
|
63
|
-
}
|
|
64
|
-
const projectPath = join(process.cwd(), ".gsd", "agent-instructions.md");
|
|
65
|
-
if (existsSync(projectPath)) {
|
|
66
|
-
try {
|
|
67
|
-
const content = readFileSync(projectPath, "utf-8").trim();
|
|
68
|
-
if (content)
|
|
69
|
-
parts.push(content);
|
|
49
|
+
// ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
|
|
50
|
+
// agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
|
|
51
|
+
// Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
|
|
52
|
+
function warnDeprecatedAgentInstructions() {
|
|
53
|
+
const paths = [
|
|
54
|
+
join(homedir(), ".gsd", "agent-instructions.md"),
|
|
55
|
+
join(process.cwd(), ".gsd", "agent-instructions.md"),
|
|
56
|
+
];
|
|
57
|
+
for (const p of paths) {
|
|
58
|
+
if (existsSync(p)) {
|
|
59
|
+
console.warn(`[GSD] DEPRECATED: ${p} is no longer loaded. ` +
|
|
60
|
+
`Migrate your instructions to AGENTS.md (or CLAUDE.md) in the same directory. ` +
|
|
61
|
+
`See https://github.com/gsd-build/GSD-2/issues/1492`);
|
|
70
62
|
}
|
|
71
|
-
catch { /* non-fatal — skip unreadable file */ }
|
|
72
63
|
}
|
|
73
|
-
if (parts.length === 0)
|
|
74
|
-
return null;
|
|
75
|
-
return parts.join("\n\n");
|
|
76
64
|
}
|
|
77
65
|
// ── Depth verification state ──────────────────────────────────────────────
|
|
78
66
|
let depthVerificationDone = false;
|
|
@@ -140,7 +128,16 @@ export default function (pi) {
|
|
|
140
128
|
// Pipe closed — nothing we can write; just exit cleanly
|
|
141
129
|
process.exit(0);
|
|
142
130
|
}
|
|
143
|
-
|
|
131
|
+
if (err.code === "ENOENT" &&
|
|
132
|
+
err.syscall?.startsWith("spawn")) {
|
|
133
|
+
// spawn ENOENT — command not found (e.g., npx on Windows).
|
|
134
|
+
// This surfaces as an uncaught exception from child_process but
|
|
135
|
+
// is not a fatal process error. Log and continue instead of
|
|
136
|
+
// crashing auto-mode (#1384).
|
|
137
|
+
process.stderr.write(`[gsd] spawn ENOENT: ${err.path ?? "unknown"} — command not found\n`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Re-throw anything that isn't EPIPE/ENOENT so real crashes still surface
|
|
144
141
|
throw err;
|
|
145
142
|
};
|
|
146
143
|
process.on("uncaughtException", _gsdEpipeGuard);
|
|
@@ -580,12 +577,8 @@ export default function (pi) {
|
|
|
580
577
|
newSkillsBlock = formatSkillsXml(newSkills);
|
|
581
578
|
}
|
|
582
579
|
}
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
const agentInstructions = loadAgentInstructions();
|
|
586
|
-
if (agentInstructions) {
|
|
587
|
-
agentInstructionsBlock = `\n\n## Agent Instructions\n\nThe following instructions were provided by the user and must be followed in every session:\n\n${agentInstructions}`;
|
|
588
|
-
}
|
|
580
|
+
// Warn if deprecated agent-instructions.md files are still present
|
|
581
|
+
warnDeprecatedAgentInstructions();
|
|
589
582
|
const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
|
|
590
583
|
// Worktree context — override the static CWD in the system prompt
|
|
591
584
|
let worktreeBlock = "";
|
|
@@ -628,7 +621,7 @@ export default function (pi) {
|
|
|
628
621
|
"Write every .gsd artifact in the worktree path above, never in the main project tree.",
|
|
629
622
|
].join("\n");
|
|
630
623
|
}
|
|
631
|
-
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${
|
|
624
|
+
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
|
|
632
625
|
stopContextTimer({
|
|
633
626
|
systemPromptSize: fullSystem.length,
|
|
634
627
|
injectionSize: injection?.length ?? 0,
|