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,189 @@
|
|
|
1
|
+
// packages/server/src/repository/authority-attempts.ts
|
|
2
|
+
//
|
|
3
|
+
// Durable checkout ledger for short-lived Agent Deck execution authority (NOT-91). A row is
|
|
4
|
+
// written BEFORE the mint call (status `acquiring`) so a crash between "asked Deck for
|
|
5
|
+
// authority" and "released it" leaves something for restart reconciliation
|
|
6
|
+
// (authority-lifecycle.ts's reconcileAuthoritiesAtStartup) and worker-death recovery
|
|
7
|
+
// (coordinator/recovery.ts) to revoke against, instead of an authority that only ever lived
|
|
8
|
+
// in one in-process async call's local variables.
|
|
9
|
+
import { v4 as uuid } from "uuid";
|
|
10
|
+
import { getDb } from "../db/index.js";
|
|
11
|
+
function rowToAttempt(row) {
|
|
12
|
+
return {
|
|
13
|
+
id: row.id,
|
|
14
|
+
ownerKind: row.owner_kind,
|
|
15
|
+
ownerId: row.owner_id,
|
|
16
|
+
idempotencyKey: row.idempotency_key,
|
|
17
|
+
authorityId: row.authority_id,
|
|
18
|
+
deckId: row.deck_id,
|
|
19
|
+
runId: row.run_id,
|
|
20
|
+
attemptId: row.attempt_id,
|
|
21
|
+
ttlMs: row.ttl_ms,
|
|
22
|
+
toolScopeHint: row.tool_scope_hint_json ? JSON.parse(row.tool_scope_hint_json) : null,
|
|
23
|
+
status: row.status,
|
|
24
|
+
staleAt: row.stale_at,
|
|
25
|
+
expiresAt: row.expires_at,
|
|
26
|
+
createdAt: row.created_at,
|
|
27
|
+
updatedAt: row.updated_at,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Inserted BEFORE the mint call — `acquiring` must never survive a process boundary. */
|
|
31
|
+
export function createAuthorityAttempt(input) {
|
|
32
|
+
const now = new Date().toISOString();
|
|
33
|
+
const row = {
|
|
34
|
+
id: uuid(),
|
|
35
|
+
owner_kind: input.ownerKind,
|
|
36
|
+
owner_id: input.ownerId,
|
|
37
|
+
idempotency_key: input.idempotencyKey,
|
|
38
|
+
authority_id: null,
|
|
39
|
+
deck_id: input.deckId,
|
|
40
|
+
run_id: input.runId,
|
|
41
|
+
attempt_id: input.attemptId,
|
|
42
|
+
ttl_ms: input.ttlMs,
|
|
43
|
+
tool_scope_hint_json: input.toolScopeHint ? JSON.stringify(input.toolScopeHint) : null,
|
|
44
|
+
status: "acquiring",
|
|
45
|
+
stale_at: null,
|
|
46
|
+
expires_at: null,
|
|
47
|
+
created_at: now,
|
|
48
|
+
updated_at: now,
|
|
49
|
+
};
|
|
50
|
+
getDb()
|
|
51
|
+
.prepare(`
|
|
52
|
+
INSERT INTO authority_attempts (
|
|
53
|
+
id, owner_kind, owner_id, idempotency_key, authority_id, deck_id, run_id, attempt_id,
|
|
54
|
+
ttl_ms, tool_scope_hint_json, status, stale_at, expires_at, created_at, updated_at
|
|
55
|
+
) VALUES (
|
|
56
|
+
@id, @owner_kind, @owner_id, @idempotency_key, @authority_id, @deck_id, @run_id, @attempt_id,
|
|
57
|
+
@ttl_ms, @tool_scope_hint_json, @status, @stale_at, @expires_at, @created_at, @updated_at
|
|
58
|
+
)
|
|
59
|
+
`)
|
|
60
|
+
.run(row);
|
|
61
|
+
return rowToAttempt(row);
|
|
62
|
+
}
|
|
63
|
+
/** `acquiring` → `active`, once mint + verify + materialize all succeed. CAS-fenced on the
|
|
64
|
+
* row still being `acquiring` — a no-op if it was already revoked out from under this call
|
|
65
|
+
* (e.g. a concurrent abort). Returns the updated row, or null if the CAS missed. */
|
|
66
|
+
export function activateAuthorityAttempt(id, input) {
|
|
67
|
+
const now = new Date().toISOString();
|
|
68
|
+
const row = getDb()
|
|
69
|
+
.prepare(`
|
|
70
|
+
UPDATE authority_attempts SET
|
|
71
|
+
status = 'active', authority_id = @authority_id, expires_at = @expires_at, updated_at = @now
|
|
72
|
+
WHERE id = @id AND status = 'acquiring'
|
|
73
|
+
RETURNING *
|
|
74
|
+
`)
|
|
75
|
+
.get({ id, authority_id: input.authorityId, expires_at: input.expiresAt, now });
|
|
76
|
+
return row ? rowToAttempt(row) : null;
|
|
77
|
+
}
|
|
78
|
+
function setTerminalStatus(id, status) {
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const row = getDb()
|
|
81
|
+
.prepare(`
|
|
82
|
+
UPDATE authority_attempts SET status = @status, updated_at = @now
|
|
83
|
+
WHERE id = @id AND status IN ('acquiring', 'active')
|
|
84
|
+
RETURNING *
|
|
85
|
+
`)
|
|
86
|
+
.get({ id, status, now });
|
|
87
|
+
return row ? rowToAttempt(row) : null;
|
|
88
|
+
}
|
|
89
|
+
/** Normal end-of-use — the caller already revoked (or is about to revoke) the authority on
|
|
90
|
+
* Deck's side; this just closes the ledger row. */
|
|
91
|
+
export function closeAuthorityAttempt(id) {
|
|
92
|
+
return setTerminalStatus(id, "closed");
|
|
93
|
+
}
|
|
94
|
+
/** Mint/verify/materialize failed — the caller already revoked (or has nothing to revoke,
|
|
95
|
+
* e.g. mint itself never returned an authorityId). */
|
|
96
|
+
export function markAuthorityAttemptFailed(id) {
|
|
97
|
+
return setTerminalStatus(id, "failed");
|
|
98
|
+
}
|
|
99
|
+
/** One row, by id — the per-row counterpart to `revokeOpenActiveAuthorityAttempts`'s
|
|
100
|
+
* per-owner bulk flip, and what a resolved `acquiring` row is set to. */
|
|
101
|
+
export function markAuthorityAttemptRevoked(id) {
|
|
102
|
+
return setTerminalStatus(id, "revoked");
|
|
103
|
+
}
|
|
104
|
+
export function getAuthorityAttempt(id) {
|
|
105
|
+
const row = getDb().prepare("SELECT * FROM authority_attempts WHERE id = ?").get(id);
|
|
106
|
+
return row ? rowToAttempt(row) : null;
|
|
107
|
+
}
|
|
108
|
+
/** Stamps a still-`acquiring` row as needing resolution — see schema.sql's `stale_at`
|
|
109
|
+
* comment. CAS-fenced on `status = 'acquiring'` (a no-op once the row has moved on) and on
|
|
110
|
+
* `stale_at IS NULL` (a no-op if already stamped), so calling this from every path that
|
|
111
|
+
* identifies a row as needing resolution is always safe to repeat. */
|
|
112
|
+
export function markAuthorityAttemptStale(id) {
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
const row = getDb()
|
|
115
|
+
.prepare(`
|
|
116
|
+
UPDATE authority_attempts SET stale_at = @now, updated_at = @now
|
|
117
|
+
WHERE id = @id AND status = 'acquiring' AND stale_at IS NULL
|
|
118
|
+
RETURNING *
|
|
119
|
+
`)
|
|
120
|
+
.get({ id, now });
|
|
121
|
+
return row ? rowToAttempt(row) : null;
|
|
122
|
+
}
|
|
123
|
+
/** Every row durably flagged as needing resolution and not yet resolved — the periodic
|
|
124
|
+
* retry sweep's input (authority-lifecycle.ts's `retryStaleAuthorityAttempts`). Unlike the
|
|
125
|
+
* startup sweep, this never re-derives staleness from a fresh scan of every open row: it
|
|
126
|
+
* only ever returns rows some other call already positively marked via
|
|
127
|
+
* `markAuthorityAttemptStale`, so it stays safe to call on a running process without risking
|
|
128
|
+
* a legitimately in-flight mint. */
|
|
129
|
+
export function listStaleAcquiringAuthorityAttempts() {
|
|
130
|
+
const rows = getDb()
|
|
131
|
+
.prepare("SELECT * FROM authority_attempts WHERE status = 'acquiring' AND stale_at IS NOT NULL ORDER BY stale_at ASC")
|
|
132
|
+
.all();
|
|
133
|
+
return rows.map(rowToAttempt);
|
|
134
|
+
}
|
|
135
|
+
/** Every row for this owner still `acquiring`/`active` — a fresh attempt (a new
|
|
136
|
+
* idempotencyKey for the same owner) revokes these first (NOT-91's "revoke-before-new-
|
|
137
|
+
* attempt"): a prior attempt's authority has no legitimate further use once a new one
|
|
138
|
+
* starts. */
|
|
139
|
+
export function listOpenAuthorityAttemptsForOwner(ownerKind, ownerId) {
|
|
140
|
+
const rows = getDb()
|
|
141
|
+
.prepare("SELECT * FROM authority_attempts WHERE owner_kind = ? AND owner_id = ? AND status IN ('acquiring', 'active') ORDER BY created_at ASC")
|
|
142
|
+
.all(ownerKind, ownerId);
|
|
143
|
+
return rows.map(rowToAttempt);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* DB-only CAS: flips every `active` row for this owner (a known `authorityId`) to `revoked`
|
|
147
|
+
* and returns them, so the caller can best-effort revoke each on Deck's side. Synchronous and
|
|
148
|
+
* side-effect-free on Deck, so it is safe to call from inside a `better-sqlite3` transaction
|
|
149
|
+
* (recovery.ts's reclaim/dead-letter, commands.ts's abortIssue) — the actual network revoke
|
|
150
|
+
* happens only after that transaction commits.
|
|
151
|
+
*
|
|
152
|
+
* Deliberately excludes `acquiring` rows (no `authorityId` yet): a coordinator can crash
|
|
153
|
+
* after Deck committed a mint but before that row was activated with the resulting id, so an
|
|
154
|
+
* `acquiring` row alone can't be assumed to have nothing live behind it. Terminalizing it here
|
|
155
|
+
* on a guess is exactly the leak NOT-91's review caught — it must instead be resolved by
|
|
156
|
+
* replaying its stored idempotencyKey (authority-lifecycle.ts's `resolveAcquiringAttempts`),
|
|
157
|
+
* which needs an async mint() call this synchronous DB-only helper can't make. Callers pair
|
|
158
|
+
* this with `listOpenAcquiringAuthorityAttempts` for that owner and resolve those separately,
|
|
159
|
+
* outside any surrounding transaction.
|
|
160
|
+
*/
|
|
161
|
+
export function revokeOpenActiveAuthorityAttempts(ownerKind, ownerId) {
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
const rows = getDb()
|
|
164
|
+
.prepare(`
|
|
165
|
+
UPDATE authority_attempts SET status = 'revoked', updated_at = @now
|
|
166
|
+
WHERE owner_kind = @owner_kind AND owner_id = @owner_id AND status = 'active'
|
|
167
|
+
RETURNING *
|
|
168
|
+
`)
|
|
169
|
+
.all({ owner_kind: ownerKind, owner_id: ownerId, now });
|
|
170
|
+
return rows.map(rowToAttempt);
|
|
171
|
+
}
|
|
172
|
+
/** Read-only: every `acquiring` row for this owner (no `authorityId` yet) — the counterpart
|
|
173
|
+
* to `revokeOpenActiveAuthorityAttempts` a caller resolves separately, asynchronously, via
|
|
174
|
+
* `authority-lifecycle.ts`'s `resolveAcquiringAttempts`. Never mutates status itself, so it is
|
|
175
|
+
* safe to call from inside a `better-sqlite3` transaction alongside the active-row revoke. */
|
|
176
|
+
export function listOpenAcquiringAuthorityAttempts(ownerKind, ownerId) {
|
|
177
|
+
const rows = getDb()
|
|
178
|
+
.prepare("SELECT * FROM authority_attempts WHERE owner_kind = ? AND owner_id = ? AND status = 'acquiring' ORDER BY created_at ASC")
|
|
179
|
+
.all(ownerKind, ownerId);
|
|
180
|
+
return rows.map(rowToAttempt);
|
|
181
|
+
}
|
|
182
|
+
/** Every row still `acquiring`/`active` for any owner — the startup sweep's input
|
|
183
|
+
* (authority-lifecycle.ts's reconcileAuthoritiesAtStartup). */
|
|
184
|
+
export function listAllOpenAuthorityAttempts() {
|
|
185
|
+
const rows = getDb()
|
|
186
|
+
.prepare("SELECT * FROM authority_attempts WHERE status IN ('acquiring', 'active') ORDER BY created_at ASC")
|
|
187
|
+
.all();
|
|
188
|
+
return rows.map(rowToAttempt);
|
|
189
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
import { getDb } from "../db/index.js";
|
|
3
|
+
function rowToFinding(row) {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
issueId: row.issue_id,
|
|
7
|
+
fingerprint: row.fingerprint,
|
|
8
|
+
severity: row.severity,
|
|
9
|
+
title: row.title,
|
|
10
|
+
rationale: row.rationale,
|
|
11
|
+
evidenceRef: row.evidence_ref,
|
|
12
|
+
file: row.file,
|
|
13
|
+
line: row.line,
|
|
14
|
+
status: row.status,
|
|
15
|
+
firstRound: row.first_round,
|
|
16
|
+
lastRound: row.last_round,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Inserts a new finding, or — if one with the same (issue, fingerprint) is
|
|
21
|
+
* open/recurring — marks it recurring and bumps last_round (PRD §6.4).
|
|
22
|
+
*/
|
|
23
|
+
export function reconcileFinding(input) {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
const existing = db
|
|
26
|
+
.prepare(`SELECT * FROM findings WHERE issue_id = ? AND fingerprint = ? AND status IN ('open', 'recurring')`)
|
|
27
|
+
.get(input.issueId, input.fingerprint);
|
|
28
|
+
if (existing) {
|
|
29
|
+
db.prepare("UPDATE findings SET status = 'recurring', last_round = ? WHERE id = ?").run(input.round, existing.id);
|
|
30
|
+
const row = db.prepare("SELECT * FROM findings WHERE id = ?").get(existing.id);
|
|
31
|
+
return rowToFinding(row);
|
|
32
|
+
}
|
|
33
|
+
const row = {
|
|
34
|
+
id: uuid(),
|
|
35
|
+
issue_id: input.issueId,
|
|
36
|
+
fingerprint: input.fingerprint,
|
|
37
|
+
severity: input.severity,
|
|
38
|
+
title: input.title,
|
|
39
|
+
rationale: input.rationale,
|
|
40
|
+
evidence_ref: input.evidenceRef ?? null,
|
|
41
|
+
file: input.file ?? null,
|
|
42
|
+
line: input.line ?? null,
|
|
43
|
+
status: "open",
|
|
44
|
+
first_round: input.round,
|
|
45
|
+
last_round: input.round,
|
|
46
|
+
};
|
|
47
|
+
db.prepare(`
|
|
48
|
+
INSERT INTO findings (
|
|
49
|
+
id, issue_id, fingerprint, severity, title, rationale, evidence_ref, file, line,
|
|
50
|
+
status, first_round, last_round
|
|
51
|
+
) VALUES (
|
|
52
|
+
@id, @issue_id, @fingerprint, @severity, @title, @rationale, @evidence_ref, @file, @line,
|
|
53
|
+
@status, @first_round, @last_round
|
|
54
|
+
)
|
|
55
|
+
`).run(row);
|
|
56
|
+
return rowToFinding(row);
|
|
57
|
+
}
|
|
58
|
+
export function resolveFinding(id) {
|
|
59
|
+
getDb().prepare("UPDATE findings SET status = 'resolved' WHERE id = ?").run(id);
|
|
60
|
+
const row = getDb().prepare("SELECT * FROM findings WHERE id = ?").get(id);
|
|
61
|
+
if (!row)
|
|
62
|
+
throw new Error(`Finding not found: ${id}`);
|
|
63
|
+
return rowToFinding(row);
|
|
64
|
+
}
|
|
65
|
+
export function listFindingsForIssue(issueId) {
|
|
66
|
+
const rows = getDb()
|
|
67
|
+
.prepare("SELECT * FROM findings WHERE issue_id = ? ORDER BY first_round ASC")
|
|
68
|
+
.all(issueId);
|
|
69
|
+
return rows.map(rowToFinding);
|
|
70
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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-findings-"));
|
|
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 } = await import("./issues.js");
|
|
10
|
+
const { reconcileFinding, resolveFinding, listFindingsForIssue } = await import("./findings.js");
|
|
11
|
+
before(() => {
|
|
12
|
+
migrate();
|
|
13
|
+
});
|
|
14
|
+
function seedIssue(title) {
|
|
15
|
+
return createIssue({
|
|
16
|
+
title,
|
|
17
|
+
repo: "acme/app",
|
|
18
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
19
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
20
|
+
baseBranch: "main",
|
|
21
|
+
maxReviewRounds: 3,
|
|
22
|
+
maxInfraAttempts: 3,
|
|
23
|
+
source: "manual"
|
|
24
|
+
}).id;
|
|
25
|
+
}
|
|
26
|
+
test("creates a new finding as open", () => {
|
|
27
|
+
const issueId = seedIssue("Finding issue 1");
|
|
28
|
+
const finding = reconcileFinding({
|
|
29
|
+
issueId,
|
|
30
|
+
fingerprint: "fp-1",
|
|
31
|
+
severity: "blocking",
|
|
32
|
+
title: "Missing null check",
|
|
33
|
+
rationale: "user can be undefined",
|
|
34
|
+
round: 1
|
|
35
|
+
});
|
|
36
|
+
assert.equal(finding.status, "open");
|
|
37
|
+
assert.equal(finding.firstRound, 1);
|
|
38
|
+
assert.equal(finding.lastRound, 1);
|
|
39
|
+
});
|
|
40
|
+
test("marks a repeated fingerprint as recurring and bumps last_round", () => {
|
|
41
|
+
const issueId = seedIssue("Finding issue 2");
|
|
42
|
+
reconcileFinding({ issueId, fingerprint: "fp-1", severity: "blocking", title: "T", rationale: "R", round: 1 });
|
|
43
|
+
const again = reconcileFinding({
|
|
44
|
+
issueId,
|
|
45
|
+
fingerprint: "fp-1",
|
|
46
|
+
severity: "blocking",
|
|
47
|
+
title: "T",
|
|
48
|
+
rationale: "R",
|
|
49
|
+
round: 2
|
|
50
|
+
});
|
|
51
|
+
assert.equal(again.status, "recurring");
|
|
52
|
+
assert.equal(again.firstRound, 1);
|
|
53
|
+
assert.equal(again.lastRound, 2);
|
|
54
|
+
});
|
|
55
|
+
test("resolves a finding", () => {
|
|
56
|
+
const issueId = seedIssue("Finding issue 3");
|
|
57
|
+
const finding = reconcileFinding({
|
|
58
|
+
issueId,
|
|
59
|
+
fingerprint: "fp-1",
|
|
60
|
+
severity: "blocking",
|
|
61
|
+
title: "T",
|
|
62
|
+
rationale: "R",
|
|
63
|
+
round: 1
|
|
64
|
+
});
|
|
65
|
+
const resolved = resolveFinding(finding.id);
|
|
66
|
+
assert.equal(resolved.status, "resolved");
|
|
67
|
+
assert.equal(listFindingsForIssue(issueId).find((f) => f.id === finding.id)?.status, "resolved");
|
|
68
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { v4 as uuid } from "uuid";
|
|
2
|
+
import { getDb } from "../db/index.js";
|
|
3
|
+
function rowToAction(row) {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
issueId: row.issue_id,
|
|
7
|
+
runId: row.run_id,
|
|
8
|
+
workflowInstanceId: row.workflow_instance_id,
|
|
9
|
+
actionType: row.action_type,
|
|
10
|
+
reason: row.reason,
|
|
11
|
+
question: row.question,
|
|
12
|
+
evidenceJson: row.evidence_json,
|
|
13
|
+
responseOptionsJson: row.response_options_json,
|
|
14
|
+
continuationPreviewJson: row.continuation_preview_json,
|
|
15
|
+
requestId: row.request_id,
|
|
16
|
+
status: row.status,
|
|
17
|
+
resolutionJson: row.resolution_json,
|
|
18
|
+
resolvedBy: row.resolved_by,
|
|
19
|
+
requestedAt: row.requested_at,
|
|
20
|
+
resolvedAt: row.resolved_at,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createHumanAction(input) {
|
|
24
|
+
if (Boolean(input.issueId) === Boolean(input.runId)) {
|
|
25
|
+
throw new Error("createHumanAction requires exactly one of issueId or runId");
|
|
26
|
+
}
|
|
27
|
+
const db = getDb();
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
const row = {
|
|
30
|
+
id: uuid(),
|
|
31
|
+
issue_id: input.issueId ?? null,
|
|
32
|
+
run_id: input.runId ?? null,
|
|
33
|
+
workflow_instance_id: input.workflowInstanceId ?? null,
|
|
34
|
+
action_type: input.actionType,
|
|
35
|
+
reason: input.reason,
|
|
36
|
+
question: input.question,
|
|
37
|
+
evidence_json: input.evidence !== undefined ? JSON.stringify(input.evidence) : null,
|
|
38
|
+
response_options_json: input.responseOptions !== undefined ? JSON.stringify(input.responseOptions) : null,
|
|
39
|
+
continuation_preview_json: input.continuationPreview !== undefined ? JSON.stringify(input.continuationPreview) : null,
|
|
40
|
+
request_id: input.requestId ?? null,
|
|
41
|
+
status: "open",
|
|
42
|
+
resolution_json: null,
|
|
43
|
+
resolved_by: null,
|
|
44
|
+
requested_at: now,
|
|
45
|
+
resolved_at: null,
|
|
46
|
+
};
|
|
47
|
+
// ON CONFLICT's target must repeat the partial index's predicate verbatim (schema.sql's
|
|
48
|
+
// idx_human_actions_open_request) — a bare `ON CONFLICT (issue_id, action_type,
|
|
49
|
+
// request_id)` throws SQLITE_ERROR against a partial unique index. A request_id-less
|
|
50
|
+
// insert (the vast majority of action types) never matches this partial index at all, so
|
|
51
|
+
// it always proceeds as a plain insert.
|
|
52
|
+
const info = db.prepare(`
|
|
53
|
+
INSERT INTO human_actions (
|
|
54
|
+
id, issue_id, run_id, workflow_instance_id, action_type, reason, question, evidence_json,
|
|
55
|
+
response_options_json, continuation_preview_json, request_id, status, resolution_json, resolved_by,
|
|
56
|
+
requested_at, resolved_at
|
|
57
|
+
) VALUES (
|
|
58
|
+
@id, @issue_id, @run_id, @workflow_instance_id, @action_type, @reason, @question, @evidence_json,
|
|
59
|
+
@response_options_json, @continuation_preview_json, @request_id, @status, @resolution_json, @resolved_by,
|
|
60
|
+
@requested_at, @resolved_at
|
|
61
|
+
)
|
|
62
|
+
ON CONFLICT (issue_id, action_type, request_id) WHERE status = 'open' AND request_id IS NOT NULL DO NOTHING
|
|
63
|
+
`).run(row);
|
|
64
|
+
if (info.changes === 0 && input.requestId && input.issueId) {
|
|
65
|
+
// Lost the race to dedupe onto an already-open action for this exact request — return
|
|
66
|
+
// the winner, never a phantom row this insert never actually created. (The partial
|
|
67
|
+
// index's NULL-issue_id rows — Run-scoped actions — never collide with each other at
|
|
68
|
+
// all under standard SQL NULL semantics, so this race is only reachable Issue-scoped.)
|
|
69
|
+
const existing = findOpenHumanActionByRequestId(input.issueId, input.actionType, input.requestId);
|
|
70
|
+
if (existing)
|
|
71
|
+
return existing;
|
|
72
|
+
}
|
|
73
|
+
return rowToAction(row);
|
|
74
|
+
}
|
|
75
|
+
export function resolveHumanAction(id, resolvedBy, resolution) {
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
getDb()
|
|
78
|
+
.prepare(`
|
|
79
|
+
UPDATE human_actions SET
|
|
80
|
+
status = 'resolved', resolution_json = ?, resolved_by = ?, resolved_at = ?
|
|
81
|
+
WHERE id = ? AND status = 'open'
|
|
82
|
+
`)
|
|
83
|
+
.run(JSON.stringify(resolution), resolvedBy, now, id);
|
|
84
|
+
const row = getDb().prepare("SELECT * FROM human_actions WHERE id = ?").get(id);
|
|
85
|
+
if (!row)
|
|
86
|
+
throw new Error(`Human action not found: ${id}`);
|
|
87
|
+
if (row.status !== "resolved")
|
|
88
|
+
throw new Error(`Human action already resolved or missing: ${id}`);
|
|
89
|
+
return rowToAction(row);
|
|
90
|
+
}
|
|
91
|
+
export function getHumanAction(id) {
|
|
92
|
+
const row = getDb().prepare("SELECT * FROM human_actions WHERE id = ?").get(id);
|
|
93
|
+
return row ? rowToAction(row) : null;
|
|
94
|
+
}
|
|
95
|
+
export function listOpenHumanActions() {
|
|
96
|
+
const rows = getDb()
|
|
97
|
+
.prepare("SELECT * FROM human_actions WHERE status = 'open' ORDER BY requested_at ASC")
|
|
98
|
+
.all();
|
|
99
|
+
return rows.map(rowToAction);
|
|
100
|
+
}
|
|
101
|
+
/** The one open action of this type for the issue, if any — used to make re-raising a
|
|
102
|
+
* pre-start action (e.g. product_scope_decision) idempotent instead of piling up
|
|
103
|
+
* duplicates across repeated calls. */
|
|
104
|
+
export function findOpenHumanAction(issueId, actionType) {
|
|
105
|
+
const row = getDb()
|
|
106
|
+
.prepare("SELECT * FROM human_actions WHERE issue_id = ? AND action_type = ? AND status = 'open' ORDER BY requested_at ASC LIMIT 1")
|
|
107
|
+
.get(issueId, actionType);
|
|
108
|
+
return row ? rowToAction(row) : null;
|
|
109
|
+
}
|
|
110
|
+
/** Dedupe key for a repeated Deck INTERACTION_REQUIRED signal that names a request id
|
|
111
|
+
* (NOT-93) — a second signal for the same request must land on the one open action it
|
|
112
|
+
* already raised, never a duplicate. Callers (reflect-trigger.ts, commands.ts) still read
|
|
113
|
+
* this first to decide whether to raise at all; the actual race-proofing against a
|
|
114
|
+
* concurrent duplicate insert is `createHumanAction`'s own `ON CONFLICT ... DO NOTHING`
|
|
115
|
+
* against `idx_human_actions_open_request` (NOT-91) — this function alone is not atomic. */
|
|
116
|
+
export function findOpenHumanActionByRequestId(issueId, actionType, requestId) {
|
|
117
|
+
const row = getDb()
|
|
118
|
+
.prepare("SELECT * FROM human_actions WHERE issue_id = ? AND action_type = ? AND request_id = ? AND status = 'open' ORDER BY requested_at ASC LIMIT 1")
|
|
119
|
+
.get(issueId, actionType, requestId);
|
|
120
|
+
return row ? rowToAction(row) : null;
|
|
121
|
+
}
|
|
122
|
+
export function listHumanActionsForIssue(issueId) {
|
|
123
|
+
const rows = getDb()
|
|
124
|
+
.prepare("SELECT * FROM human_actions WHERE issue_id = ? ORDER BY requested_at ASC")
|
|
125
|
+
.all(issueId);
|
|
126
|
+
return rows.map(rowToAction);
|
|
127
|
+
}
|
|
128
|
+
/** Run-scoped equivalent of `findOpenHumanAction` (NOT-95 — outbound-draft delivery
|
|
129
|
+
* parking has no Issue). */
|
|
130
|
+
export function findOpenHumanActionForRun(runId, actionType) {
|
|
131
|
+
const row = getDb()
|
|
132
|
+
.prepare("SELECT * FROM human_actions WHERE run_id = ? AND action_type = ? AND status = 'open' ORDER BY requested_at ASC LIMIT 1")
|
|
133
|
+
.get(runId, actionType);
|
|
134
|
+
return row ? rowToAction(row) : null;
|
|
135
|
+
}
|
|
136
|
+
/** Run-scoped equivalent of `listHumanActionsForIssue` (NOT-95). */
|
|
137
|
+
export function listHumanActionsForRun(runId) {
|
|
138
|
+
const rows = getDb()
|
|
139
|
+
.prepare("SELECT * FROM human_actions WHERE run_id = ? ORDER BY requested_at ASC")
|
|
140
|
+
.all(runId);
|
|
141
|
+
return rows.map(rowToAction);
|
|
142
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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-actions-"));
|
|
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 } = await import("./issues.js");
|
|
10
|
+
const { createRun } = await import("./runs.js");
|
|
11
|
+
const { createHumanAction, resolveHumanAction, listOpenHumanActions, listHumanActionsForIssue, findOpenHumanActionByRequestId } = await import("./human-actions.js");
|
|
12
|
+
before(() => {
|
|
13
|
+
migrate();
|
|
14
|
+
});
|
|
15
|
+
function seedIssue(title) {
|
|
16
|
+
return createIssue({
|
|
17
|
+
title,
|
|
18
|
+
repo: "acme/app",
|
|
19
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
20
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
21
|
+
baseBranch: "main",
|
|
22
|
+
maxReviewRounds: 3,
|
|
23
|
+
maxInfraAttempts: 3,
|
|
24
|
+
source: "manual"
|
|
25
|
+
}).id;
|
|
26
|
+
}
|
|
27
|
+
test("creates an open action and lists it globally and per-issue", () => {
|
|
28
|
+
const issueId = seedIssue("Action issue 1");
|
|
29
|
+
const action = createHumanAction({
|
|
30
|
+
issueId,
|
|
31
|
+
actionType: "final_review",
|
|
32
|
+
reason: "Reviewer approved",
|
|
33
|
+
question: "Accept?",
|
|
34
|
+
responseOptions: ["complete", "repair", "close"]
|
|
35
|
+
});
|
|
36
|
+
assert.equal(action.status, "open");
|
|
37
|
+
assert.ok(listOpenHumanActions().some((a) => a.id === action.id));
|
|
38
|
+
assert.deepStrictEqual(listHumanActionsForIssue(issueId).map((a) => a.id), [action.id]);
|
|
39
|
+
});
|
|
40
|
+
test("resolves an action and removes it from the open queue", () => {
|
|
41
|
+
const issueId = seedIssue("Action issue 2");
|
|
42
|
+
const action = createHumanAction({
|
|
43
|
+
issueId,
|
|
44
|
+
actionType: "final_review",
|
|
45
|
+
reason: "Reviewer approved",
|
|
46
|
+
question: "Accept?",
|
|
47
|
+
responseOptions: ["complete", "repair", "close"]
|
|
48
|
+
});
|
|
49
|
+
const resolved = resolveHumanAction(action.id, "yusuke", { choice: "complete" });
|
|
50
|
+
assert.equal(resolved.status, "resolved");
|
|
51
|
+
assert.equal(resolved.resolvedBy, "yusuke");
|
|
52
|
+
assert.equal(listOpenHumanActions().some((a) => a.id === action.id), false);
|
|
53
|
+
});
|
|
54
|
+
test("createHumanAction persists Deck's requestId and defaults to null when none is supplied (NOT-93)", () => {
|
|
55
|
+
const issueId = seedIssue("Action issue 3");
|
|
56
|
+
const withRequestId = createHumanAction({
|
|
57
|
+
issueId,
|
|
58
|
+
actionType: "deck_interaction_required",
|
|
59
|
+
reason: "Control-plane decision required",
|
|
60
|
+
question: "Resolve it in Agent Deck, then resume?",
|
|
61
|
+
responseOptions: [{ choice: "resume", label: "Resume" }],
|
|
62
|
+
requestId: "req_abc123"
|
|
63
|
+
});
|
|
64
|
+
assert.equal(withRequestId.requestId, "req_abc123");
|
|
65
|
+
const withoutRequestId = createHumanAction({
|
|
66
|
+
issueId,
|
|
67
|
+
actionType: "policy_escalation",
|
|
68
|
+
reason: "infra hiccup",
|
|
69
|
+
question: "Resume?",
|
|
70
|
+
responseOptions: [{ choice: "resume", label: "Resume" }]
|
|
71
|
+
});
|
|
72
|
+
assert.equal(withoutRequestId.requestId, null);
|
|
73
|
+
});
|
|
74
|
+
test("findOpenHumanActionByRequestId dedupes a repeated Deck signal to the one open action it already raised", () => {
|
|
75
|
+
const issueId = seedIssue("Action issue 4");
|
|
76
|
+
const action = createHumanAction({
|
|
77
|
+
issueId,
|
|
78
|
+
actionType: "deck_interaction_required",
|
|
79
|
+
reason: "Control-plane decision required",
|
|
80
|
+
question: "Resolve it in Agent Deck, then resume?",
|
|
81
|
+
responseOptions: [{ choice: "resume", label: "Resume" }],
|
|
82
|
+
requestId: "req_dup1"
|
|
83
|
+
});
|
|
84
|
+
// A repeat of the same request id on the same issue/action type finds the existing
|
|
85
|
+
// open action instead of nothing (the caller uses this to skip creating a duplicate).
|
|
86
|
+
const found = findOpenHumanActionByRequestId(issueId, "deck_interaction_required", "req_dup1");
|
|
87
|
+
assert.equal(found?.id, action.id);
|
|
88
|
+
// A different request id, action type, or issue never matches.
|
|
89
|
+
assert.equal(findOpenHumanActionByRequestId(issueId, "deck_interaction_required", "req_other"), null);
|
|
90
|
+
assert.equal(findOpenHumanActionByRequestId(issueId, "policy_escalation", "req_dup1"), null);
|
|
91
|
+
assert.equal(findOpenHumanActionByRequestId(seedIssue("Action issue 5"), "deck_interaction_required", "req_dup1"), null);
|
|
92
|
+
// Once resolved, it's no longer open — a later repeat of the same request id (a
|
|
93
|
+
// genuinely new attempt reusing an old id would be a Deck bug, not Dealer's to guard)
|
|
94
|
+
// must not resurrect or match the resolved row.
|
|
95
|
+
resolveHumanAction(action.id, "yusuke", { choice: "resume" });
|
|
96
|
+
assert.equal(findOpenHumanActionByRequestId(issueId, "deck_interaction_required", "req_dup1"), null);
|
|
97
|
+
});
|
|
98
|
+
test("the open list includes run-scoped actions that have no issue", () => {
|
|
99
|
+
// NOT-71 acceptance: deleting the standalone Human actions page was only safe because the
|
|
100
|
+
// Issues home lists *every* open action. A run-scoped action (outbound-draft delivery
|
|
101
|
+
// parking, NOT-95) has no issue to open, so if it were missing from this list it would be
|
|
102
|
+
// unreachable in the UI entirely — the gap the ticket called out by name.
|
|
103
|
+
const run = createRun({
|
|
104
|
+
title: "outbound draft",
|
|
105
|
+
agentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
106
|
+
repo: "acme/app",
|
|
107
|
+
taskCategory: "other",
|
|
108
|
+
status: "plan_pending"
|
|
109
|
+
});
|
|
110
|
+
const action = createHumanAction({
|
|
111
|
+
runId: run.id,
|
|
112
|
+
actionType: "outbound_delivery_interaction_required",
|
|
113
|
+
reason: "Delivery needs a human",
|
|
114
|
+
question: "Send the draft?",
|
|
115
|
+
responseOptions: ["complete", "close"]
|
|
116
|
+
});
|
|
117
|
+
assert.equal(action.issueId, null);
|
|
118
|
+
const listed = listOpenHumanActions().find((a) => a.id === action.id);
|
|
119
|
+
assert.ok(listed, "run-scoped action must appear in the global open list");
|
|
120
|
+
// ...and it must carry the choices the home screen resolves it with, since there is no
|
|
121
|
+
// issue page to fall back to.
|
|
122
|
+
assert.ok((listed.responseOptionsJson ?? "").includes("complete"));
|
|
123
|
+
});
|