gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.bc2e21e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-paths.js +1 -1
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
- package/dist/resources/extensions/gsd/auto-loop.js +54 -30
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -71
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
- package/dist/resources/extensions/gsd/auto.js +10 -26
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +2 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/index.js +2 -1
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -1
- package/dist/resources/extensions/gsd/preferences.js +4 -3
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +2 -1
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/package.json +1 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/gsd/auto/session.ts +5 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
- package/src/resources/extensions/gsd/auto-loop.ts +70 -63
- package/src/resources/extensions/gsd/auto-post-unit.ts +52 -42
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
- package/src/resources/extensions/gsd/auto.ts +14 -29
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands.ts +3 -1
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +3 -1
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -1
- package/src/resources/extensions/gsd/preferences.ts +5 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +3 -1
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
9
9
|
import { join, basename } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
12
|
import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
|
|
12
13
|
function parseRuleFile(filePath) {
|
|
13
14
|
let content;
|
|
@@ -56,7 +57,7 @@ function scanDir(dir) {
|
|
|
56
57
|
* Project rules override global rules with the same name.
|
|
57
58
|
*/
|
|
58
59
|
export function loadRules(cwd) {
|
|
59
|
-
const globalDir = join(
|
|
60
|
+
const globalDir = join(gsdHome, "agent", "rules");
|
|
60
61
|
const projectDir = join(cwd, ".gsd", "rules");
|
|
61
62
|
const globalRules = scanDir(globalDir);
|
|
62
63
|
const projectRules = scanDir(projectDir);
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// GSD Extension — Environment variable utilities
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
//
|
|
4
|
+
// Pure utility for checking existing env keys in .env files and process.env.
|
|
5
|
+
// Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
|
|
6
|
+
// when only env-checking is needed (e.g. from files.ts during report generation).
|
|
7
|
+
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check which keys already exist in a .env file or process.env.
|
|
12
|
+
* Returns the subset of `keys` that are already set.
|
|
13
|
+
*/
|
|
14
|
+
export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
|
|
15
|
+
let fileContent = "";
|
|
16
|
+
try {
|
|
17
|
+
fileContent = await readFile(envFilePath, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
// ENOENT or other read error — proceed with empty content
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const existing: string[] = [];
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
const regex = new RegExp(`^${escaped}\\s*=`, "m");
|
|
26
|
+
if (regex.test(fileContent) || key in process.env) {
|
|
27
|
+
existing.push(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return existing;
|
|
31
|
+
}
|
|
@@ -67,30 +67,11 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis
|
|
|
67
67
|
|
|
68
68
|
// ─── Exported utilities ───────────────────────────────────────────────────────
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*/
|
|
76
|
-
export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
|
|
77
|
-
let fileContent = "";
|
|
78
|
-
try {
|
|
79
|
-
fileContent = await readFile(envFilePath, "utf8");
|
|
80
|
-
} catch {
|
|
81
|
-
// ENOENT or other read error — proceed with empty content
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const existing: string[] = [];
|
|
85
|
-
for (const key of keys) {
|
|
86
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
87
|
-
const regex = new RegExp(`^${escaped}\\s*=`, "m");
|
|
88
|
-
if (regex.test(fileContent) || key in process.env) {
|
|
89
|
-
existing.push(key);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return existing;
|
|
93
|
-
}
|
|
70
|
+
// Re-export from env-utils.ts so existing consumers still work.
|
|
71
|
+
// The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
|
|
72
|
+
// into modules that only need env-checking (e.g. files.ts during reports).
|
|
73
|
+
import { checkExistingEnvKeys } from "./env-utils.js";
|
|
74
|
+
export { checkExistingEnvKeys };
|
|
94
75
|
|
|
95
76
|
/**
|
|
96
77
|
* Detect the write destination based on project files in basePath.
|
|
@@ -124,6 +124,9 @@ export class AutoSession {
|
|
|
124
124
|
// ── Sidecar queue ─────────────────────────────────────────────────────
|
|
125
125
|
sidecarQueue: SidecarItem[] = [];
|
|
126
126
|
|
|
127
|
+
// ── Dispatch circuit breakers ──────────────────────────────────────
|
|
128
|
+
rewriteAttemptCount = 0;
|
|
129
|
+
|
|
127
130
|
// ── Metrics ──────────────────────────────────────────────────────────────
|
|
128
131
|
autoStartTime = 0;
|
|
129
132
|
lastPromptCharCount: number | undefined;
|
|
@@ -154,7 +157,7 @@ export class AutoSession {
|
|
|
154
157
|
* events between loop iterations. The next runUnit drains this queue
|
|
155
158
|
* instead of waiting for a new event.
|
|
156
159
|
*/
|
|
157
|
-
pendingAgentEndQueue: Array<{ messages: unknown[] }> = [];
|
|
160
|
+
pendingAgentEndQueue: Array<{ messages: unknown[]; unitId?: string }> = [];
|
|
158
161
|
|
|
159
162
|
// ── Methods ──────────────────────────────────────────────────────────────
|
|
160
163
|
|
|
@@ -228,6 +231,7 @@ export class AutoSession {
|
|
|
228
231
|
this.lastBaselineCharCount = undefined;
|
|
229
232
|
this.pendingQuickTasks = [];
|
|
230
233
|
this.sidecarQueue = [];
|
|
234
|
+
this.rewriteAttemptCount = 0;
|
|
231
235
|
|
|
232
236
|
// Signal handler
|
|
233
237
|
this.sigtermHandler = null;
|
|
@@ -62,6 +62,7 @@ export interface DispatchContext {
|
|
|
62
62
|
midTitle: string;
|
|
63
63
|
state: GSDState;
|
|
64
64
|
prefs: GSDPreferences | undefined;
|
|
65
|
+
session?: import("./auto/session.js").AutoSession;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
interface DispatchRule {
|
|
@@ -82,26 +83,23 @@ function missingSliceStop(mid: string, phase: string): DispatchAction {
|
|
|
82
83
|
// ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
|
|
83
84
|
|
|
84
85
|
const MAX_REWRITE_ATTEMPTS = 3;
|
|
85
|
-
let rewriteAttemptCount = 0;
|
|
86
|
-
export function resetRewriteCircuitBreaker(): void {
|
|
87
|
-
rewriteAttemptCount = 0;
|
|
88
|
-
}
|
|
89
86
|
|
|
90
87
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
91
88
|
|
|
92
89
|
const DISPATCH_RULES: DispatchRule[] = [
|
|
93
90
|
{
|
|
94
91
|
name: "rewrite-docs (override gate)",
|
|
95
|
-
match: async ({ mid, midTitle, state, basePath }) => {
|
|
92
|
+
match: async ({ mid, midTitle, state, basePath, session }) => {
|
|
96
93
|
const pendingOverrides = await loadActiveOverrides(basePath);
|
|
97
94
|
if (pendingOverrides.length === 0) return null;
|
|
98
|
-
|
|
95
|
+
const count = session?.rewriteAttemptCount ?? 0;
|
|
96
|
+
if (count >= MAX_REWRITE_ATTEMPTS) {
|
|
99
97
|
const { resolveAllOverrides } = await import("./files.js");
|
|
100
98
|
await resolveAllOverrides(basePath);
|
|
101
|
-
rewriteAttemptCount = 0;
|
|
99
|
+
if (session) session.rewriteAttemptCount = 0;
|
|
102
100
|
return null;
|
|
103
101
|
}
|
|
104
|
-
rewriteAttemptCount++;
|
|
102
|
+
if (session) session.rewriteAttemptCount++;
|
|
105
103
|
const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
|
|
106
104
|
return {
|
|
107
105
|
action: "dispatch",
|
|
@@ -18,7 +18,7 @@ import type { GSDPreferences } from "./preferences.js";
|
|
|
18
18
|
import type { SessionLockStatus } from "./session-lock.js";
|
|
19
19
|
import type { GSDState } from "./types.js";
|
|
20
20
|
import type { CloseoutOptions } from "./auto-unit-closeout.js";
|
|
21
|
-
import type { PostUnitContext } from "./auto-post-unit.js";
|
|
21
|
+
import type { PostUnitContext, PreVerificationOpts } from "./auto-post-unit.js";
|
|
22
22
|
import type {
|
|
23
23
|
VerificationContext,
|
|
24
24
|
VerificationResult,
|
|
@@ -36,6 +36,19 @@ import type { CmuxLogLevel } from "../cmux/index.js";
|
|
|
36
36
|
*/
|
|
37
37
|
const MAX_LOOP_ITERATIONS = 500;
|
|
38
38
|
|
|
39
|
+
/** Data-driven budget threshold notifications (75/80/90%). The 100% case is
|
|
40
|
+
* handled inline because it requires break/pause/stop control flow. */
|
|
41
|
+
const BUDGET_THRESHOLDS: Array<{
|
|
42
|
+
pct: number;
|
|
43
|
+
label: string;
|
|
44
|
+
notifyLevel: "info" | "warning";
|
|
45
|
+
cmuxLevel: "progress" | "warning";
|
|
46
|
+
}> = [
|
|
47
|
+
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
48
|
+
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
49
|
+
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
50
|
+
];
|
|
51
|
+
|
|
39
52
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
40
53
|
|
|
41
54
|
/**
|
|
@@ -96,10 +109,11 @@ export function resolveAgentEnd(event: AgentEndEvent): void {
|
|
|
96
109
|
debugLog("resolveAgentEnd", {
|
|
97
110
|
status: "queued",
|
|
98
111
|
queueLength: s.pendingAgentEndQueue.length + 1,
|
|
112
|
+
unitId: s.currentUnit?.id,
|
|
99
113
|
warning:
|
|
100
114
|
"agent_end arrived between loop iterations — queued for next runUnit",
|
|
101
115
|
});
|
|
102
|
-
s.pendingAgentEndQueue.push(event);
|
|
116
|
+
s.pendingAgentEndQueue.push({ ...event, unitId: s.currentUnit?.id });
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
|
|
@@ -166,14 +180,37 @@ export async function runUnit(
|
|
|
166
180
|
];
|
|
167
181
|
}
|
|
168
182
|
if (s.pendingAgentEndQueue.length > 0) {
|
|
169
|
-
|
|
183
|
+
// Find an event matching this unit; discard stale events from other units
|
|
184
|
+
const matchIdx = s.pendingAgentEndQueue.findIndex(
|
|
185
|
+
(e) => !e.unitId || e.unitId === unitId,
|
|
186
|
+
);
|
|
187
|
+
if (matchIdx >= 0) {
|
|
188
|
+
// Discard any stale events before the match
|
|
189
|
+
if (matchIdx > 0) {
|
|
190
|
+
debugLog("runUnit", {
|
|
191
|
+
phase: "discarded-stale-events",
|
|
192
|
+
count: matchIdx,
|
|
193
|
+
unitType,
|
|
194
|
+
unitId,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const queued = s.pendingAgentEndQueue.splice(0, matchIdx + 1).pop()!;
|
|
198
|
+
debugLog("runUnit", {
|
|
199
|
+
phase: "drained-queued-event",
|
|
200
|
+
unitType,
|
|
201
|
+
unitId,
|
|
202
|
+
queueRemaining: s.pendingAgentEndQueue.length,
|
|
203
|
+
});
|
|
204
|
+
return { status: "completed", event: queued };
|
|
205
|
+
}
|
|
206
|
+
// No matching event — discard all stale events and proceed to new session
|
|
170
207
|
debugLog("runUnit", {
|
|
171
|
-
phase: "
|
|
208
|
+
phase: "discarded-all-stale-events",
|
|
209
|
+
count: s.pendingAgentEndQueue.length,
|
|
172
210
|
unitType,
|
|
173
211
|
unitId,
|
|
174
|
-
queueRemaining: s.pendingAgentEndQueue.length,
|
|
175
212
|
});
|
|
176
|
-
|
|
213
|
+
s.pendingAgentEndQueue = [];
|
|
177
214
|
}
|
|
178
215
|
|
|
179
216
|
// ── Session creation with timeout ──
|
|
@@ -383,6 +420,7 @@ export interface LoopDeps {
|
|
|
383
420
|
midTitle: string;
|
|
384
421
|
state: GSDState;
|
|
385
422
|
prefs: GSDPreferences | undefined;
|
|
423
|
+
session?: AutoSession;
|
|
386
424
|
}) => Promise<DispatchAction>;
|
|
387
425
|
runPreDispatchHooks: (
|
|
388
426
|
unitType: string,
|
|
@@ -500,6 +538,7 @@ export interface LoopDeps {
|
|
|
500
538
|
// Post-unit processing
|
|
501
539
|
postUnitPreVerification: (
|
|
502
540
|
pctx: PostUnitContext,
|
|
541
|
+
opts?: PreVerificationOpts,
|
|
503
542
|
) => Promise<"dispatched" | "continue">;
|
|
504
543
|
runPostUnitVerification: (
|
|
505
544
|
vctx: VerificationContext,
|
|
@@ -977,62 +1016,26 @@ export async function autoLoop(
|
|
|
977
1016
|
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
978
1017
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
979
1018
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
980
|
-
} else
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
985
|
-
"warning",
|
|
986
|
-
);
|
|
987
|
-
deps.sendDesktopNotification(
|
|
988
|
-
"GSD",
|
|
989
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
990
|
-
"warning",
|
|
991
|
-
"budget",
|
|
992
|
-
);
|
|
993
|
-
deps.logCmuxEvent(
|
|
994
|
-
prefs,
|
|
995
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
996
|
-
"warning",
|
|
997
|
-
);
|
|
998
|
-
} else if (newBudgetAlertLevel === 80) {
|
|
999
|
-
s.lastBudgetAlertLevel =
|
|
1000
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1001
|
-
ctx.ui.notify(
|
|
1002
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1003
|
-
"warning",
|
|
1004
|
-
);
|
|
1005
|
-
deps.sendDesktopNotification(
|
|
1006
|
-
"GSD",
|
|
1007
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1008
|
-
"warning",
|
|
1009
|
-
"budget",
|
|
1010
|
-
);
|
|
1011
|
-
deps.logCmuxEvent(
|
|
1012
|
-
prefs,
|
|
1013
|
-
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1014
|
-
"warning",
|
|
1015
|
-
);
|
|
1016
|
-
} else if (newBudgetAlertLevel === 75) {
|
|
1017
|
-
s.lastBudgetAlertLevel =
|
|
1018
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1019
|
-
ctx.ui.notify(
|
|
1020
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1021
|
-
"info",
|
|
1022
|
-
);
|
|
1023
|
-
deps.sendDesktopNotification(
|
|
1024
|
-
"GSD",
|
|
1025
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1026
|
-
"info",
|
|
1027
|
-
"budget",
|
|
1028
|
-
);
|
|
1029
|
-
deps.logCmuxEvent(
|
|
1030
|
-
prefs,
|
|
1031
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1032
|
-
"progress",
|
|
1019
|
+
} else {
|
|
1020
|
+
// Data-driven 75/80/90% threshold notifications
|
|
1021
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1022
|
+
(t) => newBudgetAlertLevel === t.pct,
|
|
1033
1023
|
);
|
|
1034
|
-
|
|
1035
|
-
|
|
1024
|
+
if (threshold) {
|
|
1025
|
+
s.lastBudgetAlertLevel =
|
|
1026
|
+
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1027
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1028
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1029
|
+
deps.sendDesktopNotification(
|
|
1030
|
+
"GSD",
|
|
1031
|
+
msg,
|
|
1032
|
+
threshold.notifyLevel,
|
|
1033
|
+
"budget",
|
|
1034
|
+
);
|
|
1035
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
1036
|
+
} else if (budgetAlertLevel === 0) {
|
|
1037
|
+
s.lastBudgetAlertLevel = 0;
|
|
1038
|
+
}
|
|
1036
1039
|
}
|
|
1037
1040
|
} else {
|
|
1038
1041
|
s.lastBudgetAlertLevel = 0;
|
|
@@ -1103,6 +1106,7 @@ export async function autoLoop(
|
|
|
1103
1106
|
midTitle: midTitle!,
|
|
1104
1107
|
state,
|
|
1105
1108
|
prefs,
|
|
1109
|
+
session: s,
|
|
1106
1110
|
});
|
|
1107
1111
|
|
|
1108
1112
|
if (dispatchResult.action === "stop") {
|
|
@@ -1661,9 +1665,12 @@ export async function autoLoop(
|
|
|
1661
1665
|
break;
|
|
1662
1666
|
}
|
|
1663
1667
|
|
|
1664
|
-
// Run pre-verification for the sidecar unit
|
|
1668
|
+
// Run pre-verification for the sidecar unit (lightweight path)
|
|
1669
|
+
const sidecarPreOpts: PreVerificationOpts = item.kind === "hook"
|
|
1670
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1671
|
+
: { skipSettleDelay: true, skipStateRebuild: true };
|
|
1665
1672
|
const sidecarPreResult =
|
|
1666
|
-
await deps.postUnitPreVerification(postUnitCtx);
|
|
1673
|
+
await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
|
|
1667
1674
|
if (sidecarPreResult === "dispatched") {
|
|
1668
1675
|
// Pre-verification caused stop/pause
|
|
1669
1676
|
debugLog("autoLoop", {
|
|
@@ -33,7 +33,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
|
|
|
33
33
|
import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
|
|
34
34
|
import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
|
|
35
35
|
import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
|
|
36
|
-
import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
|
|
37
36
|
import { isDbAvailable } from "./gsd-db.js";
|
|
38
37
|
import { consumeSignal } from "./session-status-io.js";
|
|
39
38
|
import {
|
|
@@ -56,6 +55,13 @@ import { join } from "node:path";
|
|
|
56
55
|
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
|
|
57
56
|
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
58
57
|
|
|
58
|
+
export interface PreVerificationOpts {
|
|
59
|
+
skipSettleDelay?: boolean;
|
|
60
|
+
skipDoctor?: boolean;
|
|
61
|
+
skipStateRebuild?: boolean;
|
|
62
|
+
skipWorktreeSync?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
export interface PostUnitContext {
|
|
60
66
|
s: AutoSession;
|
|
61
67
|
ctx: ExtensionContext;
|
|
@@ -73,7 +79,7 @@ export interface PostUnitContext {
|
|
|
73
79
|
*
|
|
74
80
|
* Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
|
|
75
81
|
*/
|
|
76
|
-
export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"dispatched" | "continue"> {
|
|
82
|
+
export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreVerificationOpts): Promise<"dispatched" | "continue"> {
|
|
77
83
|
const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
|
|
78
84
|
|
|
79
85
|
// ── Parallel worker signal check ──
|
|
@@ -95,8 +101,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
95
101
|
// Invalidate all caches
|
|
96
102
|
invalidateAllCaches();
|
|
97
103
|
|
|
98
|
-
// Small delay to let files settle
|
|
99
|
-
|
|
104
|
+
// Small delay to let files settle (skipped for sidecars where latency matters more)
|
|
105
|
+
if (!opts?.skipSettleDelay) {
|
|
106
|
+
await new Promise(r => setTimeout(r, 100));
|
|
107
|
+
}
|
|
100
108
|
|
|
101
109
|
// Auto-commit
|
|
102
110
|
if (s.currentUnit) {
|
|
@@ -120,8 +128,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
120
128
|
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
|
-
} catch {
|
|
124
|
-
|
|
131
|
+
} catch (e) {
|
|
132
|
+
debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
|
|
125
133
|
}
|
|
126
134
|
}
|
|
127
135
|
}
|
|
@@ -131,12 +139,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
131
139
|
if (commitMsg) {
|
|
132
140
|
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
133
141
|
}
|
|
134
|
-
} catch {
|
|
135
|
-
|
|
142
|
+
} catch (e) {
|
|
143
|
+
debugLog("postUnit", { phase: "auto-commit", error: String(e) });
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
// Doctor: fix mechanical bookkeeping
|
|
139
|
-
try {
|
|
146
|
+
// Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
|
|
147
|
+
if (!opts?.skipDoctor) try {
|
|
140
148
|
const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
|
|
141
149
|
const doctorScope = scopeParts.join("/");
|
|
142
150
|
const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
|
|
@@ -168,24 +176,26 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
168
176
|
const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
|
|
169
177
|
const structuredIssues = formatDoctorIssuesForPrompt(actionable);
|
|
170
178
|
dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
|
|
171
|
-
} catch {
|
|
172
|
-
|
|
179
|
+
} catch (e) {
|
|
180
|
+
debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
}
|
|
176
|
-
} catch {
|
|
177
|
-
|
|
184
|
+
} catch (e) {
|
|
185
|
+
debugLog("postUnit", { phase: "doctor", error: String(e) });
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
// Throttled STATE.md rebuild
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// Throttled STATE.md rebuild (skipped for lightweight sidecars)
|
|
189
|
+
if (!opts?.skipStateRebuild) {
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
|
|
192
|
+
try {
|
|
193
|
+
await rebuildState(s.basePath);
|
|
194
|
+
s.lastStateRebuildAt = now;
|
|
195
|
+
autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
|
|
@@ -193,16 +203,16 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
193
203
|
try {
|
|
194
204
|
const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
|
|
195
205
|
pruneDeadProcesses();
|
|
196
|
-
} catch {
|
|
197
|
-
|
|
206
|
+
} catch (e) {
|
|
207
|
+
debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
|
|
198
208
|
}
|
|
199
209
|
|
|
200
|
-
// Sync worktree state back to project root
|
|
201
|
-
if (s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
210
|
+
// Sync worktree state back to project root (skipped for lightweight sidecars)
|
|
211
|
+
if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
202
212
|
try {
|
|
203
213
|
syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
|
|
204
|
-
} catch {
|
|
205
|
-
|
|
214
|
+
} catch (e) {
|
|
215
|
+
debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
218
|
|
|
@@ -210,10 +220,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
210
220
|
if (s.currentUnit.type === "rewrite-docs") {
|
|
211
221
|
try {
|
|
212
222
|
await resolveAllOverrides(s.basePath);
|
|
213
|
-
|
|
223
|
+
s.rewriteAttemptCount = 0;
|
|
214
224
|
ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
|
|
215
|
-
} catch {
|
|
216
|
-
|
|
225
|
+
} catch (e) {
|
|
226
|
+
debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
|
|
217
227
|
}
|
|
218
228
|
}
|
|
219
229
|
|
|
@@ -226,8 +236,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
226
236
|
const { clearReactiveState } = await import("./reactive-graph.js");
|
|
227
237
|
clearReactiveState(s.basePath, mid, sid);
|
|
228
238
|
}
|
|
229
|
-
} catch {
|
|
230
|
-
|
|
239
|
+
} catch (e) {
|
|
240
|
+
debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
|
|
231
241
|
}
|
|
232
242
|
}
|
|
233
243
|
|
|
@@ -280,8 +290,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
280
290
|
if (triggerArtifactVerified) {
|
|
281
291
|
invalidateAllCaches();
|
|
282
292
|
}
|
|
283
|
-
} catch {
|
|
284
|
-
|
|
293
|
+
} catch (e) {
|
|
294
|
+
debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
|
|
285
295
|
}
|
|
286
296
|
} else {
|
|
287
297
|
// Hook unit completed — finalize its runtime record
|
|
@@ -292,8 +302,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
|
|
|
292
302
|
lastProgressKind: "hook-completed",
|
|
293
303
|
});
|
|
294
304
|
clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
295
|
-
} catch {
|
|
296
|
-
|
|
305
|
+
} catch (e) {
|
|
306
|
+
debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
|
|
297
307
|
}
|
|
298
308
|
}
|
|
299
309
|
}
|
|
@@ -429,8 +439,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
429
439
|
}
|
|
430
440
|
}
|
|
431
441
|
}
|
|
432
|
-
} catch {
|
|
433
|
-
|
|
442
|
+
} catch (e) {
|
|
443
|
+
debugLog("postUnit", { phase: "triage-check", error: String(e) });
|
|
434
444
|
}
|
|
435
445
|
}
|
|
436
446
|
|
|
@@ -475,8 +485,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
475
485
|
);
|
|
476
486
|
|
|
477
487
|
return "continue";
|
|
478
|
-
} catch {
|
|
479
|
-
|
|
488
|
+
} catch (e) {
|
|
489
|
+
debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
|
|
480
490
|
}
|
|
481
491
|
}
|
|
482
492
|
|
|
@@ -22,6 +22,8 @@ import { join, sep as pathSep } from "node:path";
|
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
24
24
|
|
|
25
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
26
|
+
|
|
25
27
|
// ─── Project Root → Worktree Sync ─────────────────────────────────────────
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -111,7 +113,7 @@ export function syncStateToProjectRoot(
|
|
|
111
113
|
*/
|
|
112
114
|
export function readResourceVersion(): string | null {
|
|
113
115
|
const agentDir =
|
|
114
|
-
process.env.GSD_CODING_AGENT_DIR || join(
|
|
116
|
+
process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
|
|
115
117
|
const manifestPath = join(agentDir, "managed-resources.json");
|
|
116
118
|
try {
|
|
117
119
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
@@ -622,43 +622,28 @@ export async function stopAuto(
|
|
|
622
622
|
if (existsSync(pausedPath)) unlinkSync(pausedPath);
|
|
623
623
|
} catch { /* non-fatal */ }
|
|
624
624
|
|
|
625
|
-
|
|
626
|
-
s.paused = false;
|
|
627
|
-
s.stepMode = false;
|
|
628
|
-
s.unitDispatchCount.clear();
|
|
629
|
-
s.unitRecoveryCount.clear();
|
|
630
|
-
clearInFlightTools();
|
|
631
|
-
s.lastBudgetAlertLevel = 0;
|
|
632
|
-
s.lastStateRebuildAt = 0;
|
|
633
|
-
s.unitLifetimeDispatches.clear();
|
|
634
|
-
s.currentUnit = null;
|
|
635
|
-
s.autoModeStartModel = null;
|
|
636
|
-
s.currentMilestoneId = null;
|
|
637
|
-
s.originalBasePath = "";
|
|
638
|
-
s.completedUnits = [];
|
|
639
|
-
s.pendingQuickTasks = [];
|
|
640
|
-
clearSliceProgressCache();
|
|
641
|
-
clearActivityLogState();
|
|
642
|
-
resetProactiveHealing();
|
|
643
|
-
s.pendingCrashRecovery = null;
|
|
644
|
-
s.pendingVerificationRetry = null;
|
|
645
|
-
s.verificationRetryCount.clear();
|
|
646
|
-
s.pausedSessionFile = null;
|
|
647
|
-
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
648
|
-
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
649
|
-
ctx?.ui.setFooter(undefined);
|
|
650
|
-
|
|
625
|
+
// Restore original model before reset() clears the IDs
|
|
651
626
|
if (pi && ctx && s.originalModelId && s.originalModelProvider) {
|
|
652
627
|
const original = ctx.modelRegistry.find(
|
|
653
628
|
s.originalModelProvider,
|
|
654
629
|
s.originalModelId,
|
|
655
630
|
);
|
|
656
631
|
if (original) await pi.setModel(original);
|
|
657
|
-
s.originalModelId = null;
|
|
658
|
-
s.originalModelProvider = null;
|
|
659
632
|
}
|
|
660
633
|
|
|
661
|
-
|
|
634
|
+
// External cleanup (not covered by session reset)
|
|
635
|
+
clearInFlightTools();
|
|
636
|
+
clearSliceProgressCache();
|
|
637
|
+
clearActivityLogState();
|
|
638
|
+
resetProactiveHealing();
|
|
639
|
+
|
|
640
|
+
// UI cleanup
|
|
641
|
+
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
642
|
+
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
643
|
+
ctx?.ui.setFooter(undefined);
|
|
644
|
+
|
|
645
|
+
// Reset all session state in one call
|
|
646
|
+
s.reset();
|
|
662
647
|
}
|
|
663
648
|
|
|
664
649
|
/**
|
|
@@ -11,6 +11,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
|
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
|
|
14
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
15
|
+
|
|
14
16
|
// ─── Types (mirrored from extension-registry.ts) ────────────────────────────
|
|
15
17
|
|
|
16
18
|
interface ExtensionManifest {
|
|
@@ -48,11 +50,11 @@ interface ExtensionRegistry {
|
|
|
48
50
|
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
|
49
51
|
|
|
50
52
|
function getRegistryPath(): string {
|
|
51
|
-
return join(
|
|
53
|
+
return join(gsdHome, "extensions", "registry.json");
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function getAgentExtensionsDir(): string {
|
|
55
|
-
return join(
|
|
57
|
+
return join(gsdHome, "agent", "extensions");
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
function loadRegistry(): ExtensionRegistry {
|