gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -14,8 +14,6 @@ import type {
|
|
|
14
14
|
import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import { gsdRoot } from "./paths.js";
|
|
18
|
-
import { parseUnitId } from "./unit-id.js";
|
|
19
17
|
|
|
20
18
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
21
19
|
|
|
@@ -150,7 +148,7 @@ function dequeueNextHook(basePath: string): HookDispatchResult | null {
|
|
|
150
148
|
};
|
|
151
149
|
|
|
152
150
|
// Build the prompt with variable substitution
|
|
153
|
-
const
|
|
151
|
+
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
154
152
|
const prompt = config.prompt
|
|
155
153
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
156
154
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -209,14 +207,16 @@ function handleHookCompletion(basePath: string): HookDispatchResult | null {
|
|
|
209
207
|
* - Milestone-level (M001): .gsd/M001/{artifact}
|
|
210
208
|
*/
|
|
211
209
|
export function resolveHookArtifactPath(basePath: string, unitId: string, artifactName: string): string {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
210
|
+
const parts = unitId.split("/");
|
|
211
|
+
if (parts.length === 3) {
|
|
212
|
+
const [mid, sid, tid] = parts;
|
|
213
|
+
return join(basePath, ".gsd", mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
215
214
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
215
|
+
if (parts.length === 2) {
|
|
216
|
+
const [mid, sid] = parts;
|
|
217
|
+
return join(basePath, ".gsd", mid, "slices", sid, artifactName);
|
|
218
218
|
}
|
|
219
|
-
return join(
|
|
219
|
+
return join(basePath, ".gsd", parts[0], artifactName);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -252,7 +252,7 @@ export function runPreDispatchHooks(
|
|
|
252
252
|
return { action: "proceed", prompt, firedHooks: [] };
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
const
|
|
255
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
256
256
|
const substitute = (text: string): string =>
|
|
257
257
|
text
|
|
258
258
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
@@ -310,7 +310,7 @@ export function runPreDispatchHooks(
|
|
|
310
310
|
const HOOK_STATE_FILE = "hook-state.json";
|
|
311
311
|
|
|
312
312
|
function hookStatePath(basePath: string): string {
|
|
313
|
-
return join(
|
|
313
|
+
return join(basePath, ".gsd", HOOK_STATE_FILE);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/**
|
|
@@ -323,7 +323,7 @@ export function persistHookState(basePath: string): void {
|
|
|
323
323
|
savedAt: new Date().toISOString(),
|
|
324
324
|
};
|
|
325
325
|
try {
|
|
326
|
-
const dir =
|
|
326
|
+
const dir = join(basePath, ".gsd");
|
|
327
327
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
328
328
|
writeFileSync(hookStatePath(basePath), JSON.stringify(state, null, 2), "utf-8");
|
|
329
329
|
} catch {
|
|
@@ -465,7 +465,7 @@ export function triggerHookManually(
|
|
|
465
465
|
activeHook.cycle = currentCycle;
|
|
466
466
|
|
|
467
467
|
// Build the prompt with variable substitution
|
|
468
|
-
const
|
|
468
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
469
469
|
const prompt = hook.prompt
|
|
470
470
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
471
471
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -28,246 +28,111 @@ export interface ProgressScore {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface ProgressSignal {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
detail: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Signal Evaluators ──────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
function evaluateHealthTrend(): ProgressSignal {
|
|
39
|
-
const trend = getHealthTrend();
|
|
40
|
-
|
|
41
|
-
switch (trend) {
|
|
42
|
-
case "improving":
|
|
43
|
-
return { name: "health_trend", level: "green", detail: "Health improving" };
|
|
44
|
-
case "stable":
|
|
45
|
-
return { name: "health_trend", level: "green", detail: "Health stable" };
|
|
46
|
-
case "degrading":
|
|
47
|
-
return { name: "health_trend", level: "red", detail: "Health degrading" };
|
|
48
|
-
case "unknown":
|
|
49
|
-
return { name: "health_trend", level: "green", detail: "Insufficient data" };
|
|
50
|
-
}
|
|
31
|
+
kind: "positive" | "negative" | "neutral";
|
|
32
|
+
label: string;
|
|
51
33
|
}
|
|
52
34
|
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { name: "error_streak", level: "yellow", detail: `${streak} consecutive error unit(s)` };
|
|
61
|
-
}
|
|
62
|
-
return { name: "error_streak", level: "red", detail: `${streak} consecutive error units` };
|
|
35
|
+
function escalateLevel(level: ProgressLevel, next: ProgressLevel): ProgressLevel {
|
|
36
|
+
const ranks: Record<ProgressLevel, number> = {
|
|
37
|
+
green: 0,
|
|
38
|
+
yellow: 1,
|
|
39
|
+
red: 2,
|
|
40
|
+
};
|
|
41
|
+
return ranks[next] > ranks[level] ? next : level;
|
|
63
42
|
}
|
|
64
43
|
|
|
65
|
-
|
|
66
|
-
const history = getHealthHistory();
|
|
67
|
-
if (history.length === 0) {
|
|
68
|
-
return { name: "recent_errors", level: "green", detail: "No health data yet" };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const latest = history[history.length - 1]!;
|
|
44
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
72
45
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (latest.errors <= 2) {
|
|
80
|
-
return { name: "recent_errors", level: "yellow", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
81
|
-
}
|
|
82
|
-
return { name: "recent_errors", level: "red", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
83
|
-
}
|
|
46
|
+
/**
|
|
47
|
+
* Compute the current progress score from health signals.
|
|
48
|
+
*/
|
|
49
|
+
export function computeProgressScore(): ProgressScore {
|
|
50
|
+
const signals: ProgressSignal[] = [];
|
|
51
|
+
let level: ProgressLevel = "green";
|
|
84
52
|
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
|
|
53
|
+
// Check consecutive errors
|
|
54
|
+
const consecutiveErrors = getConsecutiveErrorUnits();
|
|
55
|
+
if (consecutiveErrors >= 3) {
|
|
56
|
+
signals.push({ kind: "negative", label: `${consecutiveErrors} consecutive error units` });
|
|
57
|
+
level = escalateLevel(level, "red");
|
|
58
|
+
} else if (consecutiveErrors >= 1) {
|
|
59
|
+
signals.push({ kind: "negative", label: `${consecutiveErrors} consecutive error unit(s)` });
|
|
60
|
+
level = escalateLevel(level, "yellow");
|
|
89
61
|
}
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
63
|
+
// Check health trend
|
|
64
|
+
const trend = getHealthTrend();
|
|
65
|
+
if (trend === "degrading") {
|
|
66
|
+
signals.push({ kind: "negative", label: "Health trend declining" });
|
|
67
|
+
level = escalateLevel(level, "yellow");
|
|
68
|
+
} else if (trend === "improving") {
|
|
69
|
+
signals.push({ kind: "positive", label: "Health trend improving" });
|
|
70
|
+
} else if (trend === "stable") {
|
|
71
|
+
signals.push({ kind: "neutral", label: "Health trend stable" });
|
|
99
72
|
}
|
|
100
73
|
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function evaluateDispatchVelocity(): ProgressSignal {
|
|
74
|
+
// Check recent history
|
|
105
75
|
const history = getHealthHistory();
|
|
106
|
-
if (history.length
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Check time between recent snapshots — are units completing at a reasonable rate?
|
|
111
|
-
const recent = history.slice(-5);
|
|
112
|
-
if (recent.length < 2) {
|
|
113
|
-
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const timeDiffs: number[] = [];
|
|
117
|
-
for (let i = 1; i < recent.length; i++) {
|
|
118
|
-
timeDiffs.push(recent[i]!.timestamp - recent[i - 1]!.timestamp);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const avgTimeMs = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length;
|
|
122
|
-
const avgTimeMins = Math.round(avgTimeMs / 60_000);
|
|
123
|
-
|
|
124
|
-
// If average unit time is > 15 minutes, something might be wrong
|
|
125
|
-
if (avgTimeMins > 15) {
|
|
126
|
-
return { name: "dispatch_velocity", level: "yellow", detail: `Units averaging ${avgTimeMins}min each` };
|
|
76
|
+
if (history.length === 0) {
|
|
77
|
+
signals.push({ kind: "neutral", label: "No health data yet" });
|
|
127
78
|
}
|
|
128
79
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Compute the current progress score by evaluating all available signals.
|
|
136
|
-
* Returns a composite score with individual signal details.
|
|
137
|
-
*/
|
|
138
|
-
export function computeProgressScore(): ProgressScore {
|
|
139
|
-
const signals: ProgressSignal[] = [
|
|
140
|
-
evaluateHealthTrend(),
|
|
141
|
-
evaluateErrorStreak(),
|
|
142
|
-
evaluateRecentErrors(),
|
|
143
|
-
evaluateArtifactProduction(),
|
|
144
|
-
evaluateDispatchVelocity(),
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
// Overall level: worst of all signals
|
|
148
|
-
const level = signals.some(s => s.level === "red")
|
|
149
|
-
? "red"
|
|
150
|
-
: signals.some(s => s.level === "yellow")
|
|
151
|
-
? "yellow"
|
|
152
|
-
: "green";
|
|
153
|
-
|
|
154
|
-
// Build summary from the most important signals
|
|
155
|
-
const summary = buildSummary(level, signals);
|
|
80
|
+
const summary = level === "green"
|
|
81
|
+
? "Progressing well"
|
|
82
|
+
: level === "yellow"
|
|
83
|
+
? "Some issues detected"
|
|
84
|
+
: "Stuck or erroring";
|
|
156
85
|
|
|
157
86
|
return { level, summary, signals };
|
|
158
87
|
}
|
|
159
88
|
|
|
160
89
|
/**
|
|
161
|
-
* Compute progress score with additional context
|
|
90
|
+
* Compute progress score with additional context for dashboard display.
|
|
162
91
|
*/
|
|
163
92
|
export function computeProgressScoreWithContext(context: {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
totalUnits?: number;
|
|
168
|
-
retryCount?: number;
|
|
169
|
-
maxRetries?: number;
|
|
93
|
+
sameUnitCount?: number;
|
|
94
|
+
recoveryCount?: number;
|
|
95
|
+
completedCount?: number;
|
|
170
96
|
}): ProgressScore {
|
|
171
97
|
const base = computeProgressScore();
|
|
172
98
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
base.signals.push(retrySignal);
|
|
182
|
-
|
|
183
|
-
// Re-evaluate level
|
|
184
|
-
if (retrySignal.level === "red") base.level = "red";
|
|
185
|
-
else if (retrySignal.level === "yellow" && base.level === "green") base.level = "yellow";
|
|
99
|
+
if (context.sameUnitCount && context.sameUnitCount >= 3) {
|
|
100
|
+
base.signals.push({ kind: "negative", label: `Same unit dispatched ${context.sameUnitCount}× consecutively` });
|
|
101
|
+
base.level = escalateLevel(base.level, "red");
|
|
102
|
+
base.summary = "Stuck on same unit";
|
|
103
|
+
} else if (context.sameUnitCount && context.sameUnitCount >= 2) {
|
|
104
|
+
base.signals.push({ kind: "negative", label: `Same unit dispatched ${context.sameUnitCount}×` });
|
|
105
|
+
base.level = escalateLevel(base.level, "yellow");
|
|
186
106
|
}
|
|
187
107
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return base;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Formatting ─────────────────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
function buildSummary(level: ProgressLevel, signals: ProgressSignal[]): string {
|
|
197
|
-
switch (level) {
|
|
198
|
-
case "green":
|
|
199
|
-
return "Progressing well";
|
|
200
|
-
case "yellow": {
|
|
201
|
-
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
202
|
-
return `Struggling — ${issues[0] ?? "minor issues detected"}`;
|
|
203
|
-
}
|
|
204
|
-
case "red": {
|
|
205
|
-
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
206
|
-
return `Stuck — ${issues[0] ?? "critical issues detected"}`;
|
|
207
|
-
}
|
|
108
|
+
if (context.recoveryCount && context.recoveryCount > 0) {
|
|
109
|
+
base.signals.push({ kind: "negative", label: `${context.recoveryCount} recovery attempts` });
|
|
110
|
+
base.level = escalateLevel(base.level, "yellow");
|
|
208
111
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function buildSummaryWithContext(
|
|
212
|
-
level: ProgressLevel,
|
|
213
|
-
signals: ProgressSignal[],
|
|
214
|
-
context: {
|
|
215
|
-
currentUnitType?: string;
|
|
216
|
-
currentUnitId?: string;
|
|
217
|
-
completedUnits?: number;
|
|
218
|
-
totalUnits?: number;
|
|
219
|
-
retryCount?: number;
|
|
220
|
-
maxRetries?: number;
|
|
221
|
-
},
|
|
222
|
-
): string {
|
|
223
|
-
const unitLabel = context.currentUnitId
|
|
224
|
-
? ` ${context.currentUnitId}`
|
|
225
|
-
: "";
|
|
226
|
-
const progressLabel = context.completedUnits !== undefined && context.totalUnits !== undefined
|
|
227
|
-
? ` (${context.completedUnits} of ${context.totalUnits} done)`
|
|
228
|
-
: "";
|
|
229
112
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return `Progressing well —${unitLabel}${progressLabel}`;
|
|
233
|
-
case "yellow": {
|
|
234
|
-
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
235
|
-
const retryInfo = context.retryCount ? `, attempt ${context.retryCount}/${context.maxRetries}` : "";
|
|
236
|
-
return `Struggling —${unitLabel}${retryInfo}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "issues detected"}`;
|
|
237
|
-
}
|
|
238
|
-
case "red": {
|
|
239
|
-
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
240
|
-
return `Stuck —${unitLabel}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "critical issues"}`;
|
|
241
|
-
}
|
|
113
|
+
if (context.completedCount && context.completedCount > 0) {
|
|
114
|
+
base.signals.push({ kind: "positive", label: `${context.completedCount} units completed` });
|
|
242
115
|
}
|
|
116
|
+
|
|
117
|
+
return base;
|
|
243
118
|
}
|
|
244
119
|
|
|
245
120
|
/**
|
|
246
|
-
* Format
|
|
121
|
+
* Format a one-line progress indicator for dashboard/status display.
|
|
247
122
|
*/
|
|
248
123
|
export function formatProgressLine(score: ProgressScore): string {
|
|
249
|
-
const icon = score.level === "green" ? "
|
|
250
|
-
: score.level === "yellow" ? "\uD83D\uDFE1"
|
|
251
|
-
: "\uD83D\uDD34";
|
|
124
|
+
const icon = score.level === "green" ? "●" : score.level === "yellow" ? "◐" : "○";
|
|
252
125
|
return `${icon} ${score.summary}`;
|
|
253
126
|
}
|
|
254
127
|
|
|
255
128
|
/**
|
|
256
|
-
* Format a
|
|
129
|
+
* Format a multi-line progress report.
|
|
257
130
|
*/
|
|
258
131
|
export function formatProgressReport(score: ProgressScore): string {
|
|
259
|
-
const lines
|
|
260
|
-
|
|
261
|
-
lines.push(formatProgressLine(score));
|
|
262
|
-
lines.push("");
|
|
263
|
-
lines.push("Signals:");
|
|
264
|
-
|
|
132
|
+
const lines = [formatProgressLine(score)];
|
|
265
133
|
for (const signal of score.signals) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
: "\uD83D\uDED1";
|
|
269
|
-
lines.push(` ${icon} ${signal.name}: ${signal.detail}`);
|
|
134
|
+
const prefix = signal.kind === "positive" ? " ✓" : signal.kind === "negative" ? " ✗" : " ·";
|
|
135
|
+
lines.push(`${prefix} ${signal.label}`);
|
|
270
136
|
}
|
|
271
|
-
|
|
272
137
|
return lines.join("\n");
|
|
273
138
|
}
|
|
@@ -10,12 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
13
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { loadPrompt } from "./prompt-loader.js";
|
|
16
16
|
import { gsdRoot } from "./paths.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { GitServiceImpl, runGit } from "./git-service.js";
|
|
18
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
19
|
+
import { nativeHasStagedChanges } from "./native-git-bridge.js";
|
|
20
|
+
|
|
21
|
+
interface QuickReturnState {
|
|
22
|
+
basePath: string;
|
|
23
|
+
originalBranch: string;
|
|
24
|
+
quickBranch: string;
|
|
25
|
+
taskNum: number;
|
|
26
|
+
slug: string;
|
|
27
|
+
description: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let pendingQuickReturn: QuickReturnState | null = null;
|
|
19
31
|
|
|
20
32
|
// ─── Quick Task Helpers ───────────────────────────────────────────────────────
|
|
21
33
|
|
|
@@ -65,6 +77,84 @@ function ensureQuickDir(basePath: string, taskNum: number, slug: string): string
|
|
|
65
77
|
return taskDir;
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
function quickReturnStatePath(basePath: string): string {
|
|
81
|
+
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function persistPendingReturn(state: QuickReturnState): void {
|
|
85
|
+
pendingQuickReturn = state;
|
|
86
|
+
mkdirSync(join(gsdRoot(state.basePath), "runtime"), { recursive: true });
|
|
87
|
+
writeFileSync(quickReturnStatePath(state.basePath), JSON.stringify(state) + "\n", "utf-8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readPendingReturn(basePath: string): QuickReturnState | null {
|
|
91
|
+
if (pendingQuickReturn && pendingQuickReturn.basePath === basePath) {
|
|
92
|
+
return pendingQuickReturn;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const raw = readFileSync(quickReturnStatePath(basePath), "utf-8");
|
|
97
|
+
const parsed = JSON.parse(raw) as Partial<QuickReturnState>;
|
|
98
|
+
if (
|
|
99
|
+
typeof parsed.basePath === "string"
|
|
100
|
+
&& typeof parsed.originalBranch === "string"
|
|
101
|
+
&& typeof parsed.quickBranch === "string"
|
|
102
|
+
&& typeof parsed.taskNum === "number"
|
|
103
|
+
&& typeof parsed.slug === "string"
|
|
104
|
+
&& typeof parsed.description === "string"
|
|
105
|
+
) {
|
|
106
|
+
pendingQuickReturn = parsed as QuickReturnState;
|
|
107
|
+
return pendingQuickReturn;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// No persisted quick-return state
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clearPendingReturn(basePath: string): void {
|
|
117
|
+
if (pendingQuickReturn?.basePath === basePath) {
|
|
118
|
+
pendingQuickReturn = null;
|
|
119
|
+
}
|
|
120
|
+
rmSync(quickReturnStatePath(basePath), { force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hasStagedChanges(basePath: string): boolean {
|
|
124
|
+
return nativeHasStagedChanges(basePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function cleanupQuickBranch(basePath = process.cwd()): boolean {
|
|
128
|
+
const state = readPendingReturn(basePath);
|
|
129
|
+
if (!state) return false;
|
|
130
|
+
|
|
131
|
+
const repoPath = state.basePath;
|
|
132
|
+
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
133
|
+
const git = new GitServiceImpl(repoPath, gitPrefs);
|
|
134
|
+
|
|
135
|
+
if (git.getCurrentBranch() === state.quickBranch) {
|
|
136
|
+
try {
|
|
137
|
+
git.autoCommit("quick-task", `Q${state.taskNum}`, []);
|
|
138
|
+
} catch {
|
|
139
|
+
// Best-effort: quick work may already be committed.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (git.getCurrentBranch() !== state.originalBranch) {
|
|
144
|
+
runGit(repoPath, ["checkout", state.originalBranch]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
runGit(repoPath, ["merge", "--squash", state.quickBranch]);
|
|
148
|
+
|
|
149
|
+
if (hasStagedChanges(repoPath)) {
|
|
150
|
+
runGit(repoPath, ["commit", "-m", `quick(Q${state.taskNum}): ${state.slug}`]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
runGit(repoPath, ["branch", "-D", state.quickBranch], { allowFailure: true });
|
|
154
|
+
clearPendingReturn(repoPath);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
68
158
|
// ─── Main Handler ─────────────────────────────────────────────────────────────
|
|
69
159
|
|
|
70
160
|
export async function handleQuick(
|
|
@@ -102,33 +192,41 @@ export async function handleQuick(
|
|
|
102
192
|
const taskDirRel = `.gsd/quick/${taskNum}-${slug}`;
|
|
103
193
|
const date = new Date().toISOString().split("T")[0];
|
|
104
194
|
|
|
105
|
-
// Create git branch for the quick task
|
|
106
|
-
const
|
|
195
|
+
// Create git branch for the quick task
|
|
196
|
+
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
197
|
+
const git = new GitServiceImpl(basePath, gitPrefs);
|
|
107
198
|
const branchName = `gsd/quick/${taskNum}-${slug}`;
|
|
108
|
-
|
|
199
|
+
let originalBranch = git.getCurrentBranch();
|
|
109
200
|
|
|
110
201
|
let branchCreated = false;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
runGit(basePath, ["checkout", "-b", branchName]);
|
|
122
|
-
branchCreated = true;
|
|
123
|
-
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
// Branch creation failed — continue on current branch
|
|
126
|
-
const message = getErrorMessage(err);
|
|
127
|
-
ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning");
|
|
202
|
+
try {
|
|
203
|
+
const current = originalBranch;
|
|
204
|
+
if (current !== branchName) {
|
|
205
|
+
// Auto-commit any dirty state before switching
|
|
206
|
+
try {
|
|
207
|
+
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
208
|
+
} catch { /* nothing to commit — fine */ }
|
|
209
|
+
|
|
210
|
+
runGit(basePath, ["checkout", "-b", branchName]);
|
|
211
|
+
branchCreated = true;
|
|
128
212
|
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// Branch creation failed — continue on current branch
|
|
215
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
216
|
+
ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning");
|
|
129
217
|
}
|
|
130
218
|
|
|
131
219
|
const actualBranch = branchCreated ? branchName : git.getCurrentBranch();
|
|
220
|
+
if (actualBranch === branchName && originalBranch !== branchName) {
|
|
221
|
+
persistPendingReturn({
|
|
222
|
+
basePath,
|
|
223
|
+
originalBranch,
|
|
224
|
+
quickBranch: branchName,
|
|
225
|
+
taskNum,
|
|
226
|
+
slug,
|
|
227
|
+
description,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
132
230
|
|
|
133
231
|
// Notify user
|
|
134
232
|
ctx.ui.notify(
|
|
@@ -156,106 +254,4 @@ export async function handleQuick(
|
|
|
156
254
|
},
|
|
157
255
|
{ triggerTurn: true },
|
|
158
256
|
);
|
|
159
|
-
|
|
160
|
-
// Schedule branch merge-back after the quick task agent session ends.
|
|
161
|
-
// Without this, auto-mode resumes on the quick-task branch (#1269).
|
|
162
|
-
if (branchCreated && originalBranch) {
|
|
163
|
-
_pendingQuickBranchReturn = {
|
|
164
|
-
basePath,
|
|
165
|
-
originalBranch,
|
|
166
|
-
quickBranch: branchName,
|
|
167
|
-
taskNum,
|
|
168
|
-
slug,
|
|
169
|
-
description,
|
|
170
|
-
};
|
|
171
|
-
// Persist to disk so recovery works across session crashes (#1293).
|
|
172
|
-
persistPendingReturn(_pendingQuickBranchReturn, basePath);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
|
|
177
|
-
let _pendingQuickBranchReturn: {
|
|
178
|
-
basePath: string;
|
|
179
|
-
originalBranch: string;
|
|
180
|
-
quickBranch: string;
|
|
181
|
-
taskNum: number;
|
|
182
|
-
slug: string;
|
|
183
|
-
description: string;
|
|
184
|
-
} | null = null;
|
|
185
|
-
|
|
186
|
-
// ─── Disk Persistence ─────────────────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
/** Path to the pending quick-task return file. */
|
|
189
|
-
function pendingReturnPath(basePath: string): string {
|
|
190
|
-
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Write pending return state to disk. */
|
|
194
|
-
function persistPendingReturn(state: NonNullable<typeof _pendingQuickBranchReturn>, basePath: string): void {
|
|
195
|
-
const filePath = pendingReturnPath(basePath);
|
|
196
|
-
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
|
|
197
|
-
writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Remove pending return file from disk. */
|
|
201
|
-
function clearPendingReturn(basePath: string): void {
|
|
202
|
-
try { unlinkSync(pendingReturnPath(basePath)); } catch { /* already gone */ }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Load pending return from disk (cross-session recovery). */
|
|
206
|
-
function loadPendingReturn(basePath: string): NonNullable<typeof _pendingQuickBranchReturn> | null {
|
|
207
|
-
const filePath = pendingReturnPath(basePath);
|
|
208
|
-
if (!existsSync(filePath)) return null;
|
|
209
|
-
try {
|
|
210
|
-
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Merge the quick-task branch back to the original branch and switch.
|
|
218
|
-
* Called from the agent_end handler after a quick task completes.
|
|
219
|
-
*
|
|
220
|
-
* Checks both in-memory state (same session) and disk state (cross-session
|
|
221
|
-
* recovery for crashed/interrupted sessions).
|
|
222
|
-
*
|
|
223
|
-
* Returns true if a branch return was performed.
|
|
224
|
-
*/
|
|
225
|
-
export function cleanupQuickBranch(): boolean {
|
|
226
|
-
// Prefer in-memory state; fall back to disk for cross-session recovery
|
|
227
|
-
let state = _pendingQuickBranchReturn;
|
|
228
|
-
if (!state) {
|
|
229
|
-
// Try loading from disk — handles the case where the session that
|
|
230
|
-
// started the quick task crashed before agent_end could run (#1293).
|
|
231
|
-
const basePath = process.cwd();
|
|
232
|
-
state = loadPendingReturn(basePath);
|
|
233
|
-
}
|
|
234
|
-
if (!state) return false;
|
|
235
|
-
|
|
236
|
-
_pendingQuickBranchReturn = null;
|
|
237
|
-
const { basePath, originalBranch, quickBranch, taskNum, slug, description } = state;
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
// Auto-commit any remaining work
|
|
241
|
-
try { runGit(basePath, ["add", "-A"]); } catch {}
|
|
242
|
-
try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
|
|
243
|
-
|
|
244
|
-
// Switch back and merge
|
|
245
|
-
runGit(basePath, ["checkout", originalBranch]);
|
|
246
|
-
try {
|
|
247
|
-
runGit(basePath, ["merge", "--squash", quickBranch]);
|
|
248
|
-
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
|
|
249
|
-
} catch { /* merge conflict or nothing — non-fatal */ }
|
|
250
|
-
|
|
251
|
-
// Clean up quick branch
|
|
252
|
-
try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
|
|
253
|
-
|
|
254
|
-
// Clean up disk state
|
|
255
|
-
clearPendingReturn(basePath);
|
|
256
|
-
return true;
|
|
257
|
-
} catch {
|
|
258
|
-
// Cleanup failed — leave disk state for next attempt
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
257
|
}
|