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
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import { gsdRoot } from "./paths.js";
|
|
8
|
-
import { parseUnitId } from "./unit-id.js";
|
|
9
7
|
// ─── Hook Queue State ──────────────────────────────────────────────────────
|
|
10
8
|
/** Currently executing hook, or null if in normal dispatch flow. */
|
|
11
9
|
let activeHook = null;
|
|
@@ -112,7 +110,7 @@ function dequeueNextHook(basePath) {
|
|
|
112
110
|
pendingRetry: false,
|
|
113
111
|
};
|
|
114
112
|
// Build the prompt with variable substitution
|
|
115
|
-
const
|
|
113
|
+
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
116
114
|
const prompt = config.prompt
|
|
117
115
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
118
116
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -164,14 +162,16 @@ function handleHookCompletion(basePath) {
|
|
|
164
162
|
* - Milestone-level (M001): .gsd/M001/{artifact}
|
|
165
163
|
*/
|
|
166
164
|
export function resolveHookArtifactPath(basePath, unitId, artifactName) {
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
165
|
+
const parts = unitId.split("/");
|
|
166
|
+
if (parts.length === 3) {
|
|
167
|
+
const [mid, sid, tid] = parts;
|
|
168
|
+
return join(basePath, ".gsd", mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
170
169
|
}
|
|
171
|
-
if (
|
|
172
|
-
|
|
170
|
+
if (parts.length === 2) {
|
|
171
|
+
const [mid, sid] = parts;
|
|
172
|
+
return join(basePath, ".gsd", mid, "slices", sid, artifactName);
|
|
173
173
|
}
|
|
174
|
-
return join(
|
|
174
|
+
return join(basePath, ".gsd", parts[0], artifactName);
|
|
175
175
|
}
|
|
176
176
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
177
177
|
// Phase 2: Pre-Dispatch Hooks
|
|
@@ -196,7 +196,7 @@ export function runPreDispatchHooks(unitType, unitId, prompt, basePath) {
|
|
|
196
196
|
if (hooks.length === 0) {
|
|
197
197
|
return { action: "proceed", prompt, firedHooks: [] };
|
|
198
198
|
}
|
|
199
|
-
const
|
|
199
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
200
200
|
const substitute = (text) => text
|
|
201
201
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
202
202
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -246,7 +246,7 @@ export function runPreDispatchHooks(unitType, unitId, prompt, basePath) {
|
|
|
246
246
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
247
247
|
const HOOK_STATE_FILE = "hook-state.json";
|
|
248
248
|
function hookStatePath(basePath) {
|
|
249
|
-
return join(
|
|
249
|
+
return join(basePath, ".gsd", HOOK_STATE_FILE);
|
|
250
250
|
}
|
|
251
251
|
/**
|
|
252
252
|
* Persist current hook cycle counts to disk so they survive crashes/restarts.
|
|
@@ -258,7 +258,7 @@ export function persistHookState(basePath) {
|
|
|
258
258
|
savedAt: new Date().toISOString(),
|
|
259
259
|
};
|
|
260
260
|
try {
|
|
261
|
-
const dir =
|
|
261
|
+
const dir = join(basePath, ".gsd");
|
|
262
262
|
if (!existsSync(dir))
|
|
263
263
|
mkdirSync(dir, { recursive: true });
|
|
264
264
|
writeFileSync(hookStatePath(basePath), JSON.stringify(state, null, 2), "utf-8");
|
|
@@ -386,7 +386,7 @@ export function triggerHookManually(hookName, unitType, unitId, basePath) {
|
|
|
386
386
|
// Update active hook with the cycle count
|
|
387
387
|
activeHook.cycle = currentCycle;
|
|
388
388
|
// Build the prompt with variable substitution
|
|
389
|
-
const
|
|
389
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
390
390
|
const prompt = hook.prompt
|
|
391
391
|
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
392
392
|
.replace(/\{sliceId\}/g, sid ?? "")
|
|
@@ -10,188 +10,93 @@
|
|
|
10
10
|
* tracking, stuck detection counters, and working-tree activity.
|
|
11
11
|
*/
|
|
12
12
|
import { getHealthTrend, getConsecutiveErrorUnits, getHealthHistory, } from "./doctor-proactive.js";
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
function escalateLevel(level, next) {
|
|
14
|
+
const ranks = {
|
|
15
|
+
green: 0,
|
|
16
|
+
yellow: 1,
|
|
17
|
+
red: 2,
|
|
18
|
+
};
|
|
19
|
+
return ranks[next] > ranks[level] ? next : level;
|
|
20
|
+
}
|
|
21
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Compute the current progress score from health signals.
|
|
24
|
+
*/
|
|
25
|
+
export function computeProgressScore() {
|
|
26
|
+
const signals = [];
|
|
27
|
+
let level = "green";
|
|
28
|
+
// Check consecutive errors
|
|
29
|
+
const consecutiveErrors = getConsecutiveErrorUnits();
|
|
30
|
+
if (consecutiveErrors >= 3) {
|
|
31
|
+
signals.push({ kind: "negative", label: `${consecutiveErrors} consecutive error units` });
|
|
32
|
+
level = escalateLevel(level, "red");
|
|
33
|
+
}
|
|
34
|
+
else if (consecutiveErrors >= 1) {
|
|
35
|
+
signals.push({ kind: "negative", label: `${consecutiveErrors} consecutive error unit(s)` });
|
|
36
|
+
level = escalateLevel(level, "yellow");
|
|
37
|
+
}
|
|
38
|
+
// Check health trend
|
|
15
39
|
const trend = getHealthTrend();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
case "stable":
|
|
20
|
-
return { name: "health_trend", level: "green", detail: "Health stable" };
|
|
21
|
-
case "degrading":
|
|
22
|
-
return { name: "health_trend", level: "red", detail: "Health degrading" };
|
|
23
|
-
case "unknown":
|
|
24
|
-
return { name: "health_trend", level: "green", detail: "Insufficient data" };
|
|
40
|
+
if (trend === "degrading") {
|
|
41
|
+
signals.push({ kind: "negative", label: "Health trend declining" });
|
|
42
|
+
level = escalateLevel(level, "yellow");
|
|
25
43
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const streak = getConsecutiveErrorUnits();
|
|
29
|
-
if (streak === 0) {
|
|
30
|
-
return { name: "error_streak", level: "green", detail: "No consecutive errors" };
|
|
44
|
+
else if (trend === "improving") {
|
|
45
|
+
signals.push({ kind: "positive", label: "Health trend improving" });
|
|
31
46
|
}
|
|
32
|
-
if (
|
|
33
|
-
|
|
47
|
+
else if (trend === "stable") {
|
|
48
|
+
signals.push({ kind: "neutral", label: "Health trend stable" });
|
|
34
49
|
}
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
function evaluateRecentErrors() {
|
|
50
|
+
// Check recent history
|
|
38
51
|
const history = getHealthHistory();
|
|
39
52
|
if (history.length === 0) {
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
const latest = history[history.length - 1];
|
|
43
|
-
if (latest.errors === 0 && latest.warnings <= 1) {
|
|
44
|
-
return { name: "recent_errors", level: "green", detail: `${latest.errors}E/${latest.warnings}W` };
|
|
45
|
-
}
|
|
46
|
-
if (latest.errors === 0) {
|
|
47
|
-
return { name: "recent_errors", level: "yellow", detail: `${latest.warnings} warning(s)` };
|
|
48
|
-
}
|
|
49
|
-
if (latest.errors <= 2) {
|
|
50
|
-
return { name: "recent_errors", level: "yellow", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
51
|
-
}
|
|
52
|
-
return { name: "recent_errors", level: "red", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
53
|
-
}
|
|
54
|
-
function evaluateArtifactProduction() {
|
|
55
|
-
const history = getHealthHistory();
|
|
56
|
-
if (history.length < 2) {
|
|
57
|
-
return { name: "artifact_production", level: "green", detail: "Insufficient data" };
|
|
58
|
-
}
|
|
59
|
-
const totalFixes = history.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
60
|
-
const recent = history.slice(-3);
|
|
61
|
-
const recentFixes = recent.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
62
|
-
// If recent units are all producing fixes but errors aren't decreasing,
|
|
63
|
-
// doctor is fighting fires but not making headway
|
|
64
|
-
if (recentFixes > 3 && recent.every(s => s.errors > 0)) {
|
|
65
|
-
return { name: "artifact_production", level: "yellow", detail: "Doctor applying fixes but errors persist" };
|
|
66
|
-
}
|
|
67
|
-
return { name: "artifact_production", level: "green", detail: `${totalFixes} total fixes applied` };
|
|
68
|
-
}
|
|
69
|
-
function evaluateDispatchVelocity() {
|
|
70
|
-
const history = getHealthHistory();
|
|
71
|
-
if (history.length < 3) {
|
|
72
|
-
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
73
|
-
}
|
|
74
|
-
// Check time between recent snapshots — are units completing at a reasonable rate?
|
|
75
|
-
const recent = history.slice(-5);
|
|
76
|
-
if (recent.length < 2) {
|
|
77
|
-
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
78
|
-
}
|
|
79
|
-
const timeDiffs = [];
|
|
80
|
-
for (let i = 1; i < recent.length; i++) {
|
|
81
|
-
timeDiffs.push(recent[i].timestamp - recent[i - 1].timestamp);
|
|
53
|
+
signals.push({ kind: "neutral", label: "No health data yet" });
|
|
82
54
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
return { name: "dispatch_velocity", level: "green", detail: `Units averaging ${avgTimeMins || "<1"}min each` };
|
|
90
|
-
}
|
|
91
|
-
// ── Main API ───────────────────────────────────────────────────────────────
|
|
92
|
-
/**
|
|
93
|
-
* Compute the current progress score by evaluating all available signals.
|
|
94
|
-
* Returns a composite score with individual signal details.
|
|
95
|
-
*/
|
|
96
|
-
export function computeProgressScore() {
|
|
97
|
-
const signals = [
|
|
98
|
-
evaluateHealthTrend(),
|
|
99
|
-
evaluateErrorStreak(),
|
|
100
|
-
evaluateRecentErrors(),
|
|
101
|
-
evaluateArtifactProduction(),
|
|
102
|
-
evaluateDispatchVelocity(),
|
|
103
|
-
];
|
|
104
|
-
// Overall level: worst of all signals
|
|
105
|
-
const level = signals.some(s => s.level === "red")
|
|
106
|
-
? "red"
|
|
107
|
-
: signals.some(s => s.level === "yellow")
|
|
108
|
-
? "yellow"
|
|
109
|
-
: "green";
|
|
110
|
-
// Build summary from the most important signals
|
|
111
|
-
const summary = buildSummary(level, signals);
|
|
55
|
+
const summary = level === "green"
|
|
56
|
+
? "Progressing well"
|
|
57
|
+
: level === "yellow"
|
|
58
|
+
? "Some issues detected"
|
|
59
|
+
: "Stuck or erroring";
|
|
112
60
|
return { level, summary, signals };
|
|
113
61
|
}
|
|
114
62
|
/**
|
|
115
|
-
* Compute progress score with additional context
|
|
63
|
+
* Compute progress score with additional context for dashboard display.
|
|
116
64
|
*/
|
|
117
65
|
export function computeProgressScoreWithContext(context) {
|
|
118
66
|
const base = computeProgressScore();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
: context.retryCount <= 2
|
|
124
|
-
? { name: "retry_count", level: "yellow", detail: `Retry ${context.retryCount}/${context.maxRetries}` }
|
|
125
|
-
: { name: "retry_count", level: "red", detail: `Retry ${context.retryCount}/${context.maxRetries} — looping` };
|
|
126
|
-
base.signals.push(retrySignal);
|
|
127
|
-
// Re-evaluate level
|
|
128
|
-
if (retrySignal.level === "red")
|
|
129
|
-
base.level = "red";
|
|
130
|
-
else if (retrySignal.level === "yellow" && base.level === "green")
|
|
131
|
-
base.level = "yellow";
|
|
67
|
+
if (context.sameUnitCount && context.sameUnitCount >= 3) {
|
|
68
|
+
base.signals.push({ kind: "negative", label: `Same unit dispatched ${context.sameUnitCount}× consecutively` });
|
|
69
|
+
base.level = escalateLevel(base.level, "red");
|
|
70
|
+
base.summary = "Stuck on same unit";
|
|
132
71
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
// ── Formatting ─────────────────────────────────────────────────────────────
|
|
138
|
-
function buildSummary(level, signals) {
|
|
139
|
-
switch (level) {
|
|
140
|
-
case "green":
|
|
141
|
-
return "Progressing well";
|
|
142
|
-
case "yellow": {
|
|
143
|
-
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
144
|
-
return `Struggling — ${issues[0] ?? "minor issues detected"}`;
|
|
145
|
-
}
|
|
146
|
-
case "red": {
|
|
147
|
-
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
148
|
-
return `Stuck — ${issues[0] ?? "critical issues detected"}`;
|
|
149
|
-
}
|
|
72
|
+
else if (context.sameUnitCount && context.sameUnitCount >= 2) {
|
|
73
|
+
base.signals.push({ kind: "negative", label: `Same unit dispatched ${context.sameUnitCount}×` });
|
|
74
|
+
base.level = escalateLevel(base.level, "yellow");
|
|
150
75
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
? ` (${context.completedUnits} of ${context.totalUnits} done)`
|
|
158
|
-
: "";
|
|
159
|
-
switch (level) {
|
|
160
|
-
case "green":
|
|
161
|
-
return `Progressing well —${unitLabel}${progressLabel}`;
|
|
162
|
-
case "yellow": {
|
|
163
|
-
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
164
|
-
const retryInfo = context.retryCount ? `, attempt ${context.retryCount}/${context.maxRetries}` : "";
|
|
165
|
-
return `Struggling —${unitLabel}${retryInfo}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "issues detected"}`;
|
|
166
|
-
}
|
|
167
|
-
case "red": {
|
|
168
|
-
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
169
|
-
return `Stuck —${unitLabel}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "critical issues"}`;
|
|
170
|
-
}
|
|
76
|
+
if (context.recoveryCount && context.recoveryCount > 0) {
|
|
77
|
+
base.signals.push({ kind: "negative", label: `${context.recoveryCount} recovery attempts` });
|
|
78
|
+
base.level = escalateLevel(base.level, "yellow");
|
|
79
|
+
}
|
|
80
|
+
if (context.completedCount && context.completedCount > 0) {
|
|
81
|
+
base.signals.push({ kind: "positive", label: `${context.completedCount} units completed` });
|
|
171
82
|
}
|
|
83
|
+
return base;
|
|
172
84
|
}
|
|
173
85
|
/**
|
|
174
|
-
* Format
|
|
86
|
+
* Format a one-line progress indicator for dashboard/status display.
|
|
175
87
|
*/
|
|
176
88
|
export function formatProgressLine(score) {
|
|
177
|
-
const icon = score.level === "green" ? "
|
|
178
|
-
: score.level === "yellow" ? "\uD83D\uDFE1"
|
|
179
|
-
: "\uD83D\uDD34";
|
|
89
|
+
const icon = score.level === "green" ? "●" : score.level === "yellow" ? "◐" : "○";
|
|
180
90
|
return `${icon} ${score.summary}`;
|
|
181
91
|
}
|
|
182
92
|
/**
|
|
183
|
-
* Format a
|
|
93
|
+
* Format a multi-line progress report.
|
|
184
94
|
*/
|
|
185
95
|
export function formatProgressReport(score) {
|
|
186
|
-
const lines = [];
|
|
187
|
-
lines.push(formatProgressLine(score));
|
|
188
|
-
lines.push("");
|
|
189
|
-
lines.push("Signals:");
|
|
96
|
+
const lines = [formatProgressLine(score)];
|
|
190
97
|
for (const signal of score.signals) {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
: "\uD83D\uDED1";
|
|
194
|
-
lines.push(` ${icon} ${signal.name}: ${signal.detail}`);
|
|
98
|
+
const prefix = signal.kind === "positive" ? " ✓" : signal.kind === "negative" ? " ✗" : " ·";
|
|
99
|
+
lines.push(`${prefix} ${signal.label}`);
|
|
195
100
|
}
|
|
196
101
|
return lines.join("\n");
|
|
197
102
|
}
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* Quick tasks live in `.gsd/quick/` and are tracked in STATE.md's
|
|
9
9
|
* "Quick Tasks Completed" table.
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { loadPrompt } from "./prompt-loader.js";
|
|
14
14
|
import { gsdRoot } from "./paths.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { GitServiceImpl, runGit } from "./git-service.js";
|
|
16
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
17
|
+
import { nativeHasStagedChanges } from "./native-git-bridge.js";
|
|
18
|
+
let pendingQuickReturn = null;
|
|
17
19
|
// ─── Quick Task Helpers ───────────────────────────────────────────────────────
|
|
18
20
|
/**
|
|
19
21
|
* Generate a URL-friendly slug from a description.
|
|
@@ -62,6 +64,71 @@ function ensureQuickDir(basePath, taskNum, slug) {
|
|
|
62
64
|
mkdirSync(taskDir, { recursive: true });
|
|
63
65
|
return taskDir;
|
|
64
66
|
}
|
|
67
|
+
function quickReturnStatePath(basePath) {
|
|
68
|
+
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
69
|
+
}
|
|
70
|
+
function persistPendingReturn(state) {
|
|
71
|
+
pendingQuickReturn = state;
|
|
72
|
+
mkdirSync(join(gsdRoot(state.basePath), "runtime"), { recursive: true });
|
|
73
|
+
writeFileSync(quickReturnStatePath(state.basePath), JSON.stringify(state) + "\n", "utf-8");
|
|
74
|
+
}
|
|
75
|
+
function readPendingReturn(basePath) {
|
|
76
|
+
if (pendingQuickReturn && pendingQuickReturn.basePath === basePath) {
|
|
77
|
+
return pendingQuickReturn;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(quickReturnStatePath(basePath), "utf-8");
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
if (typeof parsed.basePath === "string"
|
|
83
|
+
&& typeof parsed.originalBranch === "string"
|
|
84
|
+
&& typeof parsed.quickBranch === "string"
|
|
85
|
+
&& typeof parsed.taskNum === "number"
|
|
86
|
+
&& typeof parsed.slug === "string"
|
|
87
|
+
&& typeof parsed.description === "string") {
|
|
88
|
+
pendingQuickReturn = parsed;
|
|
89
|
+
return pendingQuickReturn;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// No persisted quick-return state
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function clearPendingReturn(basePath) {
|
|
98
|
+
if (pendingQuickReturn?.basePath === basePath) {
|
|
99
|
+
pendingQuickReturn = null;
|
|
100
|
+
}
|
|
101
|
+
rmSync(quickReturnStatePath(basePath), { force: true });
|
|
102
|
+
}
|
|
103
|
+
function hasStagedChanges(basePath) {
|
|
104
|
+
return nativeHasStagedChanges(basePath);
|
|
105
|
+
}
|
|
106
|
+
export function cleanupQuickBranch(basePath = process.cwd()) {
|
|
107
|
+
const state = readPendingReturn(basePath);
|
|
108
|
+
if (!state)
|
|
109
|
+
return false;
|
|
110
|
+
const repoPath = state.basePath;
|
|
111
|
+
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
112
|
+
const git = new GitServiceImpl(repoPath, gitPrefs);
|
|
113
|
+
if (git.getCurrentBranch() === state.quickBranch) {
|
|
114
|
+
try {
|
|
115
|
+
git.autoCommit("quick-task", `Q${state.taskNum}`, []);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Best-effort: quick work may already be committed.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (git.getCurrentBranch() !== state.originalBranch) {
|
|
122
|
+
runGit(repoPath, ["checkout", state.originalBranch]);
|
|
123
|
+
}
|
|
124
|
+
runGit(repoPath, ["merge", "--squash", state.quickBranch]);
|
|
125
|
+
if (hasStagedChanges(repoPath)) {
|
|
126
|
+
runGit(repoPath, ["commit", "-m", `quick(Q${state.taskNum}): ${state.slug}`]);
|
|
127
|
+
}
|
|
128
|
+
runGit(repoPath, ["branch", "-D", state.quickBranch], { allowFailure: true });
|
|
129
|
+
clearPendingReturn(repoPath);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
65
132
|
// ─── Main Handler ─────────────────────────────────────────────────────────────
|
|
66
133
|
export async function handleQuick(args, ctx, pi) {
|
|
67
134
|
const basePath = process.cwd();
|
|
@@ -84,32 +151,40 @@ export async function handleQuick(args, ctx, pi) {
|
|
|
84
151
|
const taskDir = ensureQuickDir(basePath, taskNum, slug);
|
|
85
152
|
const taskDirRel = `.gsd/quick/${taskNum}-${slug}`;
|
|
86
153
|
const date = new Date().toISOString().split("T")[0];
|
|
87
|
-
// Create git branch for the quick task
|
|
88
|
-
const
|
|
154
|
+
// Create git branch for the quick task
|
|
155
|
+
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
156
|
+
const git = new GitServiceImpl(basePath, gitPrefs);
|
|
89
157
|
const branchName = `gsd/quick/${taskNum}-${slug}`;
|
|
90
|
-
|
|
158
|
+
let originalBranch = git.getCurrentBranch();
|
|
91
159
|
let branchCreated = false;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
100
|
-
}
|
|
101
|
-
catch { /* nothing to commit — fine */ }
|
|
102
|
-
runGit(basePath, ["checkout", "-b", branchName]);
|
|
103
|
-
branchCreated = true;
|
|
160
|
+
try {
|
|
161
|
+
const current = originalBranch;
|
|
162
|
+
if (current !== branchName) {
|
|
163
|
+
// Auto-commit any dirty state before switching
|
|
164
|
+
try {
|
|
165
|
+
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
104
166
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const message = getErrorMessage(err);
|
|
109
|
-
ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning");
|
|
167
|
+
catch { /* nothing to commit — fine */ }
|
|
168
|
+
runGit(basePath, ["checkout", "-b", branchName]);
|
|
169
|
+
branchCreated = true;
|
|
110
170
|
}
|
|
111
171
|
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// Branch creation failed — continue on current branch
|
|
174
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
175
|
+
ctx.ui.notify(`Could not create branch ${branchName}: ${message}. Working on current branch.`, "warning");
|
|
176
|
+
}
|
|
112
177
|
const actualBranch = branchCreated ? branchName : git.getCurrentBranch();
|
|
178
|
+
if (actualBranch === branchName && originalBranch !== branchName) {
|
|
179
|
+
persistPendingReturn({
|
|
180
|
+
basePath,
|
|
181
|
+
originalBranch,
|
|
182
|
+
quickBranch: branchName,
|
|
183
|
+
taskNum,
|
|
184
|
+
slug,
|
|
185
|
+
description,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
113
188
|
// Notify user
|
|
114
189
|
ctx.ui.notify(`Quick task ${taskNum}: ${description}\nDirectory: ${taskDirRel}\nBranch: ${actualBranch}`, "info");
|
|
115
190
|
// Build and dispatch the quick task prompt
|
|
@@ -128,103 +203,4 @@ export async function handleQuick(args, ctx, pi) {
|
|
|
128
203
|
content: prompt,
|
|
129
204
|
display: false,
|
|
130
205
|
}, { triggerTurn: true });
|
|
131
|
-
// Schedule branch merge-back after the quick task agent session ends.
|
|
132
|
-
// Without this, auto-mode resumes on the quick-task branch (#1269).
|
|
133
|
-
if (branchCreated && originalBranch) {
|
|
134
|
-
_pendingQuickBranchReturn = {
|
|
135
|
-
basePath,
|
|
136
|
-
originalBranch,
|
|
137
|
-
quickBranch: branchName,
|
|
138
|
-
taskNum,
|
|
139
|
-
slug,
|
|
140
|
-
description,
|
|
141
|
-
};
|
|
142
|
-
// Persist to disk so recovery works across session crashes (#1293).
|
|
143
|
-
persistPendingReturn(_pendingQuickBranchReturn, basePath);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
|
|
147
|
-
let _pendingQuickBranchReturn = null;
|
|
148
|
-
// ─── Disk Persistence ─────────────────────────────────────────────────────
|
|
149
|
-
/** Path to the pending quick-task return file. */
|
|
150
|
-
function pendingReturnPath(basePath) {
|
|
151
|
-
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
152
|
-
}
|
|
153
|
-
/** Write pending return state to disk. */
|
|
154
|
-
function persistPendingReturn(state, basePath) {
|
|
155
|
-
const filePath = pendingReturnPath(basePath);
|
|
156
|
-
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
|
|
157
|
-
writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
158
|
-
}
|
|
159
|
-
/** Remove pending return file from disk. */
|
|
160
|
-
function clearPendingReturn(basePath) {
|
|
161
|
-
try {
|
|
162
|
-
unlinkSync(pendingReturnPath(basePath));
|
|
163
|
-
}
|
|
164
|
-
catch { /* already gone */ }
|
|
165
|
-
}
|
|
166
|
-
/** Load pending return from disk (cross-session recovery). */
|
|
167
|
-
function loadPendingReturn(basePath) {
|
|
168
|
-
const filePath = pendingReturnPath(basePath);
|
|
169
|
-
if (!existsSync(filePath))
|
|
170
|
-
return null;
|
|
171
|
-
try {
|
|
172
|
-
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Merge the quick-task branch back to the original branch and switch.
|
|
180
|
-
* Called from the agent_end handler after a quick task completes.
|
|
181
|
-
*
|
|
182
|
-
* Checks both in-memory state (same session) and disk state (cross-session
|
|
183
|
-
* recovery for crashed/interrupted sessions).
|
|
184
|
-
*
|
|
185
|
-
* Returns true if a branch return was performed.
|
|
186
|
-
*/
|
|
187
|
-
export function cleanupQuickBranch() {
|
|
188
|
-
// Prefer in-memory state; fall back to disk for cross-session recovery
|
|
189
|
-
let state = _pendingQuickBranchReturn;
|
|
190
|
-
if (!state) {
|
|
191
|
-
// Try loading from disk — handles the case where the session that
|
|
192
|
-
// started the quick task crashed before agent_end could run (#1293).
|
|
193
|
-
const basePath = process.cwd();
|
|
194
|
-
state = loadPendingReturn(basePath);
|
|
195
|
-
}
|
|
196
|
-
if (!state)
|
|
197
|
-
return false;
|
|
198
|
-
_pendingQuickBranchReturn = null;
|
|
199
|
-
const { basePath, originalBranch, quickBranch, taskNum, slug, description } = state;
|
|
200
|
-
try {
|
|
201
|
-
// Auto-commit any remaining work
|
|
202
|
-
try {
|
|
203
|
-
runGit(basePath, ["add", "-A"]);
|
|
204
|
-
}
|
|
205
|
-
catch { }
|
|
206
|
-
try {
|
|
207
|
-
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]);
|
|
208
|
-
}
|
|
209
|
-
catch { }
|
|
210
|
-
// Switch back and merge
|
|
211
|
-
runGit(basePath, ["checkout", originalBranch]);
|
|
212
|
-
try {
|
|
213
|
-
runGit(basePath, ["merge", "--squash", quickBranch]);
|
|
214
|
-
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
|
|
215
|
-
}
|
|
216
|
-
catch { /* merge conflict or nothing — non-fatal */ }
|
|
217
|
-
// Clean up quick branch
|
|
218
|
-
try {
|
|
219
|
-
runGit(basePath, ["branch", "-D", quickBranch]);
|
|
220
|
-
}
|
|
221
|
-
catch { }
|
|
222
|
-
// Clean up disk state
|
|
223
|
-
clearPendingReturn(basePath);
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// Cleanup failed — leave disk state for next attempt
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
206
|
}
|
|
@@ -129,6 +129,18 @@ function ensureExitHandler(gsdDir) {
|
|
|
129
129
|
*/
|
|
130
130
|
export function acquireSessionLock(basePath) {
|
|
131
131
|
const lp = lockPath(basePath);
|
|
132
|
+
// Re-entrant acquire on the same path: release our current OS lock first so
|
|
133
|
+
// proper-lockfile clears its update timer before we acquire a fresh lock.
|
|
134
|
+
if (_releaseFunction && _lockedPath === basePath) {
|
|
135
|
+
try {
|
|
136
|
+
_releaseFunction();
|
|
137
|
+
}
|
|
138
|
+
catch { /* may already be released */ }
|
|
139
|
+
_releaseFunction = null;
|
|
140
|
+
_lockedPath = null;
|
|
141
|
+
_lockPid = 0;
|
|
142
|
+
_lockCompromised = false;
|
|
143
|
+
}
|
|
132
144
|
// Ensure the directory exists
|
|
133
145
|
mkdirSync(dirname(lp), { recursive: true });
|
|
134
146
|
// Clean up numbered lock file variants from cloud sync conflicts (#1315)
|
|
@@ -206,6 +218,7 @@ export function acquireSessionLock(basePath) {
|
|
|
206
218
|
_releaseFunction = release;
|
|
207
219
|
_lockedPath = basePath;
|
|
208
220
|
_lockPid = process.pid;
|
|
221
|
+
_lockCompromised = false;
|
|
209
222
|
// Safety net — uses centralized handler to avoid double-registration
|
|
210
223
|
ensureExitHandler(gsdDir);
|
|
211
224
|
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|