gsd-pi 2.35.0 → 2.36.0-dev.f887f4e
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 +17 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
- 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-inspect.js +10 -3
- package/dist/resources/extensions/gsd/commands-rate.js +31 -0
- package/dist/resources/extensions/gsd/commands.js +43 -1
- 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 +8 -2
- 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/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/dist/resources/skills/github-workflows/SKILL.md +0 -2
- package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/package.json +2 -1
- package/packages/pi-agent-core/dist/agent.d.ts +10 -2
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +19 -8
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/src/agent.ts +31 -10
- 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/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +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/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
- package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
- package/pkg/package.json +1 -1
- 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 +22 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
- 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-inspect.ts +10 -3
- package/src/resources/extensions/gsd/commands-rate.ts +55 -0
- package/src/resources/extensions/gsd/commands.ts +43 -1
- 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 +8 -2
- 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/commands-inspect-open-db.test.ts +46 -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/src/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/src/resources/skills/github-workflows/SKILL.md +0 -2
- package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/dist/resources/extensions/shared/wizard-ui.js +0 -478
- package/dist/resources/skills/swiftui/SKILL.md +0 -208
- package/dist/resources/skills/swiftui/references/animations.md +0 -921
- package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
- package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
- package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
- package/dist/resources/skills/swiftui/references/performance.md +0 -1706
- package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
- package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
- package/src/resources/extensions/shared/wizard-ui.ts +0 -551
- package/src/resources/skills/swiftui/SKILL.md +0 -208
- package/src/resources/skills/swiftui/references/animations.md +0 -921
- package/src/resources/skills/swiftui/references/architecture.md +0 -1561
- package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/src/resources/skills/swiftui/references/navigation.md +0 -1492
- package/src/resources/skills/swiftui/references/networking-async.md +0 -214
- package/src/resources/skills/swiftui/references/performance.md +0 -1706
- package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/src/resources/skills/swiftui/references/state-management.md +0 -1443
- package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
|
@@ -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,
|
|
@@ -5,10 +5,11 @@
|
|
|
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, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
8
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, 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";
|
|
12
13
|
/**
|
|
13
14
|
* Migrate a legacy in-project `.gsd/` directory to external storage.
|
|
14
15
|
*
|
|
@@ -42,6 +43,27 @@ export function migrateToExternalState(basePath) {
|
|
|
42
43
|
catch (err) {
|
|
43
44
|
return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
|
|
44
45
|
}
|
|
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
|
+
}
|
|
45
67
|
const externalPath = externalGsdRoot(basePath);
|
|
46
68
|
const migratingPath = join(basePath, ".gsd.migrating");
|
|
47
69
|
try {
|
|
@@ -89,7 +111,38 @@ export function migrateToExternalState(basePath) {
|
|
|
89
111
|
}
|
|
90
112
|
// Create symlink .gsd -> external path
|
|
91
113
|
symlinkSync(externalPath, localGsd, "junction");
|
|
92
|
-
//
|
|
114
|
+
// Verify the symlink resolves correctly before removing the backup (#1377).
|
|
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
|
|
93
146
|
rmSync(migratingPath, { recursive: true, force: true });
|
|
94
147
|
return { migrated: true };
|
|
95
148
|
}
|
|
@@ -67,8 +67,9 @@ export function findMilestoneIds(basePath) {
|
|
|
67
67
|
.filter((d) => d.isDirectory())
|
|
68
68
|
.map((d) => {
|
|
69
69
|
const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
|
|
70
|
-
return match ? match[1] :
|
|
71
|
-
})
|
|
70
|
+
return match ? match[1] : null;
|
|
71
|
+
})
|
|
72
|
+
.filter((id) => id !== null);
|
|
72
73
|
// Apply custom queue order if available, else fall back to numeric sort
|
|
73
74
|
const customOrder = loadQueueOrder(basePath);
|
|
74
75
|
return sortByQueueOrder(ids, customOrder);
|
|
@@ -9,7 +9,8 @@
|
|
|
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 } from "node:path";
|
|
12
|
+
import { join, dirname, normalize } from "node:path";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
13
14
|
import { nativeScanGsdTree } from "./native-parser-bridge.js";
|
|
14
15
|
import { DIR_CACHE_MAX } from "./constants.js";
|
|
15
16
|
// ─── Directory Listing Cache ──────────────────────────────────────────────────
|
|
@@ -263,15 +264,81 @@ const LEGACY_GSD_ROOT_FILES = {
|
|
|
263
264
|
OVERRIDES: "overrides.md",
|
|
264
265
|
KNOWLEDGE: "knowledge.md",
|
|
265
266
|
};
|
|
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
|
+
*/
|
|
266
284
|
export function gsdRoot(basePath) {
|
|
267
|
-
const
|
|
285
|
+
const cached = gsdRootCache.get(basePath);
|
|
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;
|
|
268
300
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
301
|
+
basePath = realpathSync.native(rawBasePath);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
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
|
+
}
|
|
272
339
|
}
|
|
273
|
-
|
|
274
|
-
return local;
|
|
340
|
+
// 4. Fallback for init/creation
|
|
341
|
+
return local;
|
|
275
342
|
}
|
|
276
343
|
export function milestonesDir(basePath) {
|
|
277
344
|
return join(gsdRoot(basePath), "milestones");
|
|
@@ -111,10 +111,13 @@ function dequeueNextHook(basePath) {
|
|
|
111
111
|
};
|
|
112
112
|
// Build the prompt with variable substitution
|
|
113
113
|
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
114
|
-
|
|
114
|
+
let 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.";
|
|
118
121
|
return {
|
|
119
122
|
hookName: config.name,
|
|
120
123
|
prompt,
|
|
@@ -14,9 +14,24 @@ 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
|
+
};
|
|
17
26
|
for (const key of Object.keys(preferences)) {
|
|
18
27
|
if (!KNOWN_PREFERENCE_KEYS.has(key)) {
|
|
19
|
-
|
|
28
|
+
const hint = KEY_MIGRATION_HINTS[key];
|
|
29
|
+
if (hint) {
|
|
30
|
+
warnings.push(`unknown preference key "${key}" — ${hint}`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
warnings.push(`unknown preference key "${key}" — ignored`);
|
|
34
|
+
}
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
if (preferences.version !== undefined) {
|
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { gsdRoot } from "./paths.js";
|
|
16
16
|
import { parse as parseYaml } from "yaml";
|
|
17
17
|
import { normalizeStringArray } from "../shared/mod.js";
|
|
18
|
+
import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
|
|
18
19
|
import { MODE_DEFAULTS, } from "./preferences-types.js";
|
|
19
20
|
import { validatePreferences } from "./preferences-validation.js";
|
|
20
21
|
import { formatSkillRef } from "./preferences-skills.js";
|
|
@@ -79,6 +80,17 @@ export function loadEffectiveGSDPreferences() {
|
|
|
79
80
|
...(mergedWarnings.length > 0 ? { warnings: mergedWarnings } : {}),
|
|
80
81
|
};
|
|
81
82
|
}
|
|
83
|
+
// Apply token-profile defaults as the lowest-priority layer so that
|
|
84
|
+
// `token_profile: budget` sets models and phase-skips automatically.
|
|
85
|
+
// Explicit user preferences always override profile defaults.
|
|
86
|
+
const profile = result.preferences.token_profile;
|
|
87
|
+
if (profile) {
|
|
88
|
+
const profileDefaults = _resolveProfileDefaults(profile);
|
|
89
|
+
result = {
|
|
90
|
+
...result,
|
|
91
|
+
preferences: mergePreferences(profileDefaults, result.preferences),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
82
94
|
// Apply mode defaults as the lowest-priority layer
|
|
83
95
|
if (result.preferences.mode) {
|
|
84
96
|
result = {
|
|
@@ -28,6 +28,8 @@ 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
|
+
|
|
31
33
|
**You MUST write `{{milestoneSummaryPath}}` AND update PROJECT.md before finishing.**
|
|
32
34
|
|
|
33
35
|
When done, say: "Milestone {{milestoneId}} complete."
|
|
@@ -67,4 +67,6 @@ 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
|
+
|
|
70
72
|
When done, say: "Milestone {{milestoneId}} validation complete — verdict: <verdict>."
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roadmap Mutations — shared utilities for modifying roadmap checkbox state.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the duplicated "flip slice checkbox" pattern that existed in
|
|
5
|
+
* doctor.ts, mechanical-completion.ts, and auto-recovery.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
9
|
+
import { resolveMilestoneFile } from "./paths.js";
|
|
10
|
+
import { clearParseCache } from "./files.js";
|
|
11
|
+
/**
|
|
12
|
+
* Mark a slice as done ([x]) in the milestone roadmap.
|
|
13
|
+
* Idempotent — no-op if already checked or if the slice isn't found.
|
|
14
|
+
*
|
|
15
|
+
* @returns true if the roadmap was modified, false if no change was needed
|
|
16
|
+
*/
|
|
17
|
+
export function markSliceDoneInRoadmap(basePath, mid, sid) {
|
|
18
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
19
|
+
if (!roadmapFile)
|
|
20
|
+
return false;
|
|
21
|
+
let content;
|
|
22
|
+
try {
|
|
23
|
+
content = readFileSync(roadmapFile, "utf-8");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"), `$1[x] **${sid}:`);
|
|
29
|
+
if (updated === content)
|
|
30
|
+
return false;
|
|
31
|
+
atomicWriteSync(roadmapFile, updated);
|
|
32
|
+
clearParseCache();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Mark a task as done ([x]) in the slice plan.
|
|
37
|
+
* Idempotent — no-op if already checked or if the task isn't found.
|
|
38
|
+
*
|
|
39
|
+
* @returns true if the plan was modified, false if no change was needed
|
|
40
|
+
*/
|
|
41
|
+
export function markTaskDoneInPlan(basePath, planPath, tid) {
|
|
42
|
+
let content;
|
|
43
|
+
try {
|
|
44
|
+
content = readFileSync(planPath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${tid}:`, "m"), `$1[x] **${tid}:`);
|
|
50
|
+
if (updated === content)
|
|
51
|
+
return false;
|
|
52
|
+
atomicWriteSync(planPath, updated);
|
|
53
|
+
clearParseCache();
|
|
54
|
+
return true;
|
|
55
|
+
}
|