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
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ed49f911008c62ca
|
|
@@ -105,9 +105,10 @@ export function mapUsage(sdkUsage, totalCostUsd) {
|
|
|
105
105
|
output: sdkUsage.output_tokens,
|
|
106
106
|
cacheRead: sdkUsage.cache_read_input_tokens,
|
|
107
107
|
cacheWrite: sdkUsage.cache_creation_input_tokens,
|
|
108
|
+
// Claude Agent SDK result usage is cumulative across its internal loop;
|
|
109
|
+
// repeated cache reads do not represent additional live context.
|
|
108
110
|
totalTokens: sdkUsage.input_tokens +
|
|
109
111
|
sdkUsage.output_tokens +
|
|
110
|
-
sdkUsage.cache_read_input_tokens +
|
|
111
112
|
sdkUsage.cache_creation_input_tokens,
|
|
112
113
|
cost: {
|
|
113
114
|
input: 0,
|
|
@@ -102,6 +102,9 @@ function missingSliceStop(mid, phase) {
|
|
|
102
102
|
level: "error",
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
function isRegistryMilestoneComplete(state, mid) {
|
|
106
|
+
return state.registry.some((milestone) => milestone.id === mid && milestone.status === "complete");
|
|
107
|
+
}
|
|
105
108
|
/**
|
|
106
109
|
* Check for milestone slices missing SUMMARY files.
|
|
107
110
|
* Returns array of missing slice IDs, or empty array if all present or DB unavailable.
|
|
@@ -247,6 +250,8 @@ export const DISPATCH_RULES = [
|
|
|
247
250
|
return null;
|
|
248
251
|
if (!MILESTONE_ID_RE.test(mid))
|
|
249
252
|
return null;
|
|
253
|
+
if (isRegistryMilestoneComplete(state, mid))
|
|
254
|
+
return null;
|
|
250
255
|
// Align with the plan-v2 gate's lookup semantics: whitespace-only counts
|
|
251
256
|
// as missing, and an auto worktree may fall back to GSD_PROJECT_ROOT.
|
|
252
257
|
if (hasFinalizedMilestoneContext(basePath, mid))
|
|
@@ -557,6 +562,8 @@ export const DISPATCH_RULES = [
|
|
|
557
562
|
match: async ({ state, mid, midTitle, basePath, prefs, structuredQuestionsAvailable }) => {
|
|
558
563
|
if (state.phase !== "pre-planning")
|
|
559
564
|
return null;
|
|
565
|
+
if (isRegistryMilestoneComplete(state, mid))
|
|
566
|
+
return null;
|
|
560
567
|
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
|
|
561
568
|
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
562
569
|
if (hasContext)
|
|
@@ -1091,19 +1098,19 @@ export const DISPATCH_RULES = [
|
|
|
1091
1098
|
return { action: "skip" };
|
|
1092
1099
|
}
|
|
1093
1100
|
}
|
|
1094
|
-
// Safety guard (#2675): block completion when VALIDATION
|
|
1095
|
-
//
|
|
1096
|
-
// terminal
|
|
1097
|
-
//
|
|
1101
|
+
// Safety guard (#2675, #5747): block completion when VALIDATION
|
|
1102
|
+
// verdict is non-passing. The state machine treats these verdicts as
|
|
1103
|
+
// terminal, but completing-milestone should NOT proceed — remediation
|
|
1104
|
+
// or human attention is needed.
|
|
1098
1105
|
const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
|
|
1099
1106
|
if (validationFile) {
|
|
1100
1107
|
const validationContent = await loadFile(validationFile);
|
|
1101
1108
|
if (validationContent) {
|
|
1102
1109
|
const verdict = extractVerdict(validationContent);
|
|
1103
|
-
if (verdict === "needs-remediation") {
|
|
1110
|
+
if (verdict === "needs-remediation" || verdict === "needs-attention") {
|
|
1104
1111
|
return {
|
|
1105
1112
|
action: "stop",
|
|
1106
|
-
reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "
|
|
1113
|
+
reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
|
|
1107
1114
|
level: "warning",
|
|
1108
1115
|
};
|
|
1109
1116
|
}
|
|
@@ -23,7 +23,7 @@ import { rebuildState } from "./doctor.js";
|
|
|
23
23
|
import { parseUnitId } from "./unit-id.js";
|
|
24
24
|
import { closeoutUnit } from "./auto-unit-closeout.js";
|
|
25
25
|
import { runTurnGitAction, } from "./git-service.js";
|
|
26
|
-
import { verifyExpectedArtifact, resolveExpectedArtifactPath, writeBlockerPlaceholder, diagnoseExpectedArtifact, } from "./auto-recovery.js";
|
|
26
|
+
import { verifyExpectedArtifact, resolveExpectedArtifactPath, writeBlockerPlaceholder, diagnoseExpectedArtifact, diagnoseWorktreeIntegrityFailure, } from "./auto-recovery.js";
|
|
27
27
|
import { regenerateIfMissing } from "./workflow-projections.js";
|
|
28
28
|
import { WorktreeStateProjection } from "./worktree-state-projection.js";
|
|
29
29
|
import { createWorkspace, scopeMilestone } from "./workspace.js";
|
|
@@ -303,6 +303,19 @@ export function buildStepCompleteMessage(nextState) {
|
|
|
303
303
|
return `Step complete. Next: ${next.label}\n`
|
|
304
304
|
+ `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Decide whether step mode should stop at the step wizard after a unit finishes.
|
|
308
|
+
*
|
|
309
|
+
* @param currentUnitType The just-finished unit type, such as "execute-task" or
|
|
310
|
+
* "complete-milestone"; may be null/undefined when no current unit is known.
|
|
311
|
+
* @param phaseAfterUnit The freshly derived next phase, such as "executing" or
|
|
312
|
+
* "complete"; may be null/undefined if state derivation failed.
|
|
313
|
+
* @returns true to show the step wizard; false to keep the loop running so
|
|
314
|
+
* terminal milestone completion can reach the merge/finalization path.
|
|
315
|
+
*/
|
|
316
|
+
export function shouldReturnStepWizardAfterUnit(currentUnitType, phaseAfterUnit) {
|
|
317
|
+
return currentUnitType !== "complete-milestone" && phaseAfterUnit !== "complete";
|
|
318
|
+
}
|
|
306
319
|
export const USER_DRIVEN_DEEP_UNITS = new Set([
|
|
307
320
|
"discuss-project",
|
|
308
321
|
"discuss-requirements",
|
|
@@ -318,6 +331,10 @@ function artifactValidationKind(unitType) {
|
|
|
318
331
|
return null;
|
|
319
332
|
}
|
|
320
333
|
function describeArtifactVerificationFailure(unitType, unitId, basePath) {
|
|
334
|
+
const worktreeFailure = diagnoseWorktreeIntegrityFailure(basePath);
|
|
335
|
+
if (worktreeFailure) {
|
|
336
|
+
return `${worktreeFailure} Unit: ${unitType} ${unitId}.`;
|
|
337
|
+
}
|
|
321
338
|
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
|
|
322
339
|
if (!artifactPath) {
|
|
323
340
|
return `Artifact verification failed: ${unitType} "${unitId}" has no resolvable artifact path.`;
|
|
@@ -362,7 +379,14 @@ export async function autoCommitUnit(basePath, unitType, unitId, ctx) {
|
|
|
362
379
|
return null;
|
|
363
380
|
}
|
|
364
381
|
}
|
|
365
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Execute the turn-level git action (commit, snapshot, or status-only).
|
|
384
|
+
*
|
|
385
|
+
* @param opts.softFailure - Defaults to false. When true, retry git failures,
|
|
386
|
+
* warn, and continue without pausing auto-mode; use for best-effort deferred
|
|
387
|
+
* closeout work where a git failure should not block the run.
|
|
388
|
+
*/
|
|
389
|
+
async function runCloseoutGitAction(pctx, unit, opts) {
|
|
366
390
|
const { s, ctx, pi, pauseAuto } = pctx;
|
|
367
391
|
const prefs = loadEffectiveGSDPreferences()?.preferences;
|
|
368
392
|
const uokFlags = resolveUokFlags(prefs);
|
|
@@ -390,13 +414,24 @@ async function runCloseoutGitAction(pctx, unit) {
|
|
|
390
414
|
});
|
|
391
415
|
}
|
|
392
416
|
else {
|
|
393
|
-
const
|
|
417
|
+
const maxAttempts = opts?.softFailure ? 3 : 1;
|
|
418
|
+
let gitResult = runTurnGitAction({
|
|
394
419
|
basePath: s.basePath,
|
|
395
420
|
action: turnAction,
|
|
396
421
|
unitType: unit.type,
|
|
397
422
|
unitId: unit.id,
|
|
398
423
|
taskContext,
|
|
399
424
|
});
|
|
425
|
+
for (let attempt = 1; gitResult.status === "failed" && attempt < maxAttempts; attempt++) {
|
|
426
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
|
427
|
+
gitResult = runTurnGitAction({
|
|
428
|
+
basePath: s.basePath,
|
|
429
|
+
action: turnAction,
|
|
430
|
+
unitType: unit.type,
|
|
431
|
+
unitId: unit.id,
|
|
432
|
+
taskContext,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
400
435
|
if (uokFlags.gitops) {
|
|
401
436
|
writeTurnGitTransaction({
|
|
402
437
|
basePath: s.basePath,
|
|
@@ -444,12 +479,15 @@ async function runCloseoutGitAction(pctx, unit) {
|
|
|
444
479
|
});
|
|
445
480
|
}
|
|
446
481
|
const failureMsg = `Git ${turnAction} failed: ${(gitResult.error ?? "unknown error").split("\n")[0]}`;
|
|
447
|
-
ctx.ui.notify(failureMsg, "error");
|
|
482
|
+
ctx.ui.notify(failureMsg, opts?.softFailure ? "warning" : "error");
|
|
448
483
|
debugLog("postUnit", {
|
|
449
|
-
phase: "git-action-failed-blocking",
|
|
484
|
+
phase: opts?.softFailure ? "git-action-failed-soft" : "git-action-failed-blocking",
|
|
450
485
|
action: turnAction,
|
|
451
486
|
error: gitResult.error ?? "unknown error",
|
|
452
487
|
});
|
|
488
|
+
if (opts?.softFailure) {
|
|
489
|
+
return "continue";
|
|
490
|
+
}
|
|
453
491
|
await pauseAuto(ctx, pi);
|
|
454
492
|
return "dispatched";
|
|
455
493
|
}
|
|
@@ -467,7 +505,10 @@ async function runCloseoutGitAction(pctx, unit) {
|
|
|
467
505
|
s.lastGitActionFailure = message;
|
|
468
506
|
s.lastGitActionStatus = "failed";
|
|
469
507
|
debugLog("postUnit", { phase: "git-action", error: message, action: turnAction });
|
|
470
|
-
ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`,
|
|
508
|
+
ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`, opts?.softFailure ? "warning" : "error");
|
|
509
|
+
if (opts?.softFailure) {
|
|
510
|
+
return "continue";
|
|
511
|
+
}
|
|
471
512
|
if (uokFlags.gitops) {
|
|
472
513
|
await pauseAuto(ctx, pi);
|
|
473
514
|
return "dispatched";
|
|
@@ -961,6 +1002,22 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
961
1002
|
ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries) (#4973)`, "warning");
|
|
962
1003
|
// Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
|
|
963
1004
|
}
|
|
1005
|
+
else if (!triggerArtifactVerified && diagnoseWorktreeIntegrityFailure(s.basePath)) {
|
|
1006
|
+
const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
|
|
1007
|
+
const worktreeFailure = diagnoseWorktreeIntegrityFailure(s.basePath);
|
|
1008
|
+
s.pendingVerificationRetry = null;
|
|
1009
|
+
s.verificationRetryCount.delete(retryKey);
|
|
1010
|
+
s.verificationRetryFailureHashes.delete(retryKey);
|
|
1011
|
+
debugLog("postUnit", {
|
|
1012
|
+
phase: "worktree-integrity-failure",
|
|
1013
|
+
unitType: s.currentUnit.type,
|
|
1014
|
+
unitId: s.currentUnit.id,
|
|
1015
|
+
basePath: s.basePath,
|
|
1016
|
+
});
|
|
1017
|
+
ctx.ui.notify(`${worktreeFailure} Retry ${s.currentUnit.id} after repair.`, "error");
|
|
1018
|
+
await pauseAuto(ctx, pi);
|
|
1019
|
+
return "dispatched";
|
|
1020
|
+
}
|
|
964
1021
|
else if (!triggerArtifactVerified && !isDbAvailable()) {
|
|
965
1022
|
debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
|
|
966
1023
|
const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
@@ -1032,7 +1089,7 @@ export async function postUnitPostVerification(pctx) {
|
|
|
1032
1089
|
const { s, ctx, pi, buildSnapshotOpts, lockBase, stopAuto, pauseAuto, updateProgressWidget } = pctx;
|
|
1033
1090
|
if (s.currentUnit) {
|
|
1034
1091
|
if (shouldDeferCloseoutGitAction(s.currentUnit.type)) {
|
|
1035
|
-
const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit);
|
|
1092
|
+
const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit, { softFailure: true });
|
|
1036
1093
|
if (gitActionResult === "dispatched") {
|
|
1037
1094
|
return "stopped";
|
|
1038
1095
|
}
|
|
@@ -1404,15 +1461,19 @@ export async function postUnitPostVerification(pctx) {
|
|
|
1404
1461
|
// Without this notify(), /gsd in step mode finishes a unit and silently
|
|
1405
1462
|
// exits the loop, leaving the user with no hint to /clear and /gsd again.
|
|
1406
1463
|
if (s.stepMode) {
|
|
1464
|
+
let phaseAfterUnit = null;
|
|
1407
1465
|
try {
|
|
1408
1466
|
const nextState = await deriveState(s.canonicalProjectRoot);
|
|
1467
|
+
phaseAfterUnit = nextState.phase;
|
|
1409
1468
|
ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
|
|
1410
1469
|
}
|
|
1411
1470
|
catch (e) {
|
|
1412
1471
|
debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
|
|
1413
1472
|
ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
|
|
1414
1473
|
}
|
|
1415
|
-
return
|
|
1474
|
+
return shouldReturnStepWizardAfterUnit(s.currentUnit?.type, phaseAfterUnit)
|
|
1475
|
+
? "step-wizard"
|
|
1476
|
+
: "continue";
|
|
1416
1477
|
}
|
|
1417
1478
|
return "continue";
|
|
1418
1479
|
}
|
|
@@ -14,7 +14,8 @@ import { clearParseCache } from "./files.js";
|
|
|
14
14
|
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
15
15
|
import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone, refreshOpenDatabaseFromDisk, getCompletedMilestoneTaskFileHints, getMilestoneCommitAttributionShas, recordMilestoneCommitAttribution } from "./gsd-db.js";
|
|
16
16
|
import { isValidationTerminal } from "./state.js";
|
|
17
|
-
import {
|
|
17
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
18
|
+
import { logWarning, logError } from "./workflow-logger.js";
|
|
18
19
|
import { readIntegrationBranch } from "./git-service.js";
|
|
19
20
|
import { isClosedStatus } from "./status-guards.js";
|
|
20
21
|
import { resolveSlicePath, resolveSliceFile, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, buildSliceFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
|
|
@@ -25,9 +26,33 @@ import { resolveExpectedArtifactPath, diagnoseExpectedArtifact, } from "./auto-a
|
|
|
25
26
|
import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
|
|
26
27
|
import { validateArtifact } from "./schemas/validate.js";
|
|
27
28
|
import { getProjectResearchStatus } from "./project-research-policy.js";
|
|
29
|
+
import { isGsdWorktreePath } from "./worktree-root.js";
|
|
28
30
|
// Re-export so existing consumers of auto-recovery.ts keep working.
|
|
29
31
|
export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
|
|
30
32
|
export { classifyMilestoneSummaryContent, } from "./milestone-summary-classifier.js";
|
|
33
|
+
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
34
|
+
export function diagnoseWorktreeIntegrityFailure(basePath) {
|
|
35
|
+
if (!isGsdWorktreePath(basePath))
|
|
36
|
+
return null;
|
|
37
|
+
if (!existsSync(basePath)) {
|
|
38
|
+
return `Worktree integrity failure: ${basePath} does not exist. Repair or recreate the worktree before retrying.`;
|
|
39
|
+
}
|
|
40
|
+
const gitPath = join(basePath, ".git");
|
|
41
|
+
if (!existsSync(gitPath)) {
|
|
42
|
+
return `Worktree integrity failure: ${basePath} is not a valid git worktree (.git missing). Repair or recreate the worktree before retrying.`;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
46
|
+
cwd: basePath,
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
});
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
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.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
export function refreshRecoveryDbForArtifact(unitType, unitId) {
|
|
32
57
|
if (unitType !== "plan-slice" && unitType !== "execute-task")
|
|
33
58
|
return { ok: true };
|
|
@@ -673,6 +698,11 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
|
|
|
673
698
|
return false;
|
|
674
699
|
}
|
|
675
700
|
if (!existsSync(absPath)) {
|
|
701
|
+
const worktreeFailure = diagnoseWorktreeIntegrityFailure(base);
|
|
702
|
+
if (worktreeFailure) {
|
|
703
|
+
logError("recovery", `${worktreeFailure} Unit: ${unitType} ${unitId}.`);
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
676
706
|
logWarning("recovery", `verify-fail ${unitType} ${unitId}: existsSync false for ${absPath}`);
|
|
677
707
|
return false;
|
|
678
708
|
}
|
|
@@ -606,12 +606,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
606
606
|
// worktree cleanup) was never run — the survivor branch must be merged.
|
|
607
607
|
// Applies to both worktree and branch isolation modes.
|
|
608
608
|
let hasSurvivorBranch = false;
|
|
609
|
-
|
|
609
|
+
let survivorMilestoneId = state.activeMilestone?.id ?? null;
|
|
610
|
+
if (!survivorMilestoneId && state.phase === "complete") {
|
|
611
|
+
survivorMilestoneId = findUnmergedCompletedMilestone(base, getIsolationMode(base));
|
|
612
|
+
}
|
|
613
|
+
if (survivorMilestoneId &&
|
|
610
614
|
(state.phase === "pre-planning" || state.phase === "complete") &&
|
|
611
615
|
getIsolationMode(base) !== "none" &&
|
|
612
616
|
!detectWorktreeName(base) &&
|
|
613
617
|
!base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)) {
|
|
614
|
-
const milestoneBranch = `milestone/${
|
|
618
|
+
const milestoneBranch = `milestone/${survivorMilestoneId}`;
|
|
615
619
|
const { nativeBranchExists } = await import("./native-git-bridge.js");
|
|
616
620
|
hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
|
|
617
621
|
if (hasSurvivorBranch) {
|
|
@@ -645,7 +649,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
645
649
|
// Re-evaluate via the helper — the discuss branch above may have cleared
|
|
646
650
|
// hasSurvivorBranch after a successful promotion.
|
|
647
651
|
if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "finalize") {
|
|
648
|
-
const mid =
|
|
652
|
+
const mid = survivorMilestoneId;
|
|
649
653
|
// Commit 68ef58a3c made `_mergeBranchMode` throw on wrong-branch
|
|
650
654
|
// instead of returning false silently. Wrap the call so the throw is
|
|
651
655
|
// converted into an error notify + clean bootstrap abort, not an
|
|
@@ -204,6 +204,52 @@ function gitRemoteExists(basePath, remote) {
|
|
|
204
204
|
return false;
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
function findRegularMergeChangedPaths(basePath, milestoneBranch, mainBranch) {
|
|
208
|
+
const changedPaths = new Set();
|
|
209
|
+
let mergeLog = "";
|
|
210
|
+
try {
|
|
211
|
+
mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
|
|
212
|
+
cwd: basePath,
|
|
213
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
214
|
+
encoding: "utf-8",
|
|
215
|
+
}).trim();
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
219
|
+
return changedPaths;
|
|
220
|
+
}
|
|
221
|
+
for (const line of mergeLog.split("\n").filter(Boolean)) {
|
|
222
|
+
const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
|
|
223
|
+
if (!mergeCommit || !firstParent || otherParents.length === 0)
|
|
224
|
+
continue;
|
|
225
|
+
const mergedMilestone = otherParents.some((parent) => {
|
|
226
|
+
try {
|
|
227
|
+
return nativeIsAncestor(basePath, milestoneBranch, parent);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
if (!mergedMilestone)
|
|
234
|
+
continue;
|
|
235
|
+
try {
|
|
236
|
+
const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
|
|
237
|
+
cwd: basePath,
|
|
238
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
239
|
+
encoding: "utf-8",
|
|
240
|
+
}).trim();
|
|
241
|
+
for (const path of output.split("\n").filter(Boolean)) {
|
|
242
|
+
if (!path.startsWith(".gsd/"))
|
|
243
|
+
changedPaths.add(path);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
248
|
+
}
|
|
249
|
+
return changedPaths;
|
|
250
|
+
}
|
|
251
|
+
return changedPaths;
|
|
252
|
+
}
|
|
207
253
|
function clearProjectRootStateFiles(basePath, milestoneId) {
|
|
208
254
|
const gsdDir = gsdRoot(basePath);
|
|
209
255
|
// Phase C pt 2: auto.lock removed from this list — the file is gone
|
|
@@ -1482,6 +1528,56 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1482
1528
|
});
|
|
1483
1529
|
}
|
|
1484
1530
|
}
|
|
1531
|
+
// Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
|
|
1532
|
+
if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
|
|
1533
|
+
const codeChanges = nativeDiffNumstat(originalBasePath_, mainBranch, milestoneBranch).filter((entry) => !entry.path.startsWith(".gsd/"));
|
|
1534
|
+
if (codeChanges.length > 0) {
|
|
1535
|
+
const regularMergeChangedPaths = findRegularMergeChangedPaths(originalBasePath_, milestoneBranch, mainBranch);
|
|
1536
|
+
const unanchoredCodeChanges = codeChanges.filter((entry) => regularMergeChangedPaths.has(entry.path));
|
|
1537
|
+
if (unanchoredCodeChanges.length > 0) {
|
|
1538
|
+
process.chdir(previousCwd);
|
|
1539
|
+
throw new GSDError(GSD_GIT_ERROR, `Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
|
|
1540
|
+
`but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
|
|
1541
|
+
`Aborting worktree teardown to prevent data loss.`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
debugLog("mergeMilestoneToMain", {
|
|
1545
|
+
action: "skip-squash-already-merged",
|
|
1546
|
+
milestoneId,
|
|
1547
|
+
milestoneBranch,
|
|
1548
|
+
mainBranch,
|
|
1549
|
+
});
|
|
1550
|
+
try {
|
|
1551
|
+
clearProjectRootStateFiles(originalBasePath_, milestoneId);
|
|
1552
|
+
}
|
|
1553
|
+
catch (err) {
|
|
1554
|
+
logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
removeWorktree(originalBasePath_, milestoneId, {
|
|
1558
|
+
branch: milestoneBranch,
|
|
1559
|
+
deleteBranch: false,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
catch (err) {
|
|
1563
|
+
logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
nativeBranchDelete(originalBasePath_, milestoneBranch);
|
|
1567
|
+
}
|
|
1568
|
+
catch (err) {
|
|
1569
|
+
logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1570
|
+
}
|
|
1571
|
+
setActiveWorkspace(null);
|
|
1572
|
+
nudgeGitBranchCache(previousCwd);
|
|
1573
|
+
try {
|
|
1574
|
+
process.chdir(originalBasePath_);
|
|
1575
|
+
}
|
|
1576
|
+
catch (err) {
|
|
1577
|
+
logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1578
|
+
}
|
|
1579
|
+
return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
|
|
1580
|
+
}
|
|
1485
1581
|
// 7. Shelter queued milestone directories before the squash merge (#2505).
|
|
1486
1582
|
// The milestone branch may contain copies of queued milestone dirs (via
|
|
1487
1583
|
// copyPlanningArtifacts), so `git merge --squash` rejects when those same
|
|
@@ -18,6 +18,9 @@ const MAX_NETWORK_RETRIES = 2;
|
|
|
18
18
|
function isObjectRecord(value) {
|
|
19
19
|
return !!value && typeof value === "object";
|
|
20
20
|
}
|
|
21
|
+
export function _hasEmptyAgentEndContent(content) {
|
|
22
|
+
return content == null || (Array.isArray(content) && content.length === 0);
|
|
23
|
+
}
|
|
21
24
|
/**
|
|
22
25
|
* Cap on auto-resume attempts for sustained transient-provider errors.
|
|
23
26
|
*
|
|
@@ -246,7 +249,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
246
249
|
// that carry error context — e.g. errorMessage field or non-empty content
|
|
247
250
|
// indicating a mid-stream failure. (#2695)
|
|
248
251
|
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
249
|
-
const hasEmptyContent =
|
|
252
|
+
const hasEmptyContent = _hasEmptyAgentEndContent(content);
|
|
250
253
|
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
251
254
|
if (hasEmptyContent && !hasErrorMessage) {
|
|
252
255
|
// Non-fatal: treat as a normal agent end so the loop can continue
|
|
@@ -59,6 +59,7 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
59
59
|
* true / false — shell no-ops / test exit codes
|
|
60
60
|
*/
|
|
61
61
|
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)/;
|
|
62
|
+
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)/;
|
|
62
63
|
function createEmptyWriteGateState() {
|
|
63
64
|
return {
|
|
64
65
|
verifiedDepthMilestones: new Set(),
|
|
@@ -643,6 +644,9 @@ function blockReason(unitType, mode, what) {
|
|
|
643
644
|
* and listed in the policy's allowedSubagents.
|
|
644
645
|
* - "docs" → like "planning" but also allows writes to paths
|
|
645
646
|
* matching `allowedPathGlobs` relative to basePath.
|
|
647
|
+
* - "verification"
|
|
648
|
+
* → allows Bash for project verification commands, but keeps
|
|
649
|
+
* writes restricted to .gsd/ and blocks subagent dispatch.
|
|
646
650
|
*
|
|
647
651
|
* `pathOrCommand` is the file path for write/edit-shaped tools and the
|
|
648
652
|
* shell command for bash. Other tools ignore this argument.
|
|
@@ -674,7 +678,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
674
678
|
// Unknown tool in read-only mode — block by default.
|
|
675
679
|
return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
|
|
676
680
|
}
|
|
677
|
-
// planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
|
|
681
|
+
// planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
|
|
678
682
|
if (PLANNING_SAFE_TOOLS.has(tool))
|
|
679
683
|
return { block: false };
|
|
680
684
|
if (tool.startsWith("gsd_"))
|
|
@@ -720,6 +724,14 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
|
|
|
720
724
|
return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
|
|
721
725
|
}
|
|
722
726
|
if (tool === "bash") {
|
|
727
|
+
if (policy.mode === "verification") {
|
|
728
|
+
if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
|
|
729
|
+
return { block: false };
|
|
730
|
+
return {
|
|
731
|
+
block: true,
|
|
732
|
+
reason: blockReason(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
723
735
|
if (BASH_READ_ONLY_RE.test(pathOrCommand))
|
|
724
736
|
return { block: false };
|
|
725
737
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { computeProgressScore, formatProgressLine } from "../../progress-score.js";
|
|
2
3
|
import { getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath } from "../../preferences.js";
|
|
3
4
|
import { ensurePreferencesFile, handlePrefs, handlePrefsMode, handlePrefsWizard, handleLanguage } from "../../commands-prefs-wizard.js";
|
|
@@ -197,7 +198,22 @@ export async function handleBrief(args, ctx, pi) {
|
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
199
200
|
const outputDir = getVisualBriefOutputDir();
|
|
200
|
-
|
|
201
|
+
const version = resolveGsdVersion();
|
|
202
|
+
pi.sendUserMessage(buildVisualBriefPrompt(request, { outputDir, version }));
|
|
203
|
+
}
|
|
204
|
+
const briefRequire = createRequire(import.meta.url);
|
|
205
|
+
function resolveGsdVersion() {
|
|
206
|
+
const envVersion = process.env.GSD_VERSION?.trim();
|
|
207
|
+
if (envVersion)
|
|
208
|
+
return envVersion;
|
|
209
|
+
try {
|
|
210
|
+
const pkg = briefRequire("../../../../../../package.json");
|
|
211
|
+
const fromPkg = typeof pkg.version === "string" ? pkg.version.trim() : "";
|
|
212
|
+
return fromPkg || undefined;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
201
217
|
}
|
|
202
218
|
export async function handleSetup(args, ctx, pi) {
|
|
203
219
|
const { detectProjectState, hasGlobalSetup } = await import("../../detection.js");
|
|
@@ -391,7 +391,7 @@ export function getRecentUnitKeysForProjectRoot(projectRootRealpath, limit = 20)
|
|
|
391
391
|
if (!isDbAvailable())
|
|
392
392
|
return [];
|
|
393
393
|
const db = _getAdapter();
|
|
394
|
-
const rows = db.prepare(`SELECT ud.unit_id
|
|
394
|
+
const rows = db.prepare(`SELECT ud.unit_type, ud.unit_id
|
|
395
395
|
FROM unit_dispatches ud
|
|
396
396
|
INNER JOIN workers w ON w.worker_id = ud.worker_id
|
|
397
397
|
WHERE w.project_root_realpath = :project_root_realpath
|
|
@@ -400,7 +400,7 @@ export function getRecentUnitKeysForProjectRoot(projectRootRealpath, limit = 20)
|
|
|
400
400
|
":project_root_realpath": projectRootRealpath,
|
|
401
401
|
":limit": limit,
|
|
402
402
|
});
|
|
403
|
-
return rows.reverse().map((r) => ({ key: r.unit_id }));
|
|
403
|
+
return rows.reverse().map((r) => ({ key: `${r.unit_type}/${r.unit_id}` }));
|
|
404
404
|
}
|
|
405
405
|
/**
|
|
406
406
|
* Fetch dispatches for a milestone filtered by status. Useful for janitors
|