@opengsd/gsd-pi 1.2.0-dev.4c756166 → 1.2.0-dev.955e4da0
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/bg-shell/utilities.js +2 -2
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +8 -2
- package/dist/resources/extensions/gsd/auto/orchestrator.js +33 -4
- package/dist/resources/extensions/gsd/auto/phases.js +6 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +8 -6
- package/dist/resources/extensions/gsd/auto-start.js +8 -13
- package/dist/resources/extensions/gsd/auto-worktree-repair.js +10 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +13 -270
- package/dist/resources/extensions/gsd/auto.js +4 -7
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -3
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +26 -4
- package/dist/resources/extensions/gsd/captures.js +5 -13
- package/dist/resources/extensions/gsd/closeout-recovery.js +3 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +6 -62
- package/dist/resources/extensions/gsd/db/engine.js +755 -0
- package/dist/resources/extensions/gsd/db/queries.js +372 -0
- package/dist/resources/extensions/gsd/db/sql-constants.js +11 -0
- package/dist/resources/extensions/gsd/db/writers/cascades.js +194 -0
- package/dist/resources/extensions/gsd/db/writers/import-restore.js +182 -0
- package/dist/resources/extensions/gsd/db/writers/memory.js +149 -0
- package/dist/resources/extensions/gsd/db/writers/reconcile.js +458 -0
- package/dist/resources/extensions/gsd/db/writers/status.js +70 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +8 -10
- package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -3
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +9 -2
- package/dist/resources/extensions/gsd/git-service.js +1 -0
- package/dist/resources/extensions/gsd/gitignore.js +3 -0
- package/dist/resources/extensions/gsd/gsd-db.js +171 -2048
- package/dist/resources/extensions/gsd/guided-flow.js +34 -3
- package/dist/resources/extensions/gsd/migrate/safety.js +17 -9
- package/dist/resources/extensions/gsd/migration-auto-check.js +24 -3
- package/dist/resources/extensions/gsd/model-cost-table.js +1 -0
- package/dist/resources/extensions/gsd/model-router.js +3 -0
- package/dist/resources/extensions/gsd/parallel-merge.js +14 -11
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +7 -5
- package/dist/resources/extensions/gsd/paths.js +10 -24
- package/dist/resources/extensions/gsd/preferences.js +14 -0
- package/dist/resources/extensions/gsd/recovery-classification.js +12 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +37 -4
- package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +7 -2
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -0
- package/dist/resources/extensions/gsd/state-transition-matrix.js +38 -0
- package/dist/resources/extensions/gsd/status-guards.js +56 -8
- package/dist/resources/extensions/gsd/tools/complete-slice.js +24 -43
- package/dist/resources/extensions/gsd/tools/exec-tool.js +5 -5
- package/dist/resources/extensions/gsd/tools/reopen-milestone.js +11 -29
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +14 -33
- package/dist/resources/extensions/gsd/tools/skip-slice.js +18 -36
- package/dist/resources/extensions/gsd/undo.js +8 -7
- package/dist/resources/extensions/gsd/worktree-git-recovery.js +287 -0
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +9 -1
- package/dist/resources/extensions/gsd/worktree-manager.js +45 -28
- package/dist/resources/extensions/gsd/worktree-placement.js +59 -0
- package/dist/resources/extensions/gsd/worktree-reentry.js +12 -8
- package/dist/resources/extensions/gsd/worktree-root.js +17 -6
- package/dist/resources/extensions/gsd/worktree-safety.js +8 -5
- package/dist/resources/extensions/gsd/worktree-session-state.js +12 -10
- package/dist/resources/skills/gsd-browser/SKILL.md +1 -1
- 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 +13 -13
- 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/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.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/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +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 +13 -13
- package/dist/web/standalone/.next/server/chunks/{5047.js → 5942.js} +2 -2
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +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/worktree-status-banner.js +7 -3
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +30 -21
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +266 -35
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +235 -46
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.js +3 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/bg-shell/utilities.ts +2 -2
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +6 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +28 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -1
- package/src/resources/extensions/gsd/auto/orchestrator.ts +39 -5
- package/src/resources/extensions/gsd/auto/phases.ts +10 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +12 -5
- package/src/resources/extensions/gsd/auto-start.ts +8 -14
- package/src/resources/extensions/gsd/auto-worktree-repair.ts +13 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +20 -280
- package/src/resources/extensions/gsd/auto.ts +12 -9
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +10 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -3
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +25 -3
- package/src/resources/extensions/gsd/captures.ts +5 -14
- package/src/resources/extensions/gsd/closeout-recovery.ts +2 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +6 -68
- package/src/resources/extensions/gsd/db/engine.ts +809 -0
- package/src/resources/extensions/gsd/db/queries.ts +453 -0
- package/src/resources/extensions/gsd/db/sql-constants.ts +12 -0
- package/src/resources/extensions/gsd/db/writers/cascades.ts +237 -0
- package/src/resources/extensions/gsd/db/writers/import-restore.ts +310 -0
- package/src/resources/extensions/gsd/db/writers/memory.ts +220 -0
- package/src/resources/extensions/gsd/db/writers/reconcile.ts +500 -0
- package/src/resources/extensions/gsd/db/writers/status.ts +88 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +8 -11
- package/src/resources/extensions/gsd/doctor-git-checks.ts +3 -3
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +10 -3
- package/src/resources/extensions/gsd/git-service.ts +1 -0
- package/src/resources/extensions/gsd/gitignore.ts +3 -0
- package/src/resources/extensions/gsd/gsd-db.ts +173 -2373
- package/src/resources/extensions/gsd/guided-flow.ts +34 -3
- package/src/resources/extensions/gsd/migrate/safety.ts +15 -7
- package/src/resources/extensions/gsd/migration-auto-check.ts +28 -3
- package/src/resources/extensions/gsd/model-cost-table.ts +1 -0
- package/src/resources/extensions/gsd/model-router.ts +3 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +12 -9
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +6 -5
- package/src/resources/extensions/gsd/paths.ts +9 -22
- package/src/resources/extensions/gsd/preferences.ts +18 -0
- package/src/resources/extensions/gsd/recovery-classification.ts +14 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +36 -4
- package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +7 -2
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -0
- package/src/resources/extensions/gsd/state-transition-matrix.ts +42 -0
- package/src/resources/extensions/gsd/status-guards.ts +59 -8
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-repair.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +85 -1
- package/src/resources/extensions/gsd/tests/recovery-classification-illegal-transition.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +91 -1
- package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/session-switch-clears-pending-autostart.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +43 -6
- package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +41 -4
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/worktree-placement.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +12 -6
- package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +42 -0
- package/src/resources/extensions/gsd/tools/complete-slice.ts +23 -58
- package/src/resources/extensions/gsd/tools/exec-tool.ts +5 -5
- package/src/resources/extensions/gsd/tools/reopen-milestone.ts +11 -38
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +14 -42
- package/src/resources/extensions/gsd/tools/skip-slice.ts +18 -44
- package/src/resources/extensions/gsd/undo.ts +9 -8
- package/src/resources/extensions/gsd/worktree-git-recovery.ts +308 -0
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +10 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +47 -28
- package/src/resources/extensions/gsd/worktree-placement.ts +63 -0
- package/src/resources/extensions/gsd/worktree-reentry.ts +10 -7
- package/src/resources/extensions/gsd/worktree-root.ts +17 -6
- package/src/resources/extensions/gsd/worktree-safety.ts +8 -5
- package/src/resources/extensions/gsd/worktree-session-state.ts +12 -10
- package/src/resources/skills/gsd-browser/SKILL.md +1 -1
- /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_ssgManifest.js +0 -0
|
@@ -1188,6 +1188,27 @@ function selfHealRuntimeRecords(basePath, ctx) {
|
|
|
1188
1188
|
return { cleared: 0 };
|
|
1189
1189
|
}
|
|
1190
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* True when an agent turn is currently streaming or a dispatched message is
|
|
1193
|
+
* still queued waiting to trigger one. Used by the pending-auto-start stale
|
|
1194
|
+
* check: a live discuss turn can run for minutes before writing its first
|
|
1195
|
+
* artifact, and deleting its entry as "stale" re-dispatches the workflow —
|
|
1196
|
+
* resetting the interview and producing a duplicate completion turn.
|
|
1197
|
+
*/
|
|
1198
|
+
function isAgentTurnInFlight(ctx) {
|
|
1199
|
+
try {
|
|
1200
|
+
if (typeof ctx.isIdle === "function" && !ctx.isIdle())
|
|
1201
|
+
return true;
|
|
1202
|
+
if (typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages())
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
// assertActive() throws on a stale runner context; fall through to
|
|
1207
|
+
// artifact/age staleness signals.
|
|
1208
|
+
logWarning("guided", "isAgentTurnInFlight: ctx method threw (stale runner); assuming no turn in flight");
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1191
1212
|
// ─── Milestone Actions Submenu ──────────────────────────────────────────────
|
|
1192
1213
|
/**
|
|
1193
1214
|
* Shows a submenu with Park / Discard / Skip / Back options for the active milestone.
|
|
@@ -1479,12 +1500,18 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1479
1500
|
// and fires another dispatchWorkflow, resetting the conversation mid-interview.
|
|
1480
1501
|
if (hasPendingAutoStart(basePath)) {
|
|
1481
1502
|
// #3274: If /clear interrupted the discussion, the pending entry is stale.
|
|
1482
|
-
// Detect staleness: no manifest, no milestone CONTEXT artifact,
|
|
1483
|
-
// 30s (avoids race between .set() and LLM writing
|
|
1503
|
+
// Detect staleness: no manifest, no milestone CONTEXT/CONTEXT-DRAFT artifact,
|
|
1504
|
+
// the entry is older than 30s (avoids race between .set() and LLM writing the
|
|
1505
|
+
// first artifact), AND no agent turn is in flight. A dispatched discuss turn
|
|
1506
|
+
// can think for well over 30s before its first question round writes any
|
|
1507
|
+
// artifact; deleting the entry while that turn is live re-dispatches the
|
|
1508
|
+
// workflow, which both resets the interview and queues a duplicate turn that
|
|
1509
|
+
// replays the final "context written" message after the real one.
|
|
1484
1510
|
const entry = _getPendingAutoStart(basePath);
|
|
1485
1511
|
const ageMs = Date.now() - (entry.createdAt || 0);
|
|
1486
1512
|
const manifestExists = existsSync(join(gsdRoot(basePath), "DISCUSSION-MANIFEST.json"));
|
|
1487
1513
|
const milestoneHasContext = !!resolveMilestoneFile(basePath, entry.milestoneId, "CONTEXT");
|
|
1514
|
+
const milestoneHasDraft = !!resolveMilestoneFile(basePath, entry.milestoneId, "CONTEXT-DRAFT");
|
|
1488
1515
|
const milestoneHasRoadmap = !!resolveMilestoneFile(basePath, entry.milestoneId, "ROADMAP");
|
|
1489
1516
|
const milestoneRow = isDbAvailable() ? getMilestone(entry.milestoneId) : null;
|
|
1490
1517
|
const discussPlanComplete = milestoneHasRoadmap && !!milestoneRow && milestoneRow.status !== "queued";
|
|
@@ -1493,7 +1520,11 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1493
1520
|
// Clear stale in-memory guard and continue through normal active-milestone routing.
|
|
1494
1521
|
deletePendingAutoStart(basePath);
|
|
1495
1522
|
}
|
|
1496
|
-
else if (!manifestExists &&
|
|
1523
|
+
else if (!manifestExists &&
|
|
1524
|
+
!milestoneHasContext &&
|
|
1525
|
+
!milestoneHasDraft &&
|
|
1526
|
+
ageMs > 30_000 &&
|
|
1527
|
+
!isAgentTurnInFlight(ctx)) {
|
|
1497
1528
|
// Stale entry from an interrupted discussion — clear and continue
|
|
1498
1529
|
deletePendingAutoStart(basePath);
|
|
1499
1530
|
}
|
|
@@ -78,16 +78,24 @@ export function assertMigrationHasSlices(preview) {
|
|
|
78
78
|
throw new MigrationBlockedError("Migration blocked - the legacy project would produce zero slices. Add a ROADMAP.md or phases/ content before migrating.");
|
|
79
79
|
}
|
|
80
80
|
function hasWorktreeState(targetRoot) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const containers = [
|
|
82
|
+
join(targetRoot, ".gsd-worktrees"),
|
|
83
|
+
join(gsdRoot(targetRoot), "worktrees"),
|
|
84
|
+
];
|
|
85
|
+
for (const worktreesDir of containers) {
|
|
86
|
+
if (!existsSync(worktreesDir))
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
if (readdirSync(worktreesDir, { withFileTypes: true })
|
|
90
|
+
.some((entry) => entry.isDirectory() || entry.isFile())) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
90
97
|
}
|
|
98
|
+
return false;
|
|
91
99
|
}
|
|
92
100
|
export async function assertMigrationTargetAvailable(targetRoot) {
|
|
93
101
|
const targetGsdPath = gsdRoot(targetRoot);
|
|
@@ -8,7 +8,13 @@ function zeroCounts() {
|
|
|
8
8
|
return { milestones: 0, slices: 0, tasks: 0 };
|
|
9
9
|
}
|
|
10
10
|
function emptyScan() {
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
counts: zeroCounts(),
|
|
13
|
+
milestones: new Set(),
|
|
14
|
+
slices: new Set(),
|
|
15
|
+
tasks: new Set(),
|
|
16
|
+
milestonesWithoutRoadmap: new Set(),
|
|
17
|
+
};
|
|
12
18
|
}
|
|
13
19
|
function sameCounts(a, b) {
|
|
14
20
|
return a.milestones === b.milestones && a.slices === b.slices && a.tasks === b.tasks;
|
|
@@ -65,8 +71,10 @@ export function scanMarkdownHierarchy(basePath) {
|
|
|
65
71
|
scan.counts.milestones++;
|
|
66
72
|
scan.milestones.add(milestoneId);
|
|
67
73
|
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
68
|
-
if (!roadmapPath || !existsSync(roadmapPath))
|
|
74
|
+
if (!roadmapPath || !existsSync(roadmapPath)) {
|
|
75
|
+
scan.milestonesWithoutRoadmap.add(milestoneId);
|
|
69
76
|
continue;
|
|
77
|
+
}
|
|
70
78
|
const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
|
|
71
79
|
scan.counts.slices += roadmap.slices.length;
|
|
72
80
|
for (const slice of roadmap.slices) {
|
|
@@ -112,7 +120,6 @@ export function countDbHierarchy() {
|
|
|
112
120
|
}
|
|
113
121
|
export async function checkMarkdownHierarchyAgainstDb(basePath) {
|
|
114
122
|
const markdownScan = scanMarkdownHierarchy(basePath);
|
|
115
|
-
const markdown = markdownScan.counts;
|
|
116
123
|
// Always open the DB before deciding. An empty markdown tree does NOT imply
|
|
117
124
|
// an empty project — the DB may hold authoritative rows whose markdown was
|
|
118
125
|
// lost, which is itself recoverable drift. The previous early return here
|
|
@@ -128,6 +135,20 @@ export async function checkMarkdownHierarchyAgainstDb(basePath) {
|
|
|
128
135
|
refreshWorkflowDatabaseFromDisk();
|
|
129
136
|
const dbScan = scanDbHierarchy();
|
|
130
137
|
const beforeDb = dbScan.counts;
|
|
138
|
+
// Discussion-phase scratch: a milestone dir with no ROADMAP and no DB row is
|
|
139
|
+
// a pre-registration discussion artifact (CONTEXT/CONTEXT-DRAFT only — the
|
|
140
|
+
// queued DB row is inserted only at discussion handoff). Treating it as
|
|
141
|
+
// drift would warn on every live discussion and recommend
|
|
142
|
+
// `/gsd recover --confirm`, an import that materializes abandoned-discussion
|
|
143
|
+
// dirs as ghost active milestones. Exclude such dirs from this comparison
|
|
144
|
+
// only; recover preflights use the raw scans and still see them.
|
|
145
|
+
for (const id of markdownScan.milestonesWithoutRoadmap) {
|
|
146
|
+
if (dbScan.milestones.has(id))
|
|
147
|
+
continue;
|
|
148
|
+
markdownScan.milestones.delete(id);
|
|
149
|
+
markdownScan.counts.milestones--;
|
|
150
|
+
}
|
|
151
|
+
const markdown = markdownScan.counts;
|
|
131
152
|
const markdownEmpty = sameCounts(markdown, zeroCounts());
|
|
132
153
|
const dbEmpty = sameCounts(beforeDb, zeroCounts());
|
|
133
154
|
// Genuinely empty project: nothing on disk, nothing in the DB.
|
|
@@ -13,6 +13,7 @@ export const BUNDLED_COST_TABLE = [
|
|
|
13
13
|
{ id: "claude-opus-4-6", inputPer1k: 0.005, outputPer1k: 0.025, updatedAt: "2026-04-16" },
|
|
14
14
|
{ id: "claude-opus-4-7", inputPer1k: 0.005, outputPer1k: 0.025, updatedAt: "2026-04-16" },
|
|
15
15
|
{ id: "claude-opus-4-8", inputPer1k: 0.005, outputPer1k: 0.025, updatedAt: "2026-05-28" },
|
|
16
|
+
{ id: "claude-fable-5", inputPer1k: 0.010, outputPer1k: 0.050, updatedAt: "2026-06-09" },
|
|
16
17
|
{ id: "claude-sonnet-4-6", inputPer1k: 0.003, outputPer1k: 0.015, updatedAt: "2025-03-15" },
|
|
17
18
|
{ id: "claude-haiku-4-5", inputPer1k: 0.0008, outputPer1k: 0.004, updatedAt: "2025-03-15" },
|
|
18
19
|
{ id: "claude-sonnet-4-5-20250514", inputPer1k: 0.003, outputPer1k: 0.015, updatedAt: "2025-03-15" },
|
|
@@ -35,6 +35,7 @@ export const MODEL_CAPABILITY_TIER = {
|
|
|
35
35
|
"claude-opus-4-6": "heavy",
|
|
36
36
|
"claude-opus-4-7": "heavy",
|
|
37
37
|
"claude-opus-4-8": "heavy",
|
|
38
|
+
"claude-fable-5": "heavy",
|
|
38
39
|
"claude-3-opus-latest": "heavy",
|
|
39
40
|
"gpt-4-turbo": "heavy",
|
|
40
41
|
"gpt-5": "heavy",
|
|
@@ -61,6 +62,7 @@ const MODEL_COST_PER_1K_INPUT = {
|
|
|
61
62
|
"claude-opus-4-6": 0.005,
|
|
62
63
|
"claude-opus-4-7": 0.005,
|
|
63
64
|
"claude-opus-4-8": 0.005,
|
|
65
|
+
"claude-fable-5": 0.010,
|
|
64
66
|
"gpt-4o-mini": 0.00015,
|
|
65
67
|
"gpt-4o": 0.0025,
|
|
66
68
|
"gpt-4.1": 0.002,
|
|
@@ -94,6 +96,7 @@ export const MODEL_CAPABILITY_PROFILES = {
|
|
|
94
96
|
"claude-opus-4-6": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
|
|
95
97
|
"claude-opus-4-7": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
|
|
96
98
|
"claude-opus-4-8": { coding: 97, debugging: 92, research: 87, reasoning: 97, speed: 30, longContext: 85, instruction: 92 },
|
|
99
|
+
"claude-fable-5": { coding: 97, debugging: 92, research: 87, reasoning: 97, speed: 30, longContext: 85, instruction: 92 },
|
|
97
100
|
"claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
|
|
98
101
|
"claude-sonnet-4-5-20250514": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
|
|
99
102
|
"claude-3-5-sonnet-latest": { coding: 82, debugging: 78, research: 72, reasoning: 78, speed: 62, longContext: 70, instruction: 82 },
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* with safety checks for parallel execution context.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readdirSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
8
|
import { spawnSync } from "node:child_process";
|
|
10
9
|
import { resolveGsdPathContract } from "./paths.js";
|
|
10
|
+
import { worktreePathFor, worktreesDirs } from "./worktree-placement.js";
|
|
11
11
|
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
12
12
|
import { buildWorktreeLifecycleDeps } from "./auto.js";
|
|
13
13
|
import { mergeMilestoneStandalone, } from "./worktree-lifecycle.js";
|
|
@@ -22,7 +22,7 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
22
22
|
* Returns true when milestones.status = 'complete' in project gsd.db.
|
|
23
23
|
*/
|
|
24
24
|
export function isMilestoneCompleteInProjectDb(basePath, mid) {
|
|
25
|
-
const workRoot =
|
|
25
|
+
const workRoot = worktreePathFor(basePath, mid);
|
|
26
26
|
const dbPath = resolveGsdPathContract(workRoot, basePath).projectDb;
|
|
27
27
|
if (!existsSync(dbPath))
|
|
28
28
|
return false;
|
|
@@ -41,16 +41,19 @@ export function isMilestoneCompleteInProjectDb(basePath, mid) {
|
|
|
41
41
|
*/
|
|
42
42
|
function discoverDbCompletedMilestones(basePath) {
|
|
43
43
|
const completed = new Set();
|
|
44
|
-
const worktreeDir
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
for (const worktreeDir of worktreesDirs(basePath)) {
|
|
45
|
+
if (!existsSync(worktreeDir))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
for (const entry of readdirSync(worktreeDir)) {
|
|
49
|
+
if (entry.startsWith("M") && isMilestoneCompleteInProjectDb(basePath, entry)) {
|
|
50
|
+
completed.add(entry);
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
catch (e) {
|
|
55
|
+
logWarning("parallel", `readdirSync for completed set failed: ${e.message}`);
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
return completed;
|
|
56
59
|
}
|
|
@@ -88,7 +91,7 @@ export function determineMergeOrder(workers, order = "sequential", basePath) {
|
|
|
88
91
|
title: mid,
|
|
89
92
|
pid: 0,
|
|
90
93
|
process: null,
|
|
91
|
-
worktreePath: basePath ?
|
|
94
|
+
worktreePath: basePath ? worktreePathFor(basePath, mid) : "",
|
|
92
95
|
startedAt: 0,
|
|
93
96
|
state: "stopped",
|
|
94
97
|
cost: 0,
|
|
@@ -7,6 +7,7 @@ import { matchesKey, Key } from "@gsd/pi-tui";
|
|
|
7
7
|
import { formatDuration } from "../shared/mod.js";
|
|
8
8
|
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
9
9
|
import { resolveGsdPathContract } from "./paths.js";
|
|
10
|
+
import { worktreePathFor, worktreesDirs } from "./worktree-placement.js";
|
|
10
11
|
import { renderBar, renderDialogFrame, renderKeyHints, renderProgressBar, safeLine, statusGlyph, } from "./tui/render-kit.js";
|
|
11
12
|
// ─── Data Helpers ─────────────────────────────────────────────────────────
|
|
12
13
|
function readJsonSafe(filePath) {
|
|
@@ -42,7 +43,6 @@ function tailRead(filePath, maxBytes) {
|
|
|
42
43
|
}
|
|
43
44
|
function discoverWorkers(basePath) {
|
|
44
45
|
const parallelDir = join(basePath, ".gsd", "parallel");
|
|
45
|
-
const worktreeDir = join(basePath, ".gsd", "worktrees");
|
|
46
46
|
const mids = new Set();
|
|
47
47
|
if (existsSync(parallelDir)) {
|
|
48
48
|
try {
|
|
@@ -56,7 +56,9 @@ function discoverWorkers(basePath) {
|
|
|
56
56
|
}
|
|
57
57
|
catch { /* skip */ }
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
for (const worktreeDir of worktreesDirs(basePath)) {
|
|
60
|
+
if (!existsSync(worktreeDir))
|
|
61
|
+
continue;
|
|
60
62
|
try {
|
|
61
63
|
for (const d of readdirSync(worktreeDir)) {
|
|
62
64
|
if (d.startsWith("M") && existsSync(join(worktreeDir, d, ".gsd", "auto.lock"))) {
|
|
@@ -69,7 +71,7 @@ function discoverWorkers(basePath) {
|
|
|
69
71
|
return [...mids].sort();
|
|
70
72
|
}
|
|
71
73
|
function querySliceProgress(basePath, mid) {
|
|
72
|
-
const workRoot =
|
|
74
|
+
const workRoot = worktreePathFor(basePath, mid);
|
|
73
75
|
const dbPath = resolveGsdPathContract(workRoot, basePath).projectDb;
|
|
74
76
|
if (!existsSync(dbPath))
|
|
75
77
|
return [];
|
|
@@ -115,7 +117,7 @@ function extractCostFromNdjson(basePath, mid) {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
function queryRecentCompletions(basePath, mid) {
|
|
118
|
-
const workRoot =
|
|
120
|
+
const workRoot = worktreePathFor(basePath, mid);
|
|
119
121
|
const dbPath = resolveGsdPathContract(workRoot, basePath).projectDb;
|
|
120
122
|
if (!existsSync(dbPath))
|
|
121
123
|
return [];
|
|
@@ -140,7 +142,7 @@ function collectWorkerData(basePath) {
|
|
|
140
142
|
const workers = [];
|
|
141
143
|
for (const mid of mids) {
|
|
142
144
|
const status = readJsonSafe(join(parallelDir, `${mid}.status.json`));
|
|
143
|
-
const lock = readJsonSafe(join(basePath,
|
|
145
|
+
const lock = readJsonSafe(join(worktreePathFor(basePath, mid), ".gsd", "auto.lock"));
|
|
144
146
|
const slices = querySliceProgress(basePath, mid);
|
|
145
147
|
const pid = lock?.pid || status?.pid || 0;
|
|
146
148
|
const alive = pid ? isPidAlive(pid) : false;
|
|
@@ -16,7 +16,7 @@ import { spawnSync } from "node:child_process";
|
|
|
16
16
|
import { nativeScanGsdTree } from "./native-parser-bridge.js";
|
|
17
17
|
import { DIR_CACHE_MAX } from "./constants.js";
|
|
18
18
|
import { gsdHome } from "./gsd-home.js";
|
|
19
|
-
import { isGsdWorktreePath, resolveExternalStateProjectGsdFromWorktreePath, resolveWorktreeProjectRoot } from "./worktree-root.js";
|
|
19
|
+
import { findWorktreeSegment, isGsdWorktreePath, resolveExternalStateProjectGsdFromWorktreePath, resolveWorktreeProjectRoot } from "./worktree-root.js";
|
|
20
20
|
// ─── Directory Listing Cache ──────────────────────────────────────────────────
|
|
21
21
|
const dirEntryCache = new Map();
|
|
22
22
|
const dirListCache = new Map();
|
|
@@ -422,31 +422,17 @@ function assertNotGlobalGsdHome(basePath, result) {
|
|
|
422
422
|
* When gsdRoot() is called with such a path, we must NOT walk up to the
|
|
423
423
|
* project root's .gsd — each worktree manages its own .gsd state (#2594).
|
|
424
424
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
425
|
+
* Layout matching is owned by worktree-root's findWorktreeSegment; this
|
|
426
|
+
* only adds the requirement that a non-empty worktree name follows the
|
|
427
|
+
* marker (the worktrees container dir itself is not a worktree).
|
|
427
428
|
*/
|
|
428
429
|
function isInsideGsdWorktree(p) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
`${sepNative}.gsd${sepNative}worktrees${sepNative}`,
|
|
436
|
-
];
|
|
437
|
-
for (const marker of markers) {
|
|
438
|
-
const idx = p.indexOf(marker);
|
|
439
|
-
if (idx === -1)
|
|
440
|
-
continue;
|
|
441
|
-
// Verify there's a non-empty worktree name after the marker
|
|
442
|
-
const afterMarker = p.slice(idx + marker.length);
|
|
443
|
-
// The name is everything up to the next separator (or end of string)
|
|
444
|
-
const nameEnd = afterMarker.search(/[/\\]/);
|
|
445
|
-
const name = nameEnd === -1 ? afterMarker : afterMarker.slice(0, nameEnd);
|
|
446
|
-
if (name.length > 0)
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
return false;
|
|
430
|
+
const normalized = p.replaceAll("\\", "/");
|
|
431
|
+
const segment = findWorktreeSegment(normalized);
|
|
432
|
+
if (!segment)
|
|
433
|
+
return false;
|
|
434
|
+
const name = normalized.slice(segment.afterWorktrees).split("/")[0];
|
|
435
|
+
return name.length > 0;
|
|
450
436
|
}
|
|
451
437
|
function probeGsdRoot(rawBasePath) {
|
|
452
438
|
const contract = resolveGsdPathContract(rawBasePath);
|
|
@@ -699,6 +699,20 @@ export function getIsolationMode(basePath) {
|
|
|
699
699
|
return "branch";
|
|
700
700
|
return "none"; // default — no isolation, work on current branch
|
|
701
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Resolve the isolation mode a unit actually runs under. A session whose
|
|
704
|
+
* worktree isolation has degraded (worktree creation failed) falls back to
|
|
705
|
+
* the milestone branch in the project root, so configured "worktree" becomes
|
|
706
|
+
* effective "branch". A stranded-work recovery session likewise runs under
|
|
707
|
+
* the adopted mode (`strandedRecoveryIsolationMode`) rather than the
|
|
708
|
+
* configured one until the recovered milestone merges — adopting the
|
|
709
|
+
* milestone branch in the project root is intentional, not degraded.
|
|
710
|
+
*/
|
|
711
|
+
export function resolveEffectiveUnitIsolationMode(configuredMode, isolationDegraded, strandedRecoveryIsolationMode = null) {
|
|
712
|
+
if (configuredMode === "worktree" && isolationDegraded)
|
|
713
|
+
return "branch";
|
|
714
|
+
return strandedRecoveryIsolationMode ?? configuredMode;
|
|
715
|
+
}
|
|
702
716
|
export function resolveParallelConfig(prefs) {
|
|
703
717
|
return {
|
|
704
718
|
enabled: prefs?.parallel?.enabled ?? false,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// File Purpose: ADR-015 Recovery Classification module for runtime failure taxonomy.
|
|
3
3
|
import { classifyError, isTransient } from "./error-classifier.js";
|
|
4
4
|
import { ReconciliationFailedError } from "./state-reconciliation.js";
|
|
5
|
+
import { IllegalPhaseTransitionError } from "./state-transition-matrix.js";
|
|
5
6
|
export function classifyFailure(input) {
|
|
6
7
|
const message = errorMessage(input.error);
|
|
7
8
|
// ADR-017: ReconciliationFailedError is a typed throw from the State
|
|
@@ -9,7 +10,9 @@ export function classifyFailure(input) {
|
|
|
9
10
|
// failureKind so the taxonomy stays consistent.
|
|
10
11
|
const failureKind = input.error instanceof ReconciliationFailedError
|
|
11
12
|
? "reconciliation-drift"
|
|
12
|
-
: input.
|
|
13
|
+
: input.error instanceof IllegalPhaseTransitionError
|
|
14
|
+
? "illegal-transition"
|
|
15
|
+
: input.failureKind ?? inferFailureKind(message);
|
|
13
16
|
switch (failureKind) {
|
|
14
17
|
case "tool-schema":
|
|
15
18
|
return {
|
|
@@ -75,6 +78,14 @@ export function classifyFailure(input) {
|
|
|
75
78
|
exitReason: "reconciliation-drift",
|
|
76
79
|
remediation: "Inspect the persistent or repair-failed drift kinds reported by the State Reconciliation Module before resuming.",
|
|
77
80
|
};
|
|
81
|
+
case "illegal-transition":
|
|
82
|
+
return {
|
|
83
|
+
failureKind,
|
|
84
|
+
action: "escalate",
|
|
85
|
+
reason: `Illegal phase transition${unitSuffix(input)}: ${message}`,
|
|
86
|
+
exitReason: "illegal-transition",
|
|
87
|
+
remediation: "A derived Phase edge rejected by the Phase Transition Invariant survived reconciliation; inspect deriveState and the State Reconciliation Module before resuming.",
|
|
88
|
+
};
|
|
78
89
|
case "provider": {
|
|
79
90
|
const providerClass = classifyError(message, input.retryAfterMs);
|
|
80
91
|
return {
|
|
@@ -21,9 +21,10 @@ const EXECUTION_TOOL_NAMES = new Set([
|
|
|
21
21
|
"functions.exec_command",
|
|
22
22
|
"gsd_exec",
|
|
23
23
|
"gsd_exec_search",
|
|
24
|
+
"gsd_uat_exec",
|
|
24
25
|
"powershell",
|
|
25
26
|
]);
|
|
26
|
-
const MCP_EXECUTION_TOOL_RE = /^mcp__.+
|
|
27
|
+
const MCP_EXECUTION_TOOL_RE = /^mcp__.+__gsd_(?:uat_)?exec(?:_search)?$/;
|
|
27
28
|
// ─── Module State ───────────────────────────────────────────────────────────
|
|
28
29
|
let unitEvidence = [];
|
|
29
30
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
@@ -146,11 +147,18 @@ export function clearEvidenceFromDisk(basePath, milestoneId, sliceId, taskId) {
|
|
|
146
147
|
* Exit codes and output are filled in by recordToolResult after execution.
|
|
147
148
|
*/
|
|
148
149
|
export function recordToolCall(toolCallId, toolName, input) {
|
|
150
|
+
// Idempotent by toolCallId: native tools reach this via both
|
|
151
|
+
// tool_execution_start and tool_call; external (pre-executed) tools only
|
|
152
|
+
// via tool_execution_start. First recording wins.
|
|
153
|
+
if (unitEvidence.some(e => e.toolCallId === toolCallId))
|
|
154
|
+
return;
|
|
149
155
|
if (isExecutionToolName(toolName)) {
|
|
150
156
|
unitEvidence.push({
|
|
151
157
|
kind: "bash",
|
|
152
158
|
toolCallId,
|
|
153
|
-
|
|
159
|
+
// gsd_exec / gsd_uat_exec carry the script body in `script` (or `code`);
|
|
160
|
+
// bash-style tools use `command`/`cmd`; gsd_exec_search uses `query`.
|
|
161
|
+
command: String(input.command ?? input.script ?? input.cmd ?? input.code ?? input.query ?? ""),
|
|
154
162
|
exitCode: -1,
|
|
155
163
|
outputSnippet: "",
|
|
156
164
|
timestamp: Date.now(),
|
|
@@ -185,9 +193,34 @@ export function recordToolResult(toolCallId, toolName, result, isError) {
|
|
|
185
193
|
if (entry.kind === "bash") {
|
|
186
194
|
const text = extractResultText(result);
|
|
187
195
|
entry.outputSnippet = text.slice(0, 500);
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
entry.exitCode = resolveExitCode(text, isError);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the exit code from a tool result's text. Handles the bash tool's
|
|
201
|
+
* prose marker, the gsd_exec / gsd_uat_exec JSON envelope (`"exit_code": N`),
|
|
202
|
+
* and a last-resort read of the run's persisted `.gsd/exec/<id>.meta.json`
|
|
203
|
+
* (covers truncated result text).
|
|
204
|
+
*/
|
|
205
|
+
function resolveExitCode(text, isError) {
|
|
206
|
+
const proseMatch = text.match(/Command exited with code (\d+)/);
|
|
207
|
+
if (proseMatch)
|
|
208
|
+
return Number(proseMatch[1]);
|
|
209
|
+
const jsonMatch = text.match(/"exit_code"\s*:\s*(-?\d+)/);
|
|
210
|
+
if (jsonMatch)
|
|
211
|
+
return Number(jsonMatch[1]);
|
|
212
|
+
const metaMatch = text.match(/"meta_path"\s*:\s*"([^"]+)"/);
|
|
213
|
+
if (metaMatch) {
|
|
214
|
+
try {
|
|
215
|
+
const meta = JSON.parse(readFileSync(metaMatch[1], "utf-8"));
|
|
216
|
+
if (typeof meta.exit_code === "number")
|
|
217
|
+
return meta.exit_code;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Fall through to the isError heuristic
|
|
221
|
+
}
|
|
190
222
|
}
|
|
223
|
+
return isError ? 1 : 0;
|
|
191
224
|
}
|
|
192
225
|
// ─── Internals ──────────────────────────────────────────────────────────────
|
|
193
226
|
function extractResultText(result) {
|
|
@@ -80,8 +80,13 @@ function findMatches(claimedCommand, bashCalls) {
|
|
|
80
80
|
const exact = bashCalls.filter(b => b.command.trim() === normalized);
|
|
81
81
|
if (exact.length > 0)
|
|
82
82
|
return exact;
|
|
83
|
-
// Substring match: claimed is contained in actual or actual in claimed
|
|
84
|
-
|
|
83
|
+
// Substring match: claimed is contained in actual or actual in claimed.
|
|
84
|
+
// A claimed verification command typically appears verbatim inside a
|
|
85
|
+
// larger gsd_exec script body (cd prefix, multi-line scripts), so
|
|
86
|
+
// script-containing-claim is the common direction. Blank-command entries
|
|
87
|
+
// must be excluded — `"x".includes("")` is true, so they'd match anything.
|
|
88
|
+
const substring = bashCalls.filter(b => b.command.trim().length > 0 &&
|
|
89
|
+
(b.command.includes(normalized) || normalized.includes(b.command)));
|
|
85
90
|
if (substring.length > 0)
|
|
86
91
|
return substring;
|
|
87
92
|
// Token match: split on whitespace and check significant overlap
|
|
@@ -17,6 +17,16 @@ import { logWarning } from "../workflow-logger.js";
|
|
|
17
17
|
const _require = createRequire(import.meta.url);
|
|
18
18
|
const picomatch = _require("picomatch");
|
|
19
19
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Build the effective allowlist for a unit's file-change audit.
|
|
22
|
+
*
|
|
23
|
+
* When GSD manages .gitignore (manage_gitignore unset or true), ensureGitignore()
|
|
24
|
+
* appends baseline patterns at auto-start and the edit rides into the task's
|
|
25
|
+
* auto-commit — so .gitignore changes must not be attributed to the task.
|
|
26
|
+
*/
|
|
27
|
+
export function effectiveFileChangeAllowlist(baseAllowlist, manageGitignore) {
|
|
28
|
+
return manageGitignore === false ? baseAllowlist : [...baseAllowlist, ".gitignore"];
|
|
29
|
+
}
|
|
20
30
|
/**
|
|
21
31
|
* Validate file changes after auto-commit for an execute-task unit.
|
|
22
32
|
* Returns null if task data is unavailable or DB is not loaded.
|
|
@@ -99,6 +99,44 @@ export const STATE_TRANSITION_MATRIX = [
|
|
|
99
99
|
export function findTransition(from, event) {
|
|
100
100
|
return STATE_TRANSITION_MATRIX.find((entry) => (entry.from === from || entry.from === "*") && entry.event === event);
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Edge-keyed legality check for the Phase Transition Invariant (ADR-030).
|
|
104
|
+
* `advance()` derives the next Phase and asserts the (from → to) edge here.
|
|
105
|
+
*
|
|
106
|
+
* The matrix is an assertion, not a decision-maker — `deriveState` already
|
|
107
|
+
* chose the Phase. A self-edge is trivially legal (no transition to assert). An
|
|
108
|
+
* edge is legal when some matrix entry permits it, honoring the `*` wildcard
|
|
109
|
+
* rows (e.g. any → blocked via manual-block, any → executing via
|
|
110
|
+
* retryable-failure).
|
|
111
|
+
*
|
|
112
|
+
* Note: the matrix is currently a sparse hardening spec, not a complete
|
|
113
|
+
* legal-edge graph, so `advance()` consumes this in advisory mode (telemetry
|
|
114
|
+
* only). It must be expanded to cover every edge `deriveState` emits before
|
|
115
|
+
* enforcement flips on.
|
|
116
|
+
*/
|
|
117
|
+
export function isLegalEdge(from, to) {
|
|
118
|
+
if (from === to)
|
|
119
|
+
return true;
|
|
120
|
+
return STATE_TRANSITION_MATRIX.some((entry) => (entry.from === from || entry.from === "*") && entry.to === to);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Thrown when an illegal derived Phase edge survives reconciliation. Carries
|
|
124
|
+
* both endpoints so Recovery Classification can report them. Recognized by
|
|
125
|
+
* class in `classifyFailure` and mapped to the `illegal-transition` kind.
|
|
126
|
+
*/
|
|
127
|
+
export class IllegalPhaseTransitionError extends Error {
|
|
128
|
+
// Explicit fields, not constructor parameter properties — strip-types
|
|
129
|
+
// consumers (workspace-index subprocess, integration tests) reject the
|
|
130
|
+
// parameter-property syntax with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
|
|
131
|
+
from;
|
|
132
|
+
to;
|
|
133
|
+
constructor(from, to) {
|
|
134
|
+
super(`Illegal phase transition ${from} -> ${to} survived reconciliation`);
|
|
135
|
+
this.name = "IllegalPhaseTransitionError";
|
|
136
|
+
this.from = from;
|
|
137
|
+
this.to = to;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
102
140
|
export function validateTransitionMatrix(requiredEvents) {
|
|
103
141
|
const seen = new Set();
|
|
104
142
|
const duplicateKeys = [];
|
|
@@ -1,16 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Status predicates for GSD state-machine
|
|
2
|
+
* Status predicates and the canonical status vocabulary for GSD state-machine
|
|
3
|
+
* guards (ADR-030).
|
|
3
4
|
*
|
|
4
|
-
* The DB
|
|
5
|
-
*
|
|
6
|
-
* "closed" (legacy/imported), and
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The DB column is free-form `string` so legacy/imported rows still load. Three
|
|
6
|
+
* raw values besides canonical "complete"/"skipped" indicate "closed": "done"
|
|
7
|
+
* (legacy alias), "closed" (legacy/imported), and "skipped" (user-directed skip
|
|
8
|
+
* via rethink or backtrack). `RAW_CLOSED_STATUSES` is the single source for both
|
|
9
|
+
* `isClosedStatus()` and the SQL terminal-status fragment
|
|
10
|
+
* (`db/sql-constants.ts` derives `TERMINAL_STATUS_SQL` from it), replacing the
|
|
11
|
+
* prior independent definitions.
|
|
12
|
+
*
|
|
13
|
+
* `toStatus()` is the single seam where a free-form string becomes the canonical
|
|
14
|
+
* `Status` vocabulary; the Status Transition Core writes canonical, so the store
|
|
15
|
+
* converges over time without a forced migration.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Canonical, normalized entity-status vocabulary across milestones, slices, and
|
|
19
|
+
* tasks — the single source for both the `Status` type and the runtime
|
|
20
|
+
* membership set. The in-memory domain speaks `Status`; the DB column stays
|
|
21
|
+
* free-form.
|
|
22
|
+
*/
|
|
23
|
+
export const CANONICAL_STATUSES = [
|
|
24
|
+
"pending", "queued", "active", "parked", "in_progress", "blocked", "complete", "skipped", "deferred",
|
|
25
|
+
];
|
|
26
|
+
const CANONICAL_STATUS_SET = new Set(CANONICAL_STATUSES);
|
|
27
|
+
/**
|
|
28
|
+
* Raw status values that mean a unit is closed — the single source of truth.
|
|
29
|
+
* Includes legacy/imported aliases ("done", "closed") alongside canonical
|
|
30
|
+
* "complete"/"skipped" because the DB column is free-form and older rows /
|
|
31
|
+
* imports still carry them. Order matters: `TERMINAL_STATUS_SQL` is derived
|
|
32
|
+
* from this array verbatim.
|
|
33
|
+
*/
|
|
34
|
+
export const RAW_CLOSED_STATUSES = ["complete", "done", "skipped", "closed"];
|
|
35
|
+
const RAW_CLOSED_SET = new Set(RAW_CLOSED_STATUSES);
|
|
36
|
+
/** Free-form aliases mapped to their canonical Status on read. */
|
|
37
|
+
const ALIAS_TO_CANONICAL = {
|
|
38
|
+
done: "complete",
|
|
39
|
+
closed: "complete",
|
|
40
|
+
planned: "pending",
|
|
41
|
+
"in-progress": "in_progress",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Normalize a free-form DB status string into the canonical `Status`
|
|
45
|
+
* vocabulary. Maps known aliases (done/closed → complete, planned → pending,
|
|
46
|
+
* in-progress → in_progress). An unrecognized/legacy value is **quarantined** —
|
|
47
|
+
* preserved verbatim rather than silently remapped to a wrong canonical state —
|
|
48
|
+
* so reads never fail and reconciliation/telemetry can surface it.
|
|
10
49
|
*/
|
|
50
|
+
export function toStatus(raw) {
|
|
51
|
+
const value = raw.trim();
|
|
52
|
+
if (CANONICAL_STATUS_SET.has(value))
|
|
53
|
+
return value;
|
|
54
|
+
const alias = ALIAS_TO_CANONICAL[value];
|
|
55
|
+
if (alias)
|
|
56
|
+
return alias;
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
11
59
|
/** Returns true when a milestone, slice, or task status indicates closure. */
|
|
12
60
|
export function isClosedStatus(status) {
|
|
13
|
-
return status
|
|
61
|
+
return RAW_CLOSED_SET.has(status);
|
|
14
62
|
}
|
|
15
63
|
/** Returns true when a slice status indicates it was deferred by a decision. */
|
|
16
64
|
export function isDeferredStatus(status) {
|