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.
Files changed (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. 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 { milestone: mid, slice: sid, task: tid } = parseUnitId(triggerUnitId);
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 { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
168
- if (mid && sid && tid) {
169
- return join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
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 (mid && sid) {
172
- return join(gsdRoot(basePath), mid, "slices", sid, artifactName);
170
+ if (parts.length === 2) {
171
+ const [mid, sid] = parts;
172
+ return join(basePath, ".gsd", mid, "slices", sid, artifactName);
173
173
  }
174
- return join(gsdRoot(basePath), mid, artifactName);
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 { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
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(gsdRoot(basePath), HOOK_STATE_FILE);
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 = gsdRoot(basePath);
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 { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
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
- // ── Signal Evaluators ──────────────────────────────────────────────────────
14
- function evaluateHealthTrend() {
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
- switch (trend) {
17
- case "improving":
18
- return { name: "health_trend", level: "green", detail: "Health improving" };
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
- function evaluateErrorStreak() {
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 (streak <= 2) {
33
- return { name: "error_streak", level: "yellow", detail: `${streak} consecutive error unit(s)` };
47
+ else if (trend === "stable") {
48
+ signals.push({ kind: "neutral", label: "Health trend stable" });
34
49
  }
35
- return { name: "error_streak", level: "red", detail: `${streak} consecutive error units` };
36
- }
37
- function evaluateRecentErrors() {
50
+ // Check recent history
38
51
  const history = getHealthHistory();
39
52
  if (history.length === 0) {
40
- return { name: "recent_errors", level: "green", detail: "No health data yet" };
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 avgTimeMs = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length;
84
- const avgTimeMins = Math.round(avgTimeMs / 60_000);
85
- // If average unit time is > 15 minutes, something might be wrong
86
- if (avgTimeMins > 15) {
87
- return { name: "dispatch_velocity", level: "yellow", detail: `Units averaging ${avgTimeMins}min each` };
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 from the current unit.
63
+ * Compute progress score with additional context for dashboard display.
116
64
  */
117
65
  export function computeProgressScoreWithContext(context) {
118
66
  const base = computeProgressScore();
119
- // Add retry signal if available
120
- if (context.retryCount !== undefined && context.maxRetries !== undefined) {
121
- const retrySignal = context.retryCount === 0
122
- ? { name: "retry_count", level: "green", detail: "No retries" }
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
- // Build richer summary with context
134
- base.summary = buildSummaryWithContext(base.level, base.signals, context);
135
- return base;
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
- function buildSummaryWithContext(level, signals, context) {
153
- const unitLabel = context.currentUnitId
154
- ? ` ${context.currentUnitId}`
155
- : "";
156
- const progressLabel = context.completedUnits !== undefined && context.totalUnits !== undefined
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 progress score as a single-line traffic light for TUI display.
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" ? "\uD83D\uDFE2"
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 detailed progress report showing all signals.
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 icon = signal.level === "green" ? "\u2705"
192
- : signal.level === "yellow" ? "\u26A0\uFE0F"
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, readFileSync, writeFileSync, unlinkSync } from "node:fs";
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 { createGitService, runGit } from "./git-service.js";
16
- import { getErrorMessage } from "./error-utils.js";
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 (unless isolation: none)
88
- const git = createGitService(basePath);
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
- const skipBranch = git.prefs.isolation === "none";
158
+ let originalBranch = git.getCurrentBranch();
91
159
  let branchCreated = false;
92
- let originalBranch;
93
- if (!skipBranch) {
94
- try {
95
- originalBranch = git.getCurrentBranch();
96
- if (originalBranch !== branchName) {
97
- // Auto-commit any dirty state before switching
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
- catch (err) {
107
- // Branch creation failed — continue on current branch
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));
@@ -30,6 +30,7 @@ token_profile:
30
30
  phases:
31
31
  skip_research:
32
32
  skip_reassess:
33
+ reassess_after_slice:
33
34
  skip_slice_research:
34
35
  dynamic_routing:
35
36
  enabled: