agent-dealer 1.2.7 → 1.2.8
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 +16 -6
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +74 -0
- package/bundle/server/dist/adapters/agent-health.js +14 -3
- package/bundle/server/dist/adapters/github.js +4 -1
- package/bundle/server/dist/adapters/muse-capability.js +443 -24
- package/bundle/server/dist/adapters/muse-capability.test.js +469 -25
- package/bundle/server/dist/adapters/muse-visual-qa.js +114 -0
- package/bundle/server/dist/adapters/muse-visual-qa.test.js +68 -0
- package/bundle/server/dist/capacity/muse-probe.js +56 -9
- package/bundle/server/dist/capacity/muse-probe.test.js +188 -1
- package/bundle/server/dist/coordinator/admission.js +14 -1
- package/bundle/server/dist/coordinator/admission.test.js +197 -4
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +276 -0
- package/bundle/server/dist/coordinator/auto-merge.js +39 -3
- package/bundle/server/dist/coordinator/commands.js +90 -5
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +4 -2
- package/bundle/server/dist/coordinator/developer-effect.js +79 -1
- package/bundle/server/dist/coordinator/execution-report.js +6 -0
- package/bundle/server/dist/coordinator/execution-report.test.js +7 -0
- package/bundle/server/dist/coordinator/failure-cause.js +64 -1
- package/bundle/server/dist/coordinator/failure-cause.test.js +92 -1
- package/bundle/server/dist/coordinator/failure-reason.js +7 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +14 -0
- package/bundle/server/dist/coordinator/human-resolution.js +17 -1
- package/bundle/server/dist/coordinator/merge-conflict-sync.js +647 -0
- package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +121 -0
- package/bundle/server/dist/coordinator/muse-developer.integration.test.js +9 -4
- package/bundle/server/dist/coordinator/muse-spawn.js +122 -29
- package/bundle/server/dist/coordinator/muse-spawn.test.js +210 -0
- package/bundle/server/dist/coordinator/playbook-feedback.js +690 -0
- package/bundle/server/dist/coordinator/playbook-feedback.test.js +702 -0
- package/bundle/server/dist/coordinator/prompts-execution-contract.test.js +107 -0
- package/bundle/server/dist/coordinator/prompts.js +78 -2
- package/bundle/server/dist/coordinator/prompts.test.js +83 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +33 -169
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +149 -200
- package/bundle/server/dist/coordinator/reviewer-effect.js +10 -1
- package/bundle/server/dist/coordinator/reviewer-result.js +6 -0
- package/bundle/server/dist/coordinator/routing.test.js +14 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +30 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +1 -1
- package/bundle/server/dist/coordinator/worker-loop.js +18 -4
- package/bundle/server/dist/db/index.js +5 -0
- package/bundle/server/dist/db/schema.sql +3 -0
- package/bundle/server/dist/docs-execution-analysis.test.js +1 -0
- package/bundle/server/dist/repository/artifacts-for-issue.js +3 -3
- package/bundle/server/dist/repository/human-actions.js +14 -0
- package/bundle/server/dist/repository/issues.js +29 -4
- package/bundle/server/dist/repository/worker-sessions.js +51 -2
- package/bundle/server/dist/routes/human-actions.js +10 -8
- package/bundle/server/dist/routes/issues-execution-contract.test.js +321 -0
- package/bundle/server/dist/routes/issues.js +19 -2
- package/bundle/server/dist/runners/muse-code-jsonl.js +106 -11
- package/bundle/server/dist/runners/muse-config-core.js +18 -1
- package/bundle/server/dist/runners/muse-config.test.js +37 -0
- package/bundle/server/dist/runners/muse-serve-session.js +5 -0
- package/bundle/server/dist/runners/spawn-cli.js +116 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +124 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/{index-yLyxRd-7.js → index-B6SVCzMR.js} +14 -14
- package/bundle/server/static-ui/assets/{index-DyAJNyfV.css → index-K_YcYkQU.css} +1 -1
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/execution-analysis.d.ts +22 -22
- package/bundle/shared/dist/execution-contract.d.ts +117 -0
- package/bundle/shared/dist/execution-contract.js +307 -0
- package/bundle/shared/dist/execution-contract.test.d.ts +2 -0
- package/bundle/shared/dist/execution-contract.test.js +499 -0
- package/bundle/shared/dist/execution-report.d.ts +9 -9
- package/bundle/shared/dist/execution-report.js +2 -0
- package/bundle/shared/dist/failure-cause.d.ts +4 -4
- package/bundle/shared/dist/failure-cause.js +2 -0
- package/bundle/shared/dist/human-actions.d.ts +4 -4
- package/bundle/shared/dist/human-actions.js +9 -0
- package/bundle/shared/dist/index.d.ts +85 -84
- package/bundle/shared/dist/index.js +3 -0
- package/bundle/shared/dist/issues.d.ts +146 -16
- package/bundle/shared/dist/issues.js +8 -0
- package/bundle/shared/dist/issues.test.js +1 -0
- package/bundle/shared/dist/outbound-draft.d.ts +12 -12
- package/bundle/shared/dist/worker-sessions.d.ts +10 -0
- package/bundle/shared/dist/worker-sessions.js +8 -0
- package/bundle/shared/dist/worker-sessions.test.js +1 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +10 -0
- package/bundle/shared/dist/workflow.test.js +2 -0
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
// packages/server/src/coordinator/reflect-trigger.test.ts
|
|
2
2
|
//
|
|
3
|
-
// NOT-
|
|
4
|
-
//
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
7
|
-
// NOT-106: Deck calls go through launch-fixed deck headers via `deps.callTool`
|
|
8
|
-
// (adapters/reflect-authority.ts) — no mint.
|
|
3
|
+
// NOT-305: triggerIssueReflect records completion evidence — actual-use receipts for
|
|
4
|
+
// every terminal worker session plus one idempotent `signal_only` Deck report per
|
|
5
|
+
// failure/correction trigger. It never proposes a playbook patch (`kind: update`) and
|
|
6
|
+
// never appends generic `Notes` items.
|
|
9
7
|
import { test, before } from "node:test";
|
|
10
8
|
import assert from "node:assert/strict";
|
|
11
9
|
import fs from "node:fs";
|
|
12
10
|
import os from "node:os";
|
|
13
11
|
import path from "node:path";
|
|
12
|
+
import { DECK_CORRELATION_TOOL, validateSignalOnlyArgs } from "./playbook-feedback.js";
|
|
14
13
|
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-reflect-"));
|
|
15
|
-
const { migrate
|
|
14
|
+
const { migrate } = await import("../db/index.js");
|
|
16
15
|
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
17
16
|
const { createIssue } = await import("../repository/issues.js");
|
|
18
17
|
const { createAgent } = await import("../repository/agents.js");
|
|
19
|
-
const { createWorkerSession } = await import("../repository/worker-sessions.js");
|
|
18
|
+
const { createWorkerSession, completeSession } = await import("../repository/worker-sessions.js");
|
|
20
19
|
const { buildProfileSnapshot, serializeProfileSnapshot } = await import("./profile-snapshot.js");
|
|
21
|
-
const {
|
|
22
|
-
const { createIssueArtifact } = await import("../repository/artifacts.js");
|
|
20
|
+
const { createHumanAction, resolveHumanAction } = await import("../repository/human-actions.js");
|
|
23
21
|
const { listArtifactsForIssue, listArtifactsForIssueByKind } = await import("../repository/artifacts-for-issue.js");
|
|
24
|
-
const { listHumanActionsForIssue
|
|
22
|
+
const { listHumanActionsForIssue } = await import("../repository/human-actions.js");
|
|
25
23
|
const { triggerIssueReflect, resolveReflectionInteractionAction } = await import("./reflect-trigger.js");
|
|
26
24
|
before(() => {
|
|
27
25
|
migrate();
|
|
28
26
|
});
|
|
27
|
+
const DECK = "11111111-1111-4111-a111-111111111111";
|
|
29
28
|
function seedIssue(developerAgentId) {
|
|
30
29
|
return createIssue({
|
|
31
30
|
title: "Reflect issue",
|
|
@@ -38,14 +37,11 @@ function seedIssue(developerAgentId) {
|
|
|
38
37
|
source: "manual",
|
|
39
38
|
});
|
|
40
39
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
const snapshot = { ...buildProfileSnapshot(agent, "developer"), deckId: overrides.deckId };
|
|
48
|
-
return createWorkerSession({
|
|
40
|
+
/** Terminal developer session on a NOT-149 profile: snapshot names the deck, no playbook IDs anywhere. */
|
|
41
|
+
function seedTerminalDeveloperSession(issueId, agent) {
|
|
42
|
+
assert.equal(agent.playbookIdsJson, null);
|
|
43
|
+
const snapshot = { ...buildProfileSnapshot(agent, "developer"), deckId: DECK };
|
|
44
|
+
const session = createWorkerSession({
|
|
49
45
|
issueId,
|
|
50
46
|
role: "developer",
|
|
51
47
|
round: 1,
|
|
@@ -53,185 +49,125 @@ function seedFinalDeveloperSession(issueId, agent, overrides) {
|
|
|
53
49
|
runtime: agent.runtime,
|
|
54
50
|
profileSnapshotJson: serializeProfileSnapshot(snapshot),
|
|
55
51
|
});
|
|
52
|
+
completeSession(session.id, { status: "done" });
|
|
53
|
+
return session;
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw new Error("waitFor: timed out waiting for predicate");
|
|
62
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/** Default deps: healthy deck, get_playbook/propose_playbook_patch both succeed. */
|
|
66
|
-
function makeDeps(overrides) {
|
|
67
|
-
return {
|
|
55
|
+
/** Default deps: healthy deck, correlation succeeds, signal_only succeeds. */
|
|
56
|
+
function makeDeps(overrides = {}) {
|
|
57
|
+
const calls = [];
|
|
58
|
+
const deps = {
|
|
68
59
|
checkHealth: overrides.checkHealth ?? (async () => true),
|
|
69
60
|
callTool: (async (opts) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return overrides.
|
|
73
|
-
? overrides.getPlaybook(playbookId)
|
|
74
|
-
: { ok: true, data: { id: playbookId, title: `Playbook ${playbookId}`, body: "" } };
|
|
61
|
+
calls.push({ toolName: opts.toolName, args: opts.arguments });
|
|
62
|
+
if (opts.toolName === DECK_CORRELATION_TOOL) {
|
|
63
|
+
return { ok: true, data: overrides.correlationData ?? { fetches: overrides.fetches ?? [] } };
|
|
75
64
|
}
|
|
76
65
|
if (opts.toolName === "propose_playbook_patch") {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
66
|
+
if (opts.arguments.kind !== "signal_only") {
|
|
67
|
+
throw new Error(`issue-centric reflection must only send signal_only, got ${opts.arguments.kind}`);
|
|
68
|
+
}
|
|
69
|
+
const schemaError = validateSignalOnlyArgs(opts.arguments);
|
|
70
|
+
if (schemaError) {
|
|
71
|
+
throw new Error(`signal payload violates Deck schema: ${schemaError}`);
|
|
72
|
+
}
|
|
73
|
+
return overrides.propose
|
|
74
|
+
? overrides.propose(opts.arguments)
|
|
75
|
+
: { ok: true, data: { id: `sig-${calls.length}` } };
|
|
82
76
|
}
|
|
83
77
|
throw new Error(`unexpected tool call: ${opts.toolName}`);
|
|
84
78
|
}),
|
|
85
79
|
};
|
|
80
|
+
return { deps, calls };
|
|
81
|
+
}
|
|
82
|
+
function sentSignals(issueId) {
|
|
83
|
+
return listArtifactsForIssueByKind(issueId, "deck_feedback_signal")
|
|
84
|
+
.map((a) => JSON.parse(a.contentJson))
|
|
85
|
+
.filter((s) => s.status === "sent");
|
|
86
|
+
}
|
|
87
|
+
async function waitFor(predicate, timeoutMs = 2000, intervalMs = 5) {
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
while (!predicate()) {
|
|
90
|
+
if (Date.now() - start > timeoutMs)
|
|
91
|
+
throw new Error("waitFor: timed out waiting for predicate");
|
|
92
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
|
-
test("skips when the
|
|
95
|
+
test("skips when the issue has no terminal sessions and no failure triggers", async () => {
|
|
88
96
|
const issue = seedIssue(BUILTIN_AGENT_CLAUDE_ID);
|
|
89
|
-
const
|
|
97
|
+
const { deps, calls } = makeDeps();
|
|
98
|
+
const result = await triggerIssueReflect(issue.id, deps);
|
|
90
99
|
assert.equal(result, "skipped");
|
|
100
|
+
assert.deepStrictEqual(calls, []);
|
|
91
101
|
});
|
|
92
|
-
test("
|
|
93
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId:
|
|
102
|
+
test("a clean completed run records the actual-use receipt and sends no signal", async () => {
|
|
103
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
94
104
|
const issue = seedIssue(dev.id);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
playbookIds: ["pb-1", "pb-2"],
|
|
98
|
-
});
|
|
99
|
-
const calledDeckIds = [];
|
|
100
|
-
const proposed = [];
|
|
101
|
-
const deps = makeDeps({
|
|
102
|
-
proposePatch: (playbookId) => {
|
|
103
|
-
proposed.push(playbookId);
|
|
104
|
-
return { ok: true, data: { id: `patch-${playbookId}`, playbookId } };
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
const wrapped = {
|
|
108
|
-
...deps,
|
|
109
|
-
callTool: (async (opts) => {
|
|
110
|
-
calledDeckIds.push(opts.deckId);
|
|
111
|
-
return deps.callTool(opts);
|
|
112
|
-
}),
|
|
113
|
-
};
|
|
114
|
-
const result = await triggerIssueReflect(issue.id, wrapped);
|
|
115
|
-
assert.equal(result, "triggered");
|
|
116
|
-
assert.ok(calledDeckIds.every((id) => id === "11111111-1111-4111-a111-111111111111"));
|
|
117
|
-
assert.deepStrictEqual(proposed.sort(), ["pb-1", "pb-2"]);
|
|
118
|
-
});
|
|
119
|
-
test("falls back to the live agent profile when the developer session has no frozen snapshot (legacy row)", async () => {
|
|
120
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: "33333333-3333-4333-a333-333333333333" });
|
|
121
|
-
getDb()
|
|
122
|
-
.prepare("UPDATE agents SET playbook_ids_json = ? WHERE id = ?")
|
|
123
|
-
.run(JSON.stringify(["pb-live"]), dev.id);
|
|
124
|
-
const issue = seedIssue(dev.id);
|
|
125
|
-
createWorkerSession({ issueId: issue.id, role: "developer", round: 1, agentId: dev.id, runtime: "claude_code" });
|
|
126
|
-
const proposed = [];
|
|
127
|
-
const deps = makeDeps({
|
|
128
|
-
proposePatch: (playbookId) => {
|
|
129
|
-
proposed.push(playbookId);
|
|
130
|
-
return { ok: true, data: { id: "patch", playbookId } };
|
|
131
|
-
},
|
|
132
|
-
});
|
|
105
|
+
const session = seedTerminalDeveloperSession(issue.id, dev);
|
|
106
|
+
const { deps, calls } = makeDeps({ fetches: [{ playbook_id: "pb-1" }, { playbook_id: "pb-2" }] });
|
|
133
107
|
const result = await triggerIssueReflect(issue.id, deps);
|
|
134
108
|
assert.equal(result, "triggered");
|
|
135
|
-
|
|
109
|
+
const receipts = listArtifactsForIssueByKind(issue.id, "playbook_use_receipt").map((a) => JSON.parse(a.contentJson));
|
|
110
|
+
assert.equal(receipts.length, 1);
|
|
111
|
+
assert.deepStrictEqual(receipts[0].playbookIds, ["pb-1", "pb-2"]);
|
|
112
|
+
assert.equal(receipts[0].workerSessionId, session.id);
|
|
113
|
+
assert.deepStrictEqual(calls.filter((c) => c.toolName === "propose_playbook_patch"), []);
|
|
114
|
+
assert.deepStrictEqual(listArtifactsForIssueByKind(issue.id, "deck_feedback_signal"), []);
|
|
115
|
+
assert.deepStrictEqual(sentSignals(issue.id), []);
|
|
136
116
|
});
|
|
137
|
-
test("
|
|
138
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId:
|
|
139
|
-
getDb().prepare("UPDATE agents SET playbook_id = ? WHERE id = ?").run("pb-legacy", dev.id);
|
|
117
|
+
test("a human correction produces one signal_only report; a restart sends nothing more", async () => {
|
|
118
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
140
119
|
const issue = seedIssue(dev.id);
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
},
|
|
120
|
+
seedTerminalDeveloperSession(issue.id, dev);
|
|
121
|
+
const action = createHumanAction({
|
|
122
|
+
issueId: issue.id,
|
|
123
|
+
actionType: "final_review",
|
|
124
|
+
reason: "Needs a repair round.",
|
|
125
|
+
question: "Repair?",
|
|
148
126
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
assert.
|
|
127
|
+
resolveHumanAction(action.id, "operator", { choice: "repair" });
|
|
128
|
+
const { deps, calls } = makeDeps({ fetches: [{ playbook_id: "pb-1" }] });
|
|
129
|
+
assert.equal(await triggerIssueReflect(issue.id, deps), "triggered");
|
|
130
|
+
const proposes = () => calls.filter((c) => c.toolName === "propose_playbook_patch");
|
|
131
|
+
assert.equal(proposes().length, 1);
|
|
132
|
+
assert.equal(proposes()[0].args.kind, "signal_only");
|
|
133
|
+
// Simulated restart: idempotent, no duplicate Deck report.
|
|
134
|
+
assert.equal(await triggerIssueReflect(issue.id, deps), "skipped");
|
|
135
|
+
assert.equal(proposes().length, 1);
|
|
136
|
+
assert.equal(sentSignals(issue.id).length, 1);
|
|
152
137
|
});
|
|
153
|
-
test("
|
|
154
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId:
|
|
138
|
+
test("records a visible failure and preserves state when the deck is offline", async () => {
|
|
139
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
155
140
|
const issue = seedIssue(dev.id);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
playbookIds: ["pb-1"],
|
|
159
|
-
});
|
|
160
|
-
const deps = {
|
|
161
|
-
checkHealth: async () => false,
|
|
162
|
-
callTool: (async () => {
|
|
163
|
-
throw new Error("should not be called");
|
|
164
|
-
}),
|
|
165
|
-
};
|
|
141
|
+
seedTerminalDeveloperSession(issue.id, dev);
|
|
142
|
+
const { deps, calls } = makeDeps({ checkHealth: async () => false });
|
|
166
143
|
const result = await triggerIssueReflect(issue.id, deps);
|
|
167
|
-
assert.equal(result, "
|
|
144
|
+
assert.equal(result, "failed");
|
|
145
|
+
assert.deepStrictEqual(calls, []);
|
|
168
146
|
const artifacts = listArtifactsForIssue(issue.id);
|
|
169
|
-
assert.ok(artifacts.some((a) => a.kind === "reflect_status" && JSON.parse(a.contentJson).
|
|
147
|
+
assert.ok(artifacts.some((a) => a.kind === "reflect_status" || (a.kind === "playbook_use_receipt" && JSON.parse(a.contentJson).status === "error")));
|
|
170
148
|
});
|
|
171
|
-
test("
|
|
172
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId:
|
|
149
|
+
test("a retry after a failed signal send re-sends exactly once, never duplicating", async () => {
|
|
150
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
173
151
|
const issue = seedIssue(dev.id);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
playbookIds: ["pb-1", "pb-2"],
|
|
177
|
-
});
|
|
178
|
-
createIssueArtifact({
|
|
179
|
-
issueId: issue.id,
|
|
180
|
-
kind: "implementation_conclusion",
|
|
181
|
-
author: "agent",
|
|
182
|
-
content: { text: "Implemented the widget using the shared component." },
|
|
183
|
-
});
|
|
184
|
-
appendWorkflowEvent({
|
|
152
|
+
seedTerminalDeveloperSession(issue.id, dev);
|
|
153
|
+
const action = createHumanAction({
|
|
185
154
|
issueId: issue.id,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
payload: { verdict: "changes_requested" },
|
|
190
|
-
});
|
|
191
|
-
appendWorkflowEvent({
|
|
192
|
-
issueId: issue.id,
|
|
193
|
-
type: "review.submitted",
|
|
194
|
-
actorType: "reviewer",
|
|
195
|
-
stage: "reviewing",
|
|
196
|
-
payload: { verdict: "approved" },
|
|
197
|
-
});
|
|
198
|
-
const proposed = [];
|
|
199
|
-
const deps = makeDeps({
|
|
200
|
-
proposePatch: (playbookId, rationale) => {
|
|
201
|
-
proposed.push(playbookId);
|
|
202
|
-
assert.match(rationale, /2 review round\(s\)/);
|
|
203
|
-
assert.match(rationale, /changes_requested → approved/);
|
|
204
|
-
assert.match(rationale, /Implemented the widget using the shared component\./);
|
|
205
|
-
return { ok: true, data: { id: `patch-${playbookId}`, playbookId } };
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
const result = await triggerIssueReflect(issue.id, deps);
|
|
209
|
-
assert.equal(result, "triggered");
|
|
210
|
-
assert.deepStrictEqual(proposed.sort(), ["pb-1", "pb-2"]);
|
|
211
|
-
const artifacts = listArtifactsForIssue(issue.id);
|
|
212
|
-
assert.equal(artifacts.filter((a) => a.kind === "playbook_patch").length, 2);
|
|
213
|
-
});
|
|
214
|
-
test("a retry after a mid-loop failure does not re-propose a playbook that already succeeded", async () => {
|
|
215
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: "00000000-0000-4000-a000-000000000099" });
|
|
216
|
-
const issue = seedIssue(dev.id);
|
|
217
|
-
seedFinalDeveloperSession(issue.id, dev, {
|
|
218
|
-
deckId: "11111111-1111-4111-a111-111111111111",
|
|
219
|
-
playbookIds: ["pb-ok", "pb-fail"],
|
|
155
|
+
actionType: "final_review",
|
|
156
|
+
reason: "Needs a repair round.",
|
|
157
|
+
question: "Repair?",
|
|
220
158
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const deps = makeDeps({
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return { ok: false, kind: "infra_failure", reason: "temporary deck error" };
|
|
228
|
-
}
|
|
229
|
-
return { ok: true, data: { id: `patch-${playbookId}`, playbookId } };
|
|
230
|
-
},
|
|
159
|
+
resolveHumanAction(action.id, "operator", { choice: "repair" });
|
|
160
|
+
let failPropose = true;
|
|
161
|
+
const { deps, calls } = makeDeps({
|
|
162
|
+
propose: () => failPropose
|
|
163
|
+
? { ok: false, kind: "infra_failure", reason: "temporary deck error" }
|
|
164
|
+
: { ok: true, data: { id: "sig-1" } },
|
|
231
165
|
});
|
|
166
|
+
const proposes = () => calls.filter((c) => c.toolName === "propose_playbook_patch");
|
|
232
167
|
const first = await triggerIssueReflect(issue.id, deps);
|
|
233
|
-
assert.equal(first, "triggered");
|
|
234
|
-
assert.
|
|
168
|
+
assert.equal(first, "triggered"); // the receipt still recorded
|
|
169
|
+
assert.equal(proposes().length, 1);
|
|
170
|
+
assert.equal(sentSignals(issue.id).length, 0);
|
|
235
171
|
createHumanAction({
|
|
236
172
|
issueId: issue.id,
|
|
237
173
|
actionType: "reflection_interaction_required",
|
|
@@ -243,11 +179,12 @@ test("a retry after a mid-loop failure does not re-propose a playbook that alrea
|
|
|
243
179
|
],
|
|
244
180
|
});
|
|
245
181
|
const parkedAction = listHumanActionsForIssue(issue.id).find((a) => a.actionType === "reflection_interaction_required" && a.status === "open");
|
|
246
|
-
|
|
182
|
+
failPropose = false;
|
|
247
183
|
const resolved = resolveReflectionInteractionAction(parkedAction.id, "operator", "retry", deps);
|
|
248
184
|
assert.equal(resolved.ok, true);
|
|
249
|
-
await waitFor(() =>
|
|
250
|
-
assert.
|
|
185
|
+
await waitFor(() => sentSignals(issue.id).length >= 1);
|
|
186
|
+
assert.equal(proposes().length, 2);
|
|
187
|
+
assert.equal(sentSignals(issue.id).length, 1);
|
|
251
188
|
});
|
|
252
189
|
test("resolveReflectionInteractionAction:dismiss closes the action without further Deck calls", async () => {
|
|
253
190
|
const issue = seedIssue(BUILTIN_AGENT_CLAUDE_ID);
|
|
@@ -289,36 +226,48 @@ test("resolveReflectionInteractionAction rejects an invalid choice and already-r
|
|
|
289
226
|
const alreadyResolved = resolveReflectionInteractionAction(parkedAction.id, "operator", "retry");
|
|
290
227
|
assert.equal(alreadyResolved.ok, false);
|
|
291
228
|
});
|
|
292
|
-
test("
|
|
293
|
-
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId:
|
|
229
|
+
test("reflect_status names a malformed correlation response instead of blaming an offline Deck", async () => {
|
|
230
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
294
231
|
const issue = seedIssue(dev.id);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
232
|
+
seedTerminalDeveloperSession(issue.id, dev);
|
|
233
|
+
const { deps } = makeDeps({ correlationData: { unexpected: "shape" } });
|
|
234
|
+
const result = await triggerIssueReflect(issue.id, deps);
|
|
235
|
+
assert.equal(result, "failed");
|
|
236
|
+
const statuses = listArtifactsForIssueByKind(issue.id, "reflect_status").map((a) => JSON.parse(a.contentJson));
|
|
237
|
+
assert.ok(statuses.length > 0);
|
|
238
|
+
const text = statuses.map((s) => `${s.reason ?? ""} ${s.error ?? ""}`).join("\n");
|
|
239
|
+
assert.match(text, /malformed/i);
|
|
240
|
+
assert.doesNotMatch(text, /offline/i);
|
|
241
|
+
});
|
|
242
|
+
test("reflect_status still says offline when the receipt error really is a Deck outage", async () => {
|
|
243
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
244
|
+
const issue = seedIssue(dev.id);
|
|
245
|
+
seedTerminalDeveloperSession(issue.id, dev);
|
|
246
|
+
const { deps } = makeDeps({ checkHealth: async () => false });
|
|
247
|
+
assert.equal(await triggerIssueReflect(issue.id, deps), "failed");
|
|
248
|
+
const statuses = listArtifactsForIssueByKind(issue.id, "reflect_status").map((a) => JSON.parse(a.contentJson));
|
|
249
|
+
assert.ok(statuses.some((s) => /offline/i.test(`${s.reason ?? ""} ${s.error ?? ""}`)));
|
|
250
|
+
});
|
|
251
|
+
test("never calls propose_playbook_patch with kind:update and never writes Notes items", async () => {
|
|
252
|
+
const dev = createAgent({ name: `dev-${Math.random()}`, runtime: "claude_code", deckId: DECK });
|
|
253
|
+
const issue = seedIssue(dev.id);
|
|
254
|
+
seedTerminalDeveloperSession(issue.id, dev);
|
|
255
|
+
const action = createHumanAction({
|
|
300
256
|
issueId: issue.id,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
257
|
+
actionType: "attempts_exhausted",
|
|
258
|
+
reason: "Rounds spent.",
|
|
259
|
+
question: "Close?",
|
|
304
260
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
261
|
+
resolveHumanAction(action.id, "operator", { choice: "close" });
|
|
262
|
+
const { deps, calls } = makeDeps({ fetches: [{ playbook_id: "pb-9" }] });
|
|
263
|
+
await triggerIssueReflect(issue.id, deps);
|
|
264
|
+
for (const call of calls) {
|
|
265
|
+
if (call.toolName !== "propose_playbook_patch")
|
|
266
|
+
continue;
|
|
267
|
+
assert.notEqual(call.args.kind, "update");
|
|
268
|
+
assert.equal(call.args.kind, "signal_only");
|
|
269
|
+
const ops = call.args.ops;
|
|
270
|
+
assert.ok(!ops || !ops.some((op) => op.section === "Notes"));
|
|
312
271
|
}
|
|
313
|
-
|
|
314
|
-
const deps = makeDeps({
|
|
315
|
-
proposePatch: (playbookId) => {
|
|
316
|
-
proposed.push(playbookId);
|
|
317
|
-
return { ok: true, data: { id: `patch-${playbookId}`, playbookId } };
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
const result = await triggerIssueReflect(issue.id, deps);
|
|
321
|
-
assert.equal(result, "triggered");
|
|
322
|
-
assert.deepStrictEqual(proposed, ["pb-next"]);
|
|
323
|
-
assert.equal(listArtifactsForIssueByKind(issue.id, "playbook_patch").length, 2);
|
|
272
|
+
assert.equal(listArtifactsForIssueByKind(issue.id, "playbook_patch").length, 0);
|
|
324
273
|
});
|
|
@@ -34,7 +34,7 @@ import { createRoleWorktree, safeRemoveWorktree, isWorktreeClean, mergeBase, fet
|
|
|
34
34
|
import { ensureIssueRepoCheckout, roleWorktreePathForResolution, resolveCheckoutBaseBranch, } from "../adapters/managed-repo.js";
|
|
35
35
|
import { prepareWorkerDeckConnection, releaseWorkerDeckConnection } from "../adapters/agent-deck-bind.js";
|
|
36
36
|
import { realGithubAdapter } from "../adapters/github.js";
|
|
37
|
-
import { getWorkerSession, patchRunningSession, recordSessionProcess } from "../repository/worker-sessions.js";
|
|
37
|
+
import { getOrAssignSessionCorrelationId, getWorkerSession, patchRunningSession, recordSessionProcess } from "../repository/worker-sessions.js";
|
|
38
38
|
import { COORDINATOR_PROCESS_OWNER, readProcessStartTime } from "./process-liveness.js";
|
|
39
39
|
import { reviewerSessionTimeoutMs } from "./session-timeouts.js";
|
|
40
40
|
import { getWorkItem } from "../repository/work-items.js";
|
|
@@ -196,6 +196,11 @@ function renderReviewBody(result, marker) {
|
|
|
196
196
|
`**Evidence assessment**`,
|
|
197
197
|
result.evidenceAssessment,
|
|
198
198
|
];
|
|
199
|
+
// NOT-306: the exit-predicate verdict rides with the review when the task
|
|
200
|
+
// carried an execution contract; legacy reviews simply omit the section.
|
|
201
|
+
if (result.exitPredicateAssessment?.trim()) {
|
|
202
|
+
lines.push(``, `**Exit predicate assessment**`, result.exitPredicateAssessment);
|
|
203
|
+
}
|
|
199
204
|
if (result.findings.length) {
|
|
200
205
|
lines.push(``, `**Findings**`);
|
|
201
206
|
for (const f of result.findings) {
|
|
@@ -222,6 +227,9 @@ export async function runReviewerEffect(ctx, deps = defaultDeps) {
|
|
|
222
227
|
return { kind: "session_failed" };
|
|
223
228
|
const session = getWorkerSession(sessionId);
|
|
224
229
|
const snapshot = parseProfileSnapshot(session?.profileSnapshotJson);
|
|
230
|
+
// NOT-305: opaque per-session Deck correlation UUID, persisted before spawn and
|
|
231
|
+
// carried as observability metadata in the materialized launch config below.
|
|
232
|
+
const deckCorrelationId = session?.deckCorrelationId ?? getOrAssignSessionCorrelationId(sessionId);
|
|
225
233
|
const taskSnapshot = getTaskSnapshot(issue);
|
|
226
234
|
const runtime = snapshot?.runtime ?? "claude_code";
|
|
227
235
|
const round = workItem.round;
|
|
@@ -291,6 +299,7 @@ export async function runReviewerEffect(ctx, deps = defaultDeps) {
|
|
|
291
299
|
worktreePath,
|
|
292
300
|
runtime,
|
|
293
301
|
policy,
|
|
302
|
+
correlationId: deckCorrelationId,
|
|
294
303
|
verifyCallTool: deps.deckCallTool,
|
|
295
304
|
});
|
|
296
305
|
if (!prepared.ok) {
|
|
@@ -14,6 +14,12 @@ export const ReviewerResult = z.object({
|
|
|
14
14
|
headSha: z.string(),
|
|
15
15
|
acceptanceCriteriaAssessment: z.string(),
|
|
16
16
|
evidenceAssessment: z.string(),
|
|
17
|
+
/**
|
|
18
|
+
* NOT-306: whether the frozen exit predicate held at the reviewed tip. The
|
|
19
|
+
* reviewer sets this when the task carries an execution contract; legacy
|
|
20
|
+
* contract-free tasks omit it (optional so older outputs still parse).
|
|
21
|
+
*/
|
|
22
|
+
exitPredicateAssessment: z.string().optional(),
|
|
17
23
|
findings: z.array(ReviewerFinding),
|
|
18
24
|
risks: z.array(z.string()),
|
|
19
25
|
/** Present only when verdict is "escalated" and the reviewer identifies a missing product call. */
|
|
@@ -216,6 +216,20 @@ test("timed_out with infra attempts remaining retries (same bucket as session_fa
|
|
|
216
216
|
reason: "Developer session timed out.",
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
+
test("NOT-307: an idle-watchdog timed_out routes exactly like a wall-clock timeout", () => {
|
|
220
|
+
// The idle kill reads as timed_out downstream: same bounded infra retry, same
|
|
221
|
+
// budget accounting — only the reason (and the failure cause) names the stall.
|
|
222
|
+
const outcome = {
|
|
223
|
+
kind: "timed_out",
|
|
224
|
+
reason: "Developer session made no progress for 20 minutes. (last tool: npm_test) Killed on the idle timeout.",
|
|
225
|
+
};
|
|
226
|
+
assert.deepStrictEqual(routeDeveloperOutcome(outcome, INFRA_ATTEMPTS_LEFT), {
|
|
227
|
+
next: "retry_developer",
|
|
228
|
+
reason: "Developer session made no progress for 20 minutes. (last tool: npm_test) Killed on the idle timeout.",
|
|
229
|
+
});
|
|
230
|
+
assert.deepStrictEqual(routeDeveloperOutcome({ kind: "timed_out" }, INFRA_AT_LIMIT).next, "human_action");
|
|
231
|
+
assert.deepStrictEqual(routeDeveloperOutcome(outcome, INFRA_AT_LIMIT).next, "human_action");
|
|
232
|
+
});
|
|
219
233
|
test("checks_failed with infra attempts remaining retries (same bucket as session_failed/no_pr)", () => {
|
|
220
234
|
const outcome = { kind: "checks_failed" };
|
|
221
235
|
assert.deepStrictEqual(routeDeveloperOutcome(outcome, INFRA_ATTEMPTS_LEFT), {
|
|
@@ -5,6 +5,36 @@ export function developerSessionTimeoutMs() {
|
|
|
5
5
|
export function reviewerSessionTimeoutMs() {
|
|
6
6
|
return num("REVIEWER_TIMEOUT_MS", 30 * 60_000);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* NOT-307: silent-child bound for the Muse developer lane only (other runtimes do
|
|
10
|
+
* not read this). Undefined means the watchdog is disabled. The watchdog ships
|
|
11
|
+
* ENABLED with a 30-minute default: 20 min risks false kills against the
|
|
12
|
+
* reported ~39.8 min silent gap in completed-session history; 45 min saves only
|
|
13
|
+
* 15 of the 60-min wall-clock cap; disabled does not address the observed
|
|
14
|
+
* 1-hour stalls. The ~39.8 min figure could not be re-verified — historical raw
|
|
15
|
+
* logs carry replayed `recorded_at`, not arrival times, so it may overstate
|
|
16
|
+
* true stdout/session.jsonl silence. The value is env-tunable, and the new
|
|
17
|
+
* per-event `ts`/`durationMs` plus `metadata_json` stall fields will provide
|
|
18
|
+
* the real gap distribution to recalibrate; a false kill is bounded by timeout
|
|
19
|
+
* salvage + retry from the branch. `0` disables explicitly. A non-numeric or
|
|
20
|
+
* negative value falls back to the default with a logged warning rather than
|
|
21
|
+
* silently disabling the guard (or killing healthy sessions on a typo like
|
|
22
|
+
* `-1`).
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_MUSE_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
25
|
+
export function museIdleTimeoutMs() {
|
|
26
|
+
const raw = process.env.MUSE_IDLE_TIMEOUT_MS;
|
|
27
|
+
if (raw === undefined || raw.trim() === "")
|
|
28
|
+
return DEFAULT_MUSE_IDLE_TIMEOUT_MS;
|
|
29
|
+
const parsed = Number(raw);
|
|
30
|
+
if (Number.isFinite(parsed) && parsed === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
33
|
+
console.warn(`[dealer] ignoring invalid MUSE_IDLE_TIMEOUT_MS=${JSON.stringify(raw)} — using default ${DEFAULT_MUSE_IDLE_TIMEOUT_MS}`);
|
|
34
|
+
return DEFAULT_MUSE_IDLE_TIMEOUT_MS;
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
8
38
|
export function sessionTimeoutMsFor(role) {
|
|
9
39
|
return role === "reviewer" ? reviewerSessionTimeoutMs() : developerSessionTimeoutMs();
|
|
10
40
|
}
|
|
@@ -32,8 +32,8 @@ beforeEach(() => {
|
|
|
32
32
|
DELETE FROM human_actions;
|
|
33
33
|
DELETE FROM workflow_events;
|
|
34
34
|
DELETE FROM findings;
|
|
35
|
-
DELETE FROM worker_sessions;
|
|
36
35
|
DELETE FROM artifacts;
|
|
36
|
+
DELETE FROM worker_sessions;
|
|
37
37
|
DELETE FROM usage_events;
|
|
38
38
|
DELETE FROM workflow_instances;
|
|
39
39
|
DELETE FROM issues;
|
|
@@ -83,6 +83,15 @@ function safeCompleteSession(sessionId, status, error) {
|
|
|
83
83
|
catch (err) {
|
|
84
84
|
console.error("[coordinator] completeSession", sessionId, err);
|
|
85
85
|
}
|
|
86
|
+
// NOT-305: best-effort actual-use receipt the moment the session goes terminal — a
|
|
87
|
+
// later issue-completion trigger re-attempts anything still missing, so this never
|
|
88
|
+
// throws and never changes the session outcome or the issue state.
|
|
89
|
+
try {
|
|
90
|
+
void import("./playbook-feedback.js").then(({ collectPlaybookUseReceipt }) => collectPlaybookUseReceipt(sessionId).catch(() => { }));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error("[coordinator] playbook receipt", sessionId, err);
|
|
94
|
+
}
|
|
86
95
|
}
|
|
87
96
|
/**
|
|
88
97
|
* A failed effect (handler threw, or lost its lease mid-run). One transaction, fenced on
|
|
@@ -269,10 +278,6 @@ async function processWorkItem(claimed) {
|
|
|
269
278
|
safeCompleteSession(session.id, "cancelled", { reason: result.reason });
|
|
270
279
|
return;
|
|
271
280
|
}
|
|
272
|
-
if (result.triggerReflect) {
|
|
273
|
-
const { triggerIssueReflect } = await import("./reflect-trigger.js");
|
|
274
|
-
void triggerIssueReflect(claimed.issueId).catch(() => { });
|
|
275
|
-
}
|
|
276
281
|
// NOT-136: a deck-unavailable session never spawned anything — `cancelled`, not `failed`
|
|
277
282
|
// (which would read as a worker crash) and not `done` (which would claim it ran).
|
|
278
283
|
// NOT-197: a base-fetch failure likewise never spawned (and created no branch).
|
|
@@ -298,7 +303,16 @@ async function processWorkItem(claimed) {
|
|
|
298
303
|
}),
|
|
299
304
|
}
|
|
300
305
|
: undefined;
|
|
306
|
+
// NOT-305: the session row goes terminal BEFORE the issue-level reflect trigger
|
|
307
|
+
// below. triggerIssueReflect collects receipts for every terminal session, and
|
|
308
|
+
// completeSession writes the terminal status synchronously — firing reflect first
|
|
309
|
+
// would exclude this just-finished session from the signal's worker sessions and
|
|
310
|
+
// playbook evidence.
|
|
301
311
|
safeCompleteSession(session.id, sessionStatus, errorPayload);
|
|
312
|
+
if (result.triggerReflect) {
|
|
313
|
+
const { triggerIssueReflect } = await import("./reflect-trigger.js");
|
|
314
|
+
void triggerIssueReflect(claimed.issueId).catch(() => { });
|
|
315
|
+
}
|
|
302
316
|
}
|
|
303
317
|
/**
|
|
304
318
|
* Claims and starts as many work items as free concurrency slots allow, then returns the
|
|
@@ -131,6 +131,11 @@ export function migrate() {
|
|
|
131
131
|
if (!workerSessionCols.some((c) => c.name === "process_started_at")) {
|
|
132
132
|
db.exec("ALTER TABLE worker_sessions ADD COLUMN process_started_at TEXT");
|
|
133
133
|
}
|
|
134
|
+
// NOT-305: opaque Deck correlation UUID per session. Pre-feature rows stay NULL,
|
|
135
|
+
// which readers treat as "no correlation" (receipt records a skip, never a guess).
|
|
136
|
+
if (!workerSessionCols.some((c) => c.name === "deck_correlation_id")) {
|
|
137
|
+
db.exec("ALTER TABLE worker_sessions ADD COLUMN deck_correlation_id TEXT");
|
|
138
|
+
}
|
|
134
139
|
// NOT-181: the model a session actually ran (Muse's server-confirmed one). Existing rows stay
|
|
135
140
|
// NULL, which every reader treats as "not recorded".
|
|
136
141
|
const usageEventCols = db.prepare("PRAGMA table_info(usage_events)").all();
|
|
@@ -155,6 +155,9 @@ CREATE TABLE IF NOT EXISTS worker_sessions (
|
|
|
155
155
|
process_pid INTEGER,
|
|
156
156
|
process_owner TEXT,
|
|
157
157
|
process_started_at TEXT,
|
|
158
|
+
-- NOT-305: opaque Agent Deck correlation UUID, one per session, generated before
|
|
159
|
+
-- spawn and passed as observability metadata in every runtime's Deck launch config.
|
|
160
|
+
deck_correlation_id TEXT,
|
|
158
161
|
created_at TEXT NOT NULL,
|
|
159
162
|
started_at TEXT,
|
|
160
163
|
heartbeat_at TEXT,
|