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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getDb } from "../db/index.js";
|
|
2
|
+
/** Open workflow states — exclude terminal Done / Canceled. Shared with linear-inbox seed. */
|
|
3
|
+
export const DEFAULT_LINEAR_STATE_FILTER = ["Backlog", "Todo", "In Progress", "In Review"];
|
|
2
4
|
function getJson(key, fallback) {
|
|
3
5
|
const row = getDb()
|
|
4
6
|
.prepare("SELECT value_json FROM intake_settings WHERE key = ?")
|
|
@@ -12,11 +14,6 @@ function getJson(key, fallback) {
|
|
|
12
14
|
return fallback;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
|
-
function setJson(key, value) {
|
|
16
|
-
getDb()
|
|
17
|
-
.prepare("INSERT INTO intake_settings (key, value_json) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json")
|
|
18
|
-
.run(key, JSON.stringify(value));
|
|
19
|
-
}
|
|
20
17
|
function parseStateFilterEnv(raw) {
|
|
21
18
|
if (!raw?.trim())
|
|
22
19
|
return null;
|
|
@@ -26,21 +23,13 @@ function parseStateFilterEnv(raw) {
|
|
|
26
23
|
.filter(Boolean);
|
|
27
24
|
return parts.length > 0 ? parts : null;
|
|
28
25
|
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
stateFilter: parseStateFilterEnv(process.env.LINEAR_STATE_FILTER) !== null,
|
|
32
|
-
teamId: process.env.LINEAR_TEAM_ID !== undefined && process.env.LINEAR_TEAM_ID !== "",
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
/** SQLite only — used when saving UI / PATCH. */
|
|
26
|
+
/** Persisted-only view of the Linear intake config (env overrides applied separately). */
|
|
36
27
|
export function getPersistedLinearIntakeConfig() {
|
|
37
28
|
return {
|
|
38
|
-
stateFilter: getJson("linear.stateFilter", [
|
|
29
|
+
stateFilter: getJson("linear.stateFilter", [...DEFAULT_LINEAR_STATE_FILTER]),
|
|
39
30
|
teamId: getJson("linear.teamId", null),
|
|
40
|
-
assigneeMe: getJson("linear.assigneeMe",
|
|
41
|
-
defaultAgentId: getJson("linear.defaultAgentId", null),
|
|
31
|
+
assigneeMe: getJson("linear.assigneeMe", false),
|
|
42
32
|
syncEnabled: getJson("linear.syncEnabled", true),
|
|
43
|
-
routingRules: getJson("linear.routingRules", []),
|
|
44
33
|
};
|
|
45
34
|
}
|
|
46
35
|
function applyEnvOverrides(config) {
|
|
@@ -58,32 +47,6 @@ function applyEnvOverrides(config) {
|
|
|
58
47
|
export function getLinearIntakeConfig() {
|
|
59
48
|
return applyEnvOverrides(getPersistedLinearIntakeConfig());
|
|
60
49
|
}
|
|
61
|
-
export function getLinearIntakeConfigView() {
|
|
62
|
-
const persisted = getPersistedLinearIntakeConfig();
|
|
63
|
-
return {
|
|
64
|
-
...applyEnvOverrides(persisted),
|
|
65
|
-
persisted,
|
|
66
|
-
envOverrides: linearEnvOverrides(),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export function patchLinearIntakeConfig(patch) {
|
|
70
|
-
const current = getPersistedLinearIntakeConfig();
|
|
71
|
-
const next = {
|
|
72
|
-
stateFilter: patch.stateFilter ?? current.stateFilter,
|
|
73
|
-
teamId: patch.teamId !== undefined ? patch.teamId : current.teamId,
|
|
74
|
-
assigneeMe: patch.assigneeMe ?? current.assigneeMe,
|
|
75
|
-
defaultAgentId: patch.defaultAgentId !== undefined ? patch.defaultAgentId : current.defaultAgentId,
|
|
76
|
-
syncEnabled: patch.syncEnabled ?? current.syncEnabled,
|
|
77
|
-
routingRules: patch.routingRules ?? current.routingRules,
|
|
78
|
-
};
|
|
79
|
-
setJson("linear.stateFilter", next.stateFilter);
|
|
80
|
-
setJson("linear.teamId", next.teamId);
|
|
81
|
-
setJson("linear.assigneeMe", next.assigneeMe);
|
|
82
|
-
setJson("linear.defaultAgentId", next.defaultAgentId);
|
|
83
|
-
setJson("linear.syncEnabled", next.syncEnabled);
|
|
84
|
-
setJson("linear.routingRules", next.routingRules);
|
|
85
|
-
return getLinearIntakeConfigView();
|
|
86
|
-
}
|
|
87
50
|
function parseEnvAgentDeckUrl() {
|
|
88
51
|
const raw = process.env.AGENT_DECK_API_URL;
|
|
89
52
|
if (!raw)
|
|
@@ -105,20 +68,3 @@ export function getAgentDeckConfig() {
|
|
|
105
68
|
envOverride: Boolean(process.env.AGENT_DECK_API_URL),
|
|
106
69
|
};
|
|
107
70
|
}
|
|
108
|
-
export function patchAgentDeckConfig(patch) {
|
|
109
|
-
if (process.env.AGENT_DECK_API_URL) {
|
|
110
|
-
throw new Error("AGENT_DECK_API_URL is set in env — remove it to configure port in the UI");
|
|
111
|
-
}
|
|
112
|
-
const current = getAgentDeckConfig();
|
|
113
|
-
const next = {
|
|
114
|
-
host: patch.host?.trim() || current.host,
|
|
115
|
-
port: patch.port ?? current.port,
|
|
116
|
-
envOverride: false,
|
|
117
|
-
};
|
|
118
|
-
if (next.port < 1 || next.port > 65535) {
|
|
119
|
-
throw new Error("Port must be between 1 and 65535");
|
|
120
|
-
}
|
|
121
|
-
setJson("agentDeck.host", next.host);
|
|
122
|
-
setJson("agentDeck.port", next.port);
|
|
123
|
-
return next;
|
|
124
|
-
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { canTransitionIssue, CreateIssueInput, looksLikeLocalRepoPath, parseGitHubRepoInput, TERMINAL_ISSUE_STATUSES, } from "@agent-dealer/shared";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
import { getDb } from "../db/index.js";
|
|
4
|
+
function rowToIssue(row) {
|
|
5
|
+
return {
|
|
6
|
+
id: row.id,
|
|
7
|
+
source: row.source,
|
|
8
|
+
externalId: row.external_id,
|
|
9
|
+
externalLabel: row.external_label,
|
|
10
|
+
externalUrl: row.external_url,
|
|
11
|
+
title: row.title,
|
|
12
|
+
description: row.description,
|
|
13
|
+
acceptanceCriteria: row.acceptance_criteria,
|
|
14
|
+
repo: row.repo,
|
|
15
|
+
baseBranch: row.base_branch,
|
|
16
|
+
status: row.status,
|
|
17
|
+
currentOwner: row.current_owner,
|
|
18
|
+
currentIntent: row.current_intent,
|
|
19
|
+
developerAgentId: row.developer_agent_id,
|
|
20
|
+
reviewerAgentId: row.reviewer_agent_id,
|
|
21
|
+
maxReviewRounds: row.max_review_rounds,
|
|
22
|
+
currentRound: row.current_round,
|
|
23
|
+
maxInfraAttempts: row.max_infra_attempts,
|
|
24
|
+
infraAttempts: row.infra_attempts,
|
|
25
|
+
branch: row.branch,
|
|
26
|
+
baseSha: row.base_sha,
|
|
27
|
+
headSha: row.head_sha,
|
|
28
|
+
prNumber: row.pr_number,
|
|
29
|
+
prUrl: row.pr_url,
|
|
30
|
+
autoMerge: row.auto_merge !== 0,
|
|
31
|
+
createdAt: row.created_at,
|
|
32
|
+
updatedAt: row.updated_at,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Persistable `issues.repo` value (NOT-149):
|
|
37
|
+
* - GitHub URL / owner/repo → canonical `github.com/owner/repo`
|
|
38
|
+
* - Local filesystem path → kept as-is for in-flight recovery / tests (existence is
|
|
39
|
+
* checked at checkout resolve time via classifyIssueRepo — never guess a remote)
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeStoredIssueRepo(repoRaw) {
|
|
42
|
+
const trimmed = repoRaw.trim();
|
|
43
|
+
if (looksLikeLocalRepoPath(trimmed)) {
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
return parseGitHubRepoInput(trimmed).identity;
|
|
47
|
+
}
|
|
48
|
+
export function createIssue(raw) {
|
|
49
|
+
const { repo: repoRaw, ...rest } = raw;
|
|
50
|
+
const input = CreateIssueInput.omit({ repo: true }).parse(rest);
|
|
51
|
+
const repo = normalizeStoredIssueRepo(repoRaw);
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
const id = uuid();
|
|
55
|
+
const row = {
|
|
56
|
+
id,
|
|
57
|
+
source: input.source,
|
|
58
|
+
external_id: input.externalId ?? null,
|
|
59
|
+
external_label: input.externalLabel ?? null,
|
|
60
|
+
external_url: input.externalUrl ?? null,
|
|
61
|
+
title: input.title,
|
|
62
|
+
description: input.description ?? null,
|
|
63
|
+
acceptance_criteria: input.acceptanceCriteria ?? null,
|
|
64
|
+
repo,
|
|
65
|
+
base_branch: input.baseBranch,
|
|
66
|
+
status: "ready",
|
|
67
|
+
current_owner: "system",
|
|
68
|
+
current_intent: null,
|
|
69
|
+
developer_agent_id: input.developerAgentId,
|
|
70
|
+
reviewer_agent_id: input.reviewerAgentId,
|
|
71
|
+
max_review_rounds: input.maxReviewRounds,
|
|
72
|
+
current_round: 1,
|
|
73
|
+
max_infra_attempts: input.maxInfraAttempts,
|
|
74
|
+
infra_attempts: 0,
|
|
75
|
+
branch: null,
|
|
76
|
+
base_sha: null,
|
|
77
|
+
head_sha: null,
|
|
78
|
+
pr_number: null,
|
|
79
|
+
pr_url: null,
|
|
80
|
+
auto_merge: input.autoMerge ? 1 : 0,
|
|
81
|
+
created_at: now,
|
|
82
|
+
updated_at: now,
|
|
83
|
+
};
|
|
84
|
+
db.prepare(`
|
|
85
|
+
INSERT INTO issues (
|
|
86
|
+
id, source, external_id, external_label, external_url, title, description,
|
|
87
|
+
acceptance_criteria, repo, base_branch, status, current_owner, current_intent,
|
|
88
|
+
developer_agent_id, reviewer_agent_id, max_review_rounds, current_round,
|
|
89
|
+
max_infra_attempts, infra_attempts,
|
|
90
|
+
branch, base_sha, head_sha, pr_number, pr_url, auto_merge, created_at, updated_at
|
|
91
|
+
) VALUES (
|
|
92
|
+
@id, @source, @external_id, @external_label, @external_url, @title, @description,
|
|
93
|
+
@acceptance_criteria, @repo, @base_branch, @status, @current_owner, @current_intent,
|
|
94
|
+
@developer_agent_id, @reviewer_agent_id, @max_review_rounds, @current_round,
|
|
95
|
+
@max_infra_attempts, @infra_attempts,
|
|
96
|
+
@branch, @base_sha, @head_sha, @pr_number, @pr_url, @auto_merge, @created_at, @updated_at
|
|
97
|
+
)
|
|
98
|
+
`).run(row);
|
|
99
|
+
return rowToIssue(row);
|
|
100
|
+
}
|
|
101
|
+
export function getIssue(id) {
|
|
102
|
+
const row = getDb().prepare("SELECT * FROM issues WHERE id = ?").get(id);
|
|
103
|
+
return row ? rowToIssue(row) : null;
|
|
104
|
+
}
|
|
105
|
+
export function listIssues(status) {
|
|
106
|
+
const db = getDb();
|
|
107
|
+
if (!status) {
|
|
108
|
+
const rows = db.prepare("SELECT * FROM issues ORDER BY updated_at DESC").all();
|
|
109
|
+
return rows.map(rowToIssue);
|
|
110
|
+
}
|
|
111
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
112
|
+
const placeholders = statuses.map(() => "?").join(",");
|
|
113
|
+
const rows = db
|
|
114
|
+
.prepare(`SELECT * FROM issues WHERE status IN (${placeholders}) ORDER BY updated_at DESC`)
|
|
115
|
+
.all(...statuses);
|
|
116
|
+
return rows.map(rowToIssue);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Every issue ever imported for this external id, newest first. `(source, external_id)` is
|
|
120
|
+
* deliberately non-unique (`idx_issues_external` is a plain index): one ticket may need a
|
|
121
|
+
* second pass after the first ended terminally (NOT-141).
|
|
122
|
+
*/
|
|
123
|
+
export function listIssuesByExternalId(source, externalId) {
|
|
124
|
+
const rows = getDb()
|
|
125
|
+
.prepare("SELECT * FROM issues WHERE source = ? AND external_id = ? ORDER BY created_at DESC, rowid DESC")
|
|
126
|
+
.all(source, externalId);
|
|
127
|
+
return rows.map(rowToIssue);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* NOT-141: the import guard — the issues-model analog of runs' `findActiveByExternalId`.
|
|
131
|
+
* It exists to stop two live workflows racing on one ticket, not to make a ticket
|
|
132
|
+
* single-use: a `done`/`closed` row no longer matches, so a re-import after a terminal pass
|
|
133
|
+
* creates a new issue instead of silently returning the old one.
|
|
134
|
+
*/
|
|
135
|
+
export function findActiveIssueByExternalId(source, externalId) {
|
|
136
|
+
const placeholders = TERMINAL_ISSUE_STATUSES.map(() => "?").join(",");
|
|
137
|
+
const row = getDb()
|
|
138
|
+
.prepare(`SELECT * FROM issues
|
|
139
|
+
WHERE source = ? AND external_id = ? AND status NOT IN (${placeholders})
|
|
140
|
+
ORDER BY created_at DESC, rowid DESC
|
|
141
|
+
LIMIT 1`)
|
|
142
|
+
.get(source, externalId, ...TERMINAL_ISSUE_STATUSES);
|
|
143
|
+
return row ? rowToIssue(row) : null;
|
|
144
|
+
}
|
|
145
|
+
export function transitionIssue(id, to, patch) {
|
|
146
|
+
const current = getIssue(id);
|
|
147
|
+
if (!current)
|
|
148
|
+
throw new Error(`Issue not found: ${id}`);
|
|
149
|
+
if (!canTransitionIssue(current.status, to)) {
|
|
150
|
+
throw new Error(`Invalid transition: ${current.status} → ${to}`);
|
|
151
|
+
}
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
getDb()
|
|
154
|
+
.prepare(`
|
|
155
|
+
UPDATE issues SET
|
|
156
|
+
status = @status,
|
|
157
|
+
current_owner = @current_owner,
|
|
158
|
+
current_intent = @current_intent,
|
|
159
|
+
branch = @branch,
|
|
160
|
+
base_sha = @base_sha,
|
|
161
|
+
head_sha = @head_sha,
|
|
162
|
+
pr_number = @pr_number,
|
|
163
|
+
pr_url = @pr_url,
|
|
164
|
+
updated_at = @updated_at
|
|
165
|
+
WHERE id = @id
|
|
166
|
+
`)
|
|
167
|
+
.run({
|
|
168
|
+
id,
|
|
169
|
+
status: to,
|
|
170
|
+
current_owner: patch?.currentOwner ?? current.currentOwner,
|
|
171
|
+
current_intent: patch?.currentIntent !== undefined ? patch.currentIntent : current.currentIntent,
|
|
172
|
+
branch: patch?.branch !== undefined ? patch.branch : current.branch,
|
|
173
|
+
base_sha: patch?.baseSha !== undefined ? patch.baseSha : current.baseSha,
|
|
174
|
+
head_sha: patch?.headSha !== undefined ? patch.headSha : current.headSha,
|
|
175
|
+
pr_number: patch?.prNumber !== undefined ? patch.prNumber : current.prNumber,
|
|
176
|
+
pr_url: patch?.prUrl !== undefined ? patch.prUrl : current.prUrl,
|
|
177
|
+
updated_at: now,
|
|
178
|
+
});
|
|
179
|
+
const updated = getIssue(id);
|
|
180
|
+
if (!updated)
|
|
181
|
+
throw new Error(`Issue vanished: ${id}`);
|
|
182
|
+
return updated;
|
|
183
|
+
}
|
|
184
|
+
/** Pre-start (or parked-on-`needs_human`) edits only — the route enforces no active
|
|
185
|
+
* workflow instance. Never touches status/owner/intent/branch/SHA/PR fields, which are
|
|
186
|
+
* coordinator-owned (see transitionIssue). */
|
|
187
|
+
export function updateIssue(id, patch) {
|
|
188
|
+
const current = getIssue(id);
|
|
189
|
+
if (!current)
|
|
190
|
+
throw new Error(`Issue not found: ${id}`);
|
|
191
|
+
const now = new Date().toISOString();
|
|
192
|
+
getDb()
|
|
193
|
+
.prepare(`
|
|
194
|
+
UPDATE issues SET
|
|
195
|
+
title = @title,
|
|
196
|
+
description = @description,
|
|
197
|
+
acceptance_criteria = @acceptance_criteria,
|
|
198
|
+
repo = @repo,
|
|
199
|
+
base_branch = @base_branch,
|
|
200
|
+
developer_agent_id = @developer_agent_id,
|
|
201
|
+
reviewer_agent_id = @reviewer_agent_id,
|
|
202
|
+
max_review_rounds = @max_review_rounds,
|
|
203
|
+
max_infra_attempts = @max_infra_attempts,
|
|
204
|
+
auto_merge = @auto_merge,
|
|
205
|
+
updated_at = @updated_at
|
|
206
|
+
WHERE id = @id
|
|
207
|
+
`)
|
|
208
|
+
.run({
|
|
209
|
+
id,
|
|
210
|
+
title: patch.title ?? current.title,
|
|
211
|
+
description: patch.description !== undefined ? patch.description : current.description,
|
|
212
|
+
acceptance_criteria: patch.acceptanceCriteria !== undefined ? patch.acceptanceCriteria : current.acceptanceCriteria,
|
|
213
|
+
repo: patch.repo !== undefined ? normalizeStoredIssueRepo(patch.repo) : current.repo,
|
|
214
|
+
base_branch: patch.baseBranch ?? current.baseBranch,
|
|
215
|
+
developer_agent_id: patch.developerAgentId ?? current.developerAgentId,
|
|
216
|
+
reviewer_agent_id: patch.reviewerAgentId ?? current.reviewerAgentId,
|
|
217
|
+
max_review_rounds: patch.maxReviewRounds ?? current.maxReviewRounds,
|
|
218
|
+
max_infra_attempts: patch.maxInfraAttempts ?? current.maxInfraAttempts,
|
|
219
|
+
auto_merge: (patch.autoMerge !== undefined ? patch.autoMerge : current.autoMerge) ? 1 : 0,
|
|
220
|
+
updated_at: now,
|
|
221
|
+
});
|
|
222
|
+
const updated = getIssue(id);
|
|
223
|
+
if (!updated)
|
|
224
|
+
throw new Error(`Issue vanished: ${id}`);
|
|
225
|
+
return updated;
|
|
226
|
+
}
|
|
227
|
+
/** Distinct portable GitHub repo identities from prior issues, most recently used first.
|
|
228
|
+
* Legacy local filesystem rows are excluded from create/recent surfaces (NOT-149) —
|
|
229
|
+
* recovery for those issues remains at checkout time only. */
|
|
230
|
+
export function listRecentRepos(limit = 20) {
|
|
231
|
+
const rows = getDb()
|
|
232
|
+
.prepare(`SELECT repo, MAX(updated_at) AS last_used
|
|
233
|
+
FROM issues
|
|
234
|
+
WHERE repo IS NOT NULL AND TRIM(repo) != ''
|
|
235
|
+
GROUP BY repo
|
|
236
|
+
ORDER BY last_used DESC
|
|
237
|
+
LIMIT ?`)
|
|
238
|
+
.all(Math.max(limit * 4, 40));
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const r of rows) {
|
|
241
|
+
if (looksLikeLocalRepoPath(r.repo))
|
|
242
|
+
continue;
|
|
243
|
+
out.push(r.repo);
|
|
244
|
+
if (out.length >= limit)
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
export function incrementIssueRound(id) {
|
|
250
|
+
const current = getIssue(id);
|
|
251
|
+
if (!current)
|
|
252
|
+
throw new Error(`Issue not found: ${id}`);
|
|
253
|
+
const now = new Date().toISOString();
|
|
254
|
+
getDb()
|
|
255
|
+
.prepare("UPDATE issues SET current_round = current_round + 1, updated_at = ? WHERE id = ?")
|
|
256
|
+
.run(now, id);
|
|
257
|
+
const updated = getIssue(id);
|
|
258
|
+
if (!updated)
|
|
259
|
+
throw new Error(`Issue vanished: ${id}`);
|
|
260
|
+
return updated;
|
|
261
|
+
}
|
|
262
|
+
/** Spends one infra attempt — a session/git/gh/Agent Deck/publish failure that is being
|
|
263
|
+
* retried automatically, never a reviewer's `changes_requested`. See incrementIssueRound. */
|
|
264
|
+
export function incrementIssueInfraAttempts(id) {
|
|
265
|
+
const current = getIssue(id);
|
|
266
|
+
if (!current)
|
|
267
|
+
throw new Error(`Issue not found: ${id}`);
|
|
268
|
+
const now = new Date().toISOString();
|
|
269
|
+
getDb()
|
|
270
|
+
.prepare("UPDATE issues SET infra_attempts = infra_attempts + 1, updated_at = ? WHERE id = ?")
|
|
271
|
+
.run(now, id);
|
|
272
|
+
const updated = getIssue(id);
|
|
273
|
+
if (!updated)
|
|
274
|
+
throw new Error(`Issue vanished: ${id}`);
|
|
275
|
+
return updated;
|
|
276
|
+
}
|
|
277
|
+
/** A human resuming past a `policy_escalation` gets a fresh infra-attempt budget. */
|
|
278
|
+
export function resetIssueInfraAttempts(id) {
|
|
279
|
+
const current = getIssue(id);
|
|
280
|
+
if (!current)
|
|
281
|
+
throw new Error(`Issue not found: ${id}`);
|
|
282
|
+
const now = new Date().toISOString();
|
|
283
|
+
getDb()
|
|
284
|
+
.prepare("UPDATE issues SET infra_attempts = 0, updated_at = ? WHERE id = ?")
|
|
285
|
+
.run(now, id);
|
|
286
|
+
const updated = getIssue(id);
|
|
287
|
+
if (!updated)
|
|
288
|
+
throw new Error(`Issue vanished: ${id}`);
|
|
289
|
+
return updated;
|
|
290
|
+
}
|
|
291
|
+
/** `attempts_exhausted:retry` grants one more review round instead of instantly
|
|
292
|
+
* re-exhausting: bumps current_round AND max_review_rounds together. */
|
|
293
|
+
export function grantReviewRetry(id) {
|
|
294
|
+
const current = getIssue(id);
|
|
295
|
+
if (!current)
|
|
296
|
+
throw new Error(`Issue not found: ${id}`);
|
|
297
|
+
const now = new Date().toISOString();
|
|
298
|
+
getDb()
|
|
299
|
+
.prepare("UPDATE issues SET current_round = current_round + 1, max_review_rounds = max_review_rounds + 1, updated_at = ? WHERE id = ?")
|
|
300
|
+
.run(now, id);
|
|
301
|
+
const updated = getIssue(id);
|
|
302
|
+
if (!updated)
|
|
303
|
+
throw new Error(`Issue vanished: ${id}`);
|
|
304
|
+
return updated;
|
|
305
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test, before } 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
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-issues-"));
|
|
7
|
+
const { migrate } = await import("../db/index.js");
|
|
8
|
+
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
9
|
+
const { createIssue, getIssue, listIssues, transitionIssue, incrementIssueRound, findActiveIssueByExternalId, listIssuesByExternalId } = await import("./issues.js");
|
|
10
|
+
before(() => {
|
|
11
|
+
migrate();
|
|
12
|
+
});
|
|
13
|
+
function makeInput(title) {
|
|
14
|
+
return {
|
|
15
|
+
title,
|
|
16
|
+
repo: "acme/app",
|
|
17
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
18
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
19
|
+
baseBranch: "main",
|
|
20
|
+
maxReviewRounds: 3,
|
|
21
|
+
maxInfraAttempts: 3,
|
|
22
|
+
source: "manual"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
test("creates an issue with defaults", () => {
|
|
26
|
+
const issue = createIssue(makeInput("Fix login bug"));
|
|
27
|
+
assert.equal(issue.status, "ready");
|
|
28
|
+
assert.equal(issue.currentOwner, "system");
|
|
29
|
+
assert.equal(issue.currentRound, 1);
|
|
30
|
+
assert.deepStrictEqual(getIssue(issue.id), issue);
|
|
31
|
+
});
|
|
32
|
+
test("lists issues filtered by status", () => {
|
|
33
|
+
const issue = createIssue(makeInput("Filterable issue"));
|
|
34
|
+
assert.ok(listIssues("ready").some((i) => i.id === issue.id));
|
|
35
|
+
assert.equal(listIssues("done").some((i) => i.id === issue.id), false);
|
|
36
|
+
});
|
|
37
|
+
test("transitions an issue and rejects an invalid transition", () => {
|
|
38
|
+
const issue = createIssue(makeInput("Transition me"));
|
|
39
|
+
const developing = transitionIssue(issue.id, "developing", { currentOwner: "developer" });
|
|
40
|
+
assert.equal(developing.status, "developing");
|
|
41
|
+
assert.equal(developing.currentOwner, "developer");
|
|
42
|
+
assert.throws(() => transitionIssue(issue.id, "final_review"), /Invalid transition/);
|
|
43
|
+
});
|
|
44
|
+
test("increments the round counter", () => {
|
|
45
|
+
const issue = createIssue(makeInput("Round me"));
|
|
46
|
+
const bumped = incrementIssueRound(issue.id);
|
|
47
|
+
assert.equal(bumped.currentRound, 2);
|
|
48
|
+
});
|
|
49
|
+
test("NOT-141: the active-by-external-id lookup ignores terminal rows; the list keeps every pass", () => {
|
|
50
|
+
const linear = { ...makeInput("Linear pass 1"), source: "linear", externalId: "NOT-999" };
|
|
51
|
+
const first = createIssue(linear);
|
|
52
|
+
assert.equal(findActiveIssueByExternalId("linear", "NOT-999")?.id, first.id);
|
|
53
|
+
transitionIssue(first.id, "closed");
|
|
54
|
+
assert.equal(findActiveIssueByExternalId("linear", "NOT-999"), null, "a closed pass is not in flight");
|
|
55
|
+
// The terminal pass is still on record — dependency verdicts read every pass.
|
|
56
|
+
assert.deepEqual(listIssuesByExternalId("linear", "NOT-999").map((i) => i.id), [first.id]);
|
|
57
|
+
// (source, external_id) is deliberately non-unique: a second pass is a second row.
|
|
58
|
+
const second = createIssue({ ...linear, title: "Linear pass 2" });
|
|
59
|
+
assert.notEqual(second.id, first.id);
|
|
60
|
+
assert.equal(findActiveIssueByExternalId("linear", "NOT-999")?.id, second.id);
|
|
61
|
+
// Newest first, so callers that want one answer get the current pass.
|
|
62
|
+
assert.deepEqual(listIssuesByExternalId("linear", "NOT-999").map((i) => i.id), [second.id, first.id]);
|
|
63
|
+
});
|
|
64
|
+
test("terminal-scoped lookup is per (source, externalId) pair", () => {
|
|
65
|
+
createIssue({ ...makeInput("Other source"), source: "agent", externalId: "SHARED-1" });
|
|
66
|
+
const linear = createIssue({ ...makeInput("Linear side"), source: "linear", externalId: "SHARED-1" });
|
|
67
|
+
assert.equal(findActiveIssueByExternalId("linear", "SHARED-1")?.id, linear.id);
|
|
68
|
+
assert.equal(findActiveIssueByExternalId("manual", "SHARED-1"), null);
|
|
69
|
+
});
|
|
@@ -85,6 +85,34 @@ export function revertOutboundDraftToPending(artifactId) {
|
|
|
85
85
|
.run(JSON.stringify(next), artifactId, row.content_json);
|
|
86
86
|
return result.changes === 1;
|
|
87
87
|
}
|
|
88
|
+
/** Atomic increment of the draft's delivery-attempt counter — feeds the execution authority
|
|
89
|
+
* idempotency key (`${draftArtifactId}:${deliveryAttempts}`, NOT-95). Returns the new count,
|
|
90
|
+
* or null if the artifact/content is missing. Safe to call before the draft's status has
|
|
91
|
+
* settled (claim happens separately via `markOutboundDraftSent`); a CAS retry loop guards
|
|
92
|
+
* against a lost update if something else touches the row concurrently. */
|
|
93
|
+
export function incrementOutboundDeliveryAttempt(artifactId) {
|
|
94
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
95
|
+
const db = getDb();
|
|
96
|
+
const row = db.prepare("SELECT content_json FROM artifacts WHERE id = ?").get(artifactId);
|
|
97
|
+
if (!row?.content_json)
|
|
98
|
+
return null;
|
|
99
|
+
let content;
|
|
100
|
+
try {
|
|
101
|
+
content = JSON.parse(row.content_json);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const nextCount = (content.deliveryAttempts ?? 0) + 1;
|
|
107
|
+
const next = { ...content, deliveryAttempts: nextCount };
|
|
108
|
+
const result = db
|
|
109
|
+
.prepare("UPDATE artifacts SET content_json = ? WHERE id = ? AND content_json = ?")
|
|
110
|
+
.run(JSON.stringify(next), artifactId, row.content_json);
|
|
111
|
+
if (result.changes === 1)
|
|
112
|
+
return nextCount;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
88
116
|
/** Atomic pending → sent transition. Returns false if not pending. */
|
|
89
117
|
export function markOutboundDraftSent(artifactId) {
|
|
90
118
|
const db = getDb();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// packages/server/src/repository/queue-entries.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-103: durable operator-owned issue admission queue (order + wait_reason).
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
import { getDb } from "../db/index.js";
|
|
6
|
+
import { getActiveWorkflowInstance } from "./workflow-events.js";
|
|
7
|
+
import { getIssue } from "./issues.js";
|
|
8
|
+
function rowToEntry(row) {
|
|
9
|
+
return {
|
|
10
|
+
id: row.id,
|
|
11
|
+
issueId: row.issue_id,
|
|
12
|
+
position: row.position,
|
|
13
|
+
enqueuedAt: row.enqueued_at,
|
|
14
|
+
state: row.state,
|
|
15
|
+
waitReason: row.wait_reason,
|
|
16
|
+
waitReasonAt: row.wait_reason_at,
|
|
17
|
+
...(row.issue_title !== undefined
|
|
18
|
+
? { title: row.issue_title ?? null, issueStatus: row.issue_status ?? null }
|
|
19
|
+
: {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function getQueueEntry(id) {
|
|
23
|
+
const row = getDb().prepare("SELECT * FROM queue_entries WHERE id = ?").get(id);
|
|
24
|
+
return row ? rowToEntry(row) : null;
|
|
25
|
+
}
|
|
26
|
+
export function getQueuedEntryForIssue(issueId) {
|
|
27
|
+
const row = getDb()
|
|
28
|
+
.prepare("SELECT * FROM queue_entries WHERE issue_id = ? AND state = 'queued'")
|
|
29
|
+
.get(issueId);
|
|
30
|
+
return row ? rowToEntry(row) : null;
|
|
31
|
+
}
|
|
32
|
+
/** Ordered live queue (state=queued only), with issue title/status for display. */
|
|
33
|
+
export function listQueuedEntries() {
|
|
34
|
+
const rows = getDb()
|
|
35
|
+
.prepare(`
|
|
36
|
+
SELECT q.*, i.title AS issue_title, i.status AS issue_status
|
|
37
|
+
FROM queue_entries q
|
|
38
|
+
LEFT JOIN issues i ON i.id = q.issue_id
|
|
39
|
+
WHERE q.state = 'queued'
|
|
40
|
+
ORDER BY q.position ASC, q.enqueued_at ASC
|
|
41
|
+
`)
|
|
42
|
+
.all();
|
|
43
|
+
return rows.map(rowToEntry);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Append at the end. Rejects terminal issues, active workflows, and duplicate queued rows.
|
|
47
|
+
* Partial unique index: ON CONFLICT must repeat the WHERE predicate.
|
|
48
|
+
*/
|
|
49
|
+
export function enqueueIssue(issueId) {
|
|
50
|
+
return enqueueIssueWithOutcome(issueId).entry;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The same append, reporting whether it actually wrote a row. Enqueue is idempotent, so the
|
|
54
|
+
* caller cannot infer a mutation from a returned entry — a re-import of an already-queued
|
|
55
|
+
* issue gets the same `QueueEntry` back with nothing changed. Callers that *report* to a
|
|
56
|
+
* human (NOT-141: `POST /api/issues`, and the CLI hint it feeds) must use this one, or they
|
|
57
|
+
* announce a queue action that did not happen.
|
|
58
|
+
*/
|
|
59
|
+
export function enqueueIssueWithOutcome(issueId) {
|
|
60
|
+
const issue = getIssue(issueId);
|
|
61
|
+
if (!issue)
|
|
62
|
+
throw Object.assign(new Error("Issue not found"), { code: 404 });
|
|
63
|
+
if (issue.status === "done" || issue.status === "closed") {
|
|
64
|
+
throw Object.assign(new Error(`Issue is ${issue.status} — cannot enqueue`), { code: 409 });
|
|
65
|
+
}
|
|
66
|
+
if (getActiveWorkflowInstance(issueId)) {
|
|
67
|
+
throw Object.assign(new Error("Issue already has an active workflow — cannot enqueue"), {
|
|
68
|
+
code: 409,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const existing = getQueuedEntryForIssue(issueId);
|
|
72
|
+
if (existing)
|
|
73
|
+
return { entry: existing, created: false };
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
const maxPos = getDb()
|
|
76
|
+
.prepare("SELECT COALESCE(MAX(position), 0) AS m FROM queue_entries WHERE state = 'queued'")
|
|
77
|
+
.get();
|
|
78
|
+
const id = uuid();
|
|
79
|
+
const position = maxPos.m + 1;
|
|
80
|
+
// Partial unique index requires the WHERE predicate on ON CONFLICT (SQLite).
|
|
81
|
+
const info = getDb()
|
|
82
|
+
.prepare(`
|
|
83
|
+
INSERT INTO queue_entries (id, issue_id, position, enqueued_at, state, wait_reason, wait_reason_at)
|
|
84
|
+
VALUES (?, ?, ?, ?, 'queued', NULL, NULL)
|
|
85
|
+
ON CONFLICT(issue_id) WHERE state = 'queued' DO NOTHING
|
|
86
|
+
`)
|
|
87
|
+
.run(id, issueId, position, now);
|
|
88
|
+
if (info.changes === 0) {
|
|
89
|
+
const raced = getQueuedEntryForIssue(issueId);
|
|
90
|
+
if (raced)
|
|
91
|
+
return { entry: raced, created: false };
|
|
92
|
+
throw new Error("enqueue failed without creating a row");
|
|
93
|
+
}
|
|
94
|
+
return { entry: getQueueEntry(id), created: true };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* NOT-118: move a queued entry to position 1 and renumber the rest 2..N in their current
|
|
98
|
+
* order, so `position` stays a readable 1-based rank instead of drifting into negatives.
|
|
99
|
+
* This is the only reorder primitive in the repo today — NOT-112's general reorder reuses
|
|
100
|
+
* it rather than adding a second way to renumber the queue.
|
|
101
|
+
*/
|
|
102
|
+
export function moveQueueEntryToTop(issueId) {
|
|
103
|
+
const db = getDb();
|
|
104
|
+
return db.transaction(() => {
|
|
105
|
+
const entry = getQueuedEntryForIssue(issueId);
|
|
106
|
+
if (!entry)
|
|
107
|
+
return null;
|
|
108
|
+
const rest = listQueuedEntries().filter((e) => e.issueId !== issueId);
|
|
109
|
+
const setPosition = db.prepare("UPDATE queue_entries SET position = ? WHERE id = ?");
|
|
110
|
+
setPosition.run(1, entry.id);
|
|
111
|
+
rest.forEach((e, index) => setPosition.run(index + 2, e.id));
|
|
112
|
+
return getQueueEntry(entry.id);
|
|
113
|
+
})();
|
|
114
|
+
}
|
|
115
|
+
/** Operator dequeue — marks removed (keeps history row). Returns the removed entry or null. */
|
|
116
|
+
export function dequeueIssue(issueId) {
|
|
117
|
+
const entry = getQueuedEntryForIssue(issueId);
|
|
118
|
+
if (!entry)
|
|
119
|
+
return null;
|
|
120
|
+
getDb()
|
|
121
|
+
.prepare("UPDATE queue_entries SET state = 'removed', wait_reason = NULL, wait_reason_at = NULL WHERE id = ?")
|
|
122
|
+
.run(entry.id);
|
|
123
|
+
return getQueueEntry(entry.id);
|
|
124
|
+
}
|
|
125
|
+
/** Force-admit / successful admit — no-op when not queued. */
|
|
126
|
+
export function markQueueEntryAdmitted(issueId) {
|
|
127
|
+
const entry = getQueuedEntryForIssue(issueId);
|
|
128
|
+
if (!entry)
|
|
129
|
+
return null;
|
|
130
|
+
getDb()
|
|
131
|
+
.prepare(`
|
|
132
|
+
UPDATE queue_entries
|
|
133
|
+
SET state = 'admitted', wait_reason = NULL, wait_reason_at = NULL
|
|
134
|
+
WHERE issue_id = ? AND state = 'queued'
|
|
135
|
+
`)
|
|
136
|
+
.run(issueId);
|
|
137
|
+
return getQueueEntry(entry.id);
|
|
138
|
+
}
|
|
139
|
+
/** Issue closed / done while still queued. */
|
|
140
|
+
export function markQueueEntryRemoved(issueId) {
|
|
141
|
+
getDb()
|
|
142
|
+
.prepare("UPDATE queue_entries SET state = 'removed' WHERE issue_id = ? AND state = 'queued'")
|
|
143
|
+
.run(issueId);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Persist wait_reason only when it changes (avoids write churn every coordinator tick).
|
|
147
|
+
* Returns true when a write occurred.
|
|
148
|
+
*/
|
|
149
|
+
export function setQueueWaitReason(entryId, reason) {
|
|
150
|
+
const row = getDb()
|
|
151
|
+
.prepare("SELECT wait_reason FROM queue_entries WHERE id = ? AND state = 'queued'")
|
|
152
|
+
.get(entryId);
|
|
153
|
+
if (!row)
|
|
154
|
+
return false;
|
|
155
|
+
if (row.wait_reason === reason)
|
|
156
|
+
return false;
|
|
157
|
+
const now = new Date().toISOString();
|
|
158
|
+
getDb()
|
|
159
|
+
.prepare("UPDATE queue_entries SET wait_reason = ?, wait_reason_at = ? WHERE id = ?")
|
|
160
|
+
.run(reason, reason !== null ? now : null, entryId);
|
|
161
|
+
return true;
|
|
162
|
+
}
|