gsd-pi 2.37.1 → 2.38.0-dev.add4f78
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 +1 -1
- package/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/onboarding.js +1 -0
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
- package/dist/resources/extensions/gsd/auto-loop.js +61 -31
- package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
- package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto-start.js +6 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto.js +10 -26
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +43 -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/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
- package/dist/resources/extensions/gsd/preferences.js +4 -3
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- 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/templates/task-plan.md +11 -3
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- 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/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -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 +99 -8
- package/src/resources/extensions/gsd/auto-loop.ts +83 -64
- package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
- package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -1
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto.ts +14 -29
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +47 -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/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
- package/src/resources/extensions/gsd/preferences.ts +5 -3
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- 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/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- 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/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
|
@@ -289,10 +289,17 @@ export class CmuxClient {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
async createSplit(direction: "right" | "down" | "left" | "up"): Promise<string | null> {
|
|
292
|
+
return this.createSplitFrom(this.config.surfaceId, direction);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async createSplitFrom(
|
|
296
|
+
sourceSurfaceId: string | undefined,
|
|
297
|
+
direction: "right" | "down" | "left" | "up",
|
|
298
|
+
): Promise<string | null> {
|
|
292
299
|
if (!this.config.splits) return null;
|
|
293
300
|
const before = new Set(await this.listSurfaceIds());
|
|
294
301
|
const args = ["new-split", direction];
|
|
295
|
-
const scopedArgs = this.appendSurface(this.appendWorkspace(args),
|
|
302
|
+
const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
|
|
296
303
|
await this.runAsync(scopedArgs);
|
|
297
304
|
const after = await this.listSurfaceIds();
|
|
298
305
|
for (const id of after) {
|
|
@@ -301,6 +308,55 @@ export class CmuxClient {
|
|
|
301
308
|
return null;
|
|
302
309
|
}
|
|
303
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Create a grid of surfaces for parallel agent execution.
|
|
313
|
+
*
|
|
314
|
+
* Layout strategy (gsd stays in the original surface):
|
|
315
|
+
* 1 agent: [gsd | A]
|
|
316
|
+
* 2 agents: [gsd | A]
|
|
317
|
+
* [ | B]
|
|
318
|
+
* 3 agents: [gsd | A]
|
|
319
|
+
* [ C | B]
|
|
320
|
+
* 4 agents: [gsd | A]
|
|
321
|
+
* [ C | B] (D splits from B downward)
|
|
322
|
+
* [ | D]
|
|
323
|
+
*
|
|
324
|
+
* Returns surface IDs in order, or empty array on failure.
|
|
325
|
+
*/
|
|
326
|
+
async createGridLayout(count: number): Promise<string[]> {
|
|
327
|
+
if (!this.config.splits || count <= 0) return [];
|
|
328
|
+
const surfaces: string[] = [];
|
|
329
|
+
|
|
330
|
+
// First split: create right column from the gsd surface
|
|
331
|
+
const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
|
|
332
|
+
if (!rightCol) return [];
|
|
333
|
+
surfaces.push(rightCol);
|
|
334
|
+
if (count === 1) return surfaces;
|
|
335
|
+
|
|
336
|
+
// Second split: split right column down → bottom-right
|
|
337
|
+
const bottomRight = await this.createSplitFrom(rightCol, "down");
|
|
338
|
+
if (!bottomRight) return surfaces;
|
|
339
|
+
surfaces.push(bottomRight);
|
|
340
|
+
if (count === 2) return surfaces;
|
|
341
|
+
|
|
342
|
+
// Third split: split gsd surface down → bottom-left
|
|
343
|
+
const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
|
|
344
|
+
if (!bottomLeft) return surfaces;
|
|
345
|
+
surfaces.push(bottomLeft);
|
|
346
|
+
if (count === 3) return surfaces;
|
|
347
|
+
|
|
348
|
+
// Fourth+: split subsequent surfaces down from the last created
|
|
349
|
+
let lastSurface = bottomRight;
|
|
350
|
+
for (let i = 3; i < count; i++) {
|
|
351
|
+
const next = await this.createSplitFrom(lastSurface, "down");
|
|
352
|
+
if (!next) break;
|
|
353
|
+
surfaces.push(next);
|
|
354
|
+
lastSurface = next;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return surfaces;
|
|
358
|
+
}
|
|
359
|
+
|
|
304
360
|
async sendSurface(surfaceId: string, text: string): Promise<boolean> {
|
|
305
361
|
const payload = text.endsWith("\n") ? text : `${text}\n`;
|
|
306
362
|
const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
|
|
@@ -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;
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
buildRunUatPrompt,
|
|
39
39
|
buildReassessRoadmapPrompt,
|
|
40
40
|
buildRewriteDocsPrompt,
|
|
41
|
+
buildReactiveExecutePrompt,
|
|
41
42
|
checkNeedsReassessment,
|
|
42
43
|
checkNeedsRunUat,
|
|
43
44
|
} from "./auto-prompts.js";
|
|
@@ -61,6 +62,7 @@ export interface DispatchContext {
|
|
|
61
62
|
midTitle: string;
|
|
62
63
|
state: GSDState;
|
|
63
64
|
prefs: GSDPreferences | undefined;
|
|
65
|
+
session?: import("./auto/session.js").AutoSession;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
interface DispatchRule {
|
|
@@ -81,26 +83,23 @@ function missingSliceStop(mid: string, phase: string): DispatchAction {
|
|
|
81
83
|
// ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
|
|
82
84
|
|
|
83
85
|
const MAX_REWRITE_ATTEMPTS = 3;
|
|
84
|
-
let rewriteAttemptCount = 0;
|
|
85
|
-
export function resetRewriteCircuitBreaker(): void {
|
|
86
|
-
rewriteAttemptCount = 0;
|
|
87
|
-
}
|
|
88
86
|
|
|
89
87
|
// ─── Rules ────────────────────────────────────────────────────────────────
|
|
90
88
|
|
|
91
89
|
const DISPATCH_RULES: DispatchRule[] = [
|
|
92
90
|
{
|
|
93
91
|
name: "rewrite-docs (override gate)",
|
|
94
|
-
match: async ({ mid, midTitle, state, basePath }) => {
|
|
92
|
+
match: async ({ mid, midTitle, state, basePath, session }) => {
|
|
95
93
|
const pendingOverrides = await loadActiveOverrides(basePath);
|
|
96
94
|
if (pendingOverrides.length === 0) return null;
|
|
97
|
-
|
|
95
|
+
const count = session?.rewriteAttemptCount ?? 0;
|
|
96
|
+
if (count >= MAX_REWRITE_ATTEMPTS) {
|
|
98
97
|
const { resolveAllOverrides } = await import("./files.js");
|
|
99
98
|
await resolveAllOverrides(basePath);
|
|
100
|
-
rewriteAttemptCount = 0;
|
|
99
|
+
if (session) session.rewriteAttemptCount = 0;
|
|
101
100
|
return null;
|
|
102
101
|
}
|
|
103
|
-
rewriteAttemptCount++;
|
|
102
|
+
if (session) session.rewriteAttemptCount++;
|
|
104
103
|
const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
|
|
105
104
|
return {
|
|
106
105
|
action: "dispatch",
|
|
@@ -309,6 +308,98 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
309
308
|
};
|
|
310
309
|
},
|
|
311
310
|
},
|
|
311
|
+
{
|
|
312
|
+
name: "executing → reactive-execute (parallel dispatch)",
|
|
313
|
+
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
314
|
+
if (state.phase !== "executing" || !state.activeTask) return null;
|
|
315
|
+
if (!state.activeSlice) return null; // fall through
|
|
316
|
+
|
|
317
|
+
// Only activate when reactive_execution is explicitly enabled
|
|
318
|
+
const reactiveConfig = prefs?.reactive_execution;
|
|
319
|
+
if (!reactiveConfig?.enabled) return null;
|
|
320
|
+
|
|
321
|
+
const sid = state.activeSlice.id;
|
|
322
|
+
const sTitle = state.activeSlice.title;
|
|
323
|
+
const maxParallel = reactiveConfig.max_parallel ?? 2;
|
|
324
|
+
|
|
325
|
+
// Dry-run mode: max_parallel=1 means graph is derived and logged but
|
|
326
|
+
// execution remains sequential
|
|
327
|
+
if (maxParallel <= 1) return null;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const {
|
|
331
|
+
loadSliceTaskIO,
|
|
332
|
+
deriveTaskGraph,
|
|
333
|
+
isGraphAmbiguous,
|
|
334
|
+
getReadyTasks,
|
|
335
|
+
chooseNonConflictingSubset,
|
|
336
|
+
graphMetrics,
|
|
337
|
+
} = await import("./reactive-graph.js");
|
|
338
|
+
|
|
339
|
+
const taskIO = await loadSliceTaskIO(basePath, mid, sid);
|
|
340
|
+
if (taskIO.length < 2) return null; // single task, no point
|
|
341
|
+
|
|
342
|
+
const graph = deriveTaskGraph(taskIO);
|
|
343
|
+
|
|
344
|
+
// Ambiguous graph → fall through to sequential
|
|
345
|
+
if (isGraphAmbiguous(graph)) return null;
|
|
346
|
+
|
|
347
|
+
const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
|
|
348
|
+
const readyIds = getReadyTasks(graph, completed, new Set());
|
|
349
|
+
|
|
350
|
+
// Only activate reactive dispatch when >1 task is ready
|
|
351
|
+
if (readyIds.length <= 1) return null;
|
|
352
|
+
|
|
353
|
+
const selected = chooseNonConflictingSubset(
|
|
354
|
+
readyIds,
|
|
355
|
+
graph,
|
|
356
|
+
maxParallel,
|
|
357
|
+
new Set(),
|
|
358
|
+
);
|
|
359
|
+
if (selected.length <= 1) return null;
|
|
360
|
+
|
|
361
|
+
// Log graph metrics for observability
|
|
362
|
+
const metrics = graphMetrics(graph);
|
|
363
|
+
process.stderr.write(
|
|
364
|
+
`gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
|
|
365
|
+
`ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Persist dispatched batch so verification and recovery can check
|
|
369
|
+
// exactly which tasks were sent.
|
|
370
|
+
const { saveReactiveState } = await import("./reactive-graph.js");
|
|
371
|
+
saveReactiveState(basePath, mid, sid, {
|
|
372
|
+
sliceId: sid,
|
|
373
|
+
completed: [...completed],
|
|
374
|
+
dispatched: selected,
|
|
375
|
+
graphSnapshot: metrics,
|
|
376
|
+
updatedAt: new Date().toISOString(),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Encode selected task IDs in unitId for artifact verification.
|
|
380
|
+
// Format: M001/S01/reactive+T02,T03
|
|
381
|
+
const batchSuffix = selected.join(",");
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
action: "dispatch",
|
|
385
|
+
unitType: "reactive-execute",
|
|
386
|
+
unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
|
|
387
|
+
prompt: await buildReactiveExecutePrompt(
|
|
388
|
+
mid,
|
|
389
|
+
midTitle,
|
|
390
|
+
sid,
|
|
391
|
+
sTitle,
|
|
392
|
+
selected,
|
|
393
|
+
basePath,
|
|
394
|
+
),
|
|
395
|
+
};
|
|
396
|
+
} catch (err) {
|
|
397
|
+
// Non-fatal — fall through to sequential execution
|
|
398
|
+
process.stderr.write(`gsd-reactive: graph derivation failed: ${(err as Error).message}\n`);
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
},
|
|
312
403
|
{
|
|
313
404
|
name: "executing → execute-task (recover missing task plan → plan-slice)",
|
|
314
405
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
@@ -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,
|
|
@@ -787,7 +826,7 @@ export async function autoLoop(
|
|
|
787
826
|
(m: { status: string }) =>
|
|
788
827
|
m.status !== "complete" && m.status !== "parked",
|
|
789
828
|
);
|
|
790
|
-
if (incomplete.length === 0) {
|
|
829
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
791
830
|
// All milestones complete — merge milestone branch before stopping
|
|
792
831
|
if (s.currentMilestoneId) {
|
|
793
832
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
@@ -804,6 +843,18 @@ export async function autoLoop(
|
|
|
804
843
|
"success",
|
|
805
844
|
);
|
|
806
845
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
846
|
+
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
847
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
848
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
849
|
+
ctx.ui.notify(
|
|
850
|
+
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
851
|
+
"error",
|
|
852
|
+
);
|
|
853
|
+
await deps.stopAuto(
|
|
854
|
+
ctx,
|
|
855
|
+
pi,
|
|
856
|
+
`No milestones found — check basePath resolution`,
|
|
857
|
+
);
|
|
807
858
|
} else if (state.phase === "blocked") {
|
|
808
859
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
809
860
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
@@ -965,62 +1016,26 @@ export async function autoLoop(
|
|
|
965
1016
|
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
966
1017
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
967
1018
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
968
|
-
} else
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
973
|
-
"warning",
|
|
974
|
-
);
|
|
975
|
-
deps.sendDesktopNotification(
|
|
976
|
-
"GSD",
|
|
977
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
978
|
-
"warning",
|
|
979
|
-
"budget",
|
|
980
|
-
);
|
|
981
|
-
deps.logCmuxEvent(
|
|
982
|
-
prefs,
|
|
983
|
-
`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
984
|
-
"warning",
|
|
985
|
-
);
|
|
986
|
-
} else if (newBudgetAlertLevel === 80) {
|
|
987
|
-
s.lastBudgetAlertLevel =
|
|
988
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
989
|
-
ctx.ui.notify(
|
|
990
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
991
|
-
"warning",
|
|
992
|
-
);
|
|
993
|
-
deps.sendDesktopNotification(
|
|
994
|
-
"GSD",
|
|
995
|
-
`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
996
|
-
"warning",
|
|
997
|
-
"budget",
|
|
998
|
-
);
|
|
999
|
-
deps.logCmuxEvent(
|
|
1000
|
-
prefs,
|
|
1001
|
-
`Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1002
|
-
"warning",
|
|
1003
|
-
);
|
|
1004
|
-
} else if (newBudgetAlertLevel === 75) {
|
|
1005
|
-
s.lastBudgetAlertLevel =
|
|
1006
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1007
|
-
ctx.ui.notify(
|
|
1008
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1009
|
-
"info",
|
|
1010
|
-
);
|
|
1011
|
-
deps.sendDesktopNotification(
|
|
1012
|
-
"GSD",
|
|
1013
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1014
|
-
"info",
|
|
1015
|
-
"budget",
|
|
1016
|
-
);
|
|
1017
|
-
deps.logCmuxEvent(
|
|
1018
|
-
prefs,
|
|
1019
|
-
`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`,
|
|
1020
|
-
"progress",
|
|
1019
|
+
} else {
|
|
1020
|
+
// Data-driven 75/80/90% threshold notifications
|
|
1021
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1022
|
+
(t) => newBudgetAlertLevel === t.pct,
|
|
1021
1023
|
);
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
}
|
|
1024
1039
|
}
|
|
1025
1040
|
} else {
|
|
1026
1041
|
s.lastBudgetAlertLevel = 0;
|
|
@@ -1091,6 +1106,7 @@ export async function autoLoop(
|
|
|
1091
1106
|
midTitle: midTitle!,
|
|
1092
1107
|
state,
|
|
1093
1108
|
prefs,
|
|
1109
|
+
session: s,
|
|
1094
1110
|
});
|
|
1095
1111
|
|
|
1096
1112
|
if (dispatchResult.action === "stop") {
|
|
@@ -1649,9 +1665,12 @@ export async function autoLoop(
|
|
|
1649
1665
|
break;
|
|
1650
1666
|
}
|
|
1651
1667
|
|
|
1652
|
-
// 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 };
|
|
1653
1672
|
const sidecarPreResult =
|
|
1654
|
-
await deps.postUnitPreVerification(postUnitCtx);
|
|
1673
|
+
await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
|
|
1655
1674
|
if (sidecarPreResult === "dispatched") {
|
|
1656
1675
|
// Pre-verification caused stop/pause
|
|
1657
1676
|
debugLog("autoLoop", {
|