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,200 @@
|
|
|
1
|
+
// packages/server/src/coordinator/deck-outage.integration.test.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-136 acceptance: an Agent Deck outage observed at preflight is a wait, not a failed
|
|
4
|
+
// attempt. Four preflight failures in nine seconds used to exhaust the whole infra budget
|
|
5
|
+
// and park a healthy round on a human; here the same outage must cost nothing and resolve
|
|
6
|
+
// itself the moment the deck answers again.
|
|
7
|
+
import { test, before, beforeEach, after } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-deckout-int-"));
|
|
13
|
+
process.env.MAX_COORDINATOR_CONCURRENCY = "1";
|
|
14
|
+
process.env.COORDINATOR_HEARTBEAT_MS = "20";
|
|
15
|
+
process.env.COORDINATOR_FAIL_BACKOFF_MS = "0";
|
|
16
|
+
process.env.COORDINATOR_POLL_INTERVAL_MS = "50";
|
|
17
|
+
// Keep the backoff curve observable inside a test: 20ms, 40ms, 80ms, ...
|
|
18
|
+
process.env.DECK_OUTAGE_BACKOFF_BASE_MS = "20";
|
|
19
|
+
process.env.DECK_OUTAGE_BACKOFF_MAX_MS = "5000";
|
|
20
|
+
const { migrate, getDb } = await import("../db/index.js");
|
|
21
|
+
const { createAgent } = await import("../repository/agents.js");
|
|
22
|
+
const { createIssue, getIssue } = await import("../repository/issues.js");
|
|
23
|
+
const { listWorkItemsForIssue } = await import("../repository/work-items.js");
|
|
24
|
+
const { listWorkflowEventsForIssue } = await import("../repository/workflow-events.js");
|
|
25
|
+
const { listHumanActionsForIssue } = await import("../repository/human-actions.js");
|
|
26
|
+
const { listWorkerSessionsForIssue } = await import("../repository/worker-sessions.js");
|
|
27
|
+
const { startWorkflow } = await import("./commands.js");
|
|
28
|
+
const { registerEffectHandler, resetEffectHandlers } = await import("./effect-registry.js");
|
|
29
|
+
const { runCoordinatorTick, drainCoordinator } = await import("./worker-loop.js");
|
|
30
|
+
const { deckOutageBackoffMs } = await import("./deck-outage-config.js");
|
|
31
|
+
const UNREACHABLE = "Agent Deck is unreachable — fetch failed";
|
|
32
|
+
before(() => migrate());
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
getDb().exec(`
|
|
35
|
+
DELETE FROM review_publications;
|
|
36
|
+
DELETE FROM work_items;
|
|
37
|
+
DELETE FROM human_actions;
|
|
38
|
+
DELETE FROM workflow_events;
|
|
39
|
+
DELETE FROM findings;
|
|
40
|
+
DELETE FROM worker_sessions;
|
|
41
|
+
DELETE FROM artifacts;
|
|
42
|
+
DELETE FROM usage_events;
|
|
43
|
+
DELETE FROM workflow_instances;
|
|
44
|
+
DELETE FROM issues;
|
|
45
|
+
DELETE FROM runtime_availability;
|
|
46
|
+
`);
|
|
47
|
+
});
|
|
48
|
+
after(() => resetEffectHandlers());
|
|
49
|
+
async function pump(max = 10) {
|
|
50
|
+
for (let i = 0; i < max; i++) {
|
|
51
|
+
await runCoordinatorTick();
|
|
52
|
+
await drainCoordinator();
|
|
53
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function makeIssue(name, maxInfraAttempts = 3) {
|
|
57
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), `dealer-deckout-${name}-`));
|
|
58
|
+
const dev = createAgent({ name: `dev-${name}`, runtime: "codex_local", deckId: "00000000-0000-4000-a000-000000000099" });
|
|
59
|
+
const rev = createAgent({ name: `rev-${name}`, runtime: "codex_local", deckId: "00000000-0000-4000-a000-000000000099" });
|
|
60
|
+
return createIssue({
|
|
61
|
+
title: `Deck outage ${name}`,
|
|
62
|
+
description: "d",
|
|
63
|
+
acceptanceCriteria: "ac",
|
|
64
|
+
repo,
|
|
65
|
+
baseBranch: "main",
|
|
66
|
+
developerAgentId: dev.id,
|
|
67
|
+
reviewerAgentId: rev.id,
|
|
68
|
+
maxReviewRounds: 2,
|
|
69
|
+
maxInfraAttempts,
|
|
70
|
+
source: "manual"
|
|
71
|
+
}).id;
|
|
72
|
+
}
|
|
73
|
+
test("a deck outage at preflight spends no infra attempts and never reaches needs_human", async () => {
|
|
74
|
+
const issueId = makeIssue("outage", 1);
|
|
75
|
+
let preflights = 0;
|
|
76
|
+
registerEffectHandler("developer", async () => {
|
|
77
|
+
preflights++;
|
|
78
|
+
return { kind: "deck_unavailable", reason: UNREACHABLE };
|
|
79
|
+
});
|
|
80
|
+
assert.equal(startWorkflow(issueId).ok, true);
|
|
81
|
+
await pump();
|
|
82
|
+
// The original incident: four attempts in nine seconds against a max of three.
|
|
83
|
+
assert.ok(preflights > 1, "the item must be retried, not dropped");
|
|
84
|
+
const issue = getIssue(issueId);
|
|
85
|
+
assert.equal(issue.infraAttempts, 0, "a deck outage must not spend the infra budget");
|
|
86
|
+
assert.notEqual(issue.status, "needs_human");
|
|
87
|
+
assert.match(issue.currentIntent ?? "", /Waiting for Agent Deck/);
|
|
88
|
+
const items = listWorkItemsForIssue(issueId);
|
|
89
|
+
assert.equal(items.length, 1);
|
|
90
|
+
assert.equal(items[0].status, "pending", "the item waits, it is not dead-lettered");
|
|
91
|
+
assert.equal(items[0].attemptCount, 0, "the claim-time attempt bump is reverted");
|
|
92
|
+
// Measured against when the deferral was written, not against "now": the backoff here is
|
|
93
|
+
// 20ms and the pump iterations that follow the last deferral can outlast it on a loaded
|
|
94
|
+
// runner, so a wall-clock comparison flakes without asserting anything extra.
|
|
95
|
+
assert.ok(Date.parse(items[0].availableAt) > Date.parse(items[0].updatedAt), "the next preflight is gated behind a wait");
|
|
96
|
+
assert.equal(listHumanActionsForIssue(issueId).filter((a) => a.status === "open").length, 0);
|
|
97
|
+
const events = listWorkflowEventsForIssue(issueId);
|
|
98
|
+
const deferrals = events.filter((e) => e.type === "worker.deferred");
|
|
99
|
+
assert.ok(deferrals.length > 0);
|
|
100
|
+
const payload = JSON.parse(deferrals[0].payloadJson ?? "{}");
|
|
101
|
+
assert.equal(payload.outcome, "deck_unavailable", "the timeline says the deck is unavailable");
|
|
102
|
+
assert.match(payload.reason, /unreachable/);
|
|
103
|
+
assert.equal(events.some((e) => e.type === "worker.failed"), false, "no worker failed here");
|
|
104
|
+
// Nothing spawned, so the session is cancelled rather than recorded as a crash.
|
|
105
|
+
assert.equal(listWorkerSessionsForIssue(issueId).every((s) => s.status === "cancelled"), true);
|
|
106
|
+
});
|
|
107
|
+
test("the issue proceeds on its own once the deck is reachable again", async () => {
|
|
108
|
+
const issueId = makeIssue("recover", 1);
|
|
109
|
+
let attempts = 0;
|
|
110
|
+
registerEffectHandler("developer", async () => {
|
|
111
|
+
attempts++;
|
|
112
|
+
// The deck comes back between the first and second preflight — as it would after a
|
|
113
|
+
// restart. No human action, no resume command.
|
|
114
|
+
if (attempts === 1) {
|
|
115
|
+
return { kind: "deck_unavailable", reason: UNREACHABLE };
|
|
116
|
+
}
|
|
117
|
+
return { kind: "no_pr" };
|
|
118
|
+
});
|
|
119
|
+
assert.equal(startWorkflow(issueId).ok, true);
|
|
120
|
+
await pump();
|
|
121
|
+
assert.ok(attempts >= 2, "the deferred item must be re-attempted without human input");
|
|
122
|
+
const issue = getIssue(issueId);
|
|
123
|
+
// The recovered attempt consumed exactly one infra attempt — the no_pr one, not the outage.
|
|
124
|
+
assert.equal(issue.infraAttempts, 1);
|
|
125
|
+
});
|
|
126
|
+
test("consecutive outages back off exponentially instead of retrying every ~3s", async () => {
|
|
127
|
+
const issueId = makeIssue("backoff");
|
|
128
|
+
registerEffectHandler("developer", async () => {
|
|
129
|
+
return { kind: "deck_unavailable", reason: UNREACHABLE };
|
|
130
|
+
});
|
|
131
|
+
assert.equal(startWorkflow(issueId).ok, true);
|
|
132
|
+
const waits = [];
|
|
133
|
+
for (let i = 0; i < 4; i++) {
|
|
134
|
+
await runCoordinatorTick();
|
|
135
|
+
await drainCoordinator();
|
|
136
|
+
const item = listWorkItemsForIssue(issueId)[0];
|
|
137
|
+
const deferrals = JSON.parse(item.payloadJson ?? "{}").deckUnavailableDeferrals;
|
|
138
|
+
waits.push(deferrals);
|
|
139
|
+
// Wait out this deferral so the next tick can claim the item again.
|
|
140
|
+
await new Promise((r) => setTimeout(r, Math.max(0, Date.parse(item.availableAt) - Date.now()) + 5));
|
|
141
|
+
}
|
|
142
|
+
assert.deepEqual(waits, [1, 2, 3, 4], "each outage increments this item's deferral count");
|
|
143
|
+
assert.ok(deckOutageBackoffMs(3) > deckOutageBackoffMs(0), "later deferrals wait strictly longer than the first");
|
|
144
|
+
});
|
|
145
|
+
test("a prolonged outage keeps waiting — it says how long, and still recovers on its own", async () => {
|
|
146
|
+
const prev = process.env.DECK_OUTAGE_PROLONGED_AFTER_MS;
|
|
147
|
+
// Long enough that the wait stops reading as "the deck is restarting".
|
|
148
|
+
process.env.DECK_OUTAGE_PROLONGED_AFTER_MS = "40";
|
|
149
|
+
try {
|
|
150
|
+
const issueId = makeIssue("prolonged");
|
|
151
|
+
let down = true;
|
|
152
|
+
let ranAfterRecovery = 0;
|
|
153
|
+
registerEffectHandler("developer", async () => {
|
|
154
|
+
if (down)
|
|
155
|
+
return { kind: "deck_unavailable", reason: UNREACHABLE };
|
|
156
|
+
ranAfterRecovery++;
|
|
157
|
+
return { kind: "no_pr" };
|
|
158
|
+
});
|
|
159
|
+
assert.equal(startWorkflow(issueId).ok, true);
|
|
160
|
+
await pump(1); // first observation — starts the outage clock
|
|
161
|
+
await new Promise((r) => setTimeout(r, 60)); // outlive the "prolonged" threshold
|
|
162
|
+
await pump();
|
|
163
|
+
const waiting = getIssue(issueId);
|
|
164
|
+
assert.notEqual(waiting.status, "needs_human", "a long outage is still a wait, not a handoff");
|
|
165
|
+
assert.equal(waiting.infraAttempts, 0);
|
|
166
|
+
assert.match(waiting.currentIntent ?? "", /Waiting for Agent Deck/);
|
|
167
|
+
assert.match(waiting.currentIntent ?? "", /unreachable for \d+s/, "the timeline names the outage length");
|
|
168
|
+
assert.equal(listHumanActionsForIssue(issueId).filter((a) => a.status === "open").length, 0);
|
|
169
|
+
assert.equal(listWorkItemsForIssue(issueId)[0].status, "pending", "the item is still retryable");
|
|
170
|
+
// The whole point of not escalating: whenever the deck returns, the issue moves again.
|
|
171
|
+
down = false;
|
|
172
|
+
const pending = listWorkItemsForIssue(issueId)[0];
|
|
173
|
+
await new Promise((r) => setTimeout(r, Math.max(0, Date.parse(pending.availableAt) - Date.now()) + 5));
|
|
174
|
+
await pump();
|
|
175
|
+
assert.ok(ranAfterRecovery > 0, "the deferred item resumed on its own once the deck answered");
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
if (prev === undefined)
|
|
179
|
+
delete process.env.DECK_OUTAGE_PROLONGED_AFTER_MS;
|
|
180
|
+
else
|
|
181
|
+
process.env.DECK_OUTAGE_PROLONGED_AFTER_MS = prev;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
test("a deck that is reachable but answers with an error still fails the attempt", async () => {
|
|
185
|
+
const issueId = makeIssue("realerror", 1);
|
|
186
|
+
registerEffectHandler("developer", async () => {
|
|
187
|
+
return {
|
|
188
|
+
kind: "deck_failure",
|
|
189
|
+
reason: "preflight failed: get_playbook(pb-x) returned an error: missing"
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
assert.equal(startWorkflow(issueId).ok, true);
|
|
193
|
+
await pump();
|
|
194
|
+
const issue = getIssue(issueId);
|
|
195
|
+
assert.equal(issue.infraAttempts, 1, "a real deck error still spends the infra budget");
|
|
196
|
+
assert.equal(issue.status, "needs_human");
|
|
197
|
+
const events = listWorkflowEventsForIssue(issueId);
|
|
198
|
+
assert.ok(events.some((e) => e.type === "worker.failed"));
|
|
199
|
+
assert.equal(events.some((e) => e.type === "worker.deferred"), false);
|
|
200
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// packages/server/src/coordinator/dependencies.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-104: dependency readiness from *declared* Linear blockers.
|
|
4
|
+
//
|
|
5
|
+
// Dealer never infers a dependency graph — no file overlap, no same-repo ordering. The only
|
|
6
|
+
// edge that gates admission is an explicit `blocks` relation a human wrote in Linear. This
|
|
7
|
+
// module owns the three pieces the `blockedByDependency` rule needs:
|
|
8
|
+
//
|
|
9
|
+
// 1. a batched, TTL-cached, injectable provider that turns queued issues into a snapshot
|
|
10
|
+
// of their declared blockers (`blockerSnapshotFor`),
|
|
11
|
+
// 2. the per-blocker satisfaction rule (dealer `done` beats Linear's "Done"),
|
|
12
|
+
// 3. the operator-facing wait reason.
|
|
13
|
+
//
|
|
14
|
+
// There is no `issue_dependencies` table: a dealer-linked blocker is a pure local join and is
|
|
15
|
+
// always live, so only the edge itself and the Linear state of *unlinked* blockers are remote.
|
|
16
|
+
// A persisted table can replace the provider later without touching the rule.
|
|
17
|
+
import { isTerminalIssueStatus } from "@agent-dealer/shared";
|
|
18
|
+
import { fetchLinearBlockers } from "../adapters/linear-inbox.js";
|
|
19
|
+
import { listIssuesByExternalId } from "../repository/issues.js";
|
|
20
|
+
const EMPTY_SNAPSHOT = new Map();
|
|
21
|
+
const DEFAULT_CACHE_TTL_MS = 60_000;
|
|
22
|
+
/**
|
|
23
|
+
* Admission runs inside the ~3s coordinator tick and this is the first remote call in that
|
|
24
|
+
* path, so it is hard-bounded: a hanging Linear API degrades to "unavailable" (everything
|
|
25
|
+
* parks, nothing is dropped) instead of stalling every worker dispatch behind it. The budget
|
|
26
|
+
* plus both graces below stays under that tick — a healthy Linear answers in well under a
|
|
27
|
+
* second, and one slow read must not cost the loop a whole cycle.
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 1_500;
|
|
30
|
+
/** Grace on top of the per-request budget, so the request's own error wins over the wrapper. */
|
|
31
|
+
const TIMEOUT_GRACE_MS = 250;
|
|
32
|
+
/**
|
|
33
|
+
* After a failed fetch, wait this long before asking Linear again. Only *successes* are
|
|
34
|
+
* cached, so without this the 3s coordinator tick retries an outage ~1200 times an hour —
|
|
35
|
+
* against a 1500/hr personal-key limit that the inbox poller and status write-back share.
|
|
36
|
+
* Entries stay parked either way; recovery is just delayed by at most this window.
|
|
37
|
+
*/
|
|
38
|
+
const FAILURE_BACKOFF_MS = 10_000;
|
|
39
|
+
let cacheTtlMs = DEFAULT_CACHE_TTL_MS;
|
|
40
|
+
let fetchTimeoutMs = DEFAULT_FETCH_TIMEOUT_MS;
|
|
41
|
+
let failureBackoffMs = FAILURE_BACKOFF_MS;
|
|
42
|
+
let blockersProvider = null;
|
|
43
|
+
let linearFetcher = null;
|
|
44
|
+
const cache = new Map();
|
|
45
|
+
let backoffUntil = 0;
|
|
46
|
+
let inFlight = null;
|
|
47
|
+
let lastLoggedError = null;
|
|
48
|
+
/** Tests inject a pure provider so admission never reaches the network. */
|
|
49
|
+
export function setBlockersProviderForTests(provider) {
|
|
50
|
+
blockersProvider = provider;
|
|
51
|
+
}
|
|
52
|
+
/** Narrower seam: keeps the real caching/TTL policy, replaces only the Linear call. */
|
|
53
|
+
export function setLinearBlockerFetcherForTests(fetcher) {
|
|
54
|
+
linearFetcher = fetcher;
|
|
55
|
+
}
|
|
56
|
+
export function setBlockerCacheTtlForTests(ms) {
|
|
57
|
+
cacheTtlMs = ms ?? DEFAULT_CACHE_TTL_MS;
|
|
58
|
+
}
|
|
59
|
+
export function setBlockerFetchTimeoutForTests(ms) {
|
|
60
|
+
fetchTimeoutMs = ms ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
61
|
+
}
|
|
62
|
+
/** Also re-scales a window already running, so a test can lapse one without sleeping it out. */
|
|
63
|
+
export function setBlockerFailureBackoffForTests(ms) {
|
|
64
|
+
failureBackoffMs = ms ?? FAILURE_BACKOFF_MS;
|
|
65
|
+
backoffUntil = Math.min(backoffUntil, Date.now() + failureBackoffMs);
|
|
66
|
+
}
|
|
67
|
+
export function clearBlockerCacheForTests() {
|
|
68
|
+
cache.clear();
|
|
69
|
+
backoffUntil = 0;
|
|
70
|
+
inFlight = null;
|
|
71
|
+
lastLoggedError = null;
|
|
72
|
+
}
|
|
73
|
+
export function resetDependenciesForTests() {
|
|
74
|
+
blockersProvider = null;
|
|
75
|
+
linearFetcher = null;
|
|
76
|
+
cacheTtlMs = DEFAULT_CACHE_TTL_MS;
|
|
77
|
+
fetchTimeoutMs = DEFAULT_FETCH_TIMEOUT_MS;
|
|
78
|
+
failureBackoffMs = FAILURE_BACKOFF_MS;
|
|
79
|
+
clearBlockerCacheForTests();
|
|
80
|
+
}
|
|
81
|
+
/** Only a Linear-sourced issue carries declared dependencies; manual issues are a no-op. */
|
|
82
|
+
export function isDependencyTracked(issue) {
|
|
83
|
+
return issue.source === "linear" && Boolean(issue.externalId);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The default provider: one batched Linear query per `admitNext()` that finds a free slot,
|
|
87
|
+
* memoized per issue id for `cacheTtlMs`.
|
|
88
|
+
*
|
|
89
|
+
* **No stale-while-error.** An entry past its TTL is dropped rather than reused when the
|
|
90
|
+
* refresh fails — a dependency declared *during* a Linear outage would otherwise be
|
|
91
|
+
* invisible for as long as the outage lasts, which is exactly the doomed run this prevents.
|
|
92
|
+
*/
|
|
93
|
+
export async function blockersFor(issues) {
|
|
94
|
+
const ids = [...new Set(issues.filter(isDependencyTracked).map((i) => i.externalId))];
|
|
95
|
+
const snapshot = new Map();
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const stale = [];
|
|
98
|
+
// Expired rows are never served, so drop them outright — otherwise every Linear issue ever
|
|
99
|
+
// queued stays in the map for the life of the process.
|
|
100
|
+
for (const [id, hit] of cache)
|
|
101
|
+
if (now - hit.at >= cacheTtlMs)
|
|
102
|
+
cache.delete(id);
|
|
103
|
+
for (const id of ids) {
|
|
104
|
+
const hit = cache.get(id);
|
|
105
|
+
if (hit)
|
|
106
|
+
snapshot.set(id, hit.blockers);
|
|
107
|
+
else
|
|
108
|
+
stale.push(id);
|
|
109
|
+
}
|
|
110
|
+
if (stale.length === 0)
|
|
111
|
+
return snapshot;
|
|
112
|
+
if (now < backoffUntil)
|
|
113
|
+
throw new Error("blocker fetch backing off after a recent failure");
|
|
114
|
+
// Ticks overlap (the 3s loop plus a Manual Start): the second one parks rather than opening
|
|
115
|
+
// a second connection to an API that is, by definition, slower than a tick right now.
|
|
116
|
+
if (inFlight)
|
|
117
|
+
throw new Error("blocker fetch already in flight");
|
|
118
|
+
// Throwing (rather than falling back to the expired rows) is what makes the TTL a hard
|
|
119
|
+
// edge: the caller turns a throw into "unavailable", never into "no blockers".
|
|
120
|
+
const pending = withTimeout((linearFetcher ?? defaultLinearFetcher)(stale), fetchTimeoutMs + TIMEOUT_GRACE_MS);
|
|
121
|
+
inFlight = pending;
|
|
122
|
+
let fetched;
|
|
123
|
+
try {
|
|
124
|
+
fetched = await pending;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
backoffUntil = Date.now() + failureBackoffMs;
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (inFlight === pending)
|
|
132
|
+
inFlight = null;
|
|
133
|
+
}
|
|
134
|
+
backoffUntil = 0;
|
|
135
|
+
lastLoggedError = null; // a later recurrence is news again
|
|
136
|
+
const fetchedAt = Date.now();
|
|
137
|
+
for (const id of stale) {
|
|
138
|
+
const blockers = fetched.get(id);
|
|
139
|
+
// An id Linear did not return is unknowable, not unblocked: leave it out of the
|
|
140
|
+
// snapshot (→ unavailable) and out of the cache so the next tick retries it.
|
|
141
|
+
if (!blockers) {
|
|
142
|
+
cache.delete(id);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
cache.set(id, { at: fetchedAt, blockers });
|
|
146
|
+
snapshot.set(id, blockers);
|
|
147
|
+
}
|
|
148
|
+
return snapshot;
|
|
149
|
+
}
|
|
150
|
+
function defaultLinearFetcher(issueIds) {
|
|
151
|
+
return fetchLinearBlockers(issueIds, { timeoutMs: fetchTimeoutMs });
|
|
152
|
+
}
|
|
153
|
+
function withTimeout(promise, ms) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const timer = setTimeout(() => reject(new Error(`blocker fetch timed out after ${ms}ms`)), ms);
|
|
156
|
+
promise.then((value) => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
resolve(value);
|
|
159
|
+
}, (err) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* What `admitNext()` calls: the active provider, hard-bounded in time and guaranteed not to
|
|
167
|
+
* throw into the coordinator tick. Zero Linear traffic when nothing Linear-sourced is queued
|
|
168
|
+
* (and none at all on a tick with no free slot — `admitNext` returns before reaching here).
|
|
169
|
+
*/
|
|
170
|
+
export async function blockerSnapshotFor(issues) {
|
|
171
|
+
const tracked = issues.filter(isDependencyTracked);
|
|
172
|
+
if (tracked.length === 0)
|
|
173
|
+
return EMPTY_SNAPSHOT;
|
|
174
|
+
try {
|
|
175
|
+
// Backstop over the batch's own bound, so an injected provider cannot stall the tick either.
|
|
176
|
+
return await withTimeout((blockersProvider ?? blockersFor)(tracked), fetchTimeoutMs + 2 * TIMEOUT_GRACE_MS);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
// The operator only ever sees the one constant reason, so the *cause* — missing API key,
|
|
180
|
+
// an id Linear does not know, HTTP 500, timeout — has to reach the log or a permanent
|
|
181
|
+
// misconfiguration is indistinguishable from a passing blip. Logged on change only, the
|
|
182
|
+
// same discipline wait_reason uses: an outage is one line, not one per 3s tick.
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
184
|
+
if (message !== lastLoggedError) {
|
|
185
|
+
lastLoggedError = message;
|
|
186
|
+
console.error("[coordinator] blocker fetch", err);
|
|
187
|
+
}
|
|
188
|
+
// Fail closed *and* park: an empty snapshot marks every Linear-sourced entry unavailable,
|
|
189
|
+
// which writes a wait reason and leaves the entry `queued`. Nothing is dequeued, failed
|
|
190
|
+
// or canceled, and the next successful fetch admits it with no operator action.
|
|
191
|
+
return EMPTY_SNAPSHOT;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Linear state types that mean "this work will never land in a form we must wait for". */
|
|
195
|
+
const SATISFIED_STATE_TYPES = new Set(["completed", "canceled"]);
|
|
196
|
+
/**
|
|
197
|
+
* Decision 2. Resolve the blocker against `issues.external_id`:
|
|
198
|
+
*
|
|
199
|
+
* - **dealer-linked** → satisfied only once the dealer issue is `done`, i.e. the PR actually
|
|
200
|
+
* merged. Linear status alone is not enough; issues get marked Done at PR-approval time,
|
|
201
|
+
* before the code lands, which is precisely the doomed run this rule prevents. A ticket may
|
|
202
|
+
* hold several dealer passes (NOT-141); the live one decides, terminal ones only when none
|
|
203
|
+
* is live.
|
|
204
|
+
* - **unlinked** → satisfied when the Linear state type is `completed` or `canceled`.
|
|
205
|
+
*
|
|
206
|
+
* Once a blocker resolves to a dealer issue, dealer status is the *only* answer — a Linear
|
|
207
|
+
* `canceled` on a ticket dealer is still developing would admit the dependent onto a base
|
|
208
|
+
* without that work. Canceling remains decision 6's escape hatch for a blocker dealer never
|
|
209
|
+
* picked up; for one it did, the fix is to drop the `blocks` relation.
|
|
210
|
+
*/
|
|
211
|
+
export function blockerVerdict(blocker) {
|
|
212
|
+
// NOT-141: a ticket may have several dealer passes (newest first) once a terminal one no
|
|
213
|
+
// longer blocks a re-import. The *live* pass is authoritative whenever there is one: a
|
|
214
|
+
// regression or follow-up pass reopened on a merged ticket is work the dependent must
|
|
215
|
+
// still wait for, exactly as for a first pass. Only when every pass has ended do the
|
|
216
|
+
// historical ones answer — and a merged one stays merged, so an abandoned follow-up
|
|
217
|
+
// (`closed`, never landed) cannot un-satisfy a blocker whose code is already in.
|
|
218
|
+
const dealerPasses = blocker.id ? listIssuesByExternalId("linear", blocker.id) : [];
|
|
219
|
+
if (dealerPasses.length > 0) {
|
|
220
|
+
const active = dealerPasses.find((issue) => !isTerminalIssueStatus(issue.status));
|
|
221
|
+
// The dealer status is the honest answer to "why am I still waiting" — a blocker
|
|
222
|
+
// sitting at `waiting on NOT-123 (Done)` reads like a bug. An active pass is never
|
|
223
|
+
// `done`, so naming it is always naming the pass that still has to land.
|
|
224
|
+
if (active)
|
|
225
|
+
return { satisfied: false, state: active.status };
|
|
226
|
+
const merged = dealerPasses.find((issue) => issue.status === "done");
|
|
227
|
+
return { satisfied: merged != null, state: (merged ?? dealerPasses[0]).status };
|
|
228
|
+
}
|
|
229
|
+
return { satisfied: SATISFIED_STATE_TYPES.has(blocker.stateType), state: blocker.stateName };
|
|
230
|
+
}
|
|
231
|
+
/** `waiting on NOT-123 (In Progress)` — names every unsatisfied blocker and its state. */
|
|
232
|
+
export function unsatisfiedBlockerReason(blockers) {
|
|
233
|
+
const unsatisfied = blockers
|
|
234
|
+
.map((b) => ({ blocker: b, verdict: blockerVerdict(b) }))
|
|
235
|
+
.filter((x) => !x.verdict.satisfied);
|
|
236
|
+
if (unsatisfied.length === 0)
|
|
237
|
+
return null;
|
|
238
|
+
return `waiting on ${unsatisfied
|
|
239
|
+
.map(({ blocker, verdict }) => `${blocker.identifier} (${verdict.state})`)
|
|
240
|
+
.join(", ")}`;
|
|
241
|
+
}
|
|
242
|
+
export const DEPENDENCY_STATE_UNAVAILABLE = "dependency state unavailable";
|