agent-dealer 1.2.5 → 1.2.7
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 +3 -4
- package/bundle/server/dist/adapters/agent-health.test.js +11 -9
- 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/muse-capability.js +51 -25
- package/bundle/server/dist/adapters/muse-capability.test.js +138 -52
- package/bundle/server/dist/capacity/claude-local-cache.js +59 -27
- package/bundle/server/dist/capacity/claude-local-cache.test.js +163 -9
- package/bundle/server/dist/capacity/muse-host.js +34 -32
- package/bundle/server/dist/capacity/muse-host.test.js +5 -3
- 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 +66 -5
- package/bundle/server/dist/coordinator/developer-effect.js +88 -10
- package/bundle/server/dist/coordinator/developer-effect.test.js +170 -0
- package/bundle/server/dist/coordinator/human-resolution.js +2 -0
- package/bundle/server/dist/coordinator/muse-developer.integration.test.js +291 -55
- package/bundle/server/dist/coordinator/muse-spawn.js +72 -174
- package/bundle/server/dist/coordinator/projection.js +1 -0
- package/bundle/server/dist/coordinator/prompts.js +10 -6
- package/bundle/server/dist/coordinator/prompts.test.js +16 -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 +2 -1
- 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/index.js +11 -8
- 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-DyAJNyfV.css +1 -0
- package/bundle/server/static-ui/assets/index-yLyxRd-7.js +64 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/action-presentation.d.ts +51 -0
- package/bundle/shared/dist/action-presentation.js +280 -0
- package/bundle/shared/dist/action-presentation.test.d.ts +1 -0
- package/bundle/shared/dist/action-presentation.test.js +180 -0
- package/bundle/shared/dist/attempt-waste.d.ts +2 -2
- package/bundle/shared/dist/execution-report.d.ts +2 -2
- package/bundle/shared/dist/human-actions.d.ts +12 -12
- package/bundle/shared/dist/index.d.ts +41 -40
- package/bundle/shared/dist/index.js +1 -0
- package/bundle/shared/dist/issues.d.ts +12 -12
- package/bundle/shared/dist/outbound-draft.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +2 -2
- package/bundle/shared/dist/usage-events.d.ts +2 -2
- package/bundle/shared/dist/worker-sessions.d.ts +6 -6
- package/bundle/shared/dist/workflow.d.ts +2 -2
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.js +4 -1
- 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-BII-LgB8.css +0 -1
- package/bundle/server/static-ui/assets/index-Bq8wWpZm.js +0 -60
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
// connection (`hasInflightExecution`): a capacity read must not SIGTERM
|
|
26
26
|
// an admitted turn.
|
|
27
27
|
// - The host launches under a server-owned XDG home
|
|
28
|
-
// (`prepareMuseServeHome`): the
|
|
29
|
-
//
|
|
30
|
-
// reminders) plus a symlink to the ambient login, so
|
|
31
|
-
//
|
|
28
|
+
// (`prepareMuseServeHome`): the deckless worker base posture
|
|
29
|
+
// (`buildMuseBaseSettings` — no MCP servers, no subagents, no workflows,
|
|
30
|
+
// no reminders) plus a symlink to the ambient login, so the host never
|
|
31
|
+
// inherits the operator's ambient config. It runs no sessions; deck-enabled
|
|
32
|
+
// developer turns each carry their own required `agent-deck` server on the
|
|
33
|
+
// isolated exec lane.
|
|
32
34
|
// - `usage/changed` is ingested as soon as received; the throttled
|
|
33
35
|
// on-demand `usage/read` on the same host is the final read.
|
|
34
36
|
// - Restart/crash: usage state is process-local to the host, so a dead host
|
|
@@ -51,17 +53,16 @@
|
|
|
51
53
|
// for an hour.
|
|
52
54
|
//
|
|
53
55
|
// Execution precondition (NOT-269): only a host that observes the account's
|
|
54
|
-
// provider traffic can answer `usage/read`.
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// (`refreshMuseCapacityAfterSession` in coordinator/muse-spawn.ts)
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// for
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// rows remain preserved.
|
|
56
|
+
// provider traffic can answer `usage/read`. NOT-278 removed developer turns
|
|
57
|
+
// from this host: every deck-enabled Muse developer turn runs the isolated
|
|
58
|
+
// `muse exec` lane (coordinator/muse-spawn.ts), which cannot observe the
|
|
59
|
+
// host, so the session-boundary refresh hook
|
|
60
|
+
// (`refreshMuseCapacityAfterSession` in coordinator/muse-spawn.ts) stays
|
|
61
|
+
// honest N/A with last-good rows preserved until a genuine turn observes
|
|
62
|
+
// the host again. If no genuine turn has populated a complete current pair
|
|
63
|
+
// for an hour, `muse-probe.ts` may use its own host for the bounded paid
|
|
64
|
+
// fallback. Per-session Agent Deck support for the serve protocol is a
|
|
65
|
+
// non-goal until the protocol can represent isolated session MCP identity.
|
|
65
66
|
//
|
|
66
67
|
// PRODUCT DECISION (2026-09-26, resolved via human_action on this ticket):
|
|
67
68
|
// the runner migration is in scope for NOT-270, in this same PR, not a
|
|
@@ -94,7 +95,7 @@ import path from "node:path";
|
|
|
94
95
|
import { MUSE_CLI_ENV, resolveMuseAuthFile, resolveMuseBin } from "../cli-env.js";
|
|
95
96
|
import { getDataDir } from "../db/index.js";
|
|
96
97
|
import { parseMuseVersion } from "../adapters/muse-capability.js";
|
|
97
|
-
import {
|
|
98
|
+
import { buildMuseBaseSettings } from "../runners/muse-config.js";
|
|
98
99
|
import { MSP_USAGE_CHANGED, MUSE_CLIENT_INFO, MUSE_SERVE_ARGV, assertMuseExecMethod, assertMuseReadOnlyMethod, hasMuseCredential, ingestMuseUsagePayload, museCapacityTimeoutMs, museClassifyRpcError, museEvidenceRef, museRefreshThrottleMs, museUuidv7, noteMuseCapacityFailure, parseMuseRpcLine, } from "./muse.js";
|
|
99
100
|
function readResolvedMuseVersion() {
|
|
100
101
|
return new Promise((resolve) => {
|
|
@@ -114,16 +115,16 @@ export function resolveMuseHostCwd() {
|
|
|
114
115
|
return dir;
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
117
|
-
* NOT-
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* server-owned config home carrying
|
|
122
|
-
* symlink to the ambient login (linked, never read or copied; absent
|
|
123
|
-
* the operator authenticates with META_API_KEY), and an isolated data home
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* instance, reused across restarts, removed on shutdown.
|
|
118
|
+
* NOT-278: the owned host never runs a session — it only serves the read-only
|
|
119
|
+
* handshake/`usage/read` allowlist for capacity, and it is shared across decks, so it
|
|
120
|
+
* carries the deckless worker base posture (`buildMuseBaseSettings()`: no MCP servers, no
|
|
121
|
+
* subagent delegation, no workflows, no reminder child runs) and never inherits the
|
|
122
|
+
* operator's ambient MCP/subagent config. Builds a server-owned config home carrying those
|
|
123
|
+
* base settings plus a symlink to the ambient login (linked, never read or copied; absent
|
|
124
|
+
* when the operator authenticates with META_API_KEY), and an isolated data home for the
|
|
125
|
+
* host's own session/state. Deck-enabled developer turns run on the isolated exec lane
|
|
126
|
+
* (`coordinator/muse-spawn.ts`), each with its own required `agent-deck` server. Process-
|
|
127
|
+
* scoped: one home per host instance, reused across restarts, removed on shutdown.
|
|
127
128
|
*/
|
|
128
129
|
export function prepareMuseServeHome(ambientAuthFile = resolveMuseAuthFile()) {
|
|
129
130
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-serve-"));
|
|
@@ -135,7 +136,7 @@ export function prepareMuseServeHome(ambientAuthFile = resolveMuseAuthFile()) {
|
|
|
135
136
|
fs.chmodSync(root, 0o700);
|
|
136
137
|
fs.chmodSync(configHome, 0o700);
|
|
137
138
|
fs.chmodSync(dataHome, 0o700);
|
|
138
|
-
fs.writeFileSync(path.join(configHome, "muse", "settings.json"), `${JSON.stringify(
|
|
139
|
+
fs.writeFileSync(path.join(configHome, "muse", "settings.json"), `${JSON.stringify(buildMuseBaseSettings(), null, 2)}\n`, { mode: 0o600 });
|
|
139
140
|
let authLinked = false;
|
|
140
141
|
try {
|
|
141
142
|
if (fs.existsSync(ambientAuthFile)) {
|
|
@@ -226,9 +227,9 @@ export class MuseCapacityHost {
|
|
|
226
227
|
/**
|
|
227
228
|
* Env for the host subprocess: the server-owned XDG home always wins over
|
|
228
229
|
* inherited ambient config (the server process itself may run under an
|
|
229
|
-
* operator XDG home), so
|
|
230
|
-
*
|
|
231
|
-
*
|
|
230
|
+
* operator XDG home), so the host never inherits ambient MCP servers,
|
|
231
|
+
* subagents, or workflows. Only an explicit XDG override in `opts.env`
|
|
232
|
+
* (tests) still wins.
|
|
232
233
|
*/
|
|
233
234
|
mergedEnv() {
|
|
234
235
|
const home = this.ensureServeHome();
|
|
@@ -488,8 +489,9 @@ export class MuseCapacityHost {
|
|
|
488
489
|
/**
|
|
489
490
|
* NOT-277: start (or reuse) the host, and report whether it runs `version`. A live host on any
|
|
490
491
|
* other (or an unknown) version is restarted onto the current on-disk binary when it is idle;
|
|
491
|
-
* while
|
|
492
|
-
*
|
|
492
|
+
* while the connection is in use it is left alone and this returns false. Never true for a
|
|
493
|
+
* host not known to run `version`. (NOT-278: no developer turn runs on the host any more —
|
|
494
|
+
* this only gates capacity reads, which are version-independent.)
|
|
493
495
|
*/
|
|
494
496
|
async ensureStartedOnVersion(version) {
|
|
495
497
|
if (!(await this.ensureStarted()))
|
|
@@ -473,9 +473,11 @@ test("serve host launches under a server-owned config home with worker settings"
|
|
|
473
473
|
assert.ok(!String(rec.dataHome).includes(".config"), "data home is not the operator's");
|
|
474
474
|
assert.equal(rec.settingsError, null, "settings.json parses on the launched host");
|
|
475
475
|
const settings = rec.settings;
|
|
476
|
-
//
|
|
477
|
-
// servers, no subagent delegation, no workflows, no
|
|
478
|
-
|
|
476
|
+
// NOT-278: the host runs no sessions, so it carries the deckless worker base posture
|
|
477
|
+
// (`buildMuseBaseSettings`): no MCP servers, no subagent delegation, no workflows, no
|
|
478
|
+
// reminder child runs. Deck-enabled developer turns each carry their own required
|
|
479
|
+
// `agent-deck` server on the isolated exec lane.
|
|
480
|
+
assert.ok(!("mcpServers" in settings), "the host carries no MCP servers");
|
|
479
481
|
const run = settings.run;
|
|
480
482
|
assert.equal(run.workflow_trigger_mode, "off");
|
|
481
483
|
assert.equal(run.subagent_delegation_mode, "off");
|
|
@@ -33,9 +33,10 @@ function claudeDisallowedTools(policy) {
|
|
|
33
33
|
}
|
|
34
34
|
function buildArgs(runtime, policy, prompt, model, mcpConfigPath, effort) {
|
|
35
35
|
if (runtime === "muse_code") {
|
|
36
|
-
// Muse's developer argv is built by runners/muse-
|
|
37
|
-
//
|
|
38
|
-
// for. Falling through to the Claude arm below
|
|
36
|
+
// Muse's developer argv is built by runners/muse-config.ts (`prepareMuseAttempt`,
|
|
37
|
+
// NOT-278) inside coordinator/muse-spawn.ts; it needs a per-attempt deck, session id, and
|
|
38
|
+
// XDG dirs this pure builder has no place for. Falling through to the Claude arm below
|
|
39
|
+
// would spawn `muse` with Claude flags.
|
|
39
40
|
throw new Error("muse_code is not wired through the shared arg builder — its developer argv is built in coordinator/muse-spawn.ts");
|
|
40
41
|
}
|
|
41
42
|
if (runtime === "codex_local") {
|
|
@@ -26,9 +26,11 @@ export function emitCheckpointObserved(ev) {
|
|
|
26
26
|
samplingPrecisionMs: ev.samplingPrecisionMs ?? null,
|
|
27
27
|
branch: ev.branch ?? null,
|
|
28
28
|
},
|
|
29
|
-
idempotencyKey: ev.
|
|
30
|
-
? `checkpoint:${ev.
|
|
31
|
-
:
|
|
29
|
+
idempotencyKey: ev.resumeSalvage && ev.observedSha
|
|
30
|
+
? `checkpoint:resume-salvage:${ev.issueId}:${ev.observedSha}`
|
|
31
|
+
: ev.kind === "commit" && ev.origin === "salvage"
|
|
32
|
+
? `checkpoint:${ev.workerSessionId}:commit:salvage`
|
|
33
|
+
: `checkpoint:${ev.workerSessionId}:${ev.kind}`,
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
@@ -167,3 +167,26 @@ test("checkpoints attribute per session across attempts of one issue", async ()
|
|
|
167
167
|
assert.deepEqual(records.map((r) => r.sessionId).sort(), [sessionId, second.id].sort());
|
|
168
168
|
assert.deepEqual(records.map((r) => r.observedSha).sort(), [COMMIT_SHA, "c".repeat(40)].sort());
|
|
169
169
|
});
|
|
170
|
+
test("NOT-280: resume-salvage checkpoints dedupe per issue on restart but never collide across issues with an identical commit SHA", async () => {
|
|
171
|
+
const a = await setup();
|
|
172
|
+
const b = await setup();
|
|
173
|
+
const emit = (ctx) => emitCheckpointObserved({
|
|
174
|
+
issueId: ctx.issueId,
|
|
175
|
+
workflowInstanceId: ctx.instanceId,
|
|
176
|
+
workerSessionId: ctx.sessionId,
|
|
177
|
+
role: "developer",
|
|
178
|
+
stage: "developing",
|
|
179
|
+
round: 1,
|
|
180
|
+
kind: "commit",
|
|
181
|
+
observedSha: COMMIT_SHA,
|
|
182
|
+
origin: "salvage",
|
|
183
|
+
resumeSalvage: true,
|
|
184
|
+
});
|
|
185
|
+
emit(a);
|
|
186
|
+
emit(a); // restart re-detecting the same saved tip
|
|
187
|
+
emit(b); // another issue whose salvage produced the identical commit object
|
|
188
|
+
assert.equal(checkpointsOf(a.issueId, "commit").length, 1, "restart never duplicates the row");
|
|
189
|
+
assert.equal(checkpointsOf(b.issueId, "commit").length, 1, "the second issue still gets its own checkpoint");
|
|
190
|
+
const [row] = listWorkflowEventsForIssue(b.issueId).filter((e) => e.type === "checkpoint.observed");
|
|
191
|
+
assert.equal(row.workerSessionId, b.sessionId, "tied to its own predecessor session");
|
|
192
|
+
});
|
|
@@ -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, normalizeResolutionNote, 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";
|
|
@@ -786,6 +786,19 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
786
786
|
const pushDivergence = actionType === "policy_escalation" && !resumeAsReviewer
|
|
787
787
|
? (effect.pushDivergence ?? null)
|
|
788
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
|
+
}
|
|
789
802
|
const action = createHumanAction({
|
|
790
803
|
issueId: issue.id,
|
|
791
804
|
workflowInstanceId: instance.id,
|
|
@@ -796,7 +809,9 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
796
809
|
? { review: reviewerOutcome.result, ...(nonConvergence ? { nonConvergence } : {}) }
|
|
797
810
|
: pushDivergence
|
|
798
811
|
? { [PUSH_DIVERGENCE_EVIDENCE_KEY]: pushDivergence }
|
|
799
|
-
:
|
|
812
|
+
: blockerFingerprint
|
|
813
|
+
? { [WORKTREE_BLOCKER_EVIDENCE_KEY]: { fingerprint: blockerFingerprint } }
|
|
814
|
+
: undefined,
|
|
800
815
|
// issueNow.headSha, not issue.headSha: a stale outcome that itself exhausted the
|
|
801
816
|
// infra budget already patched the newly observed head onto the issue above — the
|
|
802
817
|
// pre-transition issue param would still carry the stale SHA a "resume" must not reuse.
|
|
@@ -819,6 +834,51 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
|
|
|
819
834
|
}
|
|
820
835
|
return base;
|
|
821
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
|
+
}
|
|
822
882
|
/**
|
|
823
883
|
* NOT-194: true when the action is a merge-failure escalation (evidence carries
|
|
824
884
|
* `mergeFailure: true`). Pre-NOT-194 open merge-failure actions have no such evidence and
|
|
@@ -1292,6 +1352,7 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1292
1352
|
// Keyed on the resolved human action, not the round/attempt counters: a
|
|
1293
1353
|
// "infra" resume resets infra_attempts to 0 every time, so a counter-based key would
|
|
1294
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}`);
|
|
1295
1356
|
const next = resumeAsReviewer
|
|
1296
1357
|
? enqueueWorkItem({
|
|
1297
1358
|
issueId: issue.id,
|
|
@@ -1299,7 +1360,7 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1299
1360
|
kind: "reviewer",
|
|
1300
1361
|
round: issueNow.currentRound,
|
|
1301
1362
|
payload: { inputSha: effectiveResumeHeadSha, profileSnapshot: queuedProfileSnapshot(issue, "reviewer") },
|
|
1302
|
-
idempotencyKey:
|
|
1363
|
+
idempotencyKey: resumeKey,
|
|
1303
1364
|
})
|
|
1304
1365
|
: enqueueWorkItem({
|
|
1305
1366
|
issueId: issue.id,
|
|
@@ -1312,7 +1373,7 @@ export function resolveHumanActionAndAdvance(actionId, resolvedBy, choice, opts)
|
|
|
1312
1373
|
// payload — later rounds never see it.
|
|
1313
1374
|
...(scopeDecisionNote ? { scopeDecisionNote } : {}),
|
|
1314
1375
|
},
|
|
1315
|
-
idempotencyKey:
|
|
1376
|
+
idempotencyKey: resumeKey,
|
|
1316
1377
|
});
|
|
1317
1378
|
return {
|
|
1318
1379
|
ok: true,
|
|
@@ -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
|
});
|
|
@@ -818,8 +893,8 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
818
893
|
priorConclusion,
|
|
819
894
|
priorVerificationReceipt,
|
|
820
895
|
worktreePath,
|
|
821
|
-
deckId:
|
|
822
|
-
|
|
896
|
+
deckId: snapshot?.deckId ?? null,
|
|
897
|
+
museDeveloper: isMuse,
|
|
823
898
|
guidance: guidance.length ? guidance : undefined,
|
|
824
899
|
scopeDecisionNote,
|
|
825
900
|
});
|
|
@@ -920,6 +995,9 @@ export async function runDeveloperEffect(ctx, deps = defaultDeps) {
|
|
|
920
995
|
effort: snapshot?.effort ?? null,
|
|
921
996
|
// Frozen profile budget → Muse's `--max-model-steps`; the other runtimes ignore it.
|
|
922
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,
|
|
923
1001
|
prompt,
|
|
924
1002
|
cwd: worktreePath,
|
|
925
1003
|
timeoutMs: developerEffectConfig.sessionTimeoutMs,
|