@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- 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 +6 -6
- 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/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 +6 -6
- 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/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- 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/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.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/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f30abebf080db301
|
|
@@ -21,7 +21,7 @@ import { resolveWorkflowQuestionToolSurface } from "../gsd/question-transport.js
|
|
|
21
21
|
import { buildProjectGsdMcpServers, ensureProjectWorkflowMcpConfig } from "../gsd/mcp-project-config.js";
|
|
22
22
|
import { loadProjectGSDPreferences } from "../gsd/preferences.js";
|
|
23
23
|
import { markToolStart, markToolEnd } from "../gsd/auto.js";
|
|
24
|
-
import { markInteractiveElicitationStart, markInteractiveElicitationEnd, } from "../gsd/auto-tool-tracking.js";
|
|
24
|
+
import { markInteractiveElicitationStart, markInteractiveElicitationEnd, isInteractiveElicitationInFlight, } from "../gsd/auto-tool-tracking.js";
|
|
25
25
|
import { discoverBrowserMcpServerName, discoverMcpServers, discoverMcpServerNames, discoverUserMcpServerNames, discoverWorkflowMcpServerName, computeMcpDisallowedTools, } from "../gsd/mcp-filter.js";
|
|
26
26
|
import { RUN_UAT_CLAUDE_NATIVE_TOOL_NAMES, RUN_UAT_FORBIDDEN_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES, resolveToolPresentationPlan } from "../gsd/tool-presentation-plan.js";
|
|
27
27
|
import { getUnitToolSurfaceContract } from "../gsd/unit-tool-contracts.js";
|
|
@@ -51,6 +51,27 @@ export function resolveClaudeCodeCwd(options) {
|
|
|
51
51
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
52
52
|
/** Regex pattern that identifies field names and descriptions that should be treated as sensitive/secure inputs. */
|
|
53
53
|
const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
|
|
54
|
+
/** Mirrors @opengsd/mcp-server's ask_user_questions host elicitation timeout. */
|
|
55
|
+
const WORKFLOW_ELICIT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
56
|
+
/** Close the local form just before the server drops the MCP elicitation request. */
|
|
57
|
+
const WORKFLOW_ELICIT_TIMEOUT_SKEW_MS = 1_000;
|
|
58
|
+
export const CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS = Math.max(1, WORKFLOW_ELICIT_TIMEOUT_MS - WORKFLOW_ELICIT_TIMEOUT_SKEW_MS);
|
|
59
|
+
const ELICITATION_EXPIRED_NOTICE = "Question expired before you answered - the form was closed and no answers were sent.";
|
|
60
|
+
const activeAskUserQuestionElicitationTimeoutAborters = new Set();
|
|
61
|
+
function isAskUserQuestionsToolName(toolName) {
|
|
62
|
+
const name = String(toolName ?? "");
|
|
63
|
+
return name === "ask_user_questions" || name.endsWith("__ask_user_questions");
|
|
64
|
+
}
|
|
65
|
+
function isAskUserQuestionsTimedOutResult(toolCall, result) {
|
|
66
|
+
return isAskUserQuestionsToolName(toolCall.name) && result.details?.timed_out === true;
|
|
67
|
+
}
|
|
68
|
+
function abortActiveAskUserQuestionElicitationsForTimeout() {
|
|
69
|
+
if (!isInteractiveElicitationInFlight())
|
|
70
|
+
return;
|
|
71
|
+
for (const abort of [...activeAskUserQuestionElicitationTimeoutAborters]) {
|
|
72
|
+
abort();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
54
75
|
// ---------------------------------------------------------------------------
|
|
55
76
|
// Stream factory
|
|
56
77
|
// ---------------------------------------------------------------------------
|
|
@@ -1238,17 +1259,40 @@ export function createClaudeCodeElicitationHandler(ui) {
|
|
|
1238
1259
|
// it does not tear down the very elicitation that IS the human boundary
|
|
1239
1260
|
// (#cc-elicitation-self-cancel). It clears in the finally on every exit.
|
|
1240
1261
|
const elicId = "cc-elicit-" + (request.id ?? `${Date.now()}-${nextElicitationSeq()}`);
|
|
1262
|
+
const formAbortController = new AbortController();
|
|
1263
|
+
let expiryNoticeShown = false;
|
|
1264
|
+
const abortExpiredForm = () => {
|
|
1265
|
+
if (formAbortController.signal.aborted)
|
|
1266
|
+
return;
|
|
1267
|
+
formAbortController.abort();
|
|
1268
|
+
if (!expiryNoticeShown) {
|
|
1269
|
+
expiryNoticeShown = true;
|
|
1270
|
+
ui.notify(ELICITATION_EXPIRED_NOTICE, "warning");
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
const forwardSdkAbort = () => {
|
|
1274
|
+
if (!formAbortController.signal.aborted)
|
|
1275
|
+
formAbortController.abort();
|
|
1276
|
+
};
|
|
1277
|
+
if (signal.aborted) {
|
|
1278
|
+
forwardSdkAbort();
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
signal.addEventListener("abort", forwardSdkAbort, { once: true });
|
|
1282
|
+
}
|
|
1283
|
+
const expiryTimer = setTimeout(abortExpiredForm, CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS);
|
|
1241
1284
|
markInteractiveElicitationStart();
|
|
1242
1285
|
markToolStart(elicId, "ask_user_questions");
|
|
1286
|
+
activeAskUserQuestionElicitationTimeoutAborters.add(abortExpiredForm);
|
|
1243
1287
|
try {
|
|
1244
|
-
const interviewResult = await showInterviewRound(questions, { signal, overlay: true }, { ui }).catch(() => undefined);
|
|
1288
|
+
const interviewResult = await showInterviewRound(questions, { signal: formAbortController.signal, overlay: true }, { ui }).catch(() => undefined);
|
|
1245
1289
|
if (interviewResult === undefined) {
|
|
1246
1290
|
// `await` so the dialog human-wait stays inside try/finally and the
|
|
1247
1291
|
// in-flight guard is held until the dialog resolves. Without it,
|
|
1248
1292
|
// `finally` runs the moment the promise is created and the fallback
|
|
1249
1293
|
// wait runs with zero in-flight tools — reintroducing the
|
|
1250
1294
|
// self-cancel on this path (Bugbot #1c00624d).
|
|
1251
|
-
return await promptElicitationWithDialogs(request, questions, ui, signal);
|
|
1295
|
+
return await promptElicitationWithDialogs(request, questions, ui, formAbortController.signal);
|
|
1252
1296
|
}
|
|
1253
1297
|
if (Object.keys(interviewResult.answers).length === 0) {
|
|
1254
1298
|
// A system/host teardown (compaction, session_switch, true
|
|
@@ -1265,6 +1309,9 @@ export function createClaudeCodeElicitationHandler(ui) {
|
|
|
1265
1309
|
};
|
|
1266
1310
|
}
|
|
1267
1311
|
finally {
|
|
1312
|
+
clearTimeout(expiryTimer);
|
|
1313
|
+
signal.removeEventListener("abort", forwardSdkAbort);
|
|
1314
|
+
activeAskUserQuestionElicitationTimeoutAborters.delete(abortExpiredForm);
|
|
1268
1315
|
markToolEnd(elicId);
|
|
1269
1316
|
markInteractiveElicitationEnd();
|
|
1270
1317
|
}
|
|
@@ -2052,6 +2099,9 @@ async function pumpSdkMessages(model, context, options, stream) {
|
|
|
2052
2099
|
// Push synthetic completion events with result attached so the
|
|
2053
2100
|
// chat-controller can update pending ToolExecutionComponents.
|
|
2054
2101
|
if (block.type === "toolCall") {
|
|
2102
|
+
if (isAskUserQuestionsTimedOutResult(block, extResult)) {
|
|
2103
|
+
abortActiveAskUserQuestionElicitationsForTimeout();
|
|
2104
|
+
}
|
|
2055
2105
|
if (suppressDuplicateUnavailable) {
|
|
2056
2106
|
delete block.externalResult;
|
|
2057
2107
|
stream.push({
|
|
@@ -2082,9 +2132,13 @@ async function pumpSdkMessages(model, context, options, stream) {
|
|
|
2082
2132
|
emittedExternalToolResultIds.add(block.id);
|
|
2083
2133
|
}
|
|
2084
2134
|
else if (block.type === "serverToolUse") {
|
|
2135
|
+
const toolCall = serverToolUseToToolCallLike(block);
|
|
2136
|
+
if (isAskUserQuestionsTimedOutResult(toolCall, extResult)) {
|
|
2137
|
+
abortActiveAskUserQuestionElicitationsForTimeout();
|
|
2138
|
+
}
|
|
2085
2139
|
try {
|
|
2086
2140
|
await onExternalToolResult?.({
|
|
2087
|
-
toolCall
|
|
2141
|
+
toolCall,
|
|
2088
2142
|
result: extResult,
|
|
2089
2143
|
});
|
|
2090
2144
|
}
|
|
@@ -314,6 +314,19 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
|
|
|
314
314
|
try {
|
|
315
315
|
let taskIds = null;
|
|
316
316
|
let dbPrimary = false;
|
|
317
|
+
const planContent = readFileSync(absPath, "utf-8");
|
|
318
|
+
let parsedTaskIds = null;
|
|
319
|
+
const getParsedTaskIds = () => {
|
|
320
|
+
if (parsedTaskIds)
|
|
321
|
+
return parsedTaskIds;
|
|
322
|
+
parsedTaskIds = parseLegacyPlan(planContent).tasks.map((t) => t.id);
|
|
323
|
+
return parsedTaskIds;
|
|
324
|
+
};
|
|
325
|
+
const tasksBlockMatch = planContent.match(/<tasks>([\s\S]*?)<\/tasks>/i);
|
|
326
|
+
const tasksBlock = tasksBlockMatch?.[1] ?? "";
|
|
327
|
+
const hasEmbeddedTaskEntries = tasksBlock.length > 0 &&
|
|
328
|
+
(/^\s*- \[[xX ]\] \*\*T\d+/m.test(tasksBlock) ||
|
|
329
|
+
/^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(tasksBlock));
|
|
317
330
|
if (isDbAvailable()) {
|
|
318
331
|
const refreshed = refreshWorkflowDatabaseFromDisk();
|
|
319
332
|
if (refreshed) {
|
|
@@ -325,18 +338,17 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
|
|
|
325
338
|
}
|
|
326
339
|
}
|
|
327
340
|
if (!taskIds) {
|
|
328
|
-
const planContent = readFileSync(absPath, "utf-8");
|
|
329
341
|
const hasCheckboxTask = /^\s*- \[[xX ]\] \*\*T\d+/m.test(planContent);
|
|
330
342
|
const hasHeadingTask = /^\s*#{2,4}\s+T\d+\s*(?:--|—|:)/m.test(planContent);
|
|
331
343
|
if (!hasCheckboxTask && !hasHeadingTask) {
|
|
332
344
|
logWarning("recovery", `verify-fail ${unitType} ${unitId}: plan has no task checkbox/heading (len=${planContent.length}) at ${absPath}`);
|
|
333
345
|
return false;
|
|
334
346
|
}
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
taskIds =
|
|
347
|
+
const parsedIds = getParsedTaskIds();
|
|
348
|
+
if (parsedIds.length > 0)
|
|
349
|
+
taskIds = parsedIds;
|
|
338
350
|
}
|
|
339
|
-
if (taskIds && taskIds.length > 0) {
|
|
351
|
+
if (taskIds && taskIds.length > 0 && !hasEmbeddedTaskEntries) {
|
|
340
352
|
const tasksDir = join(dirname(absPath), "tasks");
|
|
341
353
|
if (existsSync(tasksDir)) {
|
|
342
354
|
for (const tid of taskIds) {
|
|
@@ -37,62 +37,51 @@ export async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
|
|
|
37
37
|
return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Run a milestone merge
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
40
|
+
* Run a milestone merge through Worktree Lifecycle's guarded merge option,
|
|
41
|
+
* which surrounds the inner merge with preflight stash + always-on postflight
|
|
42
|
+
* pop. The previous closeout code popped the stash only after a successful
|
|
43
|
+
* merge, which leaked `gsd-preflight-stash:M00x:*` entries whenever
|
|
44
|
+
* `mergeAndExit` threw — leaving the user's pre-merge working tree silently
|
|
45
|
+
* stashed away after a merge-conflict or other merge error. Lifecycle now
|
|
46
|
+
* restores the stash on every attempted merge path, then this adapter surfaces
|
|
47
|
+
* the merge or stash failure (in priority order) as the loop's stop reason.
|
|
47
48
|
*
|
|
48
49
|
* Returns a `break` action when auto-mode must stop, or `null` when the merge
|
|
49
50
|
* succeeded and the stash (if any) was restored cleanly.
|
|
50
51
|
*/
|
|
51
52
|
export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, options = {}) {
|
|
52
53
|
const { ctx, pi, s, deps } = ic;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const projectRoot = s.originalBasePath || s.basePath;
|
|
55
|
+
const mergeResult = deps.lifecycle.exitMilestone(milestoneId, {
|
|
56
|
+
merge: true,
|
|
57
|
+
guardedMerge: {
|
|
58
|
+
projectRoot,
|
|
59
|
+
preflightCleanRoot: deps.preflightCleanRoot,
|
|
60
|
+
postflightPopStash: deps.postflightPopStash,
|
|
61
|
+
},
|
|
62
|
+
}, ctx.ui);
|
|
63
|
+
if (mergeResult.ok) {
|
|
64
|
+
await markMilestoneMergedAndRebuild(s);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (mergeResult.reason === "postflight-stash-restore-failed") {
|
|
68
|
+
await markMilestoneMergedAndRebuild(s);
|
|
69
|
+
}
|
|
70
|
+
if (mergeResult.reason === "preflight-dirty-overlap" || mergeResult.reason === "preflight-unmerged-conflicts") {
|
|
71
|
+
const reason = mergeResult.reason === "preflight-unmerged-conflicts"
|
|
56
72
|
? `Pre-merge unresolved Git conflicts block milestone ${milestoneId}`
|
|
57
73
|
: `Pre-merge dirty working tree overlaps milestone ${milestoneId}`;
|
|
58
74
|
await deps.stopAuto(ctx, pi, reason, {
|
|
59
75
|
preserveCompletedMilestoneBranch: true,
|
|
60
76
|
preserveCloseoutTranscript: options.preserveCloseoutTranscript,
|
|
61
77
|
});
|
|
62
|
-
return {
|
|
63
|
-
action: "break",
|
|
64
|
-
reason: preflight.blockedReason === "unmerged-conflicts"
|
|
65
|
-
? "preflight-unmerged-conflicts"
|
|
66
|
-
: "preflight-dirty-overlap",
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
let mergeError = null;
|
|
70
|
-
const exitResult = deps.lifecycle.exitMilestone(milestoneId, { merge: true }, ctx.ui);
|
|
71
|
-
if (exitResult.ok) {
|
|
72
|
-
s.milestoneMergedInPhases = true;
|
|
73
|
-
try {
|
|
74
|
-
const projectRoot = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
|
|
75
|
-
const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
|
|
76
|
-
await rebuildMarkdownProjectionsFromDb(projectRoot);
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
logWarning("engine", `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
mergeError = exitResult.cause ?? new Error(`exit ${exitResult.reason}`);
|
|
84
|
-
}
|
|
85
|
-
// Always attempt to restore the stashed working tree, even on merge error.
|
|
86
|
-
// postflightPopStash itself does not throw; failures surface via the
|
|
87
|
-
// PostflightResult.needsManualRecovery flag.
|
|
88
|
-
let stashResult = null;
|
|
89
|
-
if (preflight.stashPushed) {
|
|
90
|
-
stashResult = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
78
|
+
return { action: "break", reason: mergeResult.reason };
|
|
91
79
|
}
|
|
92
80
|
// Merge failure takes priority over stash recovery — the merge is the
|
|
93
81
|
// authoritative gate. If the stash also needed manual recovery, the user
|
|
94
82
|
// already saw the postflightPopStash notify above.
|
|
95
|
-
if (
|
|
83
|
+
if (mergeResult.reason === "merge-conflict") {
|
|
84
|
+
const mergeError = mergeResult.cause;
|
|
96
85
|
if (mergeError instanceof MergeConflictError) {
|
|
97
86
|
// A merge conflict is a recoverable human checkpoint, not an
|
|
98
87
|
// infrastructure failure — the user resolves the conflict and runs
|
|
@@ -109,6 +98,9 @@ export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, option
|
|
|
109
98
|
});
|
|
110
99
|
return { action: "break", reason: "merge-conflict" };
|
|
111
100
|
}
|
|
101
|
+
}
|
|
102
|
+
if (mergeResult.reason === "merge-failed" || mergeResult.reason === "merge-conflict") {
|
|
103
|
+
const mergeError = mergeResult.cause;
|
|
112
104
|
logError("engine", "Milestone merge failed with non-conflict error", {
|
|
113
105
|
milestone: milestoneId,
|
|
114
106
|
error: String(mergeError),
|
|
@@ -125,11 +117,22 @@ export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, option
|
|
|
125
117
|
});
|
|
126
118
|
return { action: "break", reason: "merge-failed" };
|
|
127
119
|
}
|
|
128
|
-
if (
|
|
129
|
-
return stopOnPostflightRecoveryNeeded(ic,
|
|
120
|
+
if (mergeResult.postflight) {
|
|
121
|
+
return stopOnPostflightRecoveryNeeded(ic, mergeResult.postflight, milestoneId);
|
|
130
122
|
}
|
|
131
123
|
return null;
|
|
132
124
|
}
|
|
125
|
+
async function markMilestoneMergedAndRebuild(s) {
|
|
126
|
+
s.milestoneMergedInPhases = true;
|
|
127
|
+
try {
|
|
128
|
+
const rebuildBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
|
|
129
|
+
const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
|
|
130
|
+
await rebuildMarkdownProjectionsFromDb(rebuildBasePath);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logWarning("engine", `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
133
136
|
export async function _runMilestoneMergeOnceWithStashRestore(ic, milestoneId, options = {}) {
|
|
134
137
|
if (ic.s.milestoneMergedInPhases) {
|
|
135
138
|
debugLog("autoLoop", {
|
|
@@ -280,6 +280,15 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|
|
280
280
|
files: leak.files.map((file) => ({ path: file.path, status: file.status })),
|
|
281
281
|
});
|
|
282
282
|
ctx.ui.notify(message, "error");
|
|
283
|
+
try {
|
|
284
|
+
deps.checkpointWorkflowDatabase?.();
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
debugLog("autoLoop", {
|
|
288
|
+
phase: "root-write-leak-checkpoint-failed",
|
|
289
|
+
error: err instanceof Error ? err.message : String(err),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
283
292
|
await deps.stopAuto(ctx, pi, "Root-write leak during isolated auto-mode", {
|
|
284
293
|
preserveCompletedMilestoneBranch: true,
|
|
285
294
|
});
|
|
@@ -560,7 +560,7 @@ export class AutoOrchestrator {
|
|
|
560
560
|
return new GitServiceImpl(basePath, gitConfig);
|
|
561
561
|
},
|
|
562
562
|
worktreeProjection: new WorktreeStateProjection(),
|
|
563
|
-
|
|
563
|
+
mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
|
|
564
564
|
});
|
|
565
565
|
}
|
|
566
566
|
rebuildScope(rawPath, milestoneId) {
|
|
@@ -6,7 +6,7 @@ import { debugLog } from "../debug-logger.js";
|
|
|
6
6
|
import { logWarning } from "../workflow-logger.js";
|
|
7
7
|
import { resolveAutoSupervisorConfig } from "../preferences.js";
|
|
8
8
|
import { readUnitRuntimeRecord } from "../unit-runtime.js";
|
|
9
|
-
import { consumeAutoWakeup } from "./schedule-wakeup.js";
|
|
9
|
+
import { clearAutoWakeup, consumeAutoWakeup } from "./schedule-wakeup.js";
|
|
10
10
|
import { applyUnitSkillVisibility } from "../skill-scope.js";
|
|
11
11
|
const UNIT_FAILSAFE_BUFFER_MS = 30_000;
|
|
12
12
|
const UNIT_FAILSAFE_RECHECK_MS = 30_000;
|
|
@@ -192,7 +192,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
192
192
|
const UNIT_HARD_TIMEOUT_MS = Math.max(UNIT_FAILSAFE_BUFFER_MS, ((supervisor.hard_timeout_minutes ?? 30) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS);
|
|
193
193
|
const freshProgressMs = Math.max(UNIT_FAILSAFE_BUFFER_MS, ((supervisor.idle_timeout_minutes ?? 10) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS);
|
|
194
194
|
let unitTimeoutHandle;
|
|
195
|
-
let result;
|
|
195
|
+
let result = { status: "cancelled" };
|
|
196
196
|
try {
|
|
197
197
|
const awaitAgentEndWithTimeout = (agentEndPromise) => {
|
|
198
198
|
const timeoutResult = new Promise((resolve) => {
|
|
@@ -297,6 +297,9 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
297
297
|
finally {
|
|
298
298
|
if (unitTimeoutHandle)
|
|
299
299
|
clearTimeout(unitTimeoutHandle);
|
|
300
|
+
if (result.status !== "completed") {
|
|
301
|
+
clearAutoWakeup(s.basePath, unitType, unitId);
|
|
302
|
+
}
|
|
300
303
|
ctx.ui.setWorkingMessage?.(undefined);
|
|
301
304
|
setVisibleSkills?.(undefined);
|
|
302
305
|
}
|
|
@@ -13,6 +13,9 @@ export function consumeAutoWakeup(basePath, unitType, unitId) {
|
|
|
13
13
|
pendingWakeups.delete(key);
|
|
14
14
|
return wakeup;
|
|
15
15
|
}
|
|
16
|
+
export function clearAutoWakeup(basePath, unitType, unitId) {
|
|
17
|
+
pendingWakeups.delete(wakeupKey(basePath, unitType, unitId));
|
|
18
|
+
}
|
|
16
19
|
export function _resetAutoWakeupsForTest() {
|
|
17
20
|
pendingWakeups.clear();
|
|
18
21
|
}
|
|
@@ -68,7 +68,7 @@ import { MILESTONE_ID_RE } from "./milestone-ids.js";
|
|
|
68
68
|
import { updateProgressWidget as _updateProgressWidget, setCompletionProgressWidget, setAutoOutcomeWidget, setAutoActiveStatus, updateSliceProgressCache, clearSliceProgressCache, unitVerb, } from "./auto-dashboard.js";
|
|
69
69
|
import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
|
|
70
70
|
import { isDbAvailable, getMilestone, getMilestoneSlices, } from "./gsd-db.js";
|
|
71
|
-
import { closeWorkflowDatabase } from "./db-workspace.js";
|
|
71
|
+
import { checkpointWorkflowDatabase, closeWorkflowDatabase } from "./db-workspace.js";
|
|
72
72
|
import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
|
|
73
73
|
import { writeUnitRuntimeRecord } from "./unit-runtime.js";
|
|
74
74
|
import { countPendingCaptures } from "./captures.js";
|
|
@@ -1102,6 +1102,12 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
|
|
|
1102
1102
|
const installTerminalCloseoutOutcome = completionStopRequested && preserveCloseoutTranscript;
|
|
1103
1103
|
const preserveCompletionSurface = completionStopRequested || preserveCloseoutTranscript;
|
|
1104
1104
|
s.completionStopInProgress = preserveCompletionSurface;
|
|
1105
|
+
try {
|
|
1106
|
+
checkpointWorkflowDatabase();
|
|
1107
|
+
}
|
|
1108
|
+
catch (e) {
|
|
1109
|
+
debugLog("stop-checkpoint-failed", { error: e instanceof Error ? e.message : String(e) });
|
|
1110
|
+
}
|
|
1105
1111
|
playNotificationBell("stop", loadedPreferences?.notifications);
|
|
1106
1112
|
// #4764 — telemetry: record the exit reason, isolation mode, whether an auto
|
|
1107
1113
|
// worktree was active, and whether the current milestone was merged before
|
|
@@ -1732,7 +1738,7 @@ export function buildWorktreeLifecycleDeps() {
|
|
|
1732
1738
|
// C4 (#5627) — GitServiceImpl constructor → gitServiceFactory
|
|
1733
1739
|
//
|
|
1734
1740
|
// Final WorktreeLifecycleDeps shape: 3 fields (gitServiceFactory,
|
|
1735
|
-
// worktreeProjection,
|
|
1741
|
+
// worktreeProjection, mergeMilestone transaction runner). Down from 18 at slice-7
|
|
1736
1742
|
// closure.
|
|
1737
1743
|
return {
|
|
1738
1744
|
gitServiceFactory: (basePath) => {
|
|
@@ -1740,7 +1746,7 @@ export function buildWorktreeLifecycleDeps() {
|
|
|
1740
1746
|
return new GitServiceImpl(basePath, gitConfig);
|
|
1741
1747
|
},
|
|
1742
1748
|
worktreeProjection: new WorktreeStateProjection(),
|
|
1743
|
-
|
|
1749
|
+
mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
|
|
1744
1750
|
};
|
|
1745
1751
|
}
|
|
1746
1752
|
function buildLifecycle() {
|
|
@@ -1783,6 +1789,7 @@ function buildLoopDeps(pi, ctx) {
|
|
|
1783
1789
|
stopAuto,
|
|
1784
1790
|
pauseAuto,
|
|
1785
1791
|
clearUnitTimeout,
|
|
1792
|
+
checkpointWorkflowDatabase,
|
|
1786
1793
|
updateProgressWidget,
|
|
1787
1794
|
...cmux,
|
|
1788
1795
|
handleLostSessionLock: (ctx, lockStatus) => {
|
|
@@ -3,19 +3,63 @@
|
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
|
|
5
5
|
import { scheduleAutoWakeup } from "../auto/schedule-wakeup.js";
|
|
6
|
+
import { logWarning } from "../workflow-logger.js";
|
|
6
7
|
import { resolveCtxCwd } from "./dynamic-tools.js";
|
|
7
8
|
const MAX_WAKEUP_DELAY_SECONDS = 24 * 60 * 60;
|
|
9
|
+
const INTERACTIVE_WAKEUP_CUSTOM_TYPE = "gsd-schedule-wakeup";
|
|
10
|
+
// One pending interactive wakeup per session, keyed by base path — the same
|
|
11
|
+
// scoping auto-mode uses for its wakeup map (see `wakeupKey`). Re-arming cancels
|
|
12
|
+
// only that session's prior timer, so repeated polling never stacks overlapping
|
|
13
|
+
// wakeups, and concurrent projects in one host process don't cancel each other.
|
|
14
|
+
const pendingInteractiveWakeups = new Map();
|
|
15
|
+
function scheduleInteractiveWakeup(pi, key, delaySeconds, prompt, reason) {
|
|
16
|
+
const existing = pendingInteractiveWakeups.get(key);
|
|
17
|
+
if (existing)
|
|
18
|
+
clearTimeout(existing);
|
|
19
|
+
const handle = setTimeout(() => {
|
|
20
|
+
if (pendingInteractiveWakeups.get(key) === handle) {
|
|
21
|
+
pendingInteractiveWakeups.delete(key);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
void Promise.resolve(pi.sendMessage({
|
|
25
|
+
customType: INTERACTIVE_WAKEUP_CUSTOM_TYPE,
|
|
26
|
+
content: prompt,
|
|
27
|
+
display: true,
|
|
28
|
+
details: { delaySeconds, reason },
|
|
29
|
+
}, { triggerTurn: true })).catch((error) => {
|
|
30
|
+
logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logWarning("bootstrap", `ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}, delaySeconds * 1000);
|
|
37
|
+
pendingInteractiveWakeups.set(key, handle);
|
|
38
|
+
if (typeof handle === "object" &&
|
|
39
|
+
handle !== null &&
|
|
40
|
+
"unref" in handle &&
|
|
41
|
+
typeof handle.unref === "function") {
|
|
42
|
+
handle.unref();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function _resetInteractiveWakeupsForTest() {
|
|
46
|
+
for (const handle of pendingInteractiveWakeups.values()) {
|
|
47
|
+
clearTimeout(handle);
|
|
48
|
+
}
|
|
49
|
+
pendingInteractiveWakeups.clear();
|
|
50
|
+
}
|
|
8
51
|
export function registerScheduleWakeupTool(pi) {
|
|
9
52
|
pi.registerTool({
|
|
10
53
|
name: "ScheduleWakeup",
|
|
11
54
|
label: "Schedule Wakeup",
|
|
12
|
-
description: "In GSD auto-mode,
|
|
13
|
-
"
|
|
14
|
-
promptSnippet: "Schedule a
|
|
55
|
+
description: "Schedule a delayed continuation turn. In GSD auto-mode, continue the current unit in the same session; " +
|
|
56
|
+
"outside auto-mode, start a new triggered turn with the supplied wakeup prompt.",
|
|
57
|
+
promptSnippet: "Schedule a wakeup prompt after a delay.",
|
|
15
58
|
promptGuidelines: [
|
|
16
59
|
"Use ScheduleWakeup at the end of an execute-task turn when waiting for a long external process.",
|
|
17
60
|
"Include a prompt that says exactly what external state to check next and what artifact to write when done.",
|
|
18
61
|
"Re-arm ScheduleWakeup on each polling turn if the external process is still running.",
|
|
62
|
+
"Outside auto-mode, use ScheduleWakeup when the user asks you to check back or poll later.",
|
|
19
63
|
],
|
|
20
64
|
parameters: Type.Object({
|
|
21
65
|
delaySeconds: Type.Number({
|
|
@@ -34,22 +78,27 @@ export function registerScheduleWakeupTool(pi) {
|
|
|
34
78
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
35
79
|
const dash = getAutoRuntimeSnapshot();
|
|
36
80
|
const currentUnit = dash.currentUnit;
|
|
37
|
-
const
|
|
81
|
+
const delaySeconds = Math.max(1, Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)));
|
|
82
|
+
const reason = params.reason ?? "";
|
|
38
83
|
if (!dash.active || !currentUnit) {
|
|
84
|
+
const interactiveKey = dash.basePath || resolveCtxCwd(ctx);
|
|
85
|
+
scheduleInteractiveWakeup(pi, interactiveKey, delaySeconds, params.prompt, reason);
|
|
39
86
|
return {
|
|
40
|
-
content: [{
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
content: [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Wakeup scheduled for ${delaySeconds}s. GSD will start a new turn with the wakeup prompt.`,
|
|
90
|
+
}],
|
|
91
|
+
details: { operation: "schedule_wakeup", delaySeconds, mode: "interactive" },
|
|
43
92
|
};
|
|
44
93
|
}
|
|
45
|
-
const
|
|
94
|
+
const basePath = dash.basePath || resolveCtxCwd(ctx);
|
|
46
95
|
scheduleAutoWakeup({
|
|
47
96
|
basePath,
|
|
48
97
|
unitType: currentUnit.type,
|
|
49
98
|
unitId: currentUnit.id,
|
|
50
99
|
delayMs: delaySeconds * 1000,
|
|
51
100
|
prompt: params.prompt,
|
|
52
|
-
reason
|
|
101
|
+
reason,
|
|
53
102
|
createdAt: Date.now(),
|
|
54
103
|
});
|
|
55
104
|
return {
|