gsd-pi 2.62.1 → 2.63.0

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 (55) hide show
  1. package/dist/resources/extensions/gsd/auto/loop.js +8 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +10 -3
  3. package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
  4. package/dist/resources/extensions/gsd/auto-verification.js +14 -3
  5. package/dist/resources/extensions/gsd/state.js +1 -0
  6. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
  7. package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
  8. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
  9. package/dist/web/standalone/.next/BUILD_ID +1 -1
  10. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  11. package/dist/web/standalone/.next/build-manifest.json +2 -2
  12. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  13. package/dist/web/standalone/.next/required-server-files.json +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  15. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.html +1 -1
  31. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  38. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  39. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  40. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  41. package/dist/web/standalone/server.js +1 -1
  42. package/package.json +1 -1
  43. package/packages/pi-coding-agent/package.json +1 -1
  44. package/pkg/package.json +1 -1
  45. package/src/resources/extensions/gsd/auto/loop.ts +8 -1
  46. package/src/resources/extensions/gsd/auto/phases.ts +8 -6
  47. package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
  48. package/src/resources/extensions/gsd/auto-verification.ts +14 -3
  49. package/src/resources/extensions/gsd/state.ts +1 -0
  50. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
  51. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
  52. package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
  53. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
  54. /package/dist/web/standalone/.next/static/{86gWhNPP3233lZ7KPwda7 → 5FLUBNdqolRyyehCyChPd}/_buildManifest.js +0 -0
  55. /package/dist/web/standalone/.next/static/{86gWhNPP3233lZ7KPwda7 → 5FLUBNdqolRyyehCyChPd}/_ssgManifest.js +0 -0
@@ -31,7 +31,7 @@ import { existsSync, cpSync } from "node:fs";
31
31
  import { logWarning, logError } from "../workflow-logger.js";
32
32
  import { gsdRoot } from "../paths.js";
33
33
  import { atomicWriteSync } from "../atomic-write.js";
34
- import { verifyExpectedArtifact } from "../auto-recovery.js";
34
+ import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
35
35
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
36
36
 
37
37
  // ─── generateMilestoneReport ──────────────────────────────────────────────────
@@ -182,7 +182,7 @@ export async function runPreDispatch(
182
182
  }
183
183
  if (!healthGate.proceed) {
184
184
  ctx.ui.notify(
185
- healthGate.reason ?? "Pre-dispatch health check failed.",
185
+ healthGate.reason || "Pre-dispatch health check failed — run /gsd doctor for details.",
186
186
  "error",
187
187
  );
188
188
  await deps.pauseAuto(ctx, pi);
@@ -628,15 +628,17 @@ export async function runDispatch(
628
628
  unitId,
629
629
  reason: stuckSignal.reason,
630
630
  });
631
+ const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
632
+ const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
633
+ const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
634
+ if (stuckDiag) stuckParts.push(`Expected: ${stuckDiag}`);
635
+ if (stuckRemediation) stuckParts.push(`To recover:\n${stuckRemediation}`);
636
+ ctx.ui.notify(stuckParts.join(" "), "error");
631
637
  await deps.stopAuto(
632
638
  ctx,
633
639
  pi,
634
640
  `Stuck: ${stuckSignal.reason}`,
635
641
  );
636
- ctx.ui.notify(
637
- `Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
638
- "error",
639
- );
640
642
  return { action: "break", reason: "stuck-detected" };
641
643
  }
642
644
  } else {
@@ -33,6 +33,7 @@ import {
33
33
  import {
34
34
  verifyExpectedArtifact,
35
35
  resolveExpectedArtifactPath,
36
+ diagnoseExpectedArtifact,
36
37
  } from "./auto-recovery.js";
37
38
  import { regenerateIfMissing } from "./workflow-projections.js";
38
39
  import { syncStateToProjectRoot } from "./auto-worktree.js";
@@ -476,8 +477,9 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
476
477
  // db_unavailable so the artifact was never written. Retrying would
477
478
  // produce an infinite re-dispatch loop (#2517).
478
479
  debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
480
+ const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
479
481
  ctx.ui.notify(
480
- `Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} but DB is unavailable skipping retry to avoid loop (#2517)`,
482
+ `Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} DB unavailable, skipping retry.${dbSkipDiag ? ` Expected: ${dbSkipDiag}` : ""}`,
481
483
  "error",
482
484
  );
483
485
  } else if (!triggerArtifactVerified) {
@@ -486,14 +488,15 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
486
488
  const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
487
489
  const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
488
490
  s.verificationRetryCount.set(retryKey, attempt);
491
+ const retryDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
489
492
  s.pendingVerificationRetry = {
490
493
  unitId: s.currentUnit.id,
491
- failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt}).`,
494
+ failureContext: `Artifact verification failed: expected artifact for ${s.currentUnit.type} "${s.currentUnit.id}" was not found on disk after unit execution (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`,
492
495
  attempt,
493
496
  };
494
497
  debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
495
498
  ctx.ui.notify(
496
- `Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt})`,
499
+ `Artifact missing for ${s.currentUnit.type} ${s.currentUnit.id} — retrying (attempt ${attempt}).${retryDiag ? ` Expected: ${retryDiag}` : ""}`,
497
500
  "warning",
498
501
  );
499
502
  return "retry";
@@ -196,19 +196,30 @@ export async function runPostUnitVerification(
196
196
  failureContext: formatFailureContext(result),
197
197
  attempt: nextAttempt,
198
198
  };
199
+ const failedCmds = result.checks
200
+ .filter((c) => c.exitCode !== 0)
201
+ .map((c) => c.command);
202
+ const cmdSummary = failedCmds.length <= 3
203
+ ? failedCmds.join(", ")
204
+ : `${failedCmds.slice(0, 3).join(", ")}... and ${failedCmds.length - 3} more`;
199
205
  ctx.ui.notify(
200
- `Verification failed — auto-fix attempt ${nextAttempt}/${maxRetries}`,
206
+ `Verification failed (${cmdSummary}) — auto-fix attempt ${nextAttempt}/${maxRetries}`,
201
207
  "warning",
202
208
  );
203
209
  // Return "retry" — the autoLoop while loop will re-iterate with the retry context
204
210
  return "retry";
205
211
  } else {
206
212
  // Gate failed, retries exhausted
207
- const exhaustedAttempt = attempt + 1;
208
213
  s.verificationRetryCount.delete(s.currentUnit.id);
209
214
  s.pendingVerificationRetry = null;
215
+ const exhaustedFails = result.checks
216
+ .filter((c) => c.exitCode !== 0)
217
+ .map((c) => c.command);
218
+ const exhaustedSummary = exhaustedFails.length <= 3
219
+ ? exhaustedFails.join(", ")
220
+ : `${exhaustedFails.slice(0, 3).join(", ")}... and ${exhaustedFails.length - 3} more`;
210
221
  ctx.ui.notify(
211
- `Verification gate FAILED after ${exhaustedAttempt > maxRetries ? exhaustedAttempt - 1 : exhaustedAttempt} retries — pausing for human review`,
222
+ `Verification gate FAILED after ${attempt} ${attempt === 1 ? "retry" : "retries"} (${exhaustedSummary}) — pausing for human review`,
212
223
  "error",
213
224
  );
214
225
  await pauseAuto(ctx, pi);
@@ -259,6 +259,7 @@ export async function deriveState(basePath: string): Promise<GSDState> {
259
259
  _telemetry.markdownDeriveCount++;
260
260
  }
261
261
  } else {
262
+ logWarning("state", "DB unavailable — using filesystem state derivation (degraded mode)");
262
263
  result = await _deriveStateImpl(basePath);
263
264
  _telemetry.markdownDeriveCount++;
264
265
  }
@@ -217,12 +217,26 @@ describe("workflow-logger", () => {
217
217
  assert.ok(formatted.includes("\n"));
218
218
  });
219
219
 
220
- test("does not include context in formatted output", () => {
220
+ test("includes context fields in formatted output", () => {
221
221
  logError("tool", "failed", { cmd: "complete_task" });
222
222
  const entries = drainLogs();
223
223
  const formatted = formatForNotification(entries);
224
- assert.equal(formatted, "[tool] failed");
225
- assert.ok(!formatted.includes("complete_task"));
224
+ assert.equal(formatted, "[tool] failed (cmd: complete_task)");
225
+ });
226
+
227
+ test("excludes error key from context to avoid redundancy", () => {
228
+ logError("tool", "disk write failed", { error: "ENOSPC", path: "/tmp/foo" });
229
+ const entries = drainLogs();
230
+ const formatted = formatForNotification(entries);
231
+ assert.ok(formatted.includes("path: /tmp/foo"));
232
+ assert.ok(!formatted.includes("error: ENOSPC"));
233
+ });
234
+
235
+ test("formats entry without context unchanged", () => {
236
+ logError("intercept", "blocked write");
237
+ const entries = drainLogs();
238
+ const formatted = formatForNotification(entries);
239
+ assert.equal(formatted, "[intercept] blocked write");
226
240
  });
227
241
  });
228
242
 
@@ -279,44 +293,6 @@ describe("workflow-logger", () => {
279
293
  });
280
294
  });
281
295
 
282
- describe("audit log persistence", () => {
283
- let dir: string;
284
-
285
- beforeEach(() => {
286
- dir = makeTempDir("wl-audit-");
287
- });
288
-
289
- afterEach(() => {
290
- setLogBasePath("");
291
- cleanup(dir);
292
- });
293
-
294
- test("writes entry to .gsd/audit-log.jsonl after setLogBasePath", () => {
295
- setLogBasePath(dir);
296
- logError("engine", "audit test entry");
297
-
298
- const auditPath = join(dir, ".gsd", "audit-log.jsonl");
299
- assert.ok(existsSync(auditPath), "audit-log.jsonl should exist");
300
- const content = readFileSync(auditPath, "utf-8");
301
- const entry = JSON.parse(content.trim());
302
- assert.equal(entry.severity, "error");
303
- assert.equal(entry.component, "engine");
304
- assert.equal(entry.message, "audit test entry");
305
- });
306
-
307
- test("_resetLogs does not clear the audit base path", () => {
308
- setLogBasePath(dir);
309
- _resetLogs();
310
- logError("engine", "post-reset entry");
311
-
312
- const auditPath = join(dir, ".gsd", "audit-log.jsonl");
313
- assert.ok(existsSync(auditPath), "audit-log.jsonl should exist after _resetLogs");
314
- const content = readFileSync(auditPath, "utf-8");
315
- const entry = JSON.parse(content.trim());
316
- assert.equal(entry.message, "post-reset entry");
317
- });
318
- });
319
-
320
296
  describe("new log components (db, dispatch)", () => {
321
297
  test("logError with 'db' component stores correct component", () => {
322
298
  logError("db", "failed to copy DB to worktree", { error: "ENOENT" });
@@ -292,13 +292,11 @@ export async function handleCompleteSlice(
292
292
  // Toggle roadmap checkbox via renderer module
293
293
  const roadmapToggled = await renderRoadmapCheckboxes(basePath, params.milestoneId);
294
294
  if (!roadmapToggled) {
295
- process.stderr.write(
296
- `gsd-db: complete_slice — could not find roadmap for ${params.milestoneId}, skipping checkbox toggle\n`,
297
- );
295
+ logWarning("tool", `complete_slice — could not find roadmap for ${params.milestoneId}, skipping checkbox toggle`);
298
296
  }
299
297
  } catch (renderErr) {
300
298
  // Disk render failed — roll back DB status so state stays consistent
301
- logWarning("tool", `complete_slice — disk render failed, rolling back DB status: ${(renderErr as Error).message}`);
299
+ logWarning("tool", `complete_slice — disk render failed for ${params.milestoneId}/${params.sliceId}, rolling back DB status`, { error: (renderErr as Error).message });
302
300
  updateSliceStatus(params.milestoneId, params.sliceId, 'pending');
303
301
  invalidateStateCache();
304
302
  return { error: `disk render failed: ${(renderErr as Error).message}` };
@@ -325,7 +323,7 @@ export async function handleCompleteSlice(
325
323
  trigger_reason: params.triggerReason,
326
324
  });
327
325
  } catch (hookErr) {
328
- logWarning("tool", `complete-slice post-mutation hook warning: ${(hookErr as Error).message}`);
326
+ logWarning("tool", `complete-slice post-mutation hook failed for ${params.milestoneId}/${params.sliceId}`, { error: (hookErr as Error).message });
329
327
  }
330
328
 
331
329
  return {
@@ -174,17 +174,22 @@ export function summarizeLogs(): string | null {
174
174
 
175
175
  /**
176
176
  * Format entries for display (used by auto-loop post-unit notification).
177
- * Note: context fields are not included in the formatted output.
177
+ * Includes key context fields (file paths, commands) when present.
178
178
  */
179
179
  export function formatForNotification(entries: readonly LogEntry[]): string {
180
180
  if (entries.length === 0) return "";
181
- if (entries.length === 1) {
182
- const e = entries[0];
183
- return `[${e.component}] ${e.message}`;
184
- }
185
- return entries
186
- .map((e) => `[${e.component}] ${e.message}`)
187
- .join("\n");
181
+ return entries.map((e) => {
182
+ let line = `[${e.component}] ${e.message}`;
183
+ if (e.context) {
184
+ const ctxParts = Object.entries(e.context)
185
+ .filter(([k]) => k !== "error") // error is redundant with message
186
+ .map(([k, v]) => v.includes(",") ? `${k}: "${v}"` : `${k}: ${v}`);
187
+ if (ctxParts.length > 0) {
188
+ line += ` (${ctxParts.join(", ")})`;
189
+ }
190
+ }
191
+ return line;
192
+ }).join("\n");
188
193
  }
189
194
 
190
195
  /**
@@ -348,7 +348,9 @@ function _reconcileWorktreeLogsInner(
348
348
  if (conflicts.length > 0) {
349
349
  // D-04: atomic all-or-nothing — block entire merge
350
350
  writeConflictsFile(mainBasePath, conflicts, worktreeBasePath);
351
- logError("reconcile", `${conflicts.length} conflict(s) detected`, { count: String(conflicts.length), path: join(mainBasePath, ".gsd", "CONFLICTS.md") });
351
+ const conflictSummary = conflicts.slice(0, 3).map(c => `${c.entityType}:${c.entityId}`).join(", ");
352
+ const truncated = conflicts.length > 3 ? `... and ${conflicts.length - 3} more` : "";
353
+ logError("reconcile", `${conflicts.length} conflict(s) detected on ${conflictSummary}${truncated}. Details: .gsd/CONFLICTS.md`, { count: String(conflicts.length), path: join(mainBasePath, ".gsd", "CONFLICTS.md") });
352
354
  return { autoMerged: 0, conflicts };
353
355
  }
354
356