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.
- package/dist/resources/extensions/gsd/auto/loop.js +8 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
- package/dist/resources/extensions/gsd/auto-verification.js +14 -3
- package/dist/resources/extensions/gsd/state.js +1 -0
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
- package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +8 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-verification.ts +14 -3
- package/src/resources/extensions/gsd/state.ts +1 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
- package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
- /package/dist/web/standalone/.next/static/{86gWhNPP3233lZ7KPwda7 → 5FLUBNdqolRyyehCyChPd}/_buildManifest.js +0 -0
- /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
|
|
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}
|
|
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 ${
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|