agent-dealer 1.2.7 → 1.2.9
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 +84 -5
- package/bundle/server/dist/adapters/github.test.js +83 -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/adapters/worktree-deps.js +96 -0
- package/bundle/server/dist/adapters/worktree-deps.test.js +144 -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 +244 -4
- package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
- package/bundle/server/dist/coordinator/commands.js +284 -23
- package/bundle/server/dist/coordinator/commands.test.js +47 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +4 -2
- package/bundle/server/dist/coordinator/developer-effect.js +224 -4
- package/bundle/server/dist/coordinator/developer-effect.test.js +288 -3
- 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 +36 -1
- package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
- 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/operator-criteria.js +168 -0
- package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
- package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
- package/bundle/server/dist/coordinator/playbook-feedback.js +693 -0
- package/bundle/server/dist/coordinator/playbook-feedback.test.js +702 -0
- package/bundle/server/dist/coordinator/projection.js +10 -4
- package/bundle/server/dist/coordinator/projection.test.js +13 -0
- package/bundle/server/dist/coordinator/prompts-execution-contract.test.js +107 -0
- package/bundle/server/dist/coordinator/prompts.js +211 -2
- package/bundle/server/dist/coordinator/prompts.test.js +231 -0
- package/bundle/server/dist/coordinator/recovery.js +57 -3
- package/bundle/server/dist/coordinator/recovery.test.js +67 -8
- 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/repair-cycle.integration.test.js +1 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +16 -1
- package/bundle/server/dist/coordinator/reviewer-result.js +6 -0
- package/bundle/server/dist/coordinator/routing.js +45 -6
- package/bundle/server/dist/coordinator/routing.test.js +76 -15
- package/bundle/server/dist/coordinator/session-timeouts.js +30 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -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/ci-attempts-migration.test.js +70 -0
- package/bundle/server/dist/db/index.js +18 -0
- package/bundle/server/dist/db/schema.sql +5 -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 +67 -6
- package/bundle/server/dist/repository/work-items.js +7 -0
- 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-args.js +25 -0
- package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
- 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-CU6fNntg.js +64 -0
- 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 +17 -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 +176 -16
- package/bundle/shared/dist/issues.js +22 -0
- package/bundle/shared/dist/issues.test.js +3 -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 +16 -0
- package/bundle/shared/dist/workflow.test.js +3 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.js +4 -1
- package/dist/action.test.js +42 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-yLyxRd-7.js +0 -64
|
@@ -14,19 +14,25 @@ export function projectDeveloperRoute(route, currentStatus, round) {
|
|
|
14
14
|
effect: { kind: "enqueue", workItem: "reviewer", atHeadSha: route.headSha },
|
|
15
15
|
advance: "none",
|
|
16
16
|
};
|
|
17
|
-
case "retry_developer":
|
|
17
|
+
case "retry_developer": {
|
|
18
|
+
// NOT-313: a CI-repair retry spends the CI budget, never the infra one —
|
|
19
|
+
// the route's budget marker (set only by the checks_failed path) decides.
|
|
20
|
+
const isCiRepair = route.budget === "ci";
|
|
18
21
|
return {
|
|
19
22
|
projection: {
|
|
20
23
|
// developing → developing and repairing → repairing are both self-loops;
|
|
21
|
-
//
|
|
24
|
+
// a retried developer failure stays in "repairing".
|
|
22
25
|
issueStatus: currentStatus === "repairing" ? "repairing" : "developing",
|
|
23
26
|
currentOwner: "developer",
|
|
24
|
-
currentIntent:
|
|
27
|
+
currentIntent: isCiRepair
|
|
28
|
+
? `Developer retrying (CI repair attempt) — ${route.reason}`
|
|
29
|
+
: `Developer retrying (infra attempt) — ${route.reason}`,
|
|
25
30
|
events: ["worker.failed"],
|
|
26
31
|
},
|
|
27
32
|
effect: { kind: "enqueue", workItem: "developer", retryReason: route.reason },
|
|
28
|
-
advance: "infra",
|
|
33
|
+
advance: isCiRepair ? "ci" : "infra",
|
|
29
34
|
};
|
|
35
|
+
}
|
|
30
36
|
case "retry_publish":
|
|
31
37
|
return {
|
|
32
38
|
projection: {
|
|
@@ -119,6 +119,19 @@ test("infra-class retries — including a stale re-review — spend the infra bu
|
|
|
119
119
|
const stale = projectReviewerRoute(routeReviewerOutcome({ kind: "stale", currentHeadSha: "new" }, REVIEW_ROUNDS_LEFT, PINNED_HEAD), 1, false);
|
|
120
120
|
assert.equal(stale.advance, "infra");
|
|
121
121
|
});
|
|
122
|
+
test("NOT-313: a checks_failed retry spends the CI budget, never the infra one", () => {
|
|
123
|
+
const ciRetry = projectDeveloperRoute(routeDeveloperOutcome({ kind: "checks_failed" }, REVIEW_ROUNDS_LEFT), "developing", 1);
|
|
124
|
+
assert.equal(ciRetry.advance, "ci");
|
|
125
|
+
assert.deepStrictEqual(ciRetry.effect, {
|
|
126
|
+
kind: "enqueue",
|
|
127
|
+
workItem: "developer",
|
|
128
|
+
retryReason: "CI repair attempt 1 of 3: Developer's PR checks failed.",
|
|
129
|
+
});
|
|
130
|
+
assert.match(ciRetry.projection.currentIntent, /CI repair attempt/);
|
|
131
|
+
// An infra-class retry keeps the exact pre-NOT-313 shape — no budget marker.
|
|
132
|
+
const infraRetry = projectDeveloperRoute(routeDeveloperOutcome({ kind: "session_failed" }, REVIEW_ROUNDS_LEFT), "developing", 1);
|
|
133
|
+
assert.equal(infraRetry.advance, "infra");
|
|
134
|
+
});
|
|
122
135
|
test("NOT-147: empty-tip no-progress escalation projects to needs_human without spending infra budget", () => {
|
|
123
136
|
const INFRA_ATTEMPTS_LEFT = {
|
|
124
137
|
currentRound: 1,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// NOT-306: developer and reviewer prompts render the frozen execution contract
|
|
2
|
+
// as dedicated sections — mode, non-goals, exit predicate, one-PR boundary,
|
|
3
|
+
// per-criterion evidence — without telling the worker to rediscover them.
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { buildDeveloperPrompt, buildReviewerPrompt } from "./prompts.js";
|
|
7
|
+
import { parseReviewerResult } from "./reviewer-result.js";
|
|
8
|
+
const CONTRACT = {
|
|
9
|
+
version: "v1",
|
|
10
|
+
executionMode: "bug fix",
|
|
11
|
+
nonGoals: ["Rewriting the Linear ticket from Dealer", "Changing product scope after start"],
|
|
12
|
+
exitPredicate: "The login retry succeeds observably on the second attempt.",
|
|
13
|
+
onePrStoppingPoint: "Stop after the retry fix lands with its regression test.",
|
|
14
|
+
acceptanceCriteria: [
|
|
15
|
+
{ text: "Retry succeeds on the second attempt", evidence: "auth tests | npm run test:unit | green" },
|
|
16
|
+
{ text: "No new warnings in the log", evidence: null },
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
const LEGACY_SNAPSHOT = {
|
|
20
|
+
title: "Add widget",
|
|
21
|
+
description: "Build the widget.",
|
|
22
|
+
acceptanceCriteria: "Widget renders.",
|
|
23
|
+
repo: "acme/app",
|
|
24
|
+
baseBranch: "main",
|
|
25
|
+
};
|
|
26
|
+
const CONTRACT_SNAPSHOT = { ...LEGACY_SNAPSHOT, executionContract: CONTRACT };
|
|
27
|
+
test("developer prompt renders every contract section verbatim, never as pointers", () => {
|
|
28
|
+
const prompt = buildDeveloperPrompt({ taskSnapshot: CONTRACT_SNAPSHOT, round: 1 });
|
|
29
|
+
assert.match(prompt, /## Execution contract \(frozen v1/);
|
|
30
|
+
assert.match(prompt, /### Execution mode/);
|
|
31
|
+
assert.ok(prompt.includes("bug fix"), "mode value must appear verbatim");
|
|
32
|
+
assert.match(prompt, /### Non-goals/);
|
|
33
|
+
assert.ok(prompt.includes("Rewriting the Linear ticket from Dealer"));
|
|
34
|
+
assert.ok(prompt.includes("Changing product scope after start"));
|
|
35
|
+
assert.match(prompt, /### Exit predicate/);
|
|
36
|
+
assert.ok(prompt.includes("The login retry succeeds observably on the second attempt."));
|
|
37
|
+
assert.match(prompt, /### One-PR stopping point/);
|
|
38
|
+
assert.ok(prompt.includes("Stop after the retry fix lands with its regression test."));
|
|
39
|
+
assert.match(prompt, /### Per-criterion evidence/);
|
|
40
|
+
assert.ok(prompt.includes("Retry succeeds on the second attempt"));
|
|
41
|
+
assert.ok(prompt.includes("Evidence: auth tests | npm run test:unit | green"));
|
|
42
|
+
assert.ok(prompt.includes("No new warnings in the log"));
|
|
43
|
+
assert.match(prompt, /do not rediscover/i);
|
|
44
|
+
});
|
|
45
|
+
test("reviewer prompt renders the contract and demands exit-predicate plus per-criterion evidence verdicts", () => {
|
|
46
|
+
const prompt = buildReviewerPrompt({
|
|
47
|
+
taskSnapshot: CONTRACT_SNAPSHOT,
|
|
48
|
+
round: 1,
|
|
49
|
+
baseSha: "a".repeat(40),
|
|
50
|
+
headSha: "b".repeat(40),
|
|
51
|
+
diff: "diff --git a/x b/x\n+added line\n",
|
|
52
|
+
});
|
|
53
|
+
assert.match(prompt, /## Execution contract \(frozen v1/);
|
|
54
|
+
assert.ok(prompt.includes("The login retry succeeds observably on the second attempt."));
|
|
55
|
+
assert.ok(prompt.includes("Evidence: auth tests | npm run test:unit | green"));
|
|
56
|
+
// The verdict contract assesses the exit predicate and each evidence line —
|
|
57
|
+
// not just the prose acceptance criteria.
|
|
58
|
+
assert.match(prompt, /"exitPredicateAssessment"/);
|
|
59
|
+
assert.match(prompt, /EACH criterion's stated evidence/i);
|
|
60
|
+
assert.match(prompt, /never a blanket "evidence looks good"/);
|
|
61
|
+
assert.match(prompt, /"approved" additionally requires the exit predicate to hold/);
|
|
62
|
+
});
|
|
63
|
+
test("legacy snapshots without a contract omit the contract section but keep the shared verdict shape", () => {
|
|
64
|
+
const developer = buildDeveloperPrompt({ taskSnapshot: LEGACY_SNAPSHOT, round: 1 });
|
|
65
|
+
assert.doesNotMatch(developer, /## Execution contract/);
|
|
66
|
+
assert.match(developer, /## Acceptance criteria/);
|
|
67
|
+
const reviewer = buildReviewerPrompt({
|
|
68
|
+
taskSnapshot: LEGACY_SNAPSHOT,
|
|
69
|
+
round: 1,
|
|
70
|
+
baseSha: "a".repeat(40),
|
|
71
|
+
headSha: "b".repeat(40),
|
|
72
|
+
diff: "diff --git a/x b/x\n+added line\n",
|
|
73
|
+
});
|
|
74
|
+
assert.doesNotMatch(reviewer, /## Execution contract/);
|
|
75
|
+
// The exit-predicate/evidence verdict rules apply to every review, so the
|
|
76
|
+
// reviewer shape always carries the optional assessment field.
|
|
77
|
+
assert.match(reviewer, /"exitPredicateAssessment"/);
|
|
78
|
+
assert.match(reviewer, /Tasks without a contract omit "exitPredicateAssessment"/);
|
|
79
|
+
});
|
|
80
|
+
test("a null contract is identical to an absent one", () => {
|
|
81
|
+
const absent = buildDeveloperPrompt({ taskSnapshot: LEGACY_SNAPSHOT, round: 1 });
|
|
82
|
+
assert.equal(buildDeveloperPrompt({ taskSnapshot: { ...LEGACY_SNAPSHOT, executionContract: null }, round: 1 }), absent);
|
|
83
|
+
});
|
|
84
|
+
test("reviewer output parses an exit-predicate assessment; legacy outputs still parse", () => {
|
|
85
|
+
const withPredicate = {
|
|
86
|
+
verdict: "approved",
|
|
87
|
+
baseSha: "aaaa",
|
|
88
|
+
headSha: "bbbb",
|
|
89
|
+
acceptanceCriteriaAssessment: "All criteria met.",
|
|
90
|
+
evidenceAssessment: "Criterion 1 verified via auth tests (green); criterion 2 has no stated evidence.",
|
|
91
|
+
exitPredicateAssessment: "The retry succeeds observably on the second attempt — holds at this tip.",
|
|
92
|
+
findings: [],
|
|
93
|
+
risks: [],
|
|
94
|
+
};
|
|
95
|
+
const parsed = parseReviewerResult(`\`\`\`json\n${JSON.stringify(withPredicate)}\n\`\`\``);
|
|
96
|
+
assert.deepEqual(parsed, withPredicate);
|
|
97
|
+
const legacy = {
|
|
98
|
+
verdict: "approved",
|
|
99
|
+
baseSha: "aaaa",
|
|
100
|
+
headSha: "bbbb",
|
|
101
|
+
acceptanceCriteriaAssessment: "Meets criteria.",
|
|
102
|
+
evidenceAssessment: "Tests pass.",
|
|
103
|
+
findings: [],
|
|
104
|
+
risks: [],
|
|
105
|
+
};
|
|
106
|
+
assert.deepEqual(parseReviewerResult(JSON.stringify(legacy)), legacy);
|
|
107
|
+
});
|
|
@@ -1,4 +1,42 @@
|
|
|
1
|
+
import { MUSE_SANDBOX_CAPABILITIES } from "../runners/muse-code-args.js";
|
|
2
|
+
import { checkMuseVisualQa, museVisualQaPromptSection, } from "../adapters/muse-visual-qa.js";
|
|
1
3
|
import { formatVerificationReceiptSection, } from "./verification-receipt.js";
|
|
4
|
+
import { formatOperatorCriteria, } from "./operator-criteria.js";
|
|
5
|
+
/**
|
|
6
|
+
* NOT-306: render the frozen execution contract as dedicated sections. These
|
|
7
|
+
* are the ticket's own execution semantics — the worker follows them as
|
|
8
|
+
* written and never goes back to the ticket (or anywhere else) to rediscover
|
|
9
|
+
* them. Empty/absent renders nothing so legacy prompts stay byte-for-byte.
|
|
10
|
+
*/
|
|
11
|
+
function executionContractSection(contract) {
|
|
12
|
+
if (!contract)
|
|
13
|
+
return [];
|
|
14
|
+
const lines = [
|
|
15
|
+
`## Execution contract (frozen ${contract.version} — follow as written, do not rediscover)`,
|
|
16
|
+
`These execution semantics come from the Planner-authored ticket and are frozen for this workflow. Do not re-derive them, do not renegotiate scope, and do not ask the operator to restate them.`,
|
|
17
|
+
``,
|
|
18
|
+
`### Execution mode`,
|
|
19
|
+
contract.executionMode,
|
|
20
|
+
``,
|
|
21
|
+
`### Non-goals (out of scope — do not build these)`,
|
|
22
|
+
...contract.nonGoals.map((goal) => `- ${goal}`),
|
|
23
|
+
``,
|
|
24
|
+
`### Exit predicate (you are done only when this observably holds)`,
|
|
25
|
+
contract.exitPredicate,
|
|
26
|
+
``,
|
|
27
|
+
`### One-PR stopping point (stop here even if more work suggests itself)`,
|
|
28
|
+
contract.onePrStoppingPoint,
|
|
29
|
+
``,
|
|
30
|
+
`### Per-criterion evidence (verify each criterion exactly this way)`,
|
|
31
|
+
];
|
|
32
|
+
for (const criterion of contract.acceptanceCriteria) {
|
|
33
|
+
lines.push(`- ${criterion.text}`);
|
|
34
|
+
if (criterion.evidence)
|
|
35
|
+
lines.push(` Evidence: ${criterion.evidence}`);
|
|
36
|
+
}
|
|
37
|
+
lines.push(``);
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
2
40
|
function guidanceSection(guidance) {
|
|
3
41
|
if (!guidance?.length)
|
|
4
42
|
return [];
|
|
@@ -9,6 +47,135 @@ function guidanceSection(guidance) {
|
|
|
9
47
|
...guidance.flatMap((g) => [g.trim(), ``]),
|
|
10
48
|
];
|
|
11
49
|
}
|
|
50
|
+
/** NOT-310: the merge-conflict repair directive. Empty/absent renders nothing so
|
|
51
|
+
* ordinary rounds produce byte-for-byte the prompt they always did. */
|
|
52
|
+
function conflictRepairSection(directive) {
|
|
53
|
+
const base = directive?.baseBranch?.trim();
|
|
54
|
+
const branch = directive?.branch?.trim();
|
|
55
|
+
if (!base || !branch)
|
|
56
|
+
return [];
|
|
57
|
+
const files = (directive?.files ?? []).map((f) => f.trim()).filter((f) => f.length > 0);
|
|
58
|
+
return [
|
|
59
|
+
`## Merge conflict with ${base} — resolve it first`,
|
|
60
|
+
`This PR's branch \`${branch}\` cannot merge into \`${base}\`: the base moved and merging it conflicts. Do this before any other work in this round:`,
|
|
61
|
+
``,
|
|
62
|
+
`1. In your worktree: \`git fetch origin ${base}\` and \`git merge origin/${base}\`.`,
|
|
63
|
+
`2. Resolve every conflict preserving both sides' intent — the base side carries other tickets' merged work, never discard it to keep your side.`,
|
|
64
|
+
files.length > 0
|
|
65
|
+
? ` Files seen conflicting at merge time: ${files.map((f) => `\`${f}\``).join(", ")}.`
|
|
66
|
+
: ` The conflicting files are unknown — your own merge output lists them; resolve all of them.`,
|
|
67
|
+
`3. Run the full verification suite on the resolved tree, then commit the resolution.`,
|
|
68
|
+
`4. Never rebase this published branch and never force-push — the coordinator publishes your tip with a plain push.`,
|
|
69
|
+
``,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
/** NOT-314: the frozen snapshot's `[operator]` criteria for the developer.
|
|
73
|
+
* Empty/absent renders nothing so operator-free tasks produce byte-for-byte
|
|
74
|
+
* the prompt they always did. */
|
|
75
|
+
function operatorDeveloperSection(criteria) {
|
|
76
|
+
if (!criteria?.length)
|
|
77
|
+
return [];
|
|
78
|
+
return [
|
|
79
|
+
`## Operator verification required ([operator] criteria)`,
|
|
80
|
+
`These acceptance criteria can only be proven by a human operator with real credentials, a real login, or a paid session — Dealer blocks the merge until the operator records the result, so do not work around the gate. Do not attempt these; ship the ready-to-run probe and a doc, put the exact command in the PR body:`,
|
|
81
|
+
``,
|
|
82
|
+
...formatOperatorCriteria(criteria),
|
|
83
|
+
``,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
/** NOT-314: an operator_verification `repair` note, verbatim. Empty/absent
|
|
87
|
+
* renders nothing so ordinary rounds produce byte-for-byte the prompt they always did. */
|
|
88
|
+
function operatorRepairSection(note) {
|
|
89
|
+
if (!note?.trim())
|
|
90
|
+
return [];
|
|
91
|
+
return [
|
|
92
|
+
`## Operator verification feedback`,
|
|
93
|
+
`A human reviewed the operator-gated criteria and sent the work back instead of verifying. Address this before anything else, and do not ask the operator to re-verify the same head without fixing it:`,
|
|
94
|
+
``,
|
|
95
|
+
note.trim(),
|
|
96
|
+
``,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
/** NOT-314: the frozen snapshot's `[operator]` criteria for the reviewer.
|
|
100
|
+
* Missing operator evidence is NOT a defect (Dealer gates the merge itself) —
|
|
101
|
+
* but the probe and the doc must exist. Empty/absent renders nothing. */
|
|
102
|
+
function operatorReviewerSection(criteria) {
|
|
103
|
+
if (!criteria?.length)
|
|
104
|
+
return [];
|
|
105
|
+
return [
|
|
106
|
+
`## Operator-gated criteria ([operator]) — not yours to verify`,
|
|
107
|
+
`Missing operator evidence is NOT a defect: Dealer blocks the merge until a human operator records the result, so never raise a blocking finding and never escalate for an unverified [operator] criterion. What you MUST check instead is that the probe and doc exist: the diff must ship the ready-to-run probe for each criterion below, the doc must name the exact command, and the PR body must carry it. A missing probe or doc IS a blocking finding.`,
|
|
108
|
+
``,
|
|
109
|
+
...formatOperatorCriteria(criteria),
|
|
110
|
+
``,
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* NOT-316: the Muse developer sandbox limits, rendered from
|
|
115
|
+
* `MUSE_SANDBOX_CAPABILITIES` (which is tied by test to the real
|
|
116
|
+
* `--sandbox-network` launch flag) instead of hand-written prose, so the
|
|
117
|
+
* prompt cannot drift from what the sandbox actually does. Muse developers
|
|
118
|
+
* only — returns [] otherwise, so non-Muse prompts are byte-for-byte
|
|
119
|
+
* unchanged.
|
|
120
|
+
*/
|
|
121
|
+
function museEnvironmentLimitsSection() {
|
|
122
|
+
const caps = MUSE_SANDBOX_CAPABILITIES;
|
|
123
|
+
return [
|
|
124
|
+
`## Environment limits (sandbox network: ${caps.sandboxNetwork})`,
|
|
125
|
+
`This session runs inside the Muse sandbox with network \`${caps.network}\` — these limits come from the launch flags and cannot be worked around:`,
|
|
126
|
+
``,
|
|
127
|
+
`- Network: ${caps.network} (no DNS — the npm registry and GitHub API are unreachable).`,
|
|
128
|
+
`- Loopback listeners: ${caps.loopbackListen ? "yes" : "no"} — \`listen()\` fails with EPERM, so do not start servers.`,
|
|
129
|
+
`- Browser: ${caps.browser ? "yes" : "no"}.`,
|
|
130
|
+
`- Credentials/Keychain: ${caps.credentialsKeychain ? "yes" : "no"}.`,
|
|
131
|
+
`After one failed attempt at something the environment cannot do (install, listen, browser, credentials), stop and hand off; do not work around the sandbox.`,
|
|
132
|
+
``,
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
function acceptanceCriteriaHasTag(acceptanceCriteria, tag) {
|
|
136
|
+
return acceptanceCriteria.includes(tag);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* NOT-316: what `[agent]` / `[ci]` tags on acceptance criteria mean for a Muse
|
|
140
|
+
* developer. Rendered only when the AC text carries the tag (per-tag lines)
|
|
141
|
+
* and only for Muse developers — otherwise returns [] so untagged and
|
|
142
|
+
* non-Muse prompts are byte-for-byte what they always were. `[operator]`
|
|
143
|
+
* criteria are owned by the operator-gate section (`operatorDeveloperSection`)
|
|
144
|
+
* and deliberately not duplicated here.
|
|
145
|
+
*/
|
|
146
|
+
function tagSemanticsDeveloperSection(acceptanceCriteria) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
if (acceptanceCriteriaHasTag(acceptanceCriteria, "[agent]")) {
|
|
149
|
+
lines.push(`- \`[agent]\`: verify yourself — prove the criterion with your own in-session runs (targeted tests); your verification is the evidence.`);
|
|
150
|
+
}
|
|
151
|
+
if (acceptanceCriteriaHasTag(acceptanceCriteria, "[ci]")) {
|
|
152
|
+
lines.push(`- \`[ci]\`: implement or extend the named CI job/test and push — CI is the evidence, do not reproduce it locally.`);
|
|
153
|
+
}
|
|
154
|
+
if (!lines.length)
|
|
155
|
+
return [];
|
|
156
|
+
return [`## Acceptance-criterion tags ([agent] / [ci])`, ...lines, ``];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* NOT-316: how the reviewer judges tagged criteria. Rendered only when the AC
|
|
160
|
+
* text carries the tag (per-tag lines) — untagged reviewer prompts are
|
|
161
|
+
* byte-for-byte what they always were. `[ci]` is judged by the PR check
|
|
162
|
+
* status in the evidence, never by local runs; an `[operator]` criterion is
|
|
163
|
+
* gated by Dealer and missing operator evidence is never a defect (the
|
|
164
|
+
* probe/doc existence check lives in `operatorReviewerSection` and is not
|
|
165
|
+
* duplicated here).
|
|
166
|
+
*/
|
|
167
|
+
function tagSemanticsReviewerSection(acceptanceCriteria) {
|
|
168
|
+
const lines = [];
|
|
169
|
+
if (acceptanceCriteriaHasTag(acceptanceCriteria, "[ci]")) {
|
|
170
|
+
lines.push(`- Judge \`[ci]\` criteria by the PR check status in the evidence (the CI checks section), not by local runs.`);
|
|
171
|
+
}
|
|
172
|
+
if (acceptanceCriteriaHasTag(acceptanceCriteria, "[operator]")) {
|
|
173
|
+
lines.push(`- Do not mark an \`[operator]\` criterion as a defect — it is gated by Dealer.`);
|
|
174
|
+
}
|
|
175
|
+
if (!lines.length)
|
|
176
|
+
return [];
|
|
177
|
+
return [`## Criterion tags ([ci] / [operator]) — how to judge`, ...lines, ``];
|
|
178
|
+
}
|
|
12
179
|
/** NOT-272: a resolved product_scope_decision's human note, verbatim. Empty/absent
|
|
13
180
|
* renders nothing so noteless resolves produce byte-for-byte the prompt they always did. */
|
|
14
181
|
function scopeDecisionSection(note) {
|
|
@@ -78,9 +245,22 @@ export function buildDeveloperPrompt(input) {
|
|
|
78
245
|
parts.push(...formatVerificationReceiptSection(input.priorVerificationReceipt));
|
|
79
246
|
}
|
|
80
247
|
}
|
|
248
|
+
// NOT-310: the conflict repair leads — it is this round's purpose, read before the task.
|
|
249
|
+
parts.push(...conflictRepairSection(input.conflictRepair));
|
|
81
250
|
// NOT-272: the scope decision leads — the developer reads it before the task itself.
|
|
82
251
|
parts.push(...scopeDecisionSection(input.scopeDecisionNote));
|
|
83
|
-
|
|
252
|
+
// NOT-314: operator repair feedback leads for the same reason.
|
|
253
|
+
parts.push(...operatorRepairSection(input.operatorRepairNote));
|
|
254
|
+
parts.push(`## Task`, input.taskSnapshot.title, input.taskSnapshot.description, ``, `## Acceptance criteria`, input.taskSnapshot.acceptanceCriteria, ``,
|
|
255
|
+
// NOT-314: operator criteria render separately right after the criteria —
|
|
256
|
+
// the worker must not attempt them, only ship the probe + doc.
|
|
257
|
+
...operatorDeveloperSection(input.operatorCriteria),
|
|
258
|
+
// NOT-316: tag semantics render only for Muse developers whose AC text
|
|
259
|
+
// carries the tags — otherwise nothing, so untagged and non-Muse prompts
|
|
260
|
+
// stay byte-for-byte.
|
|
261
|
+
...(input.museDeveloper
|
|
262
|
+
? tagSemanticsDeveloperSection(input.taskSnapshot.acceptanceCriteria)
|
|
263
|
+
: []), ...executionContractSection(input.taskSnapshot.executionContract));
|
|
84
264
|
if (input.findings?.length) {
|
|
85
265
|
parts.push(`## Findings to address`);
|
|
86
266
|
for (const f of input.findings) {
|
|
@@ -93,7 +273,20 @@ export function buildDeveloperPrompt(input) {
|
|
|
93
273
|
parts.push(...agentDeckSection(input.worktreePath, input.deckId));
|
|
94
274
|
if (input.museDeveloper)
|
|
95
275
|
parts.push(...museCronProhibitionSection());
|
|
276
|
+
// NOT-316: sandbox limits rendered from MUSE_SANDBOX_CAPABILITIES — Muse
|
|
277
|
+
// developers only, so non-Muse prompts are untouched.
|
|
278
|
+
if (input.museDeveloper)
|
|
279
|
+
parts.push(...museEnvironmentLimitsSection());
|
|
280
|
+
// NOT-303: the screenshot-path preflight verdict is embedded before the session
|
|
281
|
+
// starts, so the worker reads it instead of discovering the Chrome.app abort
|
|
282
|
+
// mid-session. Non-Muse developers are untouched.
|
|
283
|
+
if (input.museDeveloper) {
|
|
284
|
+
parts.push(...museVisualQaPromptSection(input.museVisualQa ?? checkMuseVisualQa()));
|
|
285
|
+
}
|
|
96
286
|
parts.push(`## Required`,
|
|
287
|
+
// NOT-315: the coordinator provisions dependencies before the session spawns —
|
|
288
|
+
// the sandbox has no network, so an in-session install can never work.
|
|
289
|
+
`Dependencies are installed; do not run npm install (no network).`,
|
|
97
290
|
// NOT-115: encourage incremental commits so a mid-session death leaves less
|
|
98
291
|
// uncommitted work for dirty_worktree escalation — soft mitigation only.
|
|
99
292
|
// NOT-146: timed-spawn handoff bar is commits + targeted tests for the change.
|
|
@@ -102,7 +295,7 @@ export function buildDeveloperPrompt(input) {
|
|
|
102
295
|
`Run targeted tests for the change. A clean handoff is valid with commits plus those targeted tests — full suite / Lens checks are not required inside this timed developer spawn (they run on a separate review/verify budget). Make incremental commits with \`git\` at coherent slice boundaries (for example after the tests for that slice pass) — do not leave all work uncommitted until the very end. A final commit of any remaining changes and the **implementation conclusion** remain required before you exit.`, `Commit any remaining changes with \`git\` — do NOT push and do NOT open a pull request; the coordinator pushes your branch and opens/updates the draft PR after this session ends.`, `End your reply with a short **implementation conclusion**: what changed, why, any deviations from the acceptance criteria, and known follow-ups. This is distinct from the PR description and is required every round.`);
|
|
103
296
|
return parts.join("\n");
|
|
104
297
|
}
|
|
105
|
-
const REVIEWER_RESULT_SHAPE = '{"verdict":"approved"|"changes_requested"|"escalated","baseSha":"...","headSha":"...","acceptanceCriteriaAssessment":"...","evidenceAssessment":"...","findings":[{"fingerprint":"stable-slug","severity":"blocking"|"non_blocking","title":"...","rationale":"...","file":"...","line":0}],"risks":["..."],"productScopeQuestion":"..."}';
|
|
298
|
+
const REVIEWER_RESULT_SHAPE = '{"verdict":"approved"|"changes_requested"|"escalated","baseSha":"...","headSha":"...","acceptanceCriteriaAssessment":"...","evidenceAssessment":"...","exitPredicateAssessment":"...","findings":[{"fingerprint":"stable-slug","severity":"blocking"|"non_blocking","title":"...","rationale":"...","file":"...","line":0}],"risks":["..."],"productScopeQuestion":"..."}';
|
|
106
299
|
function reviewerContractSection(baseSha, headSha) {
|
|
107
300
|
// Verdict table matches docs/PRD_ISSUE_COORDINATION.md §6.4 / design NOT-150 — do not drift.
|
|
108
301
|
return [
|
|
@@ -113,9 +306,18 @@ function reviewerContractSection(baseSha, headSha) {
|
|
|
113
306
|
`- Set "baseSha" to exactly "${baseSha}" and "headSha" to exactly "${headSha}" — these are the coordinator-verified SHAs you were checked out at, not values you compute.`,
|
|
114
307
|
`- "fingerprint" must be a short, stable slug for the finding (e.g. "missing-null-check-args-ts") so the same issue re-found next round is recognized as recurring, not duplicated.`,
|
|
115
308
|
`- "findings" holds every blocking AND non-blocking observation; "risks" is uncertainties that are not findings tied to a location.`,
|
|
309
|
+
// NOT-306: the execution contract (when the task carries one) is judged,
|
|
310
|
+
// not just the prose criteria — the exit predicate and each criterion's
|
|
311
|
+
// stated evidence get their own assessments on top of the AC verdict.
|
|
312
|
+
`- "acceptanceCriteriaAssessment" judges the acceptance criteria; "evidenceAssessment" must judge EACH criterion's stated evidence (where to look, action/command, expected result) one by one — never a blanket "evidence looks good".`,
|
|
313
|
+
`- When the task carries an execution contract, set "exitPredicateAssessment" to whether the frozen exit predicate observably holds at this tip, and "approved" additionally requires the exit predicate to hold (AC met alone is not enough). Tasks without a contract omit "exitPredicateAssessment".`,
|
|
116
314
|
`- "approved": AC met for this tip and no finding is "blocking" (non_blocking nits allowed).`,
|
|
117
315
|
`- "changes_requested": any "blocking" finding a coding pass can address — including incomplete review because AC-critical files were omitted/truncated from the diff. List omitted paths in a blocking finding.`,
|
|
118
316
|
`- "escalated": only when acceptance criteria / product scope are ambiguous, contradictory, or need a human product call — not ordinary code defects, not "diff too large". You MUST set non-empty "productScopeQuestion"; omit the field otherwise.`,
|
|
317
|
+
// NOT-303: a missing screenshot is never a pass. Some runtimes (Muse Code: the
|
|
318
|
+
// Chrome.app headless abort) cannot do visual QA in-session and say so in the
|
|
319
|
+
// conclusion — echo that as "visual QA not run" in "evidenceAssessment".
|
|
320
|
+
`- A missing screenshot or DOM capture is never verification: if the change has UI-visible effects and no visual evidence is in the diff, the conclusion, or the checks, state "visual QA not run" in "evidenceAssessment" — do not read the absence as a pass.`,
|
|
119
321
|
`- You cannot edit files, push, or publish anything — you only return this JSON. The coordinator publishes it to GitHub on your behalf.`,
|
|
120
322
|
];
|
|
121
323
|
}
|
|
@@ -176,6 +378,13 @@ export function buildReviewerPrompt(input) {
|
|
|
176
378
|
`## Acceptance criteria`,
|
|
177
379
|
input.taskSnapshot.acceptanceCriteria,
|
|
178
380
|
``,
|
|
381
|
+
// NOT-314: missing operator evidence is NOT a defect — but the probe and
|
|
382
|
+
// doc must exist. Only rendered when the snapshot carries operator criteria.
|
|
383
|
+
...operatorReviewerSection(input.operatorCriteria),
|
|
384
|
+
// NOT-316: tagged-criterion judging lines — only when the AC text carries
|
|
385
|
+
// the tags, so untagged reviewer prompts stay byte-for-byte.
|
|
386
|
+
...tagSemanticsReviewerSection(input.taskSnapshot.acceptanceCriteria),
|
|
387
|
+
...executionContractSection(input.taskSnapshot.executionContract),
|
|
179
388
|
`## Diff (base ${input.baseSha.slice(0, 8)} → head ${input.headSha.slice(0, 8)})`,
|
|
180
389
|
"```diff",
|
|
181
390
|
formatDiffForPrompt(input.diff).text,
|