agent-dealer 0.2.0 → 1.0.0
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 +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildIssueFilter, parseLinearIssueRef } from "./linear-inbox.js";
|
|
4
|
+
test("parseLinearIssueRef accepts identifier, URL, and UUID", () => {
|
|
5
|
+
assert.equal(parseLinearIssueRef("NOT-103"), "NOT-103");
|
|
6
|
+
assert.equal(parseLinearIssueRef(" not-90 "), "NOT-90");
|
|
7
|
+
assert.equal(parseLinearIssueRef("https://linear.app/not-so-fat/issue/NOT-103/sequential-issue-queue"), "NOT-103");
|
|
8
|
+
assert.equal(parseLinearIssueRef("7a2e7533-65f8-4752-a903-d419f47b2093"), "7a2e7533-65f8-4752-a903-d419f47b2093");
|
|
9
|
+
assert.equal(parseLinearIssueRef(""), null);
|
|
10
|
+
assert.equal(parseLinearIssueRef("not a ticket"), null);
|
|
11
|
+
});
|
|
12
|
+
test("buildIssueFilter omits assignee when assigneeMe is false", () => {
|
|
13
|
+
const filter = buildIssueFilter({
|
|
14
|
+
stateFilter: ["Backlog", "Todo", "In Progress", "In Review"],
|
|
15
|
+
teamId: "team-1",
|
|
16
|
+
assigneeMe: false,
|
|
17
|
+
syncEnabled: true,
|
|
18
|
+
}, "viewer-1");
|
|
19
|
+
assert.deepEqual(filter, {
|
|
20
|
+
state: { name: { in: ["Backlog", "Todo", "In Progress", "In Review"] } },
|
|
21
|
+
team: { id: { eq: "team-1" } },
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
test("buildIssueFilter includes assignee when assigneeMe is true", () => {
|
|
25
|
+
const filter = buildIssueFilter({
|
|
26
|
+
stateFilter: ["Todo"],
|
|
27
|
+
teamId: null,
|
|
28
|
+
assigneeMe: true,
|
|
29
|
+
syncEnabled: true,
|
|
30
|
+
}, "viewer-1");
|
|
31
|
+
assert.deepEqual(filter, {
|
|
32
|
+
state: { name: { in: ["Todo"] } },
|
|
33
|
+
assignee: { id: { eq: "viewer-1" } },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { addArtifact
|
|
1
|
+
import { addArtifact } from "../repository/runs.js";
|
|
2
2
|
import { getLinearIntakeConfig } from "../repository/intake-settings.js";
|
|
3
3
|
import { getLinearIssue } from "./linear-inbox.js";
|
|
4
4
|
const LINEAR_API = "https://api.linear.app/graphql";
|
|
5
5
|
const STATE_BY_EVENT = {
|
|
6
6
|
// TODO(P2): configurable per team — see docs/LINEAR_INTEGRATION.md
|
|
7
|
-
planning_started: "Todo",
|
|
8
|
-
plan_approved: "In Progress",
|
|
9
|
-
review: "In Review",
|
|
10
|
-
retry: "In Progress",
|
|
11
7
|
done: "Done",
|
|
12
8
|
};
|
|
13
9
|
const workflowStateCache = new Map();
|
|
@@ -56,87 +52,9 @@ async function issueUpdateState(issueId, stateId) {
|
|
|
56
52
|
issueUpdate(id: $issueId, input: { stateId: $stateId }) { success }
|
|
57
53
|
}`, { issueId, stateId });
|
|
58
54
|
}
|
|
59
|
-
function
|
|
60
|
-
const plan = getLatestArtifact(run.id, "approved_plan");
|
|
61
|
-
if (!plan?.contentJson)
|
|
62
|
-
return "";
|
|
63
|
-
try {
|
|
64
|
-
const parsed = JSON.parse(plan.contentJson);
|
|
65
|
-
const md = parsed.markdown?.trim() ?? "";
|
|
66
|
-
return md.length > 600 ? `${md.slice(0, 600)}…` : md;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return "";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function resultExcerpt(run) {
|
|
73
|
-
const result = getLatestArtifact(run.id, "execution_result");
|
|
74
|
-
if (result?.contentJson) {
|
|
75
|
-
try {
|
|
76
|
-
const parsed = JSON.parse(result.contentJson);
|
|
77
|
-
const text = parsed.resultText?.trim() ?? "";
|
|
78
|
-
if (text)
|
|
79
|
-
return text.length > 500 ? `${text.slice(0, 500)}…` : text;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
/* fall through */
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const doc = listArtifacts(run.id).find((a) => a.kind === "document");
|
|
86
|
-
if (doc?.contentJson) {
|
|
87
|
-
try {
|
|
88
|
-
const parsed = JSON.parse(doc.contentJson);
|
|
89
|
-
const text = parsed.markdown?.trim() ?? "";
|
|
90
|
-
if (text)
|
|
91
|
-
return text.length > 500 ? `${text.slice(0, 500)}…` : text;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
/* ignore */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return "";
|
|
98
|
-
}
|
|
99
|
-
function buildComment(run, event) {
|
|
55
|
+
function buildComment(run) {
|
|
100
56
|
const label = run.externalLabel ?? run.externalId ?? run.id;
|
|
101
57
|
const link = `${webBaseUrl()}/?run=${run.id}`;
|
|
102
|
-
if (event === "planning_started") {
|
|
103
|
-
return [
|
|
104
|
-
`**agent-dealer** — planning started for ${label}`,
|
|
105
|
-
``,
|
|
106
|
-
`_Agent is drafting a plan._`,
|
|
107
|
-
``,
|
|
108
|
-
`[Open run](${link})`,
|
|
109
|
-
].join("\n");
|
|
110
|
-
}
|
|
111
|
-
if (event === "plan_approved") {
|
|
112
|
-
const excerpt = planExcerpt(run);
|
|
113
|
-
return [
|
|
114
|
-
`**agent-dealer** — plan approved for ${label}`,
|
|
115
|
-
``,
|
|
116
|
-
excerpt ? excerpt : `_Plan approved — see run for details._`,
|
|
117
|
-
``,
|
|
118
|
-
`[Open run](${link})`,
|
|
119
|
-
].join("\n");
|
|
120
|
-
}
|
|
121
|
-
if (event === "review") {
|
|
122
|
-
const excerpt = resultExcerpt(run);
|
|
123
|
-
return [
|
|
124
|
-
`**agent-dealer** — execution complete, awaiting review (${label})`,
|
|
125
|
-
``,
|
|
126
|
-
excerpt ? excerpt : `_Result ready for human review._`,
|
|
127
|
-
``,
|
|
128
|
-
`[Review run](${link})`,
|
|
129
|
-
].join("\n");
|
|
130
|
-
}
|
|
131
|
-
if (event === "retry") {
|
|
132
|
-
return [
|
|
133
|
-
`**agent-dealer** — retry requested for ${label}`,
|
|
134
|
-
``,
|
|
135
|
-
`_Re-executing with human feedback._`,
|
|
136
|
-
``,
|
|
137
|
-
`[Open run](${link})`,
|
|
138
|
-
].join("\n");
|
|
139
|
-
}
|
|
140
58
|
return [
|
|
141
59
|
`**agent-dealer** — approved and marked done (${label})`,
|
|
142
60
|
``,
|
|
@@ -146,28 +64,12 @@ function buildComment(run, event) {
|
|
|
146
64
|
function recordSyncAttempt(run, event, ok, detail) {
|
|
147
65
|
addArtifact(run.id, "linear_sync", { event, ok, at: new Date().toISOString(), ...detail }, "system");
|
|
148
66
|
}
|
|
149
|
-
function hasSuccessfulSync(runId, event) {
|
|
150
|
-
return listArtifacts(runId).some((a) => {
|
|
151
|
-
if (a.kind !== "linear_sync" || !a.contentJson)
|
|
152
|
-
return false;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = JSON.parse(a.contentJson);
|
|
155
|
-
return parsed.event === event && parsed.ok === true;
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
67
|
/** Non-blocking Linear write-back — callers should `.catch()` and never fail the human action. */
|
|
163
68
|
export async function syncLinearForRun(run, event) {
|
|
164
69
|
const settings = getLinearIntakeConfig();
|
|
165
70
|
if (!settings.syncEnabled || !process.env.LINEAR_API_KEY || !run.externalId) {
|
|
166
71
|
return;
|
|
167
72
|
}
|
|
168
|
-
if (event === "planning_started" && hasSuccessfulSync(run.id, event)) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
73
|
const issue = await getLinearIssue(run.externalId);
|
|
172
74
|
if (!issue?.teamId) {
|
|
173
75
|
recordSyncAttempt(run, event, false, { error: "Issue or teamId not found" });
|
|
@@ -177,7 +79,7 @@ export async function syncLinearForRun(run, event) {
|
|
|
177
79
|
const states = await getWorkflowStates(issue.teamId);
|
|
178
80
|
const stateId = states.get(targetStateName.toLowerCase());
|
|
179
81
|
try {
|
|
180
|
-
await commentCreate(run.externalId, buildComment(run
|
|
82
|
+
await commentCreate(run.externalId, buildComment(run));
|
|
181
83
|
if (stateId) {
|
|
182
84
|
await issueUpdateState(run.externalId, stateId);
|
|
183
85
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// packages/server/src/adapters/managed-repo.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-149: Dealer owns local clones and session worktrees. Issues store a portable
|
|
4
|
+
// GitHub identity (`github.com/owner/repo`); this module resolves that into a
|
|
5
|
+
// deterministic checkout under executionRoot. Legacy issue.repo values that are still
|
|
6
|
+
// local filesystem paths are recoverable only when the path still exists — never by
|
|
7
|
+
// guessing a remote from a missing origin.
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { looksLikeLocalRepoPath, parseGitHubRepoInput, } from "@agent-dealer/shared";
|
|
13
|
+
import { getExecutionRoot } from "../paths.js";
|
|
14
|
+
import { withRepoLock } from "../runners/process-registry.js";
|
|
15
|
+
const run = promisify(execFile);
|
|
16
|
+
async function git(cwd, args) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await run("git", args, { ...(cwd ? { cwd } : {}), encoding: "utf8" });
|
|
19
|
+
return { stdout: String(result.stdout), stderr: String(result.stderr) };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const e = err;
|
|
23
|
+
throw new Error(`git ${args.join(" ")} failed: ${e.stderr?.trim() || e.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function managedRepoPath(identity) {
|
|
27
|
+
return path.join(getExecutionRoot(), "repos", identity);
|
|
28
|
+
}
|
|
29
|
+
export function managedWorktreePath(identity, sessionId, role) {
|
|
30
|
+
return path.join(getExecutionRoot(), "worktrees", identity, `${sessionId}-${role}`);
|
|
31
|
+
}
|
|
32
|
+
export function managedWorktreesRoot(identity) {
|
|
33
|
+
return path.join(getExecutionRoot(), "worktrees", identity);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Classify an issue.repo value without cloning. Throws when a legacy local path is
|
|
37
|
+
* gone or when a non-local value is not a valid GitHub identity.
|
|
38
|
+
*/
|
|
39
|
+
export function classifyIssueRepo(repoField) {
|
|
40
|
+
const trimmed = repoField.trim();
|
|
41
|
+
if (looksLikeLocalRepoPath(trimmed)) {
|
|
42
|
+
if (!fs.existsSync(trimmed)) {
|
|
43
|
+
throw new Error(`Legacy issue repo path is missing (${trimmed}). Re-create the issue with a GitHub URL, or restore the checkout — Dealer will not guess a remote.`);
|
|
44
|
+
}
|
|
45
|
+
return { kind: "legacy_local", repoPath: trimmed, identity: null };
|
|
46
|
+
}
|
|
47
|
+
const parsed = parseGitHubRepoInput(trimmed);
|
|
48
|
+
return {
|
|
49
|
+
kind: "managed",
|
|
50
|
+
identity: parsed.identity,
|
|
51
|
+
parsed,
|
|
52
|
+
repoPath: managedRepoPath(parsed.identity),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Ensure a managed clone exists and is fetched; return the local path to use as the
|
|
57
|
+
* git worktree parent. For legacy local-path issues, returns the existing path as-is.
|
|
58
|
+
*
|
|
59
|
+
* When `cloneUrlOverride` is set (tests), clones from that URL/path instead of GitHub.
|
|
60
|
+
*/
|
|
61
|
+
export async function ensureIssueRepoCheckout(repoField, opts) {
|
|
62
|
+
const classified = classifyIssueRepo(repoField);
|
|
63
|
+
if (classified.kind === "legacy_local") {
|
|
64
|
+
return classified;
|
|
65
|
+
}
|
|
66
|
+
const dest = classified.repoPath;
|
|
67
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
68
|
+
const cloneUrl = opts?.cloneUrlOverride ?? classified.parsed.cloneUrl;
|
|
69
|
+
// Serialize clone/fetch against the shared managed clone — same lock createRoleWorktree
|
|
70
|
+
// uses — so two issues on one GitHub repo cannot race the first clone or prune fetch.
|
|
71
|
+
return withRepoLock(dest, async () => {
|
|
72
|
+
if (!fs.existsSync(path.join(dest, ".git")) && !fs.existsSync(path.join(dest, "HEAD"))) {
|
|
73
|
+
// Prefer a regular clone (not bare) so existing worktree helpers that expect a
|
|
74
|
+
// working tree + .git directory keep working; worktrees still hang off this repo.
|
|
75
|
+
await git(undefined, ["clone", "--filter=blob:none", cloneUrl, dest]);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Refresh remotes; ignore failure when offline — local objects may still suffice.
|
|
79
|
+
try {
|
|
80
|
+
await git(dest, ["fetch", "--prune", "origin"]);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* keep cached clone */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let defaultBranch;
|
|
87
|
+
if (opts?.fetchDefaultBranch !== false) {
|
|
88
|
+
defaultBranch = await resolveRemoteDefaultBranch(dest);
|
|
89
|
+
}
|
|
90
|
+
return { ...classified, defaultBranch };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Read origin/HEAD or fall back to main/master. */
|
|
94
|
+
export async function resolveRemoteDefaultBranch(repoPath) {
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await git(repoPath, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
97
|
+
const m = stdout.trim().match(/refs\/remotes\/origin\/(.+)$/);
|
|
98
|
+
if (m?.[1])
|
|
99
|
+
return m[1];
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* fall through */
|
|
103
|
+
}
|
|
104
|
+
for (const candidate of ["main", "master"]) {
|
|
105
|
+
try {
|
|
106
|
+
await git(repoPath, ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${candidate}`]);
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* try next */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const { stdout } = await git(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
115
|
+
const branch = stdout.trim();
|
|
116
|
+
if (branch && branch !== "HEAD")
|
|
117
|
+
return branch;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* fall through */
|
|
121
|
+
}
|
|
122
|
+
return "main";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Try to derive a portable GitHub identity from a legacy local checkout's `origin`.
|
|
126
|
+
* Returns null when origin is missing or not GitHub — callers must surface that to the
|
|
127
|
+
* operator instead of inventing a remote.
|
|
128
|
+
*/
|
|
129
|
+
export async function tryResolveOriginGitHubIdentity(localRepoPath) {
|
|
130
|
+
try {
|
|
131
|
+
const { stdout } = await git(localRepoPath, ["remote", "get-url", "origin"]);
|
|
132
|
+
const url = stdout.trim();
|
|
133
|
+
if (!url)
|
|
134
|
+
return null;
|
|
135
|
+
return parseGitHubRepoInput(url).identity;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Where role worktrees live for a resolved issue checkout (managed or legacy).
|
|
143
|
+
* Managed: `<executionRoot>/worktrees/github.com/<owner>/<repo>`
|
|
144
|
+
* Legacy: `<repo>/.agent-dealer-worktrees` (pre-NOT-149 layout, still recoverable).
|
|
145
|
+
*/
|
|
146
|
+
export function worktreesRootForResolution(resolution) {
|
|
147
|
+
if (resolution.kind === "managed") {
|
|
148
|
+
return managedWorktreesRoot(resolution.identity);
|
|
149
|
+
}
|
|
150
|
+
return path.join(resolution.repoPath, ".agent-dealer-worktrees");
|
|
151
|
+
}
|
|
152
|
+
export function roleWorktreePathForResolution(resolution, sessionId, role) {
|
|
153
|
+
if (resolution.kind === "managed") {
|
|
154
|
+
return managedWorktreePath(resolution.identity, sessionId, role);
|
|
155
|
+
}
|
|
156
|
+
return path.join(worktreesRootForResolution(resolution), `${sessionId}-${role}`);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Single base-branch rule for worktree cut, PR create/identity, and review merge-base (NOT-149).
|
|
160
|
+
* Managed checkouts use the freshly fetched remote default; legacy local paths keep
|
|
161
|
+
* `issue.baseBranch`. Call sites must not invent a second rule.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveCheckoutBaseBranch(issueBaseBranch, checkout) {
|
|
164
|
+
if (checkout.kind === "managed" && checkout.defaultBranch) {
|
|
165
|
+
return checkout.defaultBranch;
|
|
166
|
+
}
|
|
167
|
+
return issueBaseBranch;
|
|
168
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-managed-repo-"));
|
|
8
|
+
const { migrate } = await import("../db/index.js");
|
|
9
|
+
migrate();
|
|
10
|
+
const { classifyIssueRepo, ensureIssueRepoCheckout, managedRepoPath, managedWorktreePath, resolveRemoteDefaultBranch, tryResolveOriginGitHubIdentity, } = await import("./managed-repo.js");
|
|
11
|
+
const { getExecutionRoot } = await import("../paths.js");
|
|
12
|
+
function initFixtureRepo() {
|
|
13
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fixture-repo-"));
|
|
14
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: dir });
|
|
15
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir });
|
|
16
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: dir });
|
|
17
|
+
fs.writeFileSync(path.join(dir, "README.md"), "hi\n");
|
|
18
|
+
execFileSync("git", ["add", "."], { cwd: dir });
|
|
19
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd: dir });
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
test("resolveCheckoutBaseBranch: managed uses remote default; legacy keeps issue base", async () => {
|
|
23
|
+
const { resolveCheckoutBaseBranch } = await import("./managed-repo.js");
|
|
24
|
+
assert.equal(resolveCheckoutBaseBranch("develop", { kind: "managed", defaultBranch: "main" }), "main");
|
|
25
|
+
assert.equal(resolveCheckoutBaseBranch("develop", { kind: "legacy_local" }), "develop");
|
|
26
|
+
assert.equal(resolveCheckoutBaseBranch("main", { kind: "managed" }), "main");
|
|
27
|
+
});
|
|
28
|
+
test("classifyIssueRepo: GitHub identity maps to managed path under execution root", () => {
|
|
29
|
+
const r = classifyIssueRepo("acme/app");
|
|
30
|
+
assert.equal(r.kind, "managed");
|
|
31
|
+
if (r.kind === "managed") {
|
|
32
|
+
assert.equal(r.identity, "github.com/acme/app");
|
|
33
|
+
assert.equal(r.repoPath, path.join(getExecutionRoot(), "repos", "github.com/acme/app"));
|
|
34
|
+
assert.equal(managedRepoPath(r.identity), r.repoPath);
|
|
35
|
+
assert.equal(managedWorktreePath(r.identity, "sess-1", "developer"), path.join(getExecutionRoot(), "worktrees", "github.com/acme/app", "sess-1-developer"));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
test("classifyIssueRepo: legacy local path is recoverable when present, errors when missing", () => {
|
|
39
|
+
const fixture = initFixtureRepo();
|
|
40
|
+
const ok = classifyIssueRepo(fixture);
|
|
41
|
+
assert.equal(ok.kind, "legacy_local");
|
|
42
|
+
if (ok.kind === "legacy_local")
|
|
43
|
+
assert.equal(ok.repoPath, fixture);
|
|
44
|
+
assert.throws(() => classifyIssueRepo(path.join(os.tmpdir(), "missing-dealer-repo-xyz")), /Legacy issue repo path is missing/);
|
|
45
|
+
});
|
|
46
|
+
test("ensureIssueRepoCheckout clones from override into managed layout and reads default branch", async () => {
|
|
47
|
+
const fixture = initFixtureRepo();
|
|
48
|
+
const resolved = await ensureIssueRepoCheckout("test-org/fixture-app", {
|
|
49
|
+
cloneUrlOverride: fixture,
|
|
50
|
+
});
|
|
51
|
+
assert.equal(resolved.kind, "managed");
|
|
52
|
+
assert.ok(fs.existsSync(path.join(resolved.repoPath, ".git")));
|
|
53
|
+
assert.equal(resolved.defaultBranch, "main");
|
|
54
|
+
assert.equal(await resolveRemoteDefaultBranch(resolved.repoPath), "main");
|
|
55
|
+
});
|
|
56
|
+
test("tryResolveOriginGitHubIdentity returns null when origin is not GitHub", async () => {
|
|
57
|
+
const fixture = initFixtureRepo();
|
|
58
|
+
assert.equal(await tryResolveOriginGitHubIdentity(fixture), null);
|
|
59
|
+
execFileSync("git", ["remote", "add", "origin", "https://github.com/acme/from-origin.git"], {
|
|
60
|
+
cwd: fixture,
|
|
61
|
+
});
|
|
62
|
+
assert.equal(await tryResolveOriginGitHubIdentity(fixture), "github.com/acme/from-origin");
|
|
63
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// packages/server/src/adapters/outbound-delivery.ts
|
|
2
|
+
//
|
|
3
|
+
// Deck-header delivery of an approved outbound draft (NOT-106 / NOT-95). The coordinator
|
|
4
|
+
// connects to Agent Deck's MCP with `x-agent-deck-deck-id` (no Authorization). An ambiguous
|
|
5
|
+
// transport failure (request may have reached the provider) is never silently retried.
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
|
+
import { assertCallServiceToolSuccess, extractPermalink, getAgentDeckMcpUrl, toCallServiceToolPayload, } from "./agent-deck.js";
|
|
9
|
+
const TIMEOUT_MESSAGE_RE = /timed out after \d+ms$/;
|
|
10
|
+
/** Thrown for every transport failure, typed by whether the request may already have reached
|
|
11
|
+
* Deck's `call_service_tool` (NOT-95). */
|
|
12
|
+
export class OutboundDeliveryTransportError extends Error {
|
|
13
|
+
dispatched;
|
|
14
|
+
constructor(message, dispatched) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "OutboundDeliveryTransportError";
|
|
17
|
+
this.dispatched = dispatched;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Connects with the deck header and calls `call_service_tool` exactly once. */
|
|
21
|
+
export async function callServiceToolOnDeck(opts) {
|
|
22
|
+
const mcpBase = getAgentDeckMcpUrl().replace(/\/mcp\/?$/, "");
|
|
23
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${mcpBase}/mcp`), {
|
|
24
|
+
requestInit: {
|
|
25
|
+
headers: { "x-agent-deck-deck-id": opts.deckId },
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const client = new Client({ name: "agent-dealer-deliver", version: "0.0.1" });
|
|
29
|
+
const connectAndCall = async () => {
|
|
30
|
+
try {
|
|
31
|
+
await client.connect(transport);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
throw new OutboundDeliveryTransportError(e instanceof Error ? e.message : String(e), false);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return await client.callTool({ name: "call_service_tool", arguments: opts.payload });
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
throw new OutboundDeliveryTransportError(e instanceof Error ? e.message : String(e), true);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await client.close().catch(() => { });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return await Promise.race([
|
|
47
|
+
connectAndCall(),
|
|
48
|
+
new Promise((_, reject) => setTimeout(() => reject(new OutboundDeliveryTransportError(`Outbound deliver timed out after ${opts.timeoutMs}ms`, true)), opts.timeoutMs)),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Delivers one approved outbound draft under the profile/run deck header. Never throws —
|
|
53
|
+
* failures come back typed so the caller can park only on `ambiguous` (NOT-95).
|
|
54
|
+
*/
|
|
55
|
+
export async function deliverOutboundDraft(deckId, toolCall, opts) {
|
|
56
|
+
const payload = toCallServiceToolPayload(toolCall);
|
|
57
|
+
const timeoutMs = opts?.timeoutMs ?? Number(process.env.DELIVER_TIMEOUT_MS ?? 60_000);
|
|
58
|
+
const callTool = opts?.callTool ?? ((p) => callServiceToolOnDeck({ deckId, payload: p, timeoutMs }));
|
|
59
|
+
let toolResult;
|
|
60
|
+
try {
|
|
61
|
+
toolResult = await callTool(payload);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof OutboundDeliveryTransportError) {
|
|
65
|
+
if (err.dispatched) {
|
|
66
|
+
return { ok: false, kind: "ambiguous", reason: `${err.message} — whether the message was actually sent is unknown` };
|
|
67
|
+
}
|
|
68
|
+
return { ok: false, kind: "infra_failure", reason: err.message };
|
|
69
|
+
}
|
|
70
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
71
|
+
if (TIMEOUT_MESSAGE_RE.test(reason)) {
|
|
72
|
+
return { ok: false, kind: "ambiguous", reason: `${reason} — whether the message was actually sent is unknown` };
|
|
73
|
+
}
|
|
74
|
+
return { ok: false, kind: "infra_failure", reason };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
assertCallServiceToolSuccess(toolResult);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { ok: false, kind: "infra_failure", reason: err instanceof Error ? err.message : String(err) };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, toolResult, permalink: extractPermalink(toolResult) };
|
|
83
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { deliverOutboundDraft, OutboundDeliveryTransportError } from "./outbound-delivery.js";
|
|
4
|
+
const DECK = "deck-test-1";
|
|
5
|
+
const TOOL_CALL = { serviceName: "slack", toolName: "chat_postMessage", arguments: {} };
|
|
6
|
+
test("a pre-dispatch transport failure (never reached Deck) is an ordinary infra failure", async () => {
|
|
7
|
+
const result = await deliverOutboundDraft(DECK, TOOL_CALL, {
|
|
8
|
+
callTool: async () => {
|
|
9
|
+
throw new OutboundDeliveryTransportError("connect ECONNREFUSED", false);
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
assert.equal(result.ok, false);
|
|
13
|
+
if (!result.ok)
|
|
14
|
+
assert.equal(result.kind, "infra_failure");
|
|
15
|
+
});
|
|
16
|
+
test("a post-dispatch transport failure (e.g. ECONNRESET after callTool started) is ambiguous, never auto-retried", async () => {
|
|
17
|
+
const result = await deliverOutboundDraft(DECK, TOOL_CALL, {
|
|
18
|
+
callTool: async () => {
|
|
19
|
+
throw new OutboundDeliveryTransportError("read ECONNRESET", true);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
assert.equal(result.ok, false);
|
|
23
|
+
if (!result.ok)
|
|
24
|
+
assert.equal(result.kind, "ambiguous");
|
|
25
|
+
});
|
|
26
|
+
test("the race timeout is ambiguous", async () => {
|
|
27
|
+
const result = await deliverOutboundDraft(DECK, TOOL_CALL, {
|
|
28
|
+
callTool: async () => {
|
|
29
|
+
throw new OutboundDeliveryTransportError("Outbound deliver timed out after 60000ms", true);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
assert.equal(result.ok, false);
|
|
33
|
+
if (!result.ok)
|
|
34
|
+
assert.equal(result.kind, "ambiguous");
|
|
35
|
+
});
|
|
36
|
+
test("a plain Error from a legacy callTool seam falls back to the timeout-text heuristic", async () => {
|
|
37
|
+
const timedOut = await deliverOutboundDraft(DECK, TOOL_CALL, {
|
|
38
|
+
callTool: async () => {
|
|
39
|
+
throw new Error("Outbound deliver timed out after 60000ms");
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
assert.equal(timedOut.ok, false);
|
|
43
|
+
if (!timedOut.ok)
|
|
44
|
+
assert.equal(timedOut.kind, "ambiguous");
|
|
45
|
+
const other = await deliverOutboundDraft(DECK, TOOL_CALL, {
|
|
46
|
+
callTool: async () => {
|
|
47
|
+
throw new Error("some other failure");
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
assert.equal(other.ok, false);
|
|
51
|
+
if (!other.ok)
|
|
52
|
+
assert.equal(other.kind, "infra_failure");
|
|
53
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// packages/server/src/adapters/reflect-authority.ts
|
|
2
|
+
//
|
|
3
|
+
// Deck-header Agent Deck MCP calls for coordinator-side post-review reflection (NOT-106).
|
|
4
|
+
// Reflection runs after the issue's workflow instance is already terminal — there is no
|
|
5
|
+
// worker/worktree to hand a per-attempt MCP config to, so the coordinator process itself
|
|
6
|
+
// connects with `x-agent-deck-deck-id` (no Authorization).
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9
|
+
import { getAgentDeckMcpUrl } from "./agent-deck.js";
|
|
10
|
+
import { assertToolResultOk, parseDeckToolResult, } from "./agent-deck-bind.js";
|
|
11
|
+
/** One-shot: connect under the deck header, run `fn`, always close. */
|
|
12
|
+
export async function withDeckClient(deckId, timeoutMs, fn) {
|
|
13
|
+
const mcpBase = getAgentDeckMcpUrl().replace(/\/mcp\/?$/, "");
|
|
14
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${mcpBase}/mcp`), {
|
|
15
|
+
requestInit: { headers: { "x-agent-deck-deck-id": deckId } },
|
|
16
|
+
});
|
|
17
|
+
const client = new Client({ name: "agent-dealer-reflect", version: "0.0.1" });
|
|
18
|
+
await client.connect(transport);
|
|
19
|
+
try {
|
|
20
|
+
const callTool = (name, args) => Promise.race([
|
|
21
|
+
client.callTool({ name, arguments: args }),
|
|
22
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
23
|
+
]);
|
|
24
|
+
return await fn(callTool);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await client.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Call one Deck MCP tool under the launch deck header. Never throws — network/timeout/
|
|
32
|
+
* tool errors become `infra_failure`.
|
|
33
|
+
*/
|
|
34
|
+
export async function callDeckTool(opts) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await withDeckClient(opts.deckId, opts.timeoutMs, (callTool) => callTool(opts.toolName, opts.arguments));
|
|
37
|
+
assertToolResultOk(result, opts.toolName);
|
|
38
|
+
return { ok: true, data: parseDeckToolResult(result) };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return { ok: false, kind: "infra_failure", reason: err.message ?? String(err) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -75,3 +75,29 @@ export function cursorBinExists() {
|
|
|
75
75
|
const bin = resolveCursorBin();
|
|
76
76
|
return bin !== "cursor-agent" && bin !== "cursor" ? fs.existsSync(bin) : false;
|
|
77
77
|
}
|
|
78
|
+
/** Resolve Codex CLI binary (`codex` from OpenAI Codex install). */
|
|
79
|
+
export function resolveCodexBin() {
|
|
80
|
+
const home = process.env.HOME ?? os.homedir();
|
|
81
|
+
if (process.env.CODEX_CLI)
|
|
82
|
+
return process.env.CODEX_CLI;
|
|
83
|
+
return (firstExisting([
|
|
84
|
+
path.join(home, ".local/bin/codex"),
|
|
85
|
+
path.join(home, ".codex/bin/codex"),
|
|
86
|
+
"/opt/homebrew/bin/codex",
|
|
87
|
+
"/usr/local/bin/codex",
|
|
88
|
+
]) ?? "codex");
|
|
89
|
+
}
|
|
90
|
+
export function codexBinExists() {
|
|
91
|
+
const bin = resolveCodexBin();
|
|
92
|
+
return bin !== "codex" ? fs.existsSync(bin) : false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Where codex itself would look for `config.toml` *and* its file-backed login
|
|
96
|
+
* credentials (`auth.json`) absent an override — mirrors codex's own default
|
|
97
|
+
* (`$CODEX_HOME`, else `~/.codex`). Used by `agent-deck-bind.ts` to find `auth.json` to
|
|
98
|
+
* carry into a per-attempt isolated `CODEX_HOME` (config.toml there is deliberately
|
|
99
|
+
* *not* copied — that's the ambient MCP config execution authority exists to replace).
|
|
100
|
+
*/
|
|
101
|
+
export function resolveAmbientCodexHome() {
|
|
102
|
+
return process.env.CODEX_HOME ?? path.join(process.env.HOME ?? os.homedir(), ".codex");
|
|
103
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { cursorInvokeArgs } from "./cli-env.js";
|
|
3
|
+
import { cursorInvokeArgs, resolveCodexBin } from "./cli-env.js";
|
|
4
4
|
test("cursorInvokeArgs passes through for cursor-agent", () => {
|
|
5
5
|
const prev = process.env.CURSOR_CLI;
|
|
6
6
|
process.env.CURSOR_CLI = "/home/user/.local/bin/cursor-agent";
|
|
@@ -28,3 +28,16 @@ test("cursorInvokeArgs prepends agent subcommand for legacy cursor shim", () =>
|
|
|
28
28
|
process.env.CURSOR_CLI = prev;
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
|
+
test("resolveCodexBin respects CODEX_CLI override", () => {
|
|
32
|
+
const prev = process.env.CODEX_CLI;
|
|
33
|
+
process.env.CODEX_CLI = "/tmp/custom-codex";
|
|
34
|
+
try {
|
|
35
|
+
assert.equal(resolveCodexBin(), "/tmp/custom-codex");
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (prev === undefined)
|
|
39
|
+
delete process.env.CODEX_CLI;
|
|
40
|
+
else
|
|
41
|
+
process.env.CODEX_CLI = prev;
|
|
42
|
+
}
|
|
43
|
+
});
|