gsd-pi 2.82.0-dev.2841a1e44 → 2.82.0-dev.9d5798940
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/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +2 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
- package/dist/resources/extensions/gsd/auto-post-unit.js +69 -8
- package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
- package/dist/resources/extensions/gsd/auto-start.js +7 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +13 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +17 -1
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
- package/dist/resources/extensions/gsd/export-html.js +27 -425
- package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
- package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
- package/dist/resources/extensions/gsd/unit-context-manifest.js +7 -8
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +28 -7
- package/dist/resources/extensions/shared/html-shell.js +388 -0
- package/dist/resources/extensions/visual-brief/page-contract.js +2 -0
- package/dist/resources/extensions/visual-brief/prompts.js +29 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -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 +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -7
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- 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 +4 -5
- 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 +2 -5
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -7
- 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 +4 -7
- 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 +4 -5
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6a95bc41e0f7ec89.js → webpack-9a4db269f9ed63ad.js} +1 -1
- package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
- package/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +76 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
- package/src/resources/extensions/gsd/auto-start.ts +7 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +17 -1
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
- package/src/resources/extensions/gsd/export-html.ts +27 -427
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
- package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
- package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +65 -7
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +31 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +12 -9
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +34 -7
- package/src/resources/extensions/shared/html-shell.ts +412 -0
- package/src/resources/extensions/visual-brief/page-contract.ts +2 -0
- package/src/resources/extensions/visual-brief/prompts.ts +37 -1
- package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +40 -0
- package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
- package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
- package/dist/web/standalone/.next/static/css/0262768ec1b89d34.css +0 -1
- package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
- package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
- package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
- /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
resolveExpectedArtifactPath,
|
|
42
42
|
writeBlockerPlaceholder,
|
|
43
43
|
diagnoseExpectedArtifact,
|
|
44
|
+
diagnoseWorktreeIntegrityFailure,
|
|
44
45
|
} from "./auto-recovery.js";
|
|
45
46
|
import { regenerateIfMissing } from "./workflow-projections.js";
|
|
46
47
|
import { WorktreeStateProjection } from "./worktree-state-projection.js";
|
|
@@ -381,6 +382,23 @@ export function buildStepCompleteMessage(nextState: import("./types.js").GSDStat
|
|
|
381
382
|
+ `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Decide whether step mode should stop at the step wizard after a unit finishes.
|
|
387
|
+
*
|
|
388
|
+
* @param currentUnitType The just-finished unit type, such as "execute-task" or
|
|
389
|
+
* "complete-milestone"; may be null/undefined when no current unit is known.
|
|
390
|
+
* @param phaseAfterUnit The freshly derived next phase, such as "executing" or
|
|
391
|
+
* "complete"; may be null/undefined if state derivation failed.
|
|
392
|
+
* @returns true to show the step wizard; false to keep the loop running so
|
|
393
|
+
* terminal milestone completion can reach the merge/finalization path.
|
|
394
|
+
*/
|
|
395
|
+
export function shouldReturnStepWizardAfterUnit(
|
|
396
|
+
currentUnitType: string | null | undefined,
|
|
397
|
+
phaseAfterUnit: string | null | undefined,
|
|
398
|
+
): boolean {
|
|
399
|
+
return currentUnitType !== "complete-milestone" && phaseAfterUnit !== "complete";
|
|
400
|
+
}
|
|
401
|
+
|
|
384
402
|
export interface PreVerificationOpts {
|
|
385
403
|
skipSettleDelay?: boolean;
|
|
386
404
|
skipWorktreeSync?: boolean;
|
|
@@ -413,6 +431,11 @@ function artifactValidationKind(unitType: string): "project" | "requirements" |
|
|
|
413
431
|
}
|
|
414
432
|
|
|
415
433
|
function describeArtifactVerificationFailure(unitType: string, unitId: string, basePath: string): string {
|
|
434
|
+
const worktreeFailure = diagnoseWorktreeIntegrityFailure(basePath);
|
|
435
|
+
if (worktreeFailure) {
|
|
436
|
+
return `${worktreeFailure} Unit: ${unitType} ${unitId}.`;
|
|
437
|
+
}
|
|
438
|
+
|
|
416
439
|
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
|
|
417
440
|
if (!artifactPath) {
|
|
418
441
|
return `Artifact verification failed: ${unitType} "${unitId}" has no resolvable artifact path.`;
|
|
@@ -469,9 +492,17 @@ export async function autoCommitUnit(
|
|
|
469
492
|
}
|
|
470
493
|
}
|
|
471
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Execute the turn-level git action (commit, snapshot, or status-only).
|
|
497
|
+
*
|
|
498
|
+
* @param opts.softFailure - Defaults to false. When true, retry git failures,
|
|
499
|
+
* warn, and continue without pausing auto-mode; use for best-effort deferred
|
|
500
|
+
* closeout work where a git failure should not block the run.
|
|
501
|
+
*/
|
|
472
502
|
async function runCloseoutGitAction(
|
|
473
503
|
pctx: PostUnitContext,
|
|
474
504
|
unit: NonNullable<AutoSession["currentUnit"]>,
|
|
505
|
+
opts?: { softFailure?: boolean },
|
|
475
506
|
): Promise<"continue" | "dispatched"> {
|
|
476
507
|
const { s, ctx, pi, pauseAuto } = pctx;
|
|
477
508
|
const prefs = loadEffectiveGSDPreferences()?.preferences;
|
|
@@ -506,13 +537,24 @@ async function runCloseoutGitAction(
|
|
|
506
537
|
unitId: unit.id,
|
|
507
538
|
});
|
|
508
539
|
} else {
|
|
509
|
-
const
|
|
540
|
+
const maxAttempts = opts?.softFailure ? 3 : 1;
|
|
541
|
+
let gitResult = runTurnGitAction({
|
|
510
542
|
basePath: s.basePath,
|
|
511
543
|
action: turnAction,
|
|
512
544
|
unitType: unit.type,
|
|
513
545
|
unitId: unit.id,
|
|
514
546
|
taskContext,
|
|
515
547
|
});
|
|
548
|
+
for (let attempt = 1; gitResult.status === "failed" && attempt < maxAttempts; attempt++) {
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
550
|
+
gitResult = runTurnGitAction({
|
|
551
|
+
basePath: s.basePath,
|
|
552
|
+
action: turnAction,
|
|
553
|
+
unitType: unit.type,
|
|
554
|
+
unitId: unit.id,
|
|
555
|
+
taskContext,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
516
558
|
|
|
517
559
|
if (uokFlags.gitops) {
|
|
518
560
|
writeTurnGitTransaction({
|
|
@@ -563,12 +605,15 @@ async function runCloseoutGitAction(
|
|
|
563
605
|
}
|
|
564
606
|
|
|
565
607
|
const failureMsg = `Git ${turnAction} failed: ${(gitResult.error ?? "unknown error").split("\n")[0]}`;
|
|
566
|
-
ctx.ui.notify(failureMsg, "error");
|
|
608
|
+
ctx.ui.notify(failureMsg, opts?.softFailure ? "warning" : "error");
|
|
567
609
|
debugLog("postUnit", {
|
|
568
|
-
phase: "git-action-failed-blocking",
|
|
610
|
+
phase: opts?.softFailure ? "git-action-failed-soft" : "git-action-failed-blocking",
|
|
569
611
|
action: turnAction,
|
|
570
612
|
error: gitResult.error ?? "unknown error",
|
|
571
613
|
});
|
|
614
|
+
if (opts?.softFailure) {
|
|
615
|
+
return "continue";
|
|
616
|
+
}
|
|
572
617
|
await pauseAuto(ctx, pi);
|
|
573
618
|
return "dispatched";
|
|
574
619
|
}
|
|
@@ -586,7 +631,10 @@ async function runCloseoutGitAction(
|
|
|
586
631
|
s.lastGitActionFailure = message;
|
|
587
632
|
s.lastGitActionStatus = "failed";
|
|
588
633
|
debugLog("postUnit", { phase: "git-action", error: message, action: turnAction });
|
|
589
|
-
ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`,
|
|
634
|
+
ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`, opts?.softFailure ? "warning" : "error");
|
|
635
|
+
if (opts?.softFailure) {
|
|
636
|
+
return "continue";
|
|
637
|
+
}
|
|
590
638
|
if (uokFlags.gitops) {
|
|
591
639
|
await pauseAuto(ctx, pi);
|
|
592
640
|
return "dispatched";
|
|
@@ -1133,6 +1181,24 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
1133
1181
|
"warning",
|
|
1134
1182
|
);
|
|
1135
1183
|
// Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
|
|
1184
|
+
} else if (!triggerArtifactVerified && diagnoseWorktreeIntegrityFailure(s.basePath)) {
|
|
1185
|
+
const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
|
|
1186
|
+
const worktreeFailure = diagnoseWorktreeIntegrityFailure(s.basePath)!;
|
|
1187
|
+
s.pendingVerificationRetry = null;
|
|
1188
|
+
s.verificationRetryCount.delete(retryKey);
|
|
1189
|
+
s.verificationRetryFailureHashes.delete(retryKey);
|
|
1190
|
+
debugLog("postUnit", {
|
|
1191
|
+
phase: "worktree-integrity-failure",
|
|
1192
|
+
unitType: s.currentUnit.type,
|
|
1193
|
+
unitId: s.currentUnit.id,
|
|
1194
|
+
basePath: s.basePath,
|
|
1195
|
+
});
|
|
1196
|
+
ctx.ui.notify(
|
|
1197
|
+
`${worktreeFailure} Retry ${s.currentUnit.id} after repair.`,
|
|
1198
|
+
"error",
|
|
1199
|
+
);
|
|
1200
|
+
await pauseAuto(ctx, pi);
|
|
1201
|
+
return "dispatched";
|
|
1136
1202
|
} else if (!triggerArtifactVerified && !isDbAvailable()) {
|
|
1137
1203
|
debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
|
|
1138
1204
|
const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
@@ -1220,7 +1286,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
1220
1286
|
|
|
1221
1287
|
if (s.currentUnit) {
|
|
1222
1288
|
if (shouldDeferCloseoutGitAction(s.currentUnit.type)) {
|
|
1223
|
-
const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit);
|
|
1289
|
+
const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit, { softFailure: true });
|
|
1224
1290
|
if (gitActionResult === "dispatched") {
|
|
1225
1291
|
return "stopped";
|
|
1226
1292
|
}
|
|
@@ -1668,14 +1734,18 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
1668
1734
|
// Without this notify(), /gsd in step mode finishes a unit and silently
|
|
1669
1735
|
// exits the loop, leaving the user with no hint to /clear and /gsd again.
|
|
1670
1736
|
if (s.stepMode) {
|
|
1737
|
+
let phaseAfterUnit: string | null = null;
|
|
1671
1738
|
try {
|
|
1672
1739
|
const nextState = await deriveState(s.canonicalProjectRoot);
|
|
1740
|
+
phaseAfterUnit = nextState.phase;
|
|
1673
1741
|
ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
|
|
1674
1742
|
} catch (e) {
|
|
1675
1743
|
debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
|
|
1676
1744
|
ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
|
|
1677
1745
|
}
|
|
1678
|
-
return
|
|
1746
|
+
return shouldReturnStepWizardAfterUnit(s.currentUnit?.type, phaseAfterUnit)
|
|
1747
|
+
? "step-wizard"
|
|
1748
|
+
: "continue";
|
|
1679
1749
|
}
|
|
1680
1750
|
|
|
1681
1751
|
return "continue";
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
|
|
47
47
|
import { validateArtifact } from "./schemas/validate.js";
|
|
48
48
|
import { getProjectResearchStatus } from "./project-research-policy.js";
|
|
49
|
+
import { isGsdWorktreePath } from "./worktree-root.js";
|
|
49
50
|
|
|
50
51
|
// Re-export so existing consumers of auto-recovery.ts keep working.
|
|
51
52
|
export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
|
|
@@ -56,6 +57,29 @@ export {
|
|
|
56
57
|
|
|
57
58
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
58
59
|
|
|
60
|
+
export function diagnoseWorktreeIntegrityFailure(basePath: string): string | null {
|
|
61
|
+
if (!isGsdWorktreePath(basePath)) return null;
|
|
62
|
+
if (!existsSync(basePath)) {
|
|
63
|
+
return `Worktree integrity failure: ${basePath} does not exist. Repair or recreate the worktree before retrying.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const gitPath = join(basePath, ".git");
|
|
67
|
+
if (!existsSync(gitPath)) {
|
|
68
|
+
return `Worktree integrity failure: ${basePath} is not a valid git worktree (.git missing). Repair or recreate the worktree before retrying.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
73
|
+
cwd: basePath,
|
|
74
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
});
|
|
77
|
+
return null;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return `Worktree integrity failure: ${basePath} is not a valid git worktree (git rev-parse failed: ${getErrorMessage(err).split("\n")[0]}). Repair or recreate the worktree before retrying.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export type ArtifactRecoveryDbRefreshResult =
|
|
60
84
|
| { ok: true }
|
|
61
85
|
| { ok: false; fatal: boolean; message: string; reason: string };
|
|
@@ -752,6 +776,11 @@ export function verifyExpectedArtifact(
|
|
|
752
776
|
return false;
|
|
753
777
|
}
|
|
754
778
|
if (!existsSync(absPath)) {
|
|
779
|
+
const worktreeFailure = diagnoseWorktreeIntegrityFailure(base);
|
|
780
|
+
if (worktreeFailure) {
|
|
781
|
+
logError("recovery", `${worktreeFailure} Unit: ${unitType} ${unitId}.`);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
755
784
|
logWarning("recovery", `verify-fail ${unitType} ${unitId}: existsSync false for ${absPath}`);
|
|
756
785
|
return false;
|
|
757
786
|
}
|
|
@@ -818,14 +818,18 @@ export async function bootstrapAutoSession(
|
|
|
818
818
|
// worktree cleanup) was never run — the survivor branch must be merged.
|
|
819
819
|
// Applies to both worktree and branch isolation modes.
|
|
820
820
|
let hasSurvivorBranch = false;
|
|
821
|
+
let survivorMilestoneId = state.activeMilestone?.id ?? null;
|
|
822
|
+
if (!survivorMilestoneId && state.phase === "complete") {
|
|
823
|
+
survivorMilestoneId = findUnmergedCompletedMilestone(base, getIsolationMode(base));
|
|
824
|
+
}
|
|
821
825
|
if (
|
|
822
|
-
|
|
826
|
+
survivorMilestoneId &&
|
|
823
827
|
(state.phase === "pre-planning" || state.phase === "complete") &&
|
|
824
828
|
getIsolationMode(base) !== "none" &&
|
|
825
829
|
!detectWorktreeName(base) &&
|
|
826
830
|
!base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
|
|
827
831
|
) {
|
|
828
|
-
const milestoneBranch = `milestone/${
|
|
832
|
+
const milestoneBranch = `milestone/${survivorMilestoneId}`;
|
|
829
833
|
const { nativeBranchExists } = await import("./native-git-bridge.js");
|
|
830
834
|
hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
|
|
831
835
|
if (hasSurvivorBranch) {
|
|
@@ -869,7 +873,7 @@ export async function bootstrapAutoSession(
|
|
|
869
873
|
// Re-evaluate via the helper — the discuss branch above may have cleared
|
|
870
874
|
// hasSurvivorBranch after a successful promotion.
|
|
871
875
|
if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "finalize") {
|
|
872
|
-
const mid =
|
|
876
|
+
const mid = survivorMilestoneId!;
|
|
873
877
|
// Commit 68ef58a3c made `_mergeBranchMode` throw on wrong-branch
|
|
874
878
|
// instead of returning false silently. Wrap the call so the throw is
|
|
875
879
|
// converted into an error notify + clean bootstrap abort, not an
|
|
@@ -282,6 +282,50 @@ function gitRemoteExists(basePath: string, remote: string): boolean {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
function findRegularMergeChangedPaths(basePath: string, milestoneBranch: string, mainBranch: string): Set<string> {
|
|
286
|
+
const changedPaths = new Set<string>();
|
|
287
|
+
let mergeLog = "";
|
|
288
|
+
try {
|
|
289
|
+
mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
|
|
290
|
+
cwd: basePath,
|
|
291
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
292
|
+
encoding: "utf-8",
|
|
293
|
+
}).trim();
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
296
|
+
return changedPaths;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const line of mergeLog.split("\n").filter(Boolean)) {
|
|
300
|
+
const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
|
|
301
|
+
if (!mergeCommit || !firstParent || otherParents.length === 0) continue;
|
|
302
|
+
const mergedMilestone = otherParents.some((parent) => {
|
|
303
|
+
try {
|
|
304
|
+
return nativeIsAncestor(basePath, milestoneBranch, parent);
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (!mergedMilestone) continue;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
|
|
313
|
+
cwd: basePath,
|
|
314
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
315
|
+
encoding: "utf-8",
|
|
316
|
+
}).trim();
|
|
317
|
+
for (const path of output.split("\n").filter(Boolean)) {
|
|
318
|
+
if (!path.startsWith(".gsd/")) changedPaths.add(path);
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
322
|
+
}
|
|
323
|
+
return changedPaths;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return changedPaths;
|
|
327
|
+
}
|
|
328
|
+
|
|
285
329
|
function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
|
|
286
330
|
const gsdDir = gsdRoot(basePath);
|
|
287
331
|
// Phase C pt 2: auto.lock removed from this list — the file is gone
|
|
@@ -1739,6 +1783,66 @@ export function mergeMilestoneToMain(
|
|
|
1739
1783
|
}
|
|
1740
1784
|
}
|
|
1741
1785
|
|
|
1786
|
+
// Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
|
|
1787
|
+
if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
|
|
1788
|
+
const codeChanges = nativeDiffNumstat(
|
|
1789
|
+
originalBasePath_,
|
|
1790
|
+
mainBranch,
|
|
1791
|
+
milestoneBranch,
|
|
1792
|
+
).filter((entry) => !entry.path.startsWith(".gsd/"));
|
|
1793
|
+
if (codeChanges.length > 0) {
|
|
1794
|
+
const regularMergeChangedPaths = findRegularMergeChangedPaths(
|
|
1795
|
+
originalBasePath_,
|
|
1796
|
+
milestoneBranch,
|
|
1797
|
+
mainBranch,
|
|
1798
|
+
);
|
|
1799
|
+
const unanchoredCodeChanges = codeChanges.filter((entry) =>
|
|
1800
|
+
regularMergeChangedPaths.has(entry.path)
|
|
1801
|
+
);
|
|
1802
|
+
if (unanchoredCodeChanges.length > 0) {
|
|
1803
|
+
process.chdir(previousCwd);
|
|
1804
|
+
throw new GSDError(
|
|
1805
|
+
GSD_GIT_ERROR,
|
|
1806
|
+
`Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
|
|
1807
|
+
`but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
|
|
1808
|
+
`Aborting worktree teardown to prevent data loss.`,
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
debugLog("mergeMilestoneToMain", {
|
|
1813
|
+
action: "skip-squash-already-merged",
|
|
1814
|
+
milestoneId,
|
|
1815
|
+
milestoneBranch,
|
|
1816
|
+
mainBranch,
|
|
1817
|
+
});
|
|
1818
|
+
try {
|
|
1819
|
+
clearProjectRootStateFiles(originalBasePath_, milestoneId);
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
|
|
1822
|
+
}
|
|
1823
|
+
try {
|
|
1824
|
+
removeWorktree(originalBasePath_, milestoneId, {
|
|
1825
|
+
branch: milestoneBranch,
|
|
1826
|
+
deleteBranch: false,
|
|
1827
|
+
});
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
nativeBranchDelete(originalBasePath_, milestoneBranch);
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1835
|
+
}
|
|
1836
|
+
setActiveWorkspace(null);
|
|
1837
|
+
nudgeGitBranchCache(previousCwd);
|
|
1838
|
+
try {
|
|
1839
|
+
process.chdir(originalBasePath_);
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1842
|
+
}
|
|
1843
|
+
return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1742
1846
|
// 7. Shelter queued milestone directories before the squash merge (#2505).
|
|
1743
1847
|
// The milestone branch may contain copies of queued milestone dirs (via
|
|
1744
1848
|
// copyPlanningArtifacts), so `git merge --squash` rejects when those same
|
|
@@ -48,6 +48,11 @@ const MAX_NETWORK_RETRIES = 2;
|
|
|
48
48
|
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
49
49
|
return !!value && typeof value === "object";
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export function _hasEmptyAgentEndContent(content: unknown): boolean {
|
|
53
|
+
return content == null || (Array.isArray(content) && content.length === 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
/**
|
|
52
57
|
* Cap on auto-resume attempts for sustained transient-provider errors.
|
|
53
58
|
*
|
|
@@ -310,7 +315,7 @@ export async function handleAgentEnd(
|
|
|
310
315
|
// that carry error context — e.g. errorMessage field or non-empty content
|
|
311
316
|
// indicating a mid-stream failure. (#2695)
|
|
312
317
|
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
313
|
-
const hasEmptyContent =
|
|
318
|
+
const hasEmptyContent = _hasEmptyAgentEndContent(content);
|
|
314
319
|
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
315
320
|
|
|
316
321
|
if (hasEmptyContent && !hasErrorMessage) {
|
|
@@ -65,6 +65,7 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
65
65
|
* true / false — shell no-ops / test exit codes
|
|
66
66
|
*/
|
|
67
67
|
const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
|
|
68
|
+
const BASH_VERIFICATION_RE = /^\s*(npm\s+(run\s+(build|test|test:\w+|lint|lint:\w+|typecheck|type-check|verify|ci|validate)\b|test\b)|pnpm\s+(build|test|lint|typecheck|verify)\b|yarn\s+(build|test|lint|typecheck|verify)\b|vitest\b|jest\b|go\s+test\b)/;
|
|
68
69
|
|
|
69
70
|
interface InMemoryWriteGateState {
|
|
70
71
|
verifiedDepthMilestones: Set<string>;
|
|
@@ -767,6 +768,9 @@ function blockReason(unitType: string, mode: string, what: string): string {
|
|
|
767
768
|
* and listed in the policy's allowedSubagents.
|
|
768
769
|
* - "docs" → like "planning" but also allows writes to paths
|
|
769
770
|
* matching `allowedPathGlobs` relative to basePath.
|
|
771
|
+
* - "verification"
|
|
772
|
+
* → allows Bash for project verification commands, but keeps
|
|
773
|
+
* writes restricted to .gsd/ and blocks subagent dispatch.
|
|
770
774
|
*
|
|
771
775
|
* `pathOrCommand` is the file path for write/edit-shaped tools and the
|
|
772
776
|
* shell command for bash. Other tools ignore this argument.
|
|
@@ -804,7 +808,7 @@ export function shouldBlockPlanningUnit(
|
|
|
804
808
|
return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
|
|
805
809
|
}
|
|
806
810
|
|
|
807
|
-
// planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
|
|
811
|
+
// planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
|
|
808
812
|
if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
|
|
809
813
|
if (tool.startsWith("gsd_")) return { block: false };
|
|
810
814
|
|
|
@@ -862,6 +866,17 @@ export function shouldBlockPlanningUnit(
|
|
|
862
866
|
}
|
|
863
867
|
|
|
864
868
|
if (tool === "bash") {
|
|
869
|
+
if (policy.mode === "verification") {
|
|
870
|
+
if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
|
|
871
|
+
return {
|
|
872
|
+
block: true,
|
|
873
|
+
reason: blockReason(
|
|
874
|
+
unitType,
|
|
875
|
+
policy.mode,
|
|
876
|
+
`bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
|
|
877
|
+
),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
865
880
|
if (BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
|
|
866
881
|
return {
|
|
867
882
|
block: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@gsd/pi-coding-agent";
|
|
2
2
|
import type { Model } from "@gsd/pi-ai";
|
|
3
3
|
import type { GSDState } from "../../types.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
|
|
5
6
|
import { computeProgressScore, formatProgressLine } from "../../progress-score.js";
|
|
6
7
|
import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath } from "../../preferences.js";
|
|
@@ -220,7 +221,22 @@ export async function handleBrief(args: string, ctx: ExtensionCommandContext, pi
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
const outputDir = getVisualBriefOutputDir();
|
|
223
|
-
|
|
224
|
+
const version = resolveGsdVersion();
|
|
225
|
+
pi.sendUserMessage(buildVisualBriefPrompt(request, { outputDir, version }));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const briefRequire = createRequire(import.meta.url);
|
|
229
|
+
|
|
230
|
+
function resolveGsdVersion(): string | undefined {
|
|
231
|
+
const envVersion = process.env.GSD_VERSION?.trim();
|
|
232
|
+
if (envVersion) return envVersion;
|
|
233
|
+
try {
|
|
234
|
+
const pkg = briefRequire("../../../../../../package.json") as { version?: unknown };
|
|
235
|
+
const fromPkg = typeof pkg.version === "string" ? pkg.version.trim() : "";
|
|
236
|
+
return fromPkg || undefined;
|
|
237
|
+
} catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
224
240
|
}
|
|
225
241
|
|
|
226
242
|
export async function handleSetup(args: string, ctx: ExtensionCommandContext, pi?: ExtensionAPI): Promise<void> {
|
|
@@ -524,7 +524,7 @@ export function getRecentUnitKeysForProjectRoot(
|
|
|
524
524
|
if (!isDbAvailable()) return [];
|
|
525
525
|
const db = _getAdapter()!;
|
|
526
526
|
const rows = db.prepare(
|
|
527
|
-
`SELECT ud.unit_id
|
|
527
|
+
`SELECT ud.unit_type, ud.unit_id
|
|
528
528
|
FROM unit_dispatches ud
|
|
529
529
|
INNER JOIN workers w ON w.worker_id = ud.worker_id
|
|
530
530
|
WHERE w.project_root_realpath = :project_root_realpath
|
|
@@ -533,8 +533,8 @@ export function getRecentUnitKeysForProjectRoot(
|
|
|
533
533
|
).all({
|
|
534
534
|
":project_root_realpath": projectRootRealpath,
|
|
535
535
|
":limit": limit,
|
|
536
|
-
}) as Array<{ unit_id: string }>;
|
|
537
|
-
return rows.reverse().map((r) => ({ key: r.unit_id }));
|
|
536
|
+
}) as Array<{ unit_type: string; unit_id: string }>;
|
|
537
|
+
return rows.reverse().map((r) => ({ key: `${r.unit_type}/${r.unit_id}` }));
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
/**
|