gsd-pi 2.79.0 → 2.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -47
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
- package/dist/resources/extensions/gsd/auto/phases.js +61 -7
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
- package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
- package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
- package/dist/resources/extensions/gsd/auto-start.js +3 -2
- package/dist/resources/extensions/gsd/auto.js +159 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +41 -45
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
- package/dist/resources/extensions/gsd/commands/context.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +34 -1
- package/dist/resources/extensions/gsd/guided-flow.js +40 -0
- package/dist/resources/extensions/gsd/paths.js +5 -1
- package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
- package/dist/resources/extensions/gsd/preferences-types.js +20 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +82 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
- package/dist/resources/extensions/gsd/uok/audit.js +23 -9
- package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
- package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
- package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
- package/dist/resources/extensions/shared/interview-ui.js +15 -4
- 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 +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- 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/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 +9 -9
- 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/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +53 -0
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +129 -2
- package/packages/mcp-server/src/workflow-tools.ts +81 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
- package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
- package/src/resources/extensions/gsd/auto/phases.ts +88 -9
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
- package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
- package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
- package/src/resources/extensions/gsd/auto-start.ts +3 -2
- package/src/resources/extensions/gsd/auto.ts +167 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +49 -46
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
- package/src/resources/extensions/gsd/commands/context.ts +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +35 -1
- package/src/resources/extensions/gsd/guided-flow.ts +47 -0
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/paths.ts +6 -1
- package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
- package/src/resources/extensions/gsd/preferences-types.ts +23 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
- package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +132 -3
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +84 -1
- package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
- package/src/resources/extensions/gsd/uok/audit.ts +25 -9
- package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
- package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
- package/src/resources/extensions/shared/interview-ui.ts +18 -5
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
- package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
|
@@ -31,7 +31,7 @@ import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-m
|
|
|
31
31
|
import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
|
|
32
32
|
import { resetHookState, runPreDispatchHooks, restoreHookState, clearPersistedHookState, } from "./post-unit-hooks.js";
|
|
33
33
|
import { runGSDDoctor, rebuildState } from "./doctor.js";
|
|
34
|
-
import { preDispatchHealthGate, resetProactiveHealing, setLevelChangeCallback, } from "./doctor-proactive.js";
|
|
34
|
+
import { preDispatchHealthGate, recordHealthSnapshot, resetProactiveHealing, setLevelChangeCallback, } from "./doctor-proactive.js";
|
|
35
35
|
import { clearSkillSnapshot } from "./skill-discovery.js";
|
|
36
36
|
import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
|
|
37
37
|
import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
|
|
@@ -43,7 +43,7 @@ import { isAbsolute, join } from "node:path";
|
|
|
43
43
|
import { pathToFileURL } from "node:url";
|
|
44
44
|
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
45
45
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
46
|
-
import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, } from "./worktree.js";
|
|
46
|
+
import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, resolveProjectRoot, } from "./worktree.js";
|
|
47
47
|
import { GitServiceImpl } from "./git-service.js";
|
|
48
48
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
49
49
|
import { createAutoWorktree, enterAutoWorktree, enterBranchModeForMilestone, teardownAutoWorktree, isInAutoWorktree, getAutoWorktreePath, mergeMilestoneToMain, autoWorktreeBranch, syncWorktreeStateBack, syncProjectRootToWorktree, checkResourcesStale, escapeStaleWorktree, } from "./auto-worktree.js";
|
|
@@ -82,6 +82,7 @@ import { resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSess
|
|
|
82
82
|
import { runAutoLoopWithUok } from "./uok/kernel.js";
|
|
83
83
|
import { resolveUokFlags } from "./uok/flags.js";
|
|
84
84
|
import { validateDirectory } from "./validate-directory.js";
|
|
85
|
+
import { createAutoOrchestrator } from "./auto/orchestrator.js";
|
|
85
86
|
import { WorktreeResolver, } from "./worktree-resolver.js";
|
|
86
87
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
87
88
|
export { STUB_RECOVERY_THRESHOLD, NEW_SESSION_TIMEOUT_MS, } from "./auto/session.js";
|
|
@@ -904,6 +905,12 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
904
905
|
// changes the user made between sessions (#4959 / CodeRabbit).
|
|
905
906
|
if (pi)
|
|
906
907
|
clearToolBaseline(pi);
|
|
908
|
+
try {
|
|
909
|
+
await s.orchestration?.stop(reason ?? "stop");
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
debugLog("stop-orchestration-stop", { error: err instanceof Error ? err.message : String(err) });
|
|
913
|
+
}
|
|
907
914
|
// Reset all session state in one call
|
|
908
915
|
s.reset();
|
|
909
916
|
}
|
|
@@ -953,6 +960,7 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
953
960
|
activeRunDir: s.activeRunDir,
|
|
954
961
|
autoStartTime: s.autoStartTime,
|
|
955
962
|
milestoneLock: s.sessionMilestoneLock ?? undefined,
|
|
963
|
+
pauseReason: _errorContext?.message,
|
|
956
964
|
};
|
|
957
965
|
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, pausedMeta);
|
|
958
966
|
}
|
|
@@ -992,6 +1000,12 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
992
1000
|
// Unblock pending unitPromise so autoLoop exits cleanly (#1799)
|
|
993
1001
|
resolveAgentEnd({ messages: [] });
|
|
994
1002
|
_resetPendingResolve();
|
|
1003
|
+
try {
|
|
1004
|
+
await s.orchestration?.stop("pause");
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
debugLog("pause-orchestration-stop", { error: err instanceof Error ? err.message : String(err) });
|
|
1008
|
+
}
|
|
995
1009
|
s.active = false;
|
|
996
1010
|
s.paused = true;
|
|
997
1011
|
deactivateGSD();
|
|
@@ -1041,6 +1055,132 @@ function buildResolverDeps() {
|
|
|
1041
1055
|
function buildResolver() {
|
|
1042
1056
|
return new WorktreeResolver(s, buildResolverDeps());
|
|
1043
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Thin entry glue for the new Auto Orchestration module.
|
|
1060
|
+
*
|
|
1061
|
+
* This intentionally wires only dispatch + error notification today, with
|
|
1062
|
+
* no behavior changes to the existing auto loop. It provides a concrete seam
|
|
1063
|
+
* the next refactor steps can adopt incrementally.
|
|
1064
|
+
*/
|
|
1065
|
+
export function createWiredAutoOrchestrationModule(ctx, _pi, dispatchBasePath, runtimeBasePath = resolveProjectRoot(dispatchBasePath)) {
|
|
1066
|
+
const flowId = `auto-orchestrator-${Date.now()}`;
|
|
1067
|
+
let seq = 0;
|
|
1068
|
+
const deps = {
|
|
1069
|
+
dispatch: {
|
|
1070
|
+
async decideNextUnit() {
|
|
1071
|
+
const state = await deriveState(dispatchBasePath);
|
|
1072
|
+
const active = state.activeMilestone;
|
|
1073
|
+
if (!active)
|
|
1074
|
+
return null;
|
|
1075
|
+
const prefs = loadEffectiveGSDPreferences(dispatchBasePath)?.preferences;
|
|
1076
|
+
const action = await resolveDispatch({
|
|
1077
|
+
basePath: dispatchBasePath,
|
|
1078
|
+
mid: active.id,
|
|
1079
|
+
midTitle: active.title,
|
|
1080
|
+
state,
|
|
1081
|
+
prefs,
|
|
1082
|
+
});
|
|
1083
|
+
if (action.action !== "dispatch")
|
|
1084
|
+
return null;
|
|
1085
|
+
return {
|
|
1086
|
+
unitType: action.unitType,
|
|
1087
|
+
unitId: action.unitId,
|
|
1088
|
+
reason: action.matchedRule ?? "dispatch",
|
|
1089
|
+
preconditions: [],
|
|
1090
|
+
};
|
|
1091
|
+
},
|
|
1092
|
+
},
|
|
1093
|
+
recovery: {
|
|
1094
|
+
async classifyAndRecover(input) {
|
|
1095
|
+
const reason = input.error instanceof Error ? input.error.message : String(input.error ?? "unknown auto error");
|
|
1096
|
+
return { action: "escalate", reason };
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
worktree: {
|
|
1100
|
+
async prepareForUnit() { },
|
|
1101
|
+
async syncAfterUnit() { },
|
|
1102
|
+
async cleanupOnStop() { },
|
|
1103
|
+
},
|
|
1104
|
+
health: {
|
|
1105
|
+
async preAdvanceGate() {
|
|
1106
|
+
const gate = await preDispatchHealthGate(dispatchBasePath);
|
|
1107
|
+
return {
|
|
1108
|
+
allow: gate.proceed,
|
|
1109
|
+
reason: gate.reason,
|
|
1110
|
+
};
|
|
1111
|
+
},
|
|
1112
|
+
async postAdvanceRecord(result) {
|
|
1113
|
+
if (result.kind === "error") {
|
|
1114
|
+
recordHealthSnapshot(1, 0, 0, [{
|
|
1115
|
+
code: "orchestration-error",
|
|
1116
|
+
message: result.reason ?? "orchestration error",
|
|
1117
|
+
severity: "error",
|
|
1118
|
+
unitId: "orchestration",
|
|
1119
|
+
}], [], "orchestration");
|
|
1120
|
+
}
|
|
1121
|
+
else if (result.kind === "blocked") {
|
|
1122
|
+
recordHealthSnapshot(0, 1, 0, [{
|
|
1123
|
+
code: "orchestration-blocked",
|
|
1124
|
+
message: result.reason ?? "orchestration blocked",
|
|
1125
|
+
severity: "warning",
|
|
1126
|
+
unitId: "orchestration",
|
|
1127
|
+
}], [], "orchestration");
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
},
|
|
1131
|
+
runtime: {
|
|
1132
|
+
async ensureLockOwnership() {
|
|
1133
|
+
const status = getSessionLockStatus(runtimeBasePath);
|
|
1134
|
+
if (!status.valid || status.failureReason === "pid-mismatch") {
|
|
1135
|
+
throw new Error("session lock held by another process");
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
async journalTransition(event) {
|
|
1139
|
+
const eventType = event.name === "start"
|
|
1140
|
+
? "iteration-start"
|
|
1141
|
+
: event.name === "resume"
|
|
1142
|
+
? "iteration-start"
|
|
1143
|
+
: event.name === "advance"
|
|
1144
|
+
? "dispatch-match"
|
|
1145
|
+
: event.name === "advance-blocked"
|
|
1146
|
+
? "guard-block"
|
|
1147
|
+
: event.name === "advance-stopped"
|
|
1148
|
+
? "dispatch-stop"
|
|
1149
|
+
: event.name === "advance-error"
|
|
1150
|
+
? "iteration-end"
|
|
1151
|
+
: event.name === "advance-paused" || event.name === "advance-retry"
|
|
1152
|
+
? "guard-block"
|
|
1153
|
+
: event.name === "stop"
|
|
1154
|
+
? "terminal"
|
|
1155
|
+
: "iteration-end";
|
|
1156
|
+
_emitJournalEvent(runtimeBasePath, {
|
|
1157
|
+
ts: new Date().toISOString(),
|
|
1158
|
+
flowId,
|
|
1159
|
+
seq: ++seq,
|
|
1160
|
+
eventType,
|
|
1161
|
+
data: {
|
|
1162
|
+
source: "auto-orchestrator",
|
|
1163
|
+
name: event.name,
|
|
1164
|
+
reason: event.reason,
|
|
1165
|
+
unitType: event.unitType,
|
|
1166
|
+
unitId: event.unitId,
|
|
1167
|
+
},
|
|
1168
|
+
});
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
notifications: {
|
|
1172
|
+
async notifyLifecycle(event) {
|
|
1173
|
+
if (event.name === "error") {
|
|
1174
|
+
ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
};
|
|
1179
|
+
return createAutoOrchestrator(deps);
|
|
1180
|
+
}
|
|
1181
|
+
function ensureOrchestrationModule(ctx, pi, basePath) {
|
|
1182
|
+
s.orchestration = createWiredAutoOrchestrationModule(ctx, pi, basePath, lockBase());
|
|
1183
|
+
}
|
|
1044
1184
|
/**
|
|
1045
1185
|
* Build the LoopDeps object from auto.ts private scope.
|
|
1046
1186
|
* This bundles all private functions that autoLoop needs without exporting them.
|
|
@@ -1398,6 +1538,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1398
1538
|
// s.basePath may have been updated to a worktree path by enterMilestone.
|
|
1399
1539
|
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1400
1540
|
}
|
|
1541
|
+
ensureOrchestrationModule(ctx, pi, s.basePath || base);
|
|
1401
1542
|
registerSigtermHandler(lockBase());
|
|
1402
1543
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
1403
1544
|
ctx.ui.setWidget("gsd-health", undefined);
|
|
@@ -1457,6 +1598,12 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1457
1598
|
clearPausedSession("paused-session DB cleanup failed (resume activation)");
|
|
1458
1599
|
}
|
|
1459
1600
|
pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
|
|
1601
|
+
try {
|
|
1602
|
+
await s.orchestration?.resume();
|
|
1603
|
+
}
|
|
1604
|
+
catch (err) {
|
|
1605
|
+
debugLog("resume-orchestration-resume", { error: err instanceof Error ? err.message : String(err) });
|
|
1606
|
+
}
|
|
1460
1607
|
startAutoCommandPolling(s.basePath);
|
|
1461
1608
|
await runAutoLoopWithUok({
|
|
1462
1609
|
ctx,
|
|
@@ -1482,6 +1629,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1482
1629
|
// Build scope after bootstrap has populated s.basePath / s.originalBasePath /
|
|
1483
1630
|
// s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
|
|
1484
1631
|
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1632
|
+
ensureOrchestrationModule(ctx, pi, s.basePath || base);
|
|
1485
1633
|
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1486
1634
|
registerAutoWorkerForSession(s);
|
|
1487
1635
|
try {
|
|
@@ -1492,6 +1640,12 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1492
1640
|
logWarning("engine", `cmux sync failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1493
1641
|
}
|
|
1494
1642
|
pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: requestedStepMode ? "Step-mode started." : "Auto-mode started.", level: "progress" });
|
|
1643
|
+
try {
|
|
1644
|
+
await s.orchestration?.start({ basePath: s.basePath, trigger: "auto-loop" });
|
|
1645
|
+
}
|
|
1646
|
+
catch (err) {
|
|
1647
|
+
debugLog("start-orchestration-start", { error: err instanceof Error ? err.message : String(err) });
|
|
1648
|
+
}
|
|
1495
1649
|
startAutoCommandPolling(s.basePath);
|
|
1496
1650
|
// Dispatch the first unit
|
|
1497
1651
|
await runAutoLoopWithUok({
|
|
@@ -1576,6 +1730,9 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
|
|
|
1576
1730
|
s.pendingQuickTasks = [];
|
|
1577
1731
|
}
|
|
1578
1732
|
s.basePath = targetBasePath;
|
|
1733
|
+
if (!s.orchestration) {
|
|
1734
|
+
ensureOrchestrationModule(ctx, pi, s.basePath);
|
|
1735
|
+
}
|
|
1579
1736
|
const hookUnitType = `hook/${hookName}`;
|
|
1580
1737
|
const hookStartedAt = Date.now();
|
|
1581
1738
|
s.currentUnit = {
|
|
@@ -39,6 +39,14 @@ function resolveAgentEndBasePath() {
|
|
|
39
39
|
return undefined;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
export function _buildAbortedPauseContext(lastMsg) {
|
|
43
|
+
const hasErrorMessage = Object.prototype.hasOwnProperty.call(lastMsg, "errorMessage") && !!lastMsg.errorMessage;
|
|
44
|
+
return {
|
|
45
|
+
message: hasErrorMessage ? String(lastMsg.errorMessage) : "Operation aborted",
|
|
46
|
+
category: "aborted",
|
|
47
|
+
isTransient: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
42
50
|
async function pauseTransientWithBackoff(cls, pi, ctx, errorDetail, isRateLimit) {
|
|
43
51
|
retryState.consecutiveTransientCount += 1;
|
|
44
52
|
const baseRetryAfterMs = "retryAfterMs" in cls ? cls.retryAfterMs : 15_000;
|
|
@@ -134,7 +142,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
134
142
|
}
|
|
135
143
|
return;
|
|
136
144
|
}
|
|
137
|
-
await pauseAuto(ctx, pi);
|
|
145
|
+
await pauseAuto(ctx, pi, _buildAbortedPauseContext(lastMsg));
|
|
138
146
|
return;
|
|
139
147
|
}
|
|
140
148
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GSD2 — Exec (context-mode) tool registration.
|
|
2
2
|
//
|
|
3
|
-
// Exposes the
|
|
4
|
-
// `context_mode.enabled:
|
|
3
|
+
// Exposes the Context Mode runtime tools in-process. Default-on; opt out with
|
|
4
|
+
// `context_mode.enabled: false` in preferences.
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
6
|
export function registerExecTools(pi) {
|
|
7
7
|
pi.registerTool({
|
|
@@ -51,6 +51,34 @@ async function applyDisabledModelProviderPolicy(ctx) {
|
|
|
51
51
|
// Non-fatal: keep default provider visibility if preferences cannot be loaded.
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
async function writeContextModeCompactionSnapshot(basePath) {
|
|
55
|
+
try {
|
|
56
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
57
|
+
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
58
|
+
const prefs = loadEffectiveGSDPreferences(basePath);
|
|
59
|
+
if (!isContextModeEnabled(prefs?.preferences))
|
|
60
|
+
return;
|
|
61
|
+
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
62
|
+
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
63
|
+
await ensureDbOpen(basePath);
|
|
64
|
+
let activeContext = null;
|
|
65
|
+
try {
|
|
66
|
+
const state = await deriveGsdState(basePath);
|
|
67
|
+
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
68
|
+
activeContext =
|
|
69
|
+
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
70
|
+
(state.activeTask.title ? ` - ${state.activeTask.title}` : "");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* non-fatal */
|
|
75
|
+
}
|
|
76
|
+
writeCompactionSnapshot(basePath, { activeContext });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
safetyLogWarning("context-mode", `failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
54
82
|
export function registerHooks(pi, ecosystemHandlers) {
|
|
55
83
|
pi.on("session_start", async (_event, ctx) => {
|
|
56
84
|
initNotificationStore(process.cwd());
|
|
@@ -138,7 +166,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
138
166
|
const { getEcosystemReadyPromise } = await import("../ecosystem/loader.js");
|
|
139
167
|
await getEcosystemReadyPromise();
|
|
140
168
|
const beforeAgentBasePath = process.cwd();
|
|
141
|
-
const pendingApprovalGate = getPendingGate();
|
|
169
|
+
const pendingApprovalGate = getPendingGate(beforeAgentBasePath);
|
|
142
170
|
if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
|
|
143
171
|
markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
|
|
144
172
|
const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
|
|
@@ -204,15 +232,18 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
204
232
|
}
|
|
205
233
|
});
|
|
206
234
|
pi.on("session_before_compact", async () => {
|
|
235
|
+
const basePath = process.cwd();
|
|
236
|
+
// Context Mode is default-on. Write the resumable snapshot before any
|
|
237
|
+
// active-auto cancel return so auto sessions still leave re-entry context.
|
|
238
|
+
await writeContextModeCompactionSnapshot(basePath);
|
|
207
239
|
// Only cancel compaction while auto-mode is actively running.
|
|
208
240
|
// Paused auto-mode should allow compaction — the user may be doing
|
|
209
241
|
// interactive work (#3165).
|
|
210
242
|
if (isAutoActive()) {
|
|
211
243
|
return { cancel: true };
|
|
212
244
|
}
|
|
213
|
-
const basePath = process.cwd();
|
|
214
245
|
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
215
|
-
await ensureDbOpen();
|
|
246
|
+
await ensureDbOpen(basePath);
|
|
216
247
|
const state = await deriveGsdState(basePath);
|
|
217
248
|
if (!state.activeMilestone || !state.activeSlice)
|
|
218
249
|
return;
|
|
@@ -256,41 +287,6 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
256
287
|
: `Resume ${phaseLabel} work for slice ${state.activeSlice.id}.`,
|
|
257
288
|
}));
|
|
258
289
|
});
|
|
259
|
-
// Context-mode snapshot: write .gsd/last-snapshot.md before compaction so
|
|
260
|
-
// agents can call gsd_resume (or Read the file) to re-orient. Opt-in via
|
|
261
|
-
// preferences.context_mode.enabled. Runs after the auto-cancel handler
|
|
262
|
-
// above — if that one returned cancel:true, pi still fires us but the
|
|
263
|
-
// compaction won't actually happen; the snapshot is still useful then,
|
|
264
|
-
// since auto may pause and resume later.
|
|
265
|
-
pi.on("session_before_compact", async () => {
|
|
266
|
-
try {
|
|
267
|
-
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
268
|
-
const { isContextModeEnabled } = await import("../preferences-types.js");
|
|
269
|
-
const prefs = loadEffectiveGSDPreferences();
|
|
270
|
-
if (!isContextModeEnabled(prefs?.preferences))
|
|
271
|
-
return;
|
|
272
|
-
const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
|
|
273
|
-
const { ensureDbOpen } = await import("./dynamic-tools.js");
|
|
274
|
-
await ensureDbOpen();
|
|
275
|
-
const basePath = process.cwd();
|
|
276
|
-
let activeContext = null;
|
|
277
|
-
try {
|
|
278
|
-
const state = await deriveGsdState(basePath);
|
|
279
|
-
if (state.activeMilestone && state.activeSlice && state.activeTask) {
|
|
280
|
-
activeContext =
|
|
281
|
-
`Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
|
|
282
|
-
(state.activeTask.title ? ` — ${state.activeTask.title}` : "");
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
/* non-fatal */
|
|
287
|
-
}
|
|
288
|
-
writeCompactionSnapshot(basePath, { activeContext });
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
safetyLogWarning("context-mode", `failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
290
|
pi.on("message_update", async (event, ctx) => {
|
|
295
291
|
if (approvalQuestionAbortInFlight)
|
|
296
292
|
return;
|
|
@@ -367,15 +363,15 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
367
363
|
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
368
364
|
// If ask_user_questions was called with a gate ID but hasn't been confirmed,
|
|
369
365
|
// block all non-read-only tool calls to prevent the model from skipping gates.
|
|
370
|
-
if (getPendingGate()) {
|
|
366
|
+
if (getPendingGate(discussionBasePath)) {
|
|
371
367
|
const milestoneId = await getDiscussionMilestoneIdFor(discussionBasePath);
|
|
372
368
|
if (isToolCallEventType("bash", event)) {
|
|
373
|
-
const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive());
|
|
369
|
+
const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive(discussionBasePath), discussionBasePath);
|
|
374
370
|
if (bashGuard.block)
|
|
375
371
|
return bashGuard;
|
|
376
372
|
}
|
|
377
373
|
else {
|
|
378
|
-
const gateGuard = shouldBlockPendingGate(toolName, milestoneId, isQueuePhaseActive());
|
|
374
|
+
const gateGuard = shouldBlockPendingGate(toolName, milestoneId, isQueuePhaseActive(discussionBasePath), discussionBasePath);
|
|
379
375
|
if (gateGuard.block)
|
|
380
376
|
return gateGuard;
|
|
381
377
|
}
|
|
@@ -384,7 +380,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
384
380
|
// When /gsd queue is active, the agent should only create milestones,
|
|
385
381
|
// not execute work. Block write/edit to non-.gsd/ paths and bash commands
|
|
386
382
|
// that would modify files.
|
|
387
|
-
if (isQueuePhaseActive()) {
|
|
383
|
+
if (isQueuePhaseActive(discussionBasePath)) {
|
|
388
384
|
let queueInput = "";
|
|
389
385
|
if (isToolCallEventType("write", event)) {
|
|
390
386
|
queueInput = event.input.path;
|
|
@@ -449,7 +445,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
449
445
|
}
|
|
450
446
|
if (!isToolCallEventType("write", event))
|
|
451
447
|
return;
|
|
452
|
-
const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive());
|
|
448
|
+
const result = shouldBlockContextWrite(event.toolName, event.input.path, await getDiscussionMilestoneIdFor(discussionBasePath), isQueuePhaseActive(discussionBasePath), discussionBasePath);
|
|
453
449
|
if (result.block)
|
|
454
450
|
return result;
|
|
455
451
|
});
|
|
@@ -504,7 +500,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
504
500
|
return;
|
|
505
501
|
const basePath = process.cwd();
|
|
506
502
|
const milestoneId = await getDiscussionMilestoneIdFor(basePath);
|
|
507
|
-
const queueActive = isQueuePhaseActive();
|
|
503
|
+
const queueActive = isQueuePhaseActive(basePath);
|
|
508
504
|
const details = event.details;
|
|
509
505
|
// ── Discussion gate enforcement: handle gate question responses ──
|
|
510
506
|
// If the result is cancelled or has no response, the pending gate stays active
|
|
@@ -512,7 +508,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
512
508
|
// If the user responded at all (even "needs adjustment"), clear the pending gate
|
|
513
509
|
// because the user engaged — the prompt handles the re-ask-after-adjustment flow.
|
|
514
510
|
const questions = event.input?.questions ?? [];
|
|
515
|
-
const currentPendingGate = getPendingGate();
|
|
511
|
+
const currentPendingGate = getPendingGate(basePath);
|
|
516
512
|
if (currentPendingGate) {
|
|
517
513
|
if (details?.cancelled || !details?.response) {
|
|
518
514
|
// Gate stays pending. Direct the agent to the most reliable recovery
|
|
@@ -298,8 +298,8 @@ export function getPendingGate(basePath = process.cwd()) {
|
|
|
298
298
|
* Returns { block: true, reason } if the tool should be blocked.
|
|
299
299
|
* ask_user_questions itself is allowed so the model can re-ask the gate.
|
|
300
300
|
*/
|
|
301
|
-
export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive) {
|
|
302
|
-
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
|
|
301
|
+
export function shouldBlockPendingGate(toolName, milestoneId, queuePhaseActive, basePath = process.cwd()) {
|
|
302
|
+
return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(basePath), toolName, milestoneId, queuePhaseActive);
|
|
303
303
|
}
|
|
304
304
|
export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneId, _queuePhaseActive) {
|
|
305
305
|
if (!snapshot.pendingGateId)
|
|
@@ -322,8 +322,8 @@ export function shouldBlockPendingGateInSnapshot(snapshot, toolName, _milestoneI
|
|
|
322
322
|
* Check whether a bash command should be blocked because a discussion gate is pending.
|
|
323
323
|
* All bash is blocked while waiting for confirmation so the question stays visible.
|
|
324
324
|
*/
|
|
325
|
-
export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive) {
|
|
326
|
-
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
|
|
325
|
+
export function shouldBlockPendingGateBash(command, milestoneId, queuePhaseActive, basePath = process.cwd()) {
|
|
326
|
+
return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(basePath), command, milestoneId, queuePhaseActive);
|
|
327
327
|
}
|
|
328
328
|
export function shouldBlockPendingGateBashInSnapshot(snapshot, command, _milestoneId, _queuePhaseActive) {
|
|
329
329
|
if (!snapshot.pendingGateId)
|
|
@@ -363,7 +363,7 @@ export function isDepthConfirmationAnswer(selected, options) {
|
|
|
363
363
|
// Returning false prevents any free-form string from unlocking the gate.
|
|
364
364
|
return false;
|
|
365
365
|
}
|
|
366
|
-
export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive) {
|
|
366
|
+
export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive, basePath = process.cwd()) {
|
|
367
367
|
if (toolName !== "write")
|
|
368
368
|
return { block: false };
|
|
369
369
|
if (!MILESTONE_CONTEXT_RE.test(inputPath))
|
|
@@ -379,7 +379,7 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
|
|
|
379
379
|
].join(" "),
|
|
380
380
|
};
|
|
381
381
|
}
|
|
382
|
-
if (isMilestoneDepthVerified(targetMilestoneId))
|
|
382
|
+
if (isMilestoneDepthVerified(targetMilestoneId, basePath))
|
|
383
383
|
return { block: false };
|
|
384
384
|
return {
|
|
385
385
|
block: true,
|
|
@@ -397,8 +397,8 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queue
|
|
|
397
397
|
* Slice-level CONTEXT artifacts are allowed; milestone-level CONTEXT writes
|
|
398
398
|
* require the milestone to be depth-verified first.
|
|
399
399
|
*/
|
|
400
|
-
export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId) {
|
|
401
|
-
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
|
|
400
|
+
export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId, basePath = process.cwd()) {
|
|
401
|
+
return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(basePath), artifactType, milestoneId, sliceId);
|
|
402
402
|
}
|
|
403
403
|
export function shouldBlockContextArtifactSaveInSnapshot(snapshot, artifactType, milestoneId, sliceId) {
|
|
404
404
|
if (artifactType !== "CONTEXT")
|
|
@@ -669,6 +669,10 @@ function columnExists(db, table, column) {
|
|
|
669
669
|
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
670
670
|
return rows.some((row) => row["name"] === column);
|
|
671
671
|
}
|
|
672
|
+
function formatFtsUnavailableError(err) {
|
|
673
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
674
|
+
return message.replace(/\bmoduel\s*:\s*/gi, "module: ");
|
|
675
|
+
}
|
|
672
676
|
/**
|
|
673
677
|
* Create the FTS5 virtual table for memories plus the triggers that keep it
|
|
674
678
|
* in sync with the base table. FTS5 may be unavailable on stripped-down
|
|
@@ -704,7 +708,7 @@ export function tryCreateMemoriesFts(db) {
|
|
|
704
708
|
return true;
|
|
705
709
|
}
|
|
706
710
|
catch (err) {
|
|
707
|
-
logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${err
|
|
711
|
+
logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${formatFtsUnavailableError(err)}`);
|
|
708
712
|
return false;
|
|
709
713
|
}
|
|
710
714
|
}
|
|
@@ -1653,6 +1657,35 @@ export function closeDatabase() {
|
|
|
1653
1657
|
_lastDbError = null;
|
|
1654
1658
|
_lastDbPhase = null;
|
|
1655
1659
|
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Re-open the active database connection from disk.
|
|
1662
|
+
*
|
|
1663
|
+
* Auto-mode can observe artifacts written by a workflow server running in a
|
|
1664
|
+
* different process before its long-lived singleton has re-synchronized. The
|
|
1665
|
+
* recovery path uses this to force the next state derivation to read from the
|
|
1666
|
+
* current on-disk database instead of continuing with a possibly stale handle.
|
|
1667
|
+
*/
|
|
1668
|
+
export function refreshOpenDatabaseFromDisk() {
|
|
1669
|
+
if (!currentDb || !currentPath)
|
|
1670
|
+
return false;
|
|
1671
|
+
if (currentPath === ":memory:")
|
|
1672
|
+
return false;
|
|
1673
|
+
const dbPath = currentPath;
|
|
1674
|
+
const identityKey = _currentIdentityKey;
|
|
1675
|
+
try {
|
|
1676
|
+
closeDatabase();
|
|
1677
|
+
const opened = openDatabase(dbPath);
|
|
1678
|
+
if (opened && identityKey && currentDb) {
|
|
1679
|
+
_dbCache.set(identityKey, { dbPath, db: currentDb });
|
|
1680
|
+
_currentIdentityKey = identityKey;
|
|
1681
|
+
}
|
|
1682
|
+
return opened;
|
|
1683
|
+
}
|
|
1684
|
+
catch (e) {
|
|
1685
|
+
logWarning("db", `database refresh failed: ${e.message}`);
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1656
1689
|
/** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
|
|
1657
1690
|
export function vacuumDatabase() {
|
|
1658
1691
|
if (!currentDb)
|
|
@@ -44,6 +44,7 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForGuidedUnit
|
|
|
44
44
|
import { runPreparation, formatCodebaseBrief, formatPriorContextBrief, } from "./preparation.js";
|
|
45
45
|
import { verifyExpectedArtifact } from "./auto-recovery.js";
|
|
46
46
|
import { createWorkspace, scopeMilestone } from "./workspace.js";
|
|
47
|
+
import { getPendingGate, extractDepthVerificationMilestoneId } from "./bootstrap/write-gate.js";
|
|
47
48
|
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
|
48
49
|
export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, reserveMilestoneId, claimReservedId, getReservedMilestoneIds, clearReservedMilestoneIds, } from "./milestone-ids.js";
|
|
49
50
|
export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
|
|
@@ -295,6 +296,14 @@ export async function checkDeepProjectSetupAfterTurn(_event, ctx, basePath) {
|
|
|
295
296
|
return false;
|
|
296
297
|
}
|
|
297
298
|
}
|
|
299
|
+
// R2: a depth-verification gate is still pending — the LLM emitted the
|
|
300
|
+
// confirmation question (via ask_user_questions or plain chat) but the user
|
|
301
|
+
// has not approved yet. Returning false keeps the entry in the
|
|
302
|
+
// pendingDeepProjectSetupMap so the next user message can resume.
|
|
303
|
+
const pendingGateId = getPendingGate(entry.basePath);
|
|
304
|
+
if (pendingGateId) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
298
307
|
return dispatchNextDeepProjectSetupStage(entry);
|
|
299
308
|
}
|
|
300
309
|
async function dispatchNextDeepProjectSetupStage(entry) {
|
|
@@ -368,6 +377,23 @@ export function checkAutoStartAfterDiscuss() {
|
|
|
368
377
|
const roadmapFile = existsSync(roadmapFilePath) ? roadmapFilePath : null;
|
|
369
378
|
if (!contextFile && !roadmapFile)
|
|
370
379
|
return false; // neither artifact yet — keep waiting
|
|
380
|
+
// Gate 1a: a depth-verification gate is still pending for THIS milestone — the
|
|
381
|
+
// LLM emitted the confirmation question (via ask_user_questions or plain chat)
|
|
382
|
+
// but the user has not answered yet. Advancing now would skip the gate and
|
|
383
|
+
// race ahead with unverified context.
|
|
384
|
+
const basePathForGate = entry.scope.workspace.projectRoot;
|
|
385
|
+
const pendingGateId = getPendingGate(basePathForGate);
|
|
386
|
+
if (pendingGateId) {
|
|
387
|
+
const pendingMilestoneId = extractDepthVerificationMilestoneId(pendingGateId);
|
|
388
|
+
// Block advancement if the gate is for THIS milestone, OR if it's a
|
|
389
|
+
// project/requirements gate (no milestone id encoded) for the deep setup flow.
|
|
390
|
+
const isProjectGate = pendingGateId === "depth_verification_project_confirm" ||
|
|
391
|
+
pendingGateId === "depth_verification_requirements_confirm" ||
|
|
392
|
+
pendingGateId === "depth_verification_research_decision_confirm";
|
|
393
|
+
if (pendingMilestoneId === milestoneId || isProjectGate) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
371
397
|
// Gate 1b: Discriminate plan-blocked from discuss-incomplete when the DB row is queued.
|
|
372
398
|
// If the DB is available and the row is still "queued" but CONTEXT.md already exists on
|
|
373
399
|
// disk, the discuss phase completed but gsd_plan_milestone was hard-blocked by the
|
|
@@ -489,6 +515,20 @@ export function checkAutoStartAfterDiscuss() {
|
|
|
489
515
|
logWarning("guided", `manifest unlink failed: ${e.message}`);
|
|
490
516
|
}
|
|
491
517
|
}
|
|
518
|
+
// R3b: belt-and-suspenders for silent registration failure. The discuss flow
|
|
519
|
+
// finished and STATE.md exists, but the milestone may never have landed in
|
|
520
|
+
// the DB. Without this guard, the user sees "Milestone M001 ready." and then
|
|
521
|
+
// /gsd reports "No Active Milestone".
|
|
522
|
+
if (isDbAvailable()) {
|
|
523
|
+
const milestoneRow = getMilestone(milestoneId);
|
|
524
|
+
if (!milestoneRow) {
|
|
525
|
+
ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
|
|
526
|
+
`PROJECT.md may have failed to register milestones. ` +
|
|
527
|
+
`Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
|
|
528
|
+
`then re-run /gsd to recover.`, "error");
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
492
532
|
pendingAutoStartMap.delete(basePath);
|
|
493
533
|
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
|
|
494
534
|
startAutoDetached(ctx, pi, basePath, false, { step });
|
|
@@ -177,8 +177,12 @@ export function resolveDir(parentDir, idPrefix) {
|
|
|
177
177
|
const exact = entries.find(e => e.isDirectory() && e.name === idPrefix);
|
|
178
178
|
if (exact)
|
|
179
179
|
return exact.name;
|
|
180
|
+
const idLower = idPrefix.toLowerCase();
|
|
181
|
+
const exactCaseInsensitive = entries.find(e => e.isDirectory() && e.name.toLowerCase() === idLower);
|
|
182
|
+
if (exactCaseInsensitive)
|
|
183
|
+
return exactCaseInsensitive.name;
|
|
180
184
|
// Prefix match for legacy descriptor dirs: M001-SOMETHING
|
|
181
|
-
const prefixed = entries.find(e => e.isDirectory() && e.name.startsWith(
|
|
185
|
+
const prefixed = entries.find(e => e.isDirectory() && e.name.toLowerCase().startsWith(idLower + "-"));
|
|
182
186
|
return prefixed ? prefixed.name : null;
|
|
183
187
|
}
|
|
184
188
|
catch {
|