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
package/dist/app-paths.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
export const appRoot = join(homedir(), '.gsd');
|
|
3
|
+
export const appRoot = process.env.GSD_HOME || join(homedir(), '.gsd');
|
|
4
4
|
export const agentDir = join(appRoot, 'agent');
|
|
5
5
|
export const sessionsDir = join(appRoot, 'sessions');
|
|
6
6
|
export const authFilePath = join(agentDir, 'auth.json');
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The only way an extension stops loading is an explicit `gsd extensions disable <id>`.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import { appRoot } from "./app-paths.js";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
12
12
|
function isRegistry(data) {
|
|
@@ -26,7 +26,7 @@ function isManifest(data) {
|
|
|
26
26
|
}
|
|
27
27
|
// ─── Registry Path ──────────────────────────────────────────────────────────
|
|
28
28
|
export function getRegistryPath() {
|
|
29
|
-
return join(
|
|
29
|
+
return join(appRoot, "extensions", "registry.json");
|
|
30
30
|
}
|
|
31
31
|
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
|
32
32
|
function defaultRegistry() {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { appRoot } from "./app-paths.js";
|
|
13
13
|
// Inlined from preferences.ts to avoid crossing the compiled/uncompiled
|
|
14
14
|
// boundary — this file is compiled by tsc, but preferences.ts is loaded
|
|
15
15
|
// via jiti at runtime. Importing it as .js fails because no .js exists
|
|
16
16
|
// in dist/. See #592, #1110.
|
|
17
|
-
const GLOBAL_PREFERENCES_PATH = join(
|
|
17
|
+
const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md");
|
|
18
18
|
export function saveRemoteQuestionsConfig(channel, channelId) {
|
|
19
19
|
const prefsPath = GLOBAL_PREFERENCES_PATH;
|
|
20
20
|
const block = [
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
/**
|
|
9
|
+
* Check which keys already exist in a .env file or process.env.
|
|
10
|
+
* Returns the subset of `keys` that are already set.
|
|
11
|
+
*/
|
|
12
|
+
export async function checkExistingEnvKeys(keys, envFilePath) {
|
|
13
|
+
let fileContent = "";
|
|
14
|
+
try {
|
|
15
|
+
fileContent = await readFile(envFilePath, "utf8");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// ENOENT or other read error — proceed with empty content
|
|
19
|
+
}
|
|
20
|
+
const existing = [];
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
const regex = new RegExp(`^${escaped}\\s*=`, "m");
|
|
24
|
+
if (regex.test(fileContent) || key in process.env) {
|
|
25
|
+
existing.push(key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
@@ -46,30 +46,11 @@ async function writeEnvKey(filePath, key, value) {
|
|
|
46
46
|
await writeFile(filePath, content, "utf8");
|
|
47
47
|
}
|
|
48
48
|
// ─── Exported utilities ───────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*/
|
|
55
|
-
export async function checkExistingEnvKeys(keys, envFilePath) {
|
|
56
|
-
let fileContent = "";
|
|
57
|
-
try {
|
|
58
|
-
fileContent = await readFile(envFilePath, "utf8");
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// ENOENT or other read error — proceed with empty content
|
|
62
|
-
}
|
|
63
|
-
const existing = [];
|
|
64
|
-
for (const key of keys) {
|
|
65
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
-
const regex = new RegExp(`^${escaped}\\s*=`, "m");
|
|
67
|
-
if (regex.test(fileContent) || key in process.env) {
|
|
68
|
-
existing.push(key);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return existing;
|
|
72
|
-
}
|
|
49
|
+
// Re-export from env-utils.ts so existing consumers still work.
|
|
50
|
+
// The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
|
|
51
|
+
// into modules that only need env-checking (e.g. files.ts during reports).
|
|
52
|
+
import { checkExistingEnvKeys } from "./env-utils.js";
|
|
53
|
+
export { checkExistingEnvKeys };
|
|
73
54
|
/**
|
|
74
55
|
* Detect the write destination based on project files in basePath.
|
|
75
56
|
* Priority: vercel.json → convex/ dir → fallback "dotenv".
|
|
@@ -60,6 +60,8 @@ export class AutoSession {
|
|
|
60
60
|
lastStateRebuildAt = 0;
|
|
61
61
|
// ── Sidecar queue ─────────────────────────────────────────────────────
|
|
62
62
|
sidecarQueue = [];
|
|
63
|
+
// ── Dispatch circuit breakers ──────────────────────────────────────
|
|
64
|
+
rewriteAttemptCount = 0;
|
|
63
65
|
// ── Metrics ──────────────────────────────────────────────────────────────
|
|
64
66
|
autoStartTime = 0;
|
|
65
67
|
lastPromptCharCount;
|
|
@@ -160,6 +162,7 @@ export class AutoSession {
|
|
|
160
162
|
this.lastBaselineCharCount = undefined;
|
|
161
163
|
this.pendingQuickTasks = [];
|
|
162
164
|
this.sidecarQueue = [];
|
|
165
|
+
this.rewriteAttemptCount = 0;
|
|
163
166
|
// Signal handler
|
|
164
167
|
this.sigtermHandler = null;
|
|
165
168
|
// Loop promise state
|
|
@@ -22,25 +22,24 @@ function missingSliceStop(mid, phase) {
|
|
|
22
22
|
}
|
|
23
23
|
// ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
|
|
24
24
|
const MAX_REWRITE_ATTEMPTS = 3;
|
|
25
|
-
let rewriteAttemptCount = 0;
|
|
26
|
-
export function resetRewriteCircuitBreaker() {
|
|
27
|
-
rewriteAttemptCount = 0;
|
|
28
|
-
}
|
|
29
25
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
30
26
|
const DISPATCH_RULES = [
|
|
31
27
|
{
|
|
32
28
|
name: "rewrite-docs (override gate)",
|
|
33
|
-
match: async ({ mid, midTitle, state, basePath }) => {
|
|
29
|
+
match: async ({ mid, midTitle, state, basePath, session }) => {
|
|
34
30
|
const pendingOverrides = await loadActiveOverrides(basePath);
|
|
35
31
|
if (pendingOverrides.length === 0)
|
|
36
32
|
return null;
|
|
37
|
-
|
|
33
|
+
const count = session?.rewriteAttemptCount ?? 0;
|
|
34
|
+
if (count >= MAX_REWRITE_ATTEMPTS) {
|
|
38
35
|
const { resolveAllOverrides } = await import("./files.js");
|
|
39
36
|
await resolveAllOverrides(basePath);
|
|
40
|
-
|
|
37
|
+
if (session)
|
|
38
|
+
session.rewriteAttemptCount = 0;
|
|
41
39
|
return null;
|
|
42
40
|
}
|
|
43
|
-
|
|
41
|
+
if (session)
|
|
42
|
+
session.rewriteAttemptCount++;
|
|
44
43
|
const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
|
|
45
44
|
return {
|
|
46
45
|
action: "dispatch",
|
|
@@ -18,6 +18,13 @@ import { debugLog } from "./debug-logger.js";
|
|
|
18
18
|
* generous headroom including retries and sidecar work.
|
|
19
19
|
*/
|
|
20
20
|
const MAX_LOOP_ITERATIONS = 500;
|
|
21
|
+
/** Data-driven budget threshold notifications (75/80/90%). The 100% case is
|
|
22
|
+
* handled inline because it requires break/pause/stop control flow. */
|
|
23
|
+
const BUDGET_THRESHOLDS = [
|
|
24
|
+
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
25
|
+
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
26
|
+
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
27
|
+
];
|
|
21
28
|
// ─── Session-scoped promise state ───────────────────────────────────────────
|
|
22
29
|
//
|
|
23
30
|
// pendingResolve and pendingAgentEndQueue live on AutoSession (not module-level)
|
|
@@ -57,9 +64,10 @@ export function resolveAgentEnd(event) {
|
|
|
57
64
|
debugLog("resolveAgentEnd", {
|
|
58
65
|
status: "queued",
|
|
59
66
|
queueLength: s.pendingAgentEndQueue.length + 1,
|
|
67
|
+
unitId: s.currentUnit?.id,
|
|
60
68
|
warning: "agent_end arrived between loop iterations — queued for next runUnit",
|
|
61
69
|
});
|
|
62
|
-
s.pendingAgentEndQueue.push(event);
|
|
70
|
+
s.pendingAgentEndQueue.push({ ...event, unitId: s.currentUnit?.id });
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
export function isSessionSwitchInFlight() {
|
|
@@ -111,14 +119,35 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
111
119
|
];
|
|
112
120
|
}
|
|
113
121
|
if (s.pendingAgentEndQueue.length > 0) {
|
|
114
|
-
|
|
122
|
+
// Find an event matching this unit; discard stale events from other units
|
|
123
|
+
const matchIdx = s.pendingAgentEndQueue.findIndex((e) => !e.unitId || e.unitId === unitId);
|
|
124
|
+
if (matchIdx >= 0) {
|
|
125
|
+
// Discard any stale events before the match
|
|
126
|
+
if (matchIdx > 0) {
|
|
127
|
+
debugLog("runUnit", {
|
|
128
|
+
phase: "discarded-stale-events",
|
|
129
|
+
count: matchIdx,
|
|
130
|
+
unitType,
|
|
131
|
+
unitId,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const queued = s.pendingAgentEndQueue.splice(0, matchIdx + 1).pop();
|
|
135
|
+
debugLog("runUnit", {
|
|
136
|
+
phase: "drained-queued-event",
|
|
137
|
+
unitType,
|
|
138
|
+
unitId,
|
|
139
|
+
queueRemaining: s.pendingAgentEndQueue.length,
|
|
140
|
+
});
|
|
141
|
+
return { status: "completed", event: queued };
|
|
142
|
+
}
|
|
143
|
+
// No matching event — discard all stale events and proceed to new session
|
|
115
144
|
debugLog("runUnit", {
|
|
116
|
-
phase: "
|
|
145
|
+
phase: "discarded-all-stale-events",
|
|
146
|
+
count: s.pendingAgentEndQueue.length,
|
|
117
147
|
unitType,
|
|
118
148
|
unitId,
|
|
119
|
-
queueRemaining: s.pendingAgentEndQueue.length,
|
|
120
149
|
});
|
|
121
|
-
|
|
150
|
+
s.pendingAgentEndQueue = [];
|
|
122
151
|
}
|
|
123
152
|
// ── Session creation with timeout ──
|
|
124
153
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
@@ -493,29 +522,20 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
493
522
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
494
523
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
495
524
|
}
|
|
496
|
-
else
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
else if (newBudgetAlertLevel === 75) {
|
|
511
|
-
s.lastBudgetAlertLevel =
|
|
512
|
-
newBudgetAlertLevel;
|
|
513
|
-
ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
|
|
514
|
-
deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
|
|
515
|
-
deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
|
|
516
|
-
}
|
|
517
|
-
else if (budgetAlertLevel === 0) {
|
|
518
|
-
s.lastBudgetAlertLevel = 0;
|
|
525
|
+
else {
|
|
526
|
+
// Data-driven 75/80/90% threshold notifications
|
|
527
|
+
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel === t.pct);
|
|
528
|
+
if (threshold) {
|
|
529
|
+
s.lastBudgetAlertLevel =
|
|
530
|
+
newBudgetAlertLevel;
|
|
531
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
532
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
533
|
+
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
534
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
535
|
+
}
|
|
536
|
+
else if (budgetAlertLevel === 0) {
|
|
537
|
+
s.lastBudgetAlertLevel = 0;
|
|
538
|
+
}
|
|
519
539
|
}
|
|
520
540
|
}
|
|
521
541
|
else {
|
|
@@ -563,6 +583,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
563
583
|
midTitle: midTitle,
|
|
564
584
|
state,
|
|
565
585
|
prefs,
|
|
586
|
+
session: s,
|
|
566
587
|
});
|
|
567
588
|
if (dispatchResult.action === "stop") {
|
|
568
589
|
if (s.currentUnit) {
|
|
@@ -922,8 +943,11 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
922
943
|
sidecarBroke = true;
|
|
923
944
|
break;
|
|
924
945
|
}
|
|
925
|
-
// Run pre-verification for the sidecar unit
|
|
926
|
-
const
|
|
946
|
+
// Run pre-verification for the sidecar unit (lightweight path)
|
|
947
|
+
const sidecarPreOpts = item.kind === "hook"
|
|
948
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
949
|
+
: { skipSettleDelay: true, skipStateRebuild: true };
|
|
950
|
+
const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
|
|
927
951
|
if (sidecarPreResult === "dispatched") {
|
|
928
952
|
// Pre-verification caused stop/pause
|
|
929
953
|
debugLog("autoLoop", {
|
|
@@ -22,7 +22,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
|
|
|
22
22
|
import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
|
|
23
23
|
import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
|
|
24
24
|
import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
|
|
25
|
-
import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
|
|
26
25
|
import { isDbAvailable } from "./gsd-db.js";
|
|
27
26
|
import { consumeSignal } from "./session-status-io.js";
|
|
28
27
|
import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, } from "./post-unit-hooks.js";
|
|
@@ -36,7 +35,7 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
|
36
35
|
*
|
|
37
36
|
* Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
|
|
38
37
|
*/
|
|
39
|
-
export async function postUnitPreVerification(pctx) {
|
|
38
|
+
export async function postUnitPreVerification(pctx, opts) {
|
|
40
39
|
const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
|
|
41
40
|
// ── Parallel worker signal check ──
|
|
42
41
|
const milestoneLock = process.env.GSD_MILESTONE_LOCK;
|
|
@@ -55,8 +54,10 @@ export async function postUnitPreVerification(pctx) {
|
|
|
55
54
|
}
|
|
56
55
|
// Invalidate all caches
|
|
57
56
|
invalidateAllCaches();
|
|
58
|
-
// Small delay to let files settle
|
|
59
|
-
|
|
57
|
+
// Small delay to let files settle (skipped for sidecars where latency matters more)
|
|
58
|
+
if (!opts?.skipSettleDelay) {
|
|
59
|
+
await new Promise(r => setTimeout(r, 100));
|
|
60
|
+
}
|
|
60
61
|
// Auto-commit
|
|
61
62
|
if (s.currentUnit) {
|
|
62
63
|
try {
|
|
@@ -79,8 +80,8 @@ export async function postUnitPreVerification(pctx) {
|
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
catch {
|
|
83
|
-
|
|
83
|
+
catch (e) {
|
|
84
|
+
debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
@@ -90,57 +91,60 @@ export async function postUnitPreVerification(pctx) {
|
|
|
90
91
|
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
catch {
|
|
94
|
-
|
|
94
|
+
catch (e) {
|
|
95
|
+
debugLog("postUnit", { phase: "auto-commit", error: String(e) });
|
|
95
96
|
}
|
|
96
|
-
// Doctor: fix mechanical bookkeeping
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
97
|
+
// Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
|
|
98
|
+
if (!opts?.skipDoctor)
|
|
99
|
+
try {
|
|
100
|
+
const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
|
|
101
|
+
const doctorScope = scopeParts.join("/");
|
|
102
|
+
const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
|
|
103
|
+
const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" : "task";
|
|
104
|
+
const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
|
|
105
|
+
if (report.fixesApplied.length > 0) {
|
|
106
|
+
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
107
|
+
}
|
|
108
|
+
// Proactive health tracking
|
|
109
|
+
const summary = summarizeDoctorIssues(report.issues);
|
|
110
|
+
recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
|
|
111
|
+
// Check if we should escalate to LLM-assisted heal
|
|
112
|
+
if (summary.errors > 0) {
|
|
113
|
+
const unresolvedErrors = report.issues
|
|
114
|
+
.filter(i => i.severity === "error" && !i.fixable)
|
|
115
|
+
.map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
|
|
116
|
+
const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
|
|
117
|
+
if (escalation.shouldEscalate) {
|
|
118
|
+
ctx.ui.notify(`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`, "warning");
|
|
119
|
+
try {
|
|
120
|
+
const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
|
|
121
|
+
const { dispatchDoctorHeal } = await import("./commands-handlers.js");
|
|
122
|
+
const actionable = report.issues.filter(i => i.severity === "error");
|
|
123
|
+
const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
|
|
124
|
+
const structuredIssues = formatDoctorIssuesForPrompt(actionable);
|
|
125
|
+
dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
|
|
129
|
+
}
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Non-fatal
|
|
133
|
-
}
|
|
134
|
-
// Throttled STATE.md rebuild
|
|
135
|
-
const now = Date.now();
|
|
136
|
-
if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
|
|
137
|
-
try {
|
|
138
|
-
await rebuildState(s.basePath);
|
|
139
|
-
s.lastStateRebuildAt = now;
|
|
140
|
-
autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
|
|
133
|
+
catch (e) {
|
|
134
|
+
debugLog("postUnit", { phase: "doctor", error: String(e) });
|
|
141
135
|
}
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
// Throttled STATE.md rebuild (skipped for lightweight sidecars)
|
|
137
|
+
if (!opts?.skipStateRebuild) {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
|
|
140
|
+
try {
|
|
141
|
+
await rebuildState(s.basePath);
|
|
142
|
+
s.lastStateRebuildAt = now;
|
|
143
|
+
autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
|
|
147
|
+
}
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
// Prune dead bg-shell processes
|
|
@@ -148,27 +152,27 @@ export async function postUnitPreVerification(pctx) {
|
|
|
148
152
|
const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
|
|
149
153
|
pruneDeadProcesses();
|
|
150
154
|
}
|
|
151
|
-
catch {
|
|
152
|
-
|
|
155
|
+
catch (e) {
|
|
156
|
+
debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
|
|
153
157
|
}
|
|
154
|
-
// Sync worktree state back to project root
|
|
155
|
-
if (s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
158
|
+
// Sync worktree state back to project root (skipped for lightweight sidecars)
|
|
159
|
+
if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
|
|
156
160
|
try {
|
|
157
161
|
syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
|
|
158
162
|
}
|
|
159
|
-
catch {
|
|
160
|
-
|
|
163
|
+
catch (e) {
|
|
164
|
+
debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
167
|
// Rewrite-docs completion
|
|
164
168
|
if (s.currentUnit.type === "rewrite-docs") {
|
|
165
169
|
try {
|
|
166
170
|
await resolveAllOverrides(s.basePath);
|
|
167
|
-
|
|
171
|
+
s.rewriteAttemptCount = 0;
|
|
168
172
|
ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
|
|
169
173
|
}
|
|
170
|
-
catch {
|
|
171
|
-
|
|
174
|
+
catch (e) {
|
|
175
|
+
debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
// Reactive state cleanup on slice completion
|
|
@@ -181,8 +185,8 @@ export async function postUnitPreVerification(pctx) {
|
|
|
181
185
|
clearReactiveState(s.basePath, mid, sid);
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
|
-
catch {
|
|
185
|
-
|
|
188
|
+
catch (e) {
|
|
189
|
+
debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
// Post-triage: execute actionable resolutions
|
|
@@ -224,8 +228,8 @@ export async function postUnitPreVerification(pctx) {
|
|
|
224
228
|
invalidateAllCaches();
|
|
225
229
|
}
|
|
226
230
|
}
|
|
227
|
-
catch {
|
|
228
|
-
|
|
231
|
+
catch (e) {
|
|
232
|
+
debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
else {
|
|
@@ -238,8 +242,8 @@ export async function postUnitPreVerification(pctx) {
|
|
|
238
242
|
});
|
|
239
243
|
clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
240
244
|
}
|
|
241
|
-
catch {
|
|
242
|
-
|
|
245
|
+
catch (e) {
|
|
246
|
+
debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
}
|
|
@@ -352,8 +356,8 @@ export async function postUnitPostVerification(pctx) {
|
|
|
352
356
|
}
|
|
353
357
|
}
|
|
354
358
|
}
|
|
355
|
-
catch {
|
|
356
|
-
|
|
359
|
+
catch (e) {
|
|
360
|
+
debugLog("postUnit", { phase: "triage-check", error: String(e) });
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
363
|
// ── Quick-task dispatch ──
|
|
@@ -387,8 +391,8 @@ export async function postUnitPostVerification(pctx) {
|
|
|
387
391
|
ctx.ui.notify(`Executing quick-task: ${capture.id} — "${capture.text}"`, "info");
|
|
388
392
|
return "continue";
|
|
389
393
|
}
|
|
390
|
-
catch {
|
|
391
|
-
|
|
394
|
+
catch (e) {
|
|
395
|
+
debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
|
|
392
396
|
}
|
|
393
397
|
}
|
|
394
398
|
// Step mode → show wizard instead of dispatch
|
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync, unlinkSync, readdirSync, } from "node:fs";
|
|
|
13
13
|
import { join, sep as pathSep } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
16
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
16
17
|
// ─── Project Root → Worktree Sync ─────────────────────────────────────────
|
|
17
18
|
/**
|
|
18
19
|
* Sync milestone artifacts from project root INTO worktree before deriveState.
|
|
@@ -75,7 +76,7 @@ export function syncStateToProjectRoot(worktreePath, projectRoot, milestoneId) {
|
|
|
75
76
|
* doesn't falsely trigger staleness (#804).
|
|
76
77
|
*/
|
|
77
78
|
export function readResourceVersion() {
|
|
78
|
-
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(
|
|
79
|
+
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
|
|
79
80
|
const manifestPath = join(agentDir, "managed-resources.json");
|
|
80
81
|
try {
|
|
81
82
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
@@ -374,39 +374,23 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
374
374
|
unlinkSync(pausedPath);
|
|
375
375
|
}
|
|
376
376
|
catch { /* non-fatal */ }
|
|
377
|
-
|
|
378
|
-
s.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
377
|
+
// Restore original model before reset() clears the IDs
|
|
378
|
+
if (pi && ctx && s.originalModelId && s.originalModelProvider) {
|
|
379
|
+
const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
|
|
380
|
+
if (original)
|
|
381
|
+
await pi.setModel(original);
|
|
382
|
+
}
|
|
383
|
+
// External cleanup (not covered by session reset)
|
|
382
384
|
clearInFlightTools();
|
|
383
|
-
s.lastBudgetAlertLevel = 0;
|
|
384
|
-
s.lastStateRebuildAt = 0;
|
|
385
|
-
s.unitLifetimeDispatches.clear();
|
|
386
|
-
s.currentUnit = null;
|
|
387
|
-
s.autoModeStartModel = null;
|
|
388
|
-
s.currentMilestoneId = null;
|
|
389
|
-
s.originalBasePath = "";
|
|
390
|
-
s.completedUnits = [];
|
|
391
|
-
s.pendingQuickTasks = [];
|
|
392
385
|
clearSliceProgressCache();
|
|
393
386
|
clearActivityLogState();
|
|
394
387
|
resetProactiveHealing();
|
|
395
|
-
|
|
396
|
-
s.pendingVerificationRetry = null;
|
|
397
|
-
s.verificationRetryCount.clear();
|
|
398
|
-
s.pausedSessionFile = null;
|
|
388
|
+
// UI cleanup
|
|
399
389
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
400
390
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
401
391
|
ctx?.ui.setFooter(undefined);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (original)
|
|
405
|
-
await pi.setModel(original);
|
|
406
|
-
s.originalModelId = null;
|
|
407
|
-
s.originalModelProvider = null;
|
|
408
|
-
}
|
|
409
|
-
s.cmdCtx = null;
|
|
392
|
+
// Reset all session state in one call
|
|
393
|
+
s.reset();
|
|
410
394
|
}
|
|
411
395
|
/**
|
|
412
396
|
* Pause auto-mode without destroying state. Context is preserved.
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
11
12
|
// ─── Registry I/O ───────────────────────────────────────────────────────────
|
|
12
13
|
function getRegistryPath() {
|
|
13
|
-
return join(
|
|
14
|
+
return join(gsdHome, "extensions", "registry.json");
|
|
14
15
|
}
|
|
15
16
|
function getAgentExtensionsDir() {
|
|
16
|
-
return join(
|
|
17
|
+
return join(gsdHome, "agent", "extensions");
|
|
17
18
|
}
|
|
18
19
|
function loadRegistry() {
|
|
19
20
|
const filePath = getRegistryPath();
|
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { gsdRoot } from "./paths.js";
|
|
10
|
+
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
10
11
|
import { enableDebug } from "./debug-logger.js";
|
|
11
12
|
import { deriveState } from "./state.js";
|
|
12
13
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
@@ -437,7 +438,7 @@ export function registerGSDCommand(pi) {
|
|
|
437
438
|
if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
|
|
438
439
|
const idPrefix = parts[2] ?? "";
|
|
439
440
|
try {
|
|
440
|
-
const extDir = join(
|
|
441
|
+
const extDir = join(gsdHome, "agent", "extensions");
|
|
441
442
|
const ids = [];
|
|
442
443
|
for (const entry of readdirSync(extDir, { withFileTypes: true })) {
|
|
443
444
|
if (!entry.isDirectory())
|