agent-dealer 1.2.4 → 1.2.6
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/bundle/server/dist/adapters/agent-deck-bind.js +33 -3
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +60 -2
- package/bundle/server/dist/adapters/agent-health.js +28 -6
- package/bundle/server/dist/adapters/agent-health.test.js +66 -11
- package/bundle/server/dist/adapters/git-worktree.js +187 -18
- package/bundle/server/dist/adapters/git-worktree.test.js +293 -0
- package/bundle/server/dist/adapters/github.js +110 -12
- package/bundle/server/dist/adapters/github.test.js +274 -3
- package/bundle/server/dist/adapters/muse-capability.js +356 -0
- package/bundle/server/dist/adapters/muse-capability.test.js +464 -0
- package/bundle/server/dist/capacity/claude-local-cache.js +266 -81
- package/bundle/server/dist/capacity/claude-local-cache.test.js +335 -36
- package/bundle/server/dist/capacity/muse-host.js +92 -32
- package/bundle/server/dist/capacity/muse-host.test.js +130 -4
- package/bundle/server/dist/coordinator/admission.test.js +85 -0
- package/bundle/server/dist/coordinator/args.js +4 -3
- package/bundle/server/dist/coordinator/checkpoint.js +5 -3
- package/bundle/server/dist/coordinator/checkpoint.test.js +23 -0
- package/bundle/server/dist/coordinator/commands.js +100 -12
- package/bundle/server/dist/coordinator/developer-effect.js +128 -16
- package/bundle/server/dist/coordinator/developer-effect.test.js +291 -12
- package/bundle/server/dist/coordinator/human-resolution.js +33 -1
- package/bundle/server/dist/coordinator/muse-developer.integration.test.js +291 -55
- package/bundle/server/dist/coordinator/muse-spawn.js +72 -164
- package/bundle/server/dist/coordinator/projection.js +1 -0
- package/bundle/server/dist/coordinator/prompts.js +25 -6
- package/bundle/server/dist/coordinator/prompts.test.js +36 -8
- package/bundle/server/dist/coordinator/routing.js +1 -0
- package/bundle/server/dist/coordinator/routing.test.js +20 -0
- package/bundle/server/dist/coordinator/spawn.js +9 -1
- package/bundle/server/dist/coordinator/worktree-cwd-guard.js +48 -0
- package/bundle/server/dist/coordinator/worktree-cwd-guard.test.js +83 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +6 -1
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +2 -1
- package/bundle/server/dist/repository/human-actions.js +13 -0
- package/bundle/server/dist/routes/human-actions.js +10 -1
- package/bundle/server/dist/routes/human-actions.test.js +117 -1
- package/bundle/server/dist/routes/index.js +12 -5
- package/bundle/server/dist/routes/runtime-capacity.test.js +3 -2
- package/bundle/server/dist/routes/version.js +30 -0
- package/bundle/server/dist/routes/version.test.js +21 -0
- package/bundle/server/dist/runners/muse-config-core.js +63 -16
- package/bundle/server/dist/runners/muse-config.js +1 -1
- package/bundle/server/dist/runners/muse-config.test.js +135 -29
- package/bundle/server/dist/runners/spawn-cli.js +17 -3
- package/bundle/server/dist/runners/spawn-cli.test.js +59 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/{index-CYqRh_-S.css → index-BII-LgB8.css} +1 -1
- package/bundle/server/static-ui/assets/index-CoDzZcsz.js +60 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +15 -15
- package/bundle/shared/dist/agents.js +6 -0
- package/bundle/shared/dist/index.d.ts +7 -7
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +7 -3
- package/dist/doctor.js +21 -13
- package/dist/doctor.test.js +9 -8
- package/dist/managed/index.d.ts +1 -1
- package/dist/managed/index.js +1 -1
- package/dist/managed/updater.d.ts +16 -6
- package/dist/managed/updater.js +28 -11
- package/dist/managed-update-restart.test.d.ts +1 -0
- package/dist/managed-update-restart.test.js +227 -0
- package/dist/ports.d.ts +6 -0
- package/dist/ports.js +10 -0
- package/dist/runtime-state.d.ts +10 -0
- package/dist/runtime-state.js +26 -0
- package/dist/start.js +67 -2
- package/dist/status.js +30 -2
- package/dist/update-check.js +3 -6
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +0 -60
|
@@ -11,7 +11,7 @@ import { canTransitionIssue } from "@agent-dealer/shared";
|
|
|
11
11
|
import { getDb } from "../db/index.js";
|
|
12
12
|
import { getIssue, incrementIssueRound, incrementIssueInfraAttempts, resetIssueInfraAttempts, grantReviewRetry, transitionIssue, } from "../repository/issues.js";
|
|
13
13
|
import { appendWorkflowEvent, completeWorkflowInstance, getActiveWorkflowInstance, getWorkflowInstance, listWorkflowEventsForIssue, startWorkflowInstance, WorkflowAlreadyActiveError, } from "../repository/workflow-events.js";
|
|
14
|
-
import { createHumanAction, findOpenHumanAction, getHumanAction, listHumanActionsForIssue, resolveHumanAction, updateOpenHumanAction, } from "../repository/human-actions.js";
|
|
14
|
+
import { createHumanAction, findOpenHumanAction, getHumanAction, listHumanActionsForIssue, reopenHumanAction, resolveHumanAction, updateOpenHumanAction, } from "../repository/human-actions.js";
|
|
15
15
|
import { pushLeaseToSha, readRemoteTip } from "../adapters/git-worktree.js";
|
|
16
16
|
import { ensureIssueRepoCheckout } from "../adapters/managed-repo.js";
|
|
17
17
|
import { reconcileFinding, resolveFindingsAbsentFromRound } from "../repository/findings.js";
|
|
@@ -28,7 +28,7 @@ import { reasonForWorkerFailedEvent } from "./failure-reason.js";
|
|
|
28
28
|
import { recordCausesForWorkerFailedEvent } from "./failure-cause.js";
|
|
29
29
|
import { routeDeveloperOutcome, routeReviewerOutcome, } from "./routing.js";
|
|
30
30
|
import { projectDeveloperRoute, projectReviewerRoute } from "./projection.js";
|
|
31
|
-
import { MERGE_FAILURE_EVIDENCE_KEY, MERGE_FAILURE_RESPONSE_OPTIONS, PUSH_DIVERGENCE_EVIDENCE_KEY, PUSH_DIVERGENCE_RESPONSE_OPTIONS, parseHumanResolution, resolveHumanActionOutcome, } from "./human-resolution.js";
|
|
31
|
+
import { MERGE_FAILURE_EVIDENCE_KEY, MERGE_FAILURE_RESPONSE_OPTIONS, PUSH_DIVERGENCE_EVIDENCE_KEY, PUSH_DIVERGENCE_RESPONSE_OPTIONS, WORKTREE_BLOCKER_EVIDENCE_KEY, normalizeResolutionNote, parseHumanResolution, resolveHumanActionOutcome, } from "./human-resolution.js";
|
|
32
32
|
import { AUTO_MERGE_INTENT, finalizeAutoMerge } from "./auto-merge.js";
|
|
33
33
|
import { externalMergeStateForIssue } from "./external-merge.js";
|
|
34
34
|
import { detectNonConvergence, formatNonConvergenceReason, reviewHistoryFromEvents, } from "./non-convergence.js";
|
|
@@ -197,7 +197,7 @@ export function activeWorkflowConflictMessage(issueId) {
|
|
|
197
197
|
* the gate stays on startWorkflow; clearing a stale gate after AC lands lives here so
|
|
198
198
|
* admitNext cannot leave it dangling.
|
|
199
199
|
*/
|
|
200
|
-
export function startWorkflowCore(issueId) {
|
|
200
|
+
export function startWorkflowCore(issueId, opts) {
|
|
201
201
|
const issue = getIssue(issueId);
|
|
202
202
|
if (!issue)
|
|
203
203
|
throw new StartPreconditionError(404, "Issue not found");
|
|
@@ -233,7 +233,12 @@ export function startWorkflowCore(issueId) {
|
|
|
233
233
|
workflowInstanceId: instance.id,
|
|
234
234
|
kind: "developer",
|
|
235
235
|
round: 1,
|
|
236
|
-
payload: {
|
|
236
|
+
payload: {
|
|
237
|
+
profileSnapshot: queuedProfileSnapshot(issue, "developer"),
|
|
238
|
+
// NOT-272: carry the scope decision only on this first round's payload — later
|
|
239
|
+
// rounds never see it (the prompt reads it from here, not from the DB).
|
|
240
|
+
...(opts?.scopeDecisionNote ? { scopeDecisionNote: opts.scopeDecisionNote } : {}),
|
|
241
|
+
},
|
|
237
242
|
idempotencyKey: `${instance.id}:developer:1`,
|
|
238
243
|
});
|
|
239
244
|
// NOT-103: every successful start is a force-admit — keep queue state in sync whether
|
|
@@ -781,6 +786,19 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
781
786
|
const pushDivergence = actionType === "policy_escalation" && !resumeAsReviewer
|
|
782
787
|
? (effect.pushDivergence ?? null)
|
|
783
788
|
: null;
|
|
789
|
+
// NOT-280: an unchanged worktree blocker (same fingerprint) lands on the action it
|
|
790
|
+
// already raised — still open, or reopened when a Resume changed nothing — instead of
|
|
791
|
+
// opening an identical one on every Resume.
|
|
792
|
+
const blockerFingerprint = effect.kind === "human_action" ? (effect.blockerFingerprint ?? null) : null;
|
|
793
|
+
if (blockerFingerprint) {
|
|
794
|
+
const same = sameBlockerAction(issue.id, blockerFingerprint);
|
|
795
|
+
if (same) {
|
|
796
|
+
ev.emit("human_action.requested", {
|
|
797
|
+
payload: { actionType: same.actionType, actionId: same.id, reopened: true, blockerFingerprint },
|
|
798
|
+
});
|
|
799
|
+
return { ...base, humanActionId: same.id };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
784
802
|
const action = createHumanAction({
|
|
785
803
|
issueId: issue.id,
|
|
786
804
|
workflowInstanceId: instance.id,
|
|
@@ -791,7 +809,9 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
791
809
|
? { review: reviewerOutcome.result, ...(nonConvergence ? { nonConvergence } : {}) }
|
|
792
810
|
: pushDivergence
|
|
793
811
|
? { [PUSH_DIVERGENCE_EVIDENCE_KEY]: pushDivergence }
|
|
794
|
-
:
|
|
812
|
+
: blockerFingerprint
|
|
813
|
+
? { [WORKTREE_BLOCKER_EVIDENCE_KEY]: { fingerprint: blockerFingerprint } }
|
|
814
|
+
: undefined,
|
|
795
815
|
// issueNow.headSha, not issue.headSha: a stale outcome that itself exhausted the
|
|
796
816
|
// infra budget already patched the newly observed head onto the issue above — the
|
|
797
817
|
// pre-transition issue param would still carry the stale SHA a "resume" must not reuse.
|
|
@@ -814,6 +834,51 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
814
834
|
}
|
|
815
835
|
return base;
|
|
816
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* NOT-280: an action reopened for an unchanged blocker (`sameBlockerAction`) can be
|
|
839
|
+
* resumed again — each Resume of it needs its own work item, so later resolutions of the
|
|
840
|
+
* same action get an ordinal suffix. The first Resume keeps the plain action-keyed name.
|
|
841
|
+
*/
|
|
842
|
+
function resumeWorkItemKey(issueId, baseKey) {
|
|
843
|
+
const taken = new Set(listWorkItemsForIssue(issueId).map((w) => w.idempotencyKey));
|
|
844
|
+
if (!taken.has(baseKey))
|
|
845
|
+
return baseKey;
|
|
846
|
+
let n = 2;
|
|
847
|
+
while (taken.has(`${baseKey}:${n}`))
|
|
848
|
+
n++;
|
|
849
|
+
return `${baseKey}:${n}`;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* NOT-280: the issue's most recent action, when it was raised for exactly this worktree
|
|
853
|
+
* blocker fingerprint and is either still open or was resolved by a Resume — reopened in
|
|
854
|
+
* that case. Anything else in between (another action, a different choice, a changed
|
|
855
|
+
* blocker) means state moved, so the caller raises a fresh action.
|
|
856
|
+
*/
|
|
857
|
+
function sameBlockerAction(issueId, fingerprint) {
|
|
858
|
+
const actions = listHumanActionsForIssue(issueId);
|
|
859
|
+
const latest = actions[actions.length - 1];
|
|
860
|
+
if (!latest?.evidenceJson)
|
|
861
|
+
return null;
|
|
862
|
+
try {
|
|
863
|
+
const evidence = JSON.parse(latest.evidenceJson);
|
|
864
|
+
if (evidence[WORKTREE_BLOCKER_EVIDENCE_KEY]?.fingerprint !== fingerprint)
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
if (latest.status === "open")
|
|
871
|
+
return latest;
|
|
872
|
+
try {
|
|
873
|
+
const resolution = latest.resolutionJson ? JSON.parse(latest.resolutionJson) : null;
|
|
874
|
+
if (resolution?.choice !== "resume")
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
return reopenHumanAction(latest.id);
|
|
881
|
+
}
|
|
817
882
|
/**
|
|
818
883
|
* NOT-194: true when the action is a merge-failure escalation (evidence carries
|
|
819
884
|
* `mergeFailure: true`). Pre-NOT-194 open merge-failure actions have no such evidence and
|
|
@@ -982,10 +1047,23 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
982
1047
|
return { ok: false, code: 404, error: "Human action not found" };
|
|
983
1048
|
if (action.status !== "open")
|
|
984
1049
|
return { ok: false, code: 409, error: "Human action already resolved" };
|
|
985
|
-
|
|
1050
|
+
// NOT-272: same shape validation as the HTTP route (which already applied it) —
|
|
1051
|
+
// defense in depth for direct/CLI callers of this sync entry point.
|
|
1052
|
+
const normalizedNote = normalizeResolutionNote(opts?.note);
|
|
1053
|
+
if (!normalizedNote.ok)
|
|
1054
|
+
return { ok: false, code: 400, error: normalizedNote.error };
|
|
1055
|
+
const resolution = parseHumanResolution(action.actionType, choice, normalizedNote.note);
|
|
986
1056
|
if (!resolution) {
|
|
987
1057
|
return { ok: false, code: 400, error: `Invalid choice "${choice}" for ${action.actionType}` };
|
|
988
1058
|
}
|
|
1059
|
+
// NOT-272: the typed resolution carries the note only for product_scope_decision
|
|
1060
|
+
// (parseHumanResolution drops it for every other action type).
|
|
1061
|
+
const scopeDecisionNote = resolution.actionType === "product_scope_decision" && "note" in resolution
|
|
1062
|
+
? resolution.note
|
|
1063
|
+
: undefined;
|
|
1064
|
+
const storedResolution = scopeDecisionNote
|
|
1065
|
+
? { choice, note: scopeDecisionNote }
|
|
1066
|
+
: { choice };
|
|
989
1067
|
// NOT-194: narrow policy_escalation choices per action. A merge-failure action offers
|
|
990
1068
|
// retry_merge/repair/close only (resume would re-run development on approved work);
|
|
991
1069
|
// every other policy_escalation keeps resume/close only. Pre-NOT-194 open merge-failure
|
|
@@ -1077,7 +1155,7 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1077
1155
|
}
|
|
1078
1156
|
try {
|
|
1079
1157
|
return getDb().transaction(() => {
|
|
1080
|
-
resolveHumanAction(actionId, resolvedBy,
|
|
1158
|
+
resolveHumanAction(actionId, resolvedBy, storedResolution);
|
|
1081
1159
|
appendWorkflowEvent({
|
|
1082
1160
|
issueId: issue.id,
|
|
1083
1161
|
type: "human_action.resolved",
|
|
@@ -1086,7 +1164,10 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1086
1164
|
stage: issue.status,
|
|
1087
1165
|
payload: { actionType: action.actionType, choice },
|
|
1088
1166
|
});
|
|
1089
|
-
|
|
1167
|
+
// NOT-272: this branch is always product_scope_decision (legacy terminal
|
|
1168
|
+
// actions returned above) — the note rides the round-1 payload so the very
|
|
1169
|
+
// next developer round reads it in its prompt.
|
|
1170
|
+
const { workItem } = startWorkflowCore(issue.id, { scopeDecisionNote: scopeDecisionNote });
|
|
1090
1171
|
return {
|
|
1091
1172
|
ok: true,
|
|
1092
1173
|
issueStatus: getIssue(issue.id).status,
|
|
@@ -1165,7 +1246,7 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1165
1246
|
: {};
|
|
1166
1247
|
return getDb().transaction(() => {
|
|
1167
1248
|
resolveHumanAction(actionId, resolvedBy, {
|
|
1168
|
-
|
|
1249
|
+
...storedResolution,
|
|
1169
1250
|
// NOT-226: record the re-pin so the resolved action itself shows which head the
|
|
1170
1251
|
// reviewer was re-queued at when the branch had moved while parked.
|
|
1171
1252
|
...(rePinnedResumeHeadSha
|
|
@@ -1271,6 +1352,7 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1271
1352
|
// Keyed on the resolved human action, not the round/attempt counters: a
|
|
1272
1353
|
// "infra" resume resets infra_attempts to 0 every time, so a counter-based key would
|
|
1273
1354
|
// collide across repeated escalate→resume cycles within the same round.
|
|
1355
|
+
const resumeKey = resumeWorkItemKey(issue.id, `${instance.id}:${resumeAsReviewer ? "reviewer" : "developer"}:resume:${action.id}`);
|
|
1274
1356
|
const next = resumeAsReviewer
|
|
1275
1357
|
? enqueueWorkItem({
|
|
1276
1358
|
issueId: issue.id,
|
|
@@ -1278,15 +1360,20 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1278
1360
|
kind: "reviewer",
|
|
1279
1361
|
round: issueNow.currentRound,
|
|
1280
1362
|
payload: { inputSha: effectiveResumeHeadSha, profileSnapshot: queuedProfileSnapshot(issue, "reviewer") },
|
|
1281
|
-
idempotencyKey:
|
|
1363
|
+
idempotencyKey: resumeKey,
|
|
1282
1364
|
})
|
|
1283
1365
|
: enqueueWorkItem({
|
|
1284
1366
|
issueId: issue.id,
|
|
1285
1367
|
workflowInstanceId: instance.id,
|
|
1286
1368
|
kind: "developer",
|
|
1287
1369
|
round: issueNow.currentRound,
|
|
1288
|
-
payload: {
|
|
1289
|
-
|
|
1370
|
+
payload: {
|
|
1371
|
+
profileSnapshot: queuedProfileSnapshot(issue, "developer"),
|
|
1372
|
+
// NOT-272: a product_scope_decision note rides only this next round's
|
|
1373
|
+
// payload — later rounds never see it.
|
|
1374
|
+
...(scopeDecisionNote ? { scopeDecisionNote } : {}),
|
|
1375
|
+
},
|
|
1376
|
+
idempotencyKey: resumeKey,
|
|
1290
1377
|
});
|
|
1291
1378
|
return {
|
|
1292
1379
|
ok: true,
|
|
@@ -1347,6 +1434,7 @@ export async function resolveHumanActionAndAdvanceAsync(actionId, resolvedBy, ch
|
|
|
1347
1434
|
const result = resolveHumanActionAndAdvance(actionId, resolvedBy, choice, {
|
|
1348
1435
|
resumeLiveHeadSha,
|
|
1349
1436
|
externalMergeState,
|
|
1437
|
+
note: opts?.note,
|
|
1350
1438
|
});
|
|
1351
1439
|
if (!result.ok || !result.pendingMerge)
|
|
1352
1440
|
return result;
|
|
@@ -21,11 +21,11 @@ import { getTaskSnapshot } from "./commands.js";
|
|
|
21
21
|
import { buildDeveloperPrompt } from "./prompts.js";
|
|
22
22
|
import { guidanceForNextSession } from "./guidance.js";
|
|
23
23
|
import { realDeveloperSpawn, developerSessionLogPath } from "./spawn.js";
|
|
24
|
-
import { DEFAULT_BASE_FETCH_TIMEOUT_MS, fastForwardLocalBranchToSha, resolveDeveloperWorktree, safeRemoveWorktree, isWorktreeClean, commitsAhead, pushBranch, mergeBase, branchExists, pushBranchRef, revParseHead, revParseRef, fetchRef, salvageDirtyWorktree, dirtyWorktreeRecoveryCommands, withRepoLock, } from "../adapters/git-worktree.js";
|
|
24
|
+
import { DEFAULT_BASE_FETCH_TIMEOUT_MS, fastForwardLocalBranchToSha, resolveDeveloperWorktree, safeRemoveWorktree, isWorktreeClean, commitsAhead, pushBranch, mergeBase, branchExists, pushBranchRef, revParseHead, revParseRef, fetchRef, salvageDirtyWorktree, dirtyWorktreeRecoveryCommands, withRepoLock, leftoverFingerprint, } from "../adapters/git-worktree.js";
|
|
25
25
|
import { recordIssueBaseSha } from "../repository/issues.js";
|
|
26
26
|
import { ensureIssueRepoCheckout, roleWorktreePathForResolution, resolveCheckoutBaseBranch, } from "../adapters/managed-repo.js";
|
|
27
27
|
import { baseRefCandidates, inspectBranchProgress } from "./branch-progress.js";
|
|
28
|
-
import { prepareWorkerDeckConnection, releaseWorkerDeckConnection } from "../adapters/agent-deck-bind.js";
|
|
28
|
+
import { prepareWorkerDeckConnection, releaseWorkerDeckConnection, verifyWorkerDeckConnection } from "../adapters/agent-deck-bind.js";
|
|
29
29
|
import { realGithubAdapter, pollPrChecks } from "../adapters/github.js";
|
|
30
30
|
import { getWorkerSession, patchRunningSession, recordSessionProcess, setSessionInputSha } from "../repository/worker-sessions.js";
|
|
31
31
|
import { COORDINATOR_PROCESS_OWNER, readProcessStartTime } from "./process-liveness.js";
|
|
@@ -675,7 +675,13 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
675
675
|
return { kind: "base_fetch_failed", reason: resolved.reason };
|
|
676
676
|
}
|
|
677
677
|
if (resolved.kind === "conflict") {
|
|
678
|
-
return {
|
|
678
|
+
return {
|
|
679
|
+
kind: "worktree_conflict",
|
|
680
|
+
path: resolved.path,
|
|
681
|
+
reason: resolved.reason,
|
|
682
|
+
recoveryCommands: resolved.recoveryCommands,
|
|
683
|
+
...(resolved.fingerprint ? { fingerprint: resolved.fingerprint } : {}),
|
|
684
|
+
};
|
|
679
685
|
}
|
|
680
686
|
if (resolved.kind === "live_owner") {
|
|
681
687
|
return {
|
|
@@ -687,6 +693,56 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
687
693
|
}
|
|
688
694
|
worktreePath = resolved.path;
|
|
689
695
|
worktreeReused = resolved.kind === "reused";
|
|
696
|
+
if (resolved.kind === "reused" && resolved.salvage) {
|
|
697
|
+
// NOT-280: a dead predecessor's dirt was committed on the issue branch (or found
|
|
698
|
+
// already committed after a restart) — one durable checkpoint per salvage commit,
|
|
699
|
+
// tied to the predecessor session when Dealer still has its row.
|
|
700
|
+
// Unlike ordinary checkpoint evidence, this row is a deliverable: if it cannot be
|
|
701
|
+
// persisted (after one retry) the attempt stops before spawning, leaving the clean
|
|
702
|
+
// checkout and its salvage tip in place. The next Resume re-detects that tip
|
|
703
|
+
// (`recovered`) and retries the idempotent insert; an unchanged failure keeps the
|
|
704
|
+
// same fingerprint, so it reuses its human action instead of opening another.
|
|
705
|
+
const salvage = resolved.salvage;
|
|
706
|
+
const recordSalvage = () => emitCheckpointObserved({
|
|
707
|
+
issueId: issue.id,
|
|
708
|
+
workflowInstanceId: instance.id,
|
|
709
|
+
workerSessionId: getWorkerSession(salvage.predecessorSessionId)
|
|
710
|
+
? salvage.predecessorSessionId
|
|
711
|
+
: sessionId,
|
|
712
|
+
role: "developer",
|
|
713
|
+
stage,
|
|
714
|
+
round,
|
|
715
|
+
kind: "commit",
|
|
716
|
+
observedSha: salvage.commitSha,
|
|
717
|
+
origin: "salvage",
|
|
718
|
+
branch: branchName,
|
|
719
|
+
resumeSalvage: true,
|
|
720
|
+
});
|
|
721
|
+
let checkpointError = null;
|
|
722
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
723
|
+
try {
|
|
724
|
+
recordSalvage();
|
|
725
|
+
checkpointError = null;
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
checkpointError = err;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (checkpointError !== null) {
|
|
733
|
+
const reason = `Salvage tip ${salvage.commitSha.slice(0, 7)} of dead session ${salvage.predecessorSessionId}'s work ` +
|
|
734
|
+
`was committed on ${branchName} at ${worktreePath}, but its durable checkpoint could not be recorded: ` +
|
|
735
|
+
`${checkpointError instanceof Error ? checkpointError.message : String(checkpointError)}. ` +
|
|
736
|
+
"The checkout was left intact; Resume retries the checkpoint without re-committing.";
|
|
737
|
+
return {
|
|
738
|
+
kind: "worktree_conflict",
|
|
739
|
+
path: worktreePath,
|
|
740
|
+
reason,
|
|
741
|
+
recoveryCommands: [`cd ${worktreePath}`, "git log --oneline -3", "# fix the Dealer database, then Resume"],
|
|
742
|
+
fingerprint: await leftoverFingerprint(worktreePath, reason),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
690
746
|
if (resolved.kind === "created" && resolved.baseSha) {
|
|
691
747
|
// NOT-197: record the true branch point while it is known — the verified handoff
|
|
692
748
|
// re-checks it via merge-base, but crash/timeout progress inspection below already
|
|
@@ -729,8 +785,7 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
729
785
|
// the spawn args' tool surface further down. They were resolved separately and had to
|
|
730
786
|
// agree by inspection — the exact shape this stack exists to remove (NOT-134 review).
|
|
731
787
|
const policy = snapshot?.permissionPolicy ?? roleCeiling("developer");
|
|
732
|
-
// NOT-
|
|
733
|
-
// verified or released for them, whatever deck the profile happens to carry.
|
|
788
|
+
// NOT-278: every runtime, Muse included, is fail-closed without a deck — nothing spawns.
|
|
734
789
|
const isMuse = runtime === "muse_code";
|
|
735
790
|
let workerAuthority = null;
|
|
736
791
|
// NOT-225: marks the setup/spawn boundary for the outer catch below. Only a throw
|
|
@@ -739,14 +794,14 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
739
794
|
// adapter_failure path, since the session did start, run, and possibly commit.
|
|
740
795
|
let sessionStarted = false;
|
|
741
796
|
try {
|
|
742
|
-
if (!
|
|
797
|
+
if (!snapshot?.deckId) {
|
|
743
798
|
await bestEffortRemove(repoPath, worktreePath);
|
|
744
799
|
return {
|
|
745
800
|
kind: "deck_failure",
|
|
746
801
|
reason: "Agent profile has no Agent Deck — workers never start without one",
|
|
747
802
|
};
|
|
748
803
|
}
|
|
749
|
-
if (!isMuse
|
|
804
|
+
if (!isMuse) {
|
|
750
805
|
const prepared = await prepareWorkerDeckConnection({
|
|
751
806
|
deckId: snapshot.deckId,
|
|
752
807
|
worktreePath,
|
|
@@ -769,7 +824,27 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
769
824
|
deckId: snapshot.deckId,
|
|
770
825
|
});
|
|
771
826
|
}
|
|
772
|
-
|
|
827
|
+
else {
|
|
828
|
+
// NOT-278: Muse runs the same live deck preflight without materializing a runtime MCP
|
|
829
|
+
// config — its settings.json comes only from `prepareMuseAttempt` in the spawn path.
|
|
830
|
+
// Same mapping: silence is `deck_unavailable`, a bad answer is `deck_failure`.
|
|
831
|
+
const verified = await verifyWorkerDeckConnection({
|
|
832
|
+
deckId: snapshot.deckId,
|
|
833
|
+
worktreePath,
|
|
834
|
+
callTool: deps.deckCallTool,
|
|
835
|
+
});
|
|
836
|
+
if (!verified.ok) {
|
|
837
|
+
await bestEffortRemove(repoPath, worktreePath);
|
|
838
|
+
return verified.kind === "deck_unavailable"
|
|
839
|
+
? { kind: "deck_unavailable", reason: verified.reason }
|
|
840
|
+
: { kind: "deck_failure", reason: verified.reason };
|
|
841
|
+
}
|
|
842
|
+
milestone("deck.connected", `Developer · deck connected (round ${round})`, {
|
|
843
|
+
deckId: snapshot.deckId,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
// NOT-278: Muse is deck-bound like every other runtime, so the same brief rule applies.
|
|
847
|
+
if (taskBriefIsComplete(taskSnapshot)) {
|
|
773
848
|
milestone("brief.resolved", `Developer · brief ready (Task/AC complete)`, {
|
|
774
849
|
resolution: "task_complete",
|
|
775
850
|
});
|
|
@@ -782,6 +857,11 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
782
857
|
const openFindings = listFindingsForIssue(issue.id).filter((f) => f.status === "open" || f.status === "recurring");
|
|
783
858
|
const guidance = guidanceForNextSession(issue.id, sessionId);
|
|
784
859
|
const retryReason = payload.retryReason ?? undefined;
|
|
860
|
+
// NOT-272: the scope note rides only the work item queued by the resolution —
|
|
861
|
+
// exactly this round reads it; nothing is re-read from the DB.
|
|
862
|
+
const scopeDecisionNote = typeof payload.scopeDecisionNote === "string" && payload.scopeDecisionNote.trim()
|
|
863
|
+
? payload.scopeDecisionNote.trim()
|
|
864
|
+
: undefined;
|
|
785
865
|
let priorConclusion;
|
|
786
866
|
let priorVerificationReceipt;
|
|
787
867
|
if (retryReason) {
|
|
@@ -813,9 +893,10 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
813
893
|
priorConclusion,
|
|
814
894
|
priorVerificationReceipt,
|
|
815
895
|
worktreePath,
|
|
816
|
-
deckId:
|
|
817
|
-
|
|
896
|
+
deckId: snapshot?.deckId ?? null,
|
|
897
|
+
museDeveloper: isMuse,
|
|
818
898
|
guidance: guidance.length ? guidance : undefined,
|
|
899
|
+
scopeDecisionNote,
|
|
819
900
|
});
|
|
820
901
|
// NOT-83 review finding: the session is marked `running` (worker-loop.ts) before this
|
|
821
902
|
// handler ever runs, so an abort landing during worktree setup/deck-bind above already
|
|
@@ -914,6 +995,9 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
914
995
|
effort: snapshot?.effort ?? null,
|
|
915
996
|
// Frozen profile budget → Muse's `--max-model-steps`; the other runtimes ignore it.
|
|
916
997
|
maxModelSteps: parsePhaseBudget(snapshot?.budgetJson)?.maxTurns ?? null,
|
|
998
|
+
// NOT-278: frozen profile deck → the Muse exec attempt (deck headers + pre-spawn
|
|
999
|
+
// verify). Other runtimes carry their deck via mcpConfigPath/mcpEnv and ignore this.
|
|
1000
|
+
deckId: snapshot?.deckId ?? null,
|
|
917
1001
|
prompt,
|
|
918
1002
|
cwd: worktreePath,
|
|
919
1003
|
timeoutMs: developerEffectConfig.sessionTimeoutMs,
|
|
@@ -1286,13 +1370,41 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
1286
1370
|
author: "system",
|
|
1287
1371
|
blobPath: spawned.logPath,
|
|
1288
1372
|
});
|
|
1373
|
+
// NOT-275: a clean exit (exitCode 0, not timed out, no usage cap) that leaves
|
|
1374
|
+
// the worktree dirty hit the same escalation as a crash before this — the
|
|
1375
|
+
// session did real ticket-scoped work but never ran `git commit`. Salvage
|
|
1376
|
+
// first (same mechanism as the crash/timeout path above): on success fall
|
|
1377
|
+
// through to push/PR as if the agent had committed itself; only when the
|
|
1378
|
+
// salvage commit itself fails do we escalate dirty_worktree with recovery.
|
|
1289
1379
|
if (!(await isWorktreeClean(worktreePath))) {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1380
|
+
const salvaged = await salvageDirtyWorktree(worktreePath, "crash");
|
|
1381
|
+
if (!salvaged.ok) {
|
|
1382
|
+
return {
|
|
1383
|
+
kind: "dirty_worktree",
|
|
1384
|
+
reason: `${reasonForDirtyWorktree(spawned.logPath, runtime)} Auto-commit salvage failed: ${salvaged.reason}`,
|
|
1385
|
+
path: worktreePath,
|
|
1386
|
+
recoveryCommands: dirtyWorktreeRecoveryCommands(repoPath, worktreePath),
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
// NOT-172: durable salvage-commit checkpoint at the existing success point.
|
|
1390
|
+
try {
|
|
1391
|
+
emitCheckpointObserved({
|
|
1392
|
+
issueId: issue.id,
|
|
1393
|
+
workflowInstanceId: instance.id,
|
|
1394
|
+
workerSessionId: sessionId,
|
|
1395
|
+
role: "developer",
|
|
1396
|
+
stage,
|
|
1397
|
+
round,
|
|
1398
|
+
kind: "commit",
|
|
1399
|
+
observedSha: salvaged.commitSha,
|
|
1400
|
+
origin: "salvage",
|
|
1401
|
+
inputSha: samplerInputSha,
|
|
1402
|
+
branch: branchName,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
// checkpoint evidence must never fail the attempt itself
|
|
1407
|
+
}
|
|
1296
1408
|
}
|
|
1297
1409
|
const ahead = await commitsAhead({ worktreePath, baseRef: `origin/${baseBranch}` });
|
|
1298
1410
|
if (ahead === 0) {
|