gsd-pi 2.35.0-dev.30eec3f → 2.35.0-dev.640d5c7
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/cli.js +2 -7
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +1 -13
- package/dist/resources/extensions/async-jobs/await-tool.js +2 -0
- package/dist/resources/extensions/async-jobs/job-manager.js +6 -0
- package/dist/resources/extensions/bg-shell/output-formatter.js +19 -1
- package/dist/resources/extensions/bg-shell/process-manager.js +4 -0
- package/dist/resources/extensions/bg-shell/types.js +2 -0
- package/dist/resources/extensions/context7/index.js +0 -5
- package/dist/resources/extensions/get-secrets-from-user.js +30 -2
- package/dist/resources/extensions/google-search/index.js +0 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -43
- package/dist/resources/extensions/gsd/auto-loop.js +1 -10
- package/dist/resources/extensions/gsd/auto-recovery.js +0 -35
- package/dist/resources/extensions/gsd/auto-start.js +2 -35
- package/dist/resources/extensions/gsd/auto.js +4 -59
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/doctor-environment.js +17 -26
- package/dist/resources/extensions/gsd/files.js +1 -9
- package/dist/resources/extensions/gsd/gitignore.js +7 -54
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/health-widget.js +46 -97
- package/dist/resources/extensions/gsd/index.js +1 -10
- package/dist/resources/extensions/gsd/migrate-external.js +2 -55
- package/dist/resources/extensions/gsd/paths.js +7 -74
- package/dist/resources/extensions/gsd/post-unit-hooks.js +1 -4
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -16
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/dist/resources/extensions/gsd/session-lock.js +2 -26
- package/dist/resources/extensions/gsd/templates/plan.md +0 -8
- package/dist/resources/extensions/gsd/worktree-resolver.js +0 -12
- package/dist/resources/extensions/remote-questions/remote-command.js +22 -2
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +0 -30
- package/dist/resources/extensions/shared/wizard-ui.js +478 -0
- package/dist/resources/extensions/subagent/index.js +14 -6
- package/package.json +1 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +2 -13
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resource-loader.ts +2 -13
- package/src/resources/extensions/async-jobs/await-tool.ts +2 -0
- package/src/resources/extensions/async-jobs/job-manager.ts +7 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +17 -0
- package/src/resources/extensions/bg-shell/process-manager.ts +4 -0
- package/src/resources/extensions/bg-shell/types.ts +12 -0
- package/src/resources/extensions/context7/index.ts +0 -7
- package/src/resources/extensions/get-secrets-from-user.ts +35 -2
- package/src/resources/extensions/google-search/index.ts +0 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -49
- package/src/resources/extensions/gsd/auto-loop.ts +1 -11
- package/src/resources/extensions/gsd/auto-recovery.ts +0 -39
- package/src/resources/extensions/gsd/auto-start.ts +2 -42
- package/src/resources/extensions/gsd/auto.ts +3 -61
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/doctor-environment.ts +16 -26
- package/src/resources/extensions/gsd/files.ts +1 -10
- package/src/resources/extensions/gsd/gitignore.ts +7 -54
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/health-widget.ts +59 -103
- package/src/resources/extensions/gsd/index.ts +1 -10
- package/src/resources/extensions/gsd/migrate-external.ts +2 -47
- package/src/resources/extensions/gsd/paths.ts +7 -73
- package/src/resources/extensions/gsd/post-unit-hooks.ts +1 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -16
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +0 -2
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +0 -2
- package/src/resources/extensions/gsd/session-lock.ts +2 -29
- package/src/resources/extensions/gsd/templates/plan.md +0 -8
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -12
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +0 -15
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -32
- package/src/resources/extensions/gsd/worktree-resolver.ts +0 -11
- package/src/resources/extensions/remote-questions/remote-command.ts +23 -2
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +0 -36
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/subagent/index.ts +12 -6
- package/dist/resources/extensions/gsd/health-widget-core.js +0 -96
- package/dist/resources/extensions/gsd/roadmap-mutations.js +0 -55
- package/src/resources/extensions/gsd/health-widget-core.ts +0 -129
- package/src/resources/extensions/gsd/roadmap-mutations.ts +0 -66
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +0 -214
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +0 -158
- package/src/resources/extensions/gsd/tests/paths.test.ts +0 -113
- package/src/resources/extensions/gsd/tests/test-utils.ts +0 -165
|
@@ -705,7 +705,7 @@ export async function inlinePriorMilestoneSummary(mid, base) {
|
|
|
705
705
|
* Returns `null` when no manifest file exists (path resolution failure or
|
|
706
706
|
* file not on disk) - callers can distinguish "no manifest" from "empty manifest".
|
|
707
707
|
*/
|
|
708
|
-
export async function getManifestStatus(base, milestoneId
|
|
708
|
+
export async function getManifestStatus(base, milestoneId) {
|
|
709
709
|
const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
|
|
710
710
|
if (!resolvedPath)
|
|
711
711
|
return null;
|
|
@@ -714,16 +714,8 @@ export async function getManifestStatus(base, milestoneId, projectRoot) {
|
|
|
714
714
|
return null;
|
|
715
715
|
const manifest = parseSecretsManifest(content);
|
|
716
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.
|
|
720
717
|
const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
|
|
721
718
|
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
|
-
}
|
|
727
719
|
const result = {
|
|
728
720
|
pending: [],
|
|
729
721
|
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,
|
|
10
|
-
import { nativeRmCached
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { nativeRmCached } from "./native-git-bridge.js";
|
|
11
11
|
import { gsdRoot } from "./paths.js";
|
|
12
12
|
/**
|
|
13
13
|
* GSD runtime patterns for git index cleanup.
|
|
@@ -67,48 +67,12 @@ const BASELINE_PATTERNS = [
|
|
|
67
67
|
"tmp/",
|
|
68
68
|
];
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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.
|
|
70
|
+
* Ensure basePath/.gitignore contains a blanket `.gsd/` ignore.
|
|
71
|
+
* Creates the file if missing; appends `.gsd/` if not present.
|
|
106
72
|
* Returns true if the file was created or modified, false if already complete.
|
|
107
73
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* is excluded to prevent data loss. Only the `.gsd` pattern is affected —
|
|
111
|
-
* all other baseline patterns are still applied normally.
|
|
74
|
+
* `.gsd/` state is managed externally (symlinked to `~/.gsd/projects/<hash>/`),
|
|
75
|
+
* so the entire directory is always gitignored.
|
|
112
76
|
*/
|
|
113
77
|
export function ensureGitignore(basePath, options) {
|
|
114
78
|
// If manage_gitignore is explicitly false, do not touch .gitignore at all
|
|
@@ -124,14 +88,8 @@ export function ensureGitignore(basePath, options) {
|
|
|
124
88
|
.split("\n")
|
|
125
89
|
.map((l) => l.trim())
|
|
126
90
|
.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;
|
|
133
91
|
// Find patterns not yet present
|
|
134
|
-
const missing =
|
|
92
|
+
const missing = BASELINE_PATTERNS.filter((p) => !existingLines.has(p));
|
|
135
93
|
if (missing.length === 0)
|
|
136
94
|
return false;
|
|
137
95
|
// Build the block to append
|
|
@@ -153,11 +111,6 @@ export function ensureGitignore(basePath, options) {
|
|
|
153
111
|
* already in the index even after .gitignore is updated.
|
|
154
112
|
*
|
|
155
113
|
* 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.
|
|
161
114
|
*/
|
|
162
115
|
export function untrackRuntimeFiles(basePath) {
|
|
163
116
|
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 ?? "~", ".pi", "GSD-WORKFLOW.md");
|
|
158
158
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
159
159
|
pi.sendMessage({
|
|
160
160
|
customType,
|
|
@@ -11,23 +11,21 @@ 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";
|
|
15
14
|
import { projectRoot } from "./commands.js";
|
|
16
|
-
import { deriveState, invalidateStateCache } from "./state.js";
|
|
17
|
-
import { buildHealthLines, detectHealthWidgetProjectState, } from "./health-widget-core.js";
|
|
18
15
|
// ── Data loader ────────────────────────────────────────────────────────────────
|
|
19
|
-
function
|
|
16
|
+
function loadHealthWidgetData(basePath) {
|
|
17
|
+
let hasProject = false;
|
|
20
18
|
let budgetCeiling;
|
|
21
19
|
let budgetSpent = 0;
|
|
22
20
|
let providerIssue = null;
|
|
23
21
|
let environmentErrorCount = 0;
|
|
24
22
|
let environmentWarningCount = 0;
|
|
25
|
-
const projectState = detectHealthWidgetProjectState(basePath);
|
|
26
23
|
try {
|
|
27
24
|
const prefs = loadEffectiveGSDPreferences();
|
|
28
25
|
budgetCeiling = prefs?.preferences?.budget_ceiling;
|
|
29
26
|
const ledger = loadLedgerFromDisk(basePath);
|
|
30
27
|
if (ledger) {
|
|
28
|
+
hasProject = true;
|
|
31
29
|
const totals = getProjectTotals(ledger.units ?? []);
|
|
32
30
|
budgetSpent = totals.cost;
|
|
33
31
|
}
|
|
@@ -49,7 +47,7 @@ function loadBaseHealthWidgetData(basePath) {
|
|
|
49
47
|
}
|
|
50
48
|
catch { /* non-fatal */ }
|
|
51
49
|
return {
|
|
52
|
-
|
|
50
|
+
hasProject,
|
|
53
51
|
budgetCeiling,
|
|
54
52
|
budgetSpent,
|
|
55
53
|
providerIssue,
|
|
@@ -58,85 +56,50 @@ function loadBaseHealthWidgetData(basePath) {
|
|
|
58
56
|
lastRefreshed: Date.now(),
|
|
59
57
|
};
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return trimmed;
|
|
65
|
-
return `${trimmed.slice(0, max - 1).trimEnd()}…`;
|
|
59
|
+
// ── Rendering ──────────────────────────────────────────────────────────────────
|
|
60
|
+
function formatCost(n) {
|
|
61
|
+
return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
|
|
66
62
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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";
|
|
63
|
+
/**
|
|
64
|
+
* Build compact health lines for the widget.
|
|
65
|
+
* Returns a string array suitable for setWidget().
|
|
66
|
+
*/
|
|
67
|
+
export function buildHealthLines(data) {
|
|
68
|
+
if (!data.hasProject) {
|
|
69
|
+
return [" GSD No project loaded — run /gsd to start"];
|
|
81
70
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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);
|
|
71
|
+
const parts = [];
|
|
72
|
+
// System status signal
|
|
73
|
+
const totalIssues = data.environmentErrorCount + data.environmentWarningCount + (data.providerIssue ? 1 : 0);
|
|
74
|
+
if (totalIssues === 0) {
|
|
75
|
+
parts.push("● System OK");
|
|
109
76
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
};
|
|
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" : ""}`);
|
|
136
82
|
}
|
|
137
|
-
|
|
138
|
-
|
|
83
|
+
// Budget
|
|
84
|
+
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
85
|
+
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
86
|
+
parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
|
|
139
87
|
}
|
|
88
|
+
else if (data.budgetSpent > 0) {
|
|
89
|
+
parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
|
|
90
|
+
}
|
|
91
|
+
// Provider issue (if any)
|
|
92
|
+
if (data.providerIssue) {
|
|
93
|
+
parts.push(data.providerIssue);
|
|
94
|
+
}
|
|
95
|
+
// Environment issues
|
|
96
|
+
if (data.environmentErrorCount > 0) {
|
|
97
|
+
parts.push(`Env: ${data.environmentErrorCount} error${data.environmentErrorCount > 1 ? "s" : ""}`);
|
|
98
|
+
}
|
|
99
|
+
else if (data.environmentWarningCount > 0) {
|
|
100
|
+
parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
|
|
101
|
+
}
|
|
102
|
+
return [` ${parts.join(" │ ")}`];
|
|
140
103
|
}
|
|
141
104
|
// ── Widget init ────────────────────────────────────────────────────────────────
|
|
142
105
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
@@ -149,33 +112,19 @@ export function initHealthWidget(ctx) {
|
|
|
149
112
|
return;
|
|
150
113
|
const basePath = projectRoot();
|
|
151
114
|
// String-array fallback — used in RPC mode (factory is a no-op there)
|
|
152
|
-
const initialData =
|
|
115
|
+
const initialData = loadHealthWidgetData(basePath);
|
|
153
116
|
ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
|
|
154
117
|
// Factory-based widget for TUI mode — replaces the string-array above
|
|
155
118
|
ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
|
|
156
119
|
let data = initialData;
|
|
157
120
|
let cachedLines;
|
|
158
|
-
|
|
159
|
-
const refresh = async () => {
|
|
160
|
-
if (refreshInFlight)
|
|
161
|
-
return;
|
|
162
|
-
refreshInFlight = true;
|
|
121
|
+
const refreshTimer = setInterval(() => {
|
|
163
122
|
try {
|
|
164
|
-
|
|
165
|
-
data = await enrichHealthWidgetData(basePath, baseData);
|
|
123
|
+
data = loadHealthWidgetData(basePath);
|
|
166
124
|
cachedLines = undefined;
|
|
167
125
|
_tui.requestRender();
|
|
168
126
|
}
|
|
169
127
|
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();
|
|
179
128
|
}, REFRESH_INTERVAL_MS);
|
|
180
129
|
return {
|
|
181
130
|
render(_width) {
|
|
@@ -140,16 +140,7 @@ export default function (pi) {
|
|
|
140
140
|
// Pipe closed — nothing we can write; just exit cleanly
|
|
141
141
|
process.exit(0);
|
|
142
142
|
}
|
|
143
|
-
|
|
144
|
-
err.syscall?.startsWith("spawn")) {
|
|
145
|
-
// spawn ENOENT — command not found (e.g., npx on Windows).
|
|
146
|
-
// This surfaces as an uncaught exception from child_process but
|
|
147
|
-
// is not a fatal process error. Log and continue instead of
|
|
148
|
-
// crashing auto-mode (#1384).
|
|
149
|
-
process.stderr.write(`[gsd] spawn ENOENT: ${err.path ?? "unknown"} — command not found\n`);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// Re-throw anything that isn't EPIPE/ENOENT so real crashes still surface
|
|
143
|
+
// Re-throw anything that isn't EPIPE so real crashes still surface
|
|
153
144
|
throw err;
|
|
154
145
|
};
|
|
155
146
|
process.on("uncaughtException", _gsdEpipeGuard);
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* `~/.gsd/projects/<hash>/` state directory. After migration, a
|
|
6
6
|
* symlink replaces the original directory so all paths remain valid.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync,
|
|
8
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { externalGsdRoot } from "./repo-identity.js";
|
|
11
11
|
import { getErrorMessage } from "./error-utils.js";
|
|
12
|
-
import { hasGitTrackedGsdFiles } from "./gitignore.js";
|
|
13
12
|
/**
|
|
14
13
|
* Migrate a legacy in-project `.gsd/` directory to external storage.
|
|
15
14
|
*
|
|
@@ -43,27 +42,6 @@ export function migrateToExternalState(basePath) {
|
|
|
43
42
|
catch (err) {
|
|
44
43
|
return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
|
|
45
44
|
}
|
|
46
|
-
// Skip if .gsd/ contains git-tracked files — the project intentionally
|
|
47
|
-
// keeps .gsd/ in version control and migration would destroy that.
|
|
48
|
-
if (hasGitTrackedGsdFiles(basePath)) {
|
|
49
|
-
return { migrated: false };
|
|
50
|
-
}
|
|
51
|
-
// Skip if .gsd/worktrees/ has active worktree directories (#1337).
|
|
52
|
-
// On Windows, active git worktrees hold OS-level directory handles that
|
|
53
|
-
// prevent rename/delete. Attempting migration causes EBUSY and data loss.
|
|
54
|
-
const worktreesDir = join(localGsd, "worktrees");
|
|
55
|
-
if (existsSync(worktreesDir)) {
|
|
56
|
-
try {
|
|
57
|
-
const entries = readdirSync(worktreesDir, { withFileTypes: true });
|
|
58
|
-
if (entries.some(e => e.isDirectory())) {
|
|
59
|
-
return { migrated: false };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Can't read worktrees dir — skip migration to be safe
|
|
64
|
-
return { migrated: false };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
45
|
const externalPath = externalGsdRoot(basePath);
|
|
68
46
|
const migratingPath = join(basePath, ".gsd.migrating");
|
|
69
47
|
try {
|
|
@@ -111,38 +89,7 @@ export function migrateToExternalState(basePath) {
|
|
|
111
89
|
}
|
|
112
90
|
// Create symlink .gsd -> external path
|
|
113
91
|
symlinkSync(externalPath, localGsd, "junction");
|
|
114
|
-
//
|
|
115
|
-
// On Windows, junction creation can silently succeed but resolve to the wrong
|
|
116
|
-
// target, or the external dir may not be accessible. If verification fails,
|
|
117
|
-
// restore from the backup.
|
|
118
|
-
try {
|
|
119
|
-
const resolved = realpathSync(localGsd);
|
|
120
|
-
const resolvedExternal = realpathSync(externalPath);
|
|
121
|
-
if (resolved !== resolvedExternal) {
|
|
122
|
-
// Symlink points to wrong target — restore backup
|
|
123
|
-
try {
|
|
124
|
-
rmSync(localGsd, { force: true });
|
|
125
|
-
}
|
|
126
|
-
catch { /* may not exist */ }
|
|
127
|
-
renameSync(migratingPath, localGsd);
|
|
128
|
-
return { migrated: false, error: `Migration verification failed: symlink resolves to ${resolved}, expected ${resolvedExternal}` };
|
|
129
|
-
}
|
|
130
|
-
// Verify we can read through the symlink
|
|
131
|
-
readdirSync(localGsd);
|
|
132
|
-
}
|
|
133
|
-
catch (verifyErr) {
|
|
134
|
-
// Symlink broken or unreadable — restore backup
|
|
135
|
-
try {
|
|
136
|
-
rmSync(localGsd, { force: true });
|
|
137
|
-
}
|
|
138
|
-
catch { /* may not exist */ }
|
|
139
|
-
try {
|
|
140
|
-
renameSync(migratingPath, localGsd);
|
|
141
|
-
}
|
|
142
|
-
catch { /* best-effort restore */ }
|
|
143
|
-
return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
|
|
144
|
-
}
|
|
145
|
-
// Remove .gsd.migrating only after symlink is verified
|
|
92
|
+
// Remove .gsd.migrating
|
|
146
93
|
rmSync(migratingPath, { recursive: true, force: true });
|
|
147
94
|
return { migrated: true };
|
|
148
95
|
}
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* via prefix matching, so existing projects work without migration.
|
|
10
10
|
*/
|
|
11
11
|
import { readdirSync, existsSync, realpathSync, Dirent } from "node:fs";
|
|
12
|
-
import { join
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { join } from "node:path";
|
|
14
13
|
import { nativeScanGsdTree } from "./native-parser-bridge.js";
|
|
15
14
|
import { DIR_CACHE_MAX } from "./constants.js";
|
|
16
15
|
// ─── Directory Listing Cache ──────────────────────────────────────────────────
|
|
@@ -264,81 +263,15 @@ const LEGACY_GSD_ROOT_FILES = {
|
|
|
264
263
|
OVERRIDES: "overrides.md",
|
|
265
264
|
KNOWLEDGE: "knowledge.md",
|
|
266
265
|
};
|
|
267
|
-
// ─── GSD Root Discovery ───────────────────────────────────────────────────────
|
|
268
|
-
const gsdRootCache = new Map();
|
|
269
|
-
/** Exported for tests only — do not call in production code. */
|
|
270
|
-
export function _clearGsdRootCache() {
|
|
271
|
-
gsdRootCache.clear();
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Resolve the `.gsd` directory for a given project base path.
|
|
275
|
-
*
|
|
276
|
-
* Probe order:
|
|
277
|
-
* 1. basePath/.gsd — fast path (common case)
|
|
278
|
-
* 2. git rev-parse root — handles cwd-is-a-subdirectory
|
|
279
|
-
* 3. Walk up from basePath — handles moved .gsd in an ancestor (bounded by git root)
|
|
280
|
-
* 4. basePath/.gsd — creation fallback (init scenario)
|
|
281
|
-
*
|
|
282
|
-
* Result is cached per basePath for the process lifetime.
|
|
283
|
-
*/
|
|
284
266
|
export function gsdRoot(basePath) {
|
|
285
|
-
const
|
|
286
|
-
if (cached)
|
|
287
|
-
return cached;
|
|
288
|
-
const result = probeGsdRoot(basePath);
|
|
289
|
-
gsdRootCache.set(basePath, result);
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
|
-
function probeGsdRoot(rawBasePath) {
|
|
293
|
-
// 1. Fast path — check the input path directly
|
|
294
|
-
const local = join(rawBasePath, ".gsd");
|
|
295
|
-
if (existsSync(local))
|
|
296
|
-
return local;
|
|
297
|
-
// Resolve symlinks so path comparisons work correctly across platforms
|
|
298
|
-
// (e.g. macOS /var → /private/var). Use rawBasePath as fallback if not resolvable.
|
|
299
|
-
let basePath;
|
|
267
|
+
const local = join(basePath, ".gsd");
|
|
300
268
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
basePath = rawBasePath;
|
|
305
|
-
}
|
|
306
|
-
// 2. Git root anchor — used as both probe target and walk-up boundary
|
|
307
|
-
// Only walk if we're inside a git project — prevents escaping into
|
|
308
|
-
// unrelated filesystem territory when running outside any repo.
|
|
309
|
-
let gitRoot = null;
|
|
310
|
-
try {
|
|
311
|
-
const out = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
312
|
-
cwd: basePath,
|
|
313
|
-
encoding: "utf-8",
|
|
314
|
-
});
|
|
315
|
-
if (out.status === 0) {
|
|
316
|
-
const r = out.stdout.trim();
|
|
317
|
-
if (r)
|
|
318
|
-
gitRoot = normalize(r);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
catch { /* git not available */ }
|
|
322
|
-
if (gitRoot) {
|
|
323
|
-
const candidate = join(gitRoot, ".gsd");
|
|
324
|
-
if (existsSync(candidate))
|
|
325
|
-
return candidate;
|
|
326
|
-
}
|
|
327
|
-
// 3. Walk up from basePath to the git root (only if we are in a subdirectory)
|
|
328
|
-
if (gitRoot && basePath !== gitRoot) {
|
|
329
|
-
let cur = dirname(basePath);
|
|
330
|
-
while (cur !== basePath) {
|
|
331
|
-
const candidate = join(cur, ".gsd");
|
|
332
|
-
if (existsSync(candidate))
|
|
333
|
-
return candidate;
|
|
334
|
-
if (cur === gitRoot)
|
|
335
|
-
break;
|
|
336
|
-
basePath = cur;
|
|
337
|
-
cur = dirname(cur);
|
|
338
|
-
}
|
|
269
|
+
const resolved = realpathSync(local);
|
|
270
|
+
if (resolved !== local)
|
|
271
|
+
return resolved; // symlink resolved
|
|
339
272
|
}
|
|
340
|
-
|
|
341
|
-
return local;
|
|
273
|
+
catch { /* doesn't exist yet — fall through */ }
|
|
274
|
+
return local; // backwards compat: unmigrated projects
|
|
342
275
|
}
|
|
343
276
|
export function milestonesDir(basePath) {
|
|
344
277
|
return join(gsdRoot(basePath), "milestones");
|
|
@@ -111,13 +111,10 @@ function dequeueNextHook(basePath) {
|
|
|
111
111
|
};
|
|
112
112
|
// Build the prompt with variable substitution
|
|
113
113
|
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
114
|
-
|
|
114
|
+
const prompt = config.prompt
|
|
115
115
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
116
116
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
117
117
|
.replace(/\{taskId\}/g, tid ?? "");
|
|
118
|
-
// Inject browser safety instruction for hooks that may use browser tools (#1345).
|
|
119
|
-
// Vite HMR and other persistent connections prevent networkidle from resolving.
|
|
120
|
-
prompt += "\n\n**Browser tool safety:** Do NOT use `browser_wait_for` with `condition: \"network_idle\"` — it hangs indefinitely when dev servers keep persistent connections (Vite HMR, WebSocket). Use `selector_visible`, `text_visible`, or `delay` instead.";
|
|
121
118
|
return {
|
|
122
119
|
hookName: config.name,
|
|
123
120
|
prompt,
|
|
@@ -14,24 +14,9 @@ export function validatePreferences(preferences) {
|
|
|
14
14
|
const warnings = [];
|
|
15
15
|
const validated = {};
|
|
16
16
|
// ─── Unknown Key Detection ──────────────────────────────────────────
|
|
17
|
-
// Common key migration hints for pi-level settings that don't map to GSD prefs
|
|
18
|
-
const KEY_MIGRATION_HINTS = {
|
|
19
|
-
taskIsolation: 'use "git.isolation" instead (values: worktree, branch, none)',
|
|
20
|
-
task_isolation: 'use "git.isolation" instead (values: worktree, branch, none)',
|
|
21
|
-
isolation: 'use "git.isolation" instead (values: worktree, branch, none)',
|
|
22
|
-
manage_gitignore: 'use "git.manage_gitignore" instead',
|
|
23
|
-
auto_push: 'use "git.auto_push" instead',
|
|
24
|
-
main_branch: 'use "git.main_branch" instead',
|
|
25
|
-
};
|
|
26
17
|
for (const key of Object.keys(preferences)) {
|
|
27
18
|
if (!KNOWN_PREFERENCE_KEYS.has(key)) {
|
|
28
|
-
|
|
29
|
-
if (hint) {
|
|
30
|
-
warnings.push(`unknown preference key "${key}" — ${hint}`);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
warnings.push(`unknown preference key "${key}" — ignored`);
|
|
34
|
-
}
|
|
19
|
+
warnings.push(`unknown preference key "${key}" — ignored`);
|
|
35
20
|
}
|
|
36
21
|
}
|
|
37
22
|
if (preferences.version !== undefined) {
|
|
@@ -28,8 +28,6 @@ Then:
|
|
|
28
28
|
|
|
29
29
|
**Important:** Do NOT skip the success criteria and definition of done verification (steps 3-4). The milestone summary must reflect actual verified outcomes, not assumed success. If any criterion was not met, document it clearly in the summary and do not mark the milestone as passing verification.
|
|
30
30
|
|
|
31
|
-
**File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories.
|
|
32
|
-
|
|
33
31
|
**You MUST write `{{milestoneSummaryPath}}` AND update PROJECT.md before finishing.**
|
|
34
32
|
|
|
35
33
|
When done, say: "Milestone {{milestoneId}} complete."
|
|
@@ -67,6 +67,4 @@ If verdict is `needs-remediation`:
|
|
|
67
67
|
|
|
68
68
|
**You MUST write `{{validationPath}}` before finishing.**
|
|
69
69
|
|
|
70
|
-
**File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories.
|
|
71
|
-
|
|
72
70
|
When done, say: "Milestone {{milestoneId}} validation complete — verdict: <verdict>."
|
|
@@ -32,17 +32,8 @@ let _lockPid = 0;
|
|
|
32
32
|
let _lockCompromised = false;
|
|
33
33
|
/** Whether we've already registered a process.on('exit') handler. */
|
|
34
34
|
let _exitHandlerRegistered = false;
|
|
35
|
-
/** Snapshotted lock file path — captured at acquireSessionLock time to avoid
|
|
36
|
-
* gsdRoot() resolving differently in worktree vs project root contexts (#1363). */
|
|
37
|
-
let _snapshotLockPath = null;
|
|
38
|
-
/** Timestamp when the session lock was acquired — used to detect false-positive
|
|
39
|
-
* onCompromised events from event loop stalls within the stale window (#1362). */
|
|
40
|
-
let _lockAcquiredAt = 0;
|
|
41
35
|
const LOCK_FILE = "auto.lock";
|
|
42
36
|
function lockPath(basePath) {
|
|
43
|
-
// If we have a snapshotted path from acquisition, use it for consistency
|
|
44
|
-
if (_snapshotLockPath)
|
|
45
|
-
return _snapshotLockPath;
|
|
46
37
|
return join(gsdRoot(basePath), LOCK_FILE);
|
|
47
38
|
}
|
|
48
39
|
// ─── Stray Lock Cleanup ─────────────────────────────────────────────────────
|
|
@@ -184,17 +175,8 @@ export function acquireSessionLock(basePath) {
|
|
|
184
175
|
onCompromised: () => {
|
|
185
176
|
// proper-lockfile detected mtime drift (system sleep, event loop stall, etc.).
|
|
186
177
|
// Default handler throws inside setTimeout — an uncaught exception that crashes
|
|
187
|
-
// or corrupts process state.
|
|
188
|
-
//
|
|
189
|
-
// False-positive suppression (#1362): If we're still within the stale window
|
|
190
|
-
// (30 min since acquisition), the mtime mismatch is from an event loop stall
|
|
191
|
-
// during a long LLM call — not a real takeover. Log and continue.
|
|
192
|
-
const elapsed = Date.now() - _lockAcquiredAt;
|
|
193
|
-
if (elapsed < 1_800_000) {
|
|
194
|
-
process.stderr.write(`[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`);
|
|
195
|
-
return; // Suppress false positive
|
|
196
|
-
}
|
|
197
|
-
// Past the stale window — this is a real compromise
|
|
178
|
+
// or corrupts process state. Instead, set a flag so validateSessionLock() can
|
|
179
|
+
// detect the compromise gracefully on the next dispatch cycle.
|
|
198
180
|
_lockCompromised = true;
|
|
199
181
|
_releaseFunction = null;
|
|
200
182
|
},
|
|
@@ -203,8 +185,6 @@ export function acquireSessionLock(basePath) {
|
|
|
203
185
|
_lockedPath = basePath;
|
|
204
186
|
_lockPid = process.pid;
|
|
205
187
|
_lockCompromised = false;
|
|
206
|
-
_lockAcquiredAt = Date.now();
|
|
207
|
-
_snapshotLockPath = lp; // Snapshot the resolved path for consistent access (#1363)
|
|
208
188
|
// Safety net: clean up lock dir on process exit if _releaseFunction
|
|
209
189
|
// wasn't called (e.g., normal exit after clean completion) (#1245).
|
|
210
190
|
ensureExitHandler(gsdDir);
|
|
@@ -239,8 +219,6 @@ export function acquireSessionLock(basePath) {
|
|
|
239
219
|
_lockedPath = basePath;
|
|
240
220
|
_lockPid = process.pid;
|
|
241
221
|
_lockCompromised = false;
|
|
242
|
-
_lockAcquiredAt = Date.now();
|
|
243
|
-
_snapshotLockPath = lp; // Snapshot for retry path too (#1363)
|
|
244
222
|
// Safety net — uses centralized handler to avoid double-registration
|
|
245
223
|
ensureExitHandler(gsdDir);
|
|
246
224
|
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
|
@@ -370,8 +348,6 @@ export function releaseSessionLock(basePath) {
|
|
|
370
348
|
_lockedPath = null;
|
|
371
349
|
_lockPid = 0;
|
|
372
350
|
_lockCompromised = false;
|
|
373
|
-
_lockAcquiredAt = 0;
|
|
374
|
-
_snapshotLockPath = null;
|
|
375
351
|
}
|
|
376
352
|
/**
|
|
377
353
|
* Check if a session lock exists and return its data (for crash recovery).
|