@nilsr0711/drydock 0.1.1 → 0.1.3
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +6 -3
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +20 -0
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/adrs/page.js +2 -2
- package/.next/standalone/.next/server/app/adrs/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/adrs/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/cost/export/route.js +1 -0
- package/.next/standalone/.next/server/app/api/cost/export/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/cost/export/route_client-reference-manifest.js +1 -0
- package/.next/standalone/.next/server/app/api/sse/dashboard/route.js +4 -0
- package/.next/standalone/.next/server/app/api/sse/dashboard/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/sse/dashboard/route_client-reference-manifest.js +1 -0
- package/.next/standalone/.next/server/app/api/sse/jobs/[id]/route.js +2 -2
- package/.next/standalone/.next/server/app/api/sse/jobs/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/webhooks/[repoId]/route.js +1 -0
- package/.next/standalone/.next/server/app/api/webhooks/[repoId]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/webhooks/[repoId]/route_client-reference-manifest.js +1 -0
- package/.next/standalone/.next/server/app/costs/page.js +2 -2
- package/.next/standalone/.next/server/app/costs/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/costs/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/jobs/[id]/page.js +5 -2
- package/.next/standalone/.next/server/app/jobs/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/jobs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/needs-human/page.js +2 -2
- package/.next/standalone/.next/server/app/needs-human/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/needs-human/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/prompts/page.js +2 -2
- package/.next/standalone/.next/server/app/prompts/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/prompts/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/repos/[id]/page.js +2 -2
- package/.next/standalone/.next/server/app/repos/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/repos/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +6 -3
- package/.next/standalone/.next/server/chunks/235.js +1 -0
- package/.next/standalone/.next/server/chunks/309.js +1 -0
- package/.next/standalone/.next/server/chunks/350.js +9 -0
- package/.next/standalone/.next/server/chunks/39.js +3 -0
- package/.next/standalone/.next/server/chunks/447.js +1 -0
- package/.next/standalone/.next/server/chunks/507.js +9 -0
- package/.next/standalone/.next/server/chunks/541.js +1 -0
- package/.next/standalone/.next/server/chunks/710.js +1 -0
- package/.next/standalone/.next/server/chunks/75.js +1 -0
- package/.next/standalone/.next/server/chunks/777.js +1 -0
- package/.next/standalone/.next/server/chunks/895.js +62 -0
- package/.next/standalone/.next/server/chunks/9.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0-539cfb6a78b205f0.js +1 -0
- package/.next/standalone/.next/static/chunks/66-a23941cbbf88d6cd.js +1 -0
- package/.next/standalone/.next/static/chunks/app/_global-error/page-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/app/adrs/{page-881f748d299c703e.js → page-54954f500bc3e3c6.js} +1 -1
- package/.next/standalone/.next/static/chunks/app/api/cost/export/route-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/app/api/sse/dashboard/route-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/app/api/sse/jobs/[id]/route-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/app/api/webhooks/[repoId]/route-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/app/costs/page-737c89e70adff98b.js +1 -0
- package/.next/standalone/.next/static/chunks/app/jobs/[id]/page-217e04f338b50a7e.js +1 -0
- package/.next/standalone/.next/static/chunks/app/layout-f6048a8c9f541e0d.js +1 -0
- package/.next/standalone/.next/static/chunks/app/needs-human/page-c047e3e842681979.js +1 -0
- package/.next/standalone/.next/static/chunks/app/page-382b7fef1f749214.js +1 -0
- package/.next/standalone/.next/static/chunks/app/prompts/page-9e32fb8f51e7873e.js +1 -0
- package/.next/standalone/.next/static/chunks/app/repos/[id]/page-55b03bdcf107eefe.js +1 -0
- package/.next/standalone/.next/static/chunks/app/settings/page-5b2b8683fa6afdf4.js +1 -0
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-abc1c30fe033f680.js +1 -0
- package/.next/standalone/.next/static/css/64af283f7f467bc0.css +3 -0
- package/.next/standalone/.next/static/g7t6i0qpogjrDGpmNgFei/_buildManifest.js +1 -0
- package/.next/standalone/drizzle/0013_true_red_wolf.sql +1 -0
- package/.next/standalone/drizzle/0014_dry_medusa.sql +1 -0
- package/.next/standalone/drizzle/0015_dapper_ben_grimm.sql +1 -0
- package/.next/standalone/drizzle/0016_charming_natasha_romanoff.sql +14 -0
- package/.next/standalone/drizzle/0017_free_warbound.sql +1 -0
- package/.next/standalone/drizzle/0018_confused_franklin_storm.sql +1 -0
- package/.next/standalone/drizzle/0019_uneven_meggan.sql +22 -0
- package/.next/standalone/drizzle/0020_chunky_meggan.sql +1 -0
- package/.next/standalone/drizzle/meta/0013_snapshot.json +1487 -0
- package/.next/standalone/drizzle/meta/0014_snapshot.json +1494 -0
- package/.next/standalone/drizzle/meta/0015_snapshot.json +1502 -0
- package/.next/standalone/drizzle/meta/0016_snapshot.json +1600 -0
- package/.next/standalone/drizzle/meta/0017_snapshot.json +1607 -0
- package/.next/standalone/drizzle/meta/0018_snapshot.json +1614 -0
- package/.next/standalone/drizzle/meta/0019_snapshot.json +1773 -0
- package/.next/standalone/drizzle/meta/0020_snapshot.json +1780 -0
- package/.next/standalone/drizzle/meta/_journal.json +56 -0
- package/.next/standalone/package.json +1 -1
- package/.next/static/chunks/0-539cfb6a78b205f0.js +1 -0
- package/.next/static/chunks/66-a23941cbbf88d6cd.js +1 -0
- package/.next/static/chunks/app/_global-error/page-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/app/adrs/{page-881f748d299c703e.js → page-54954f500bc3e3c6.js} +1 -1
- package/.next/static/chunks/app/api/cost/export/route-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/app/api/sse/dashboard/route-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/app/api/sse/jobs/[id]/route-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/app/api/webhooks/[repoId]/route-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/app/costs/page-737c89e70adff98b.js +1 -0
- package/.next/static/chunks/app/jobs/[id]/page-217e04f338b50a7e.js +1 -0
- package/.next/static/chunks/app/layout-f6048a8c9f541e0d.js +1 -0
- package/.next/static/chunks/app/needs-human/page-c047e3e842681979.js +1 -0
- package/.next/static/chunks/app/page-382b7fef1f749214.js +1 -0
- package/.next/static/chunks/app/prompts/page-9e32fb8f51e7873e.js +1 -0
- package/.next/static/chunks/app/repos/[id]/page-55b03bdcf107eefe.js +1 -0
- package/.next/static/chunks/app/settings/page-5b2b8683fa6afdf4.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-abc1c30fe033f680.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-abc1c30fe033f680.js +1 -0
- package/.next/static/css/64af283f7f467bc0.css +3 -0
- package/.next/static/g7t6i0qpogjrDGpmNgFei/_buildManifest.js +1 -0
- package/README.md +25 -12
- package/bin/drydock.mjs +132 -7
- package/drizzle/0013_true_red_wolf.sql +1 -0
- package/drizzle/0014_dry_medusa.sql +1 -0
- package/drizzle/0015_dapper_ben_grimm.sql +1 -0
- package/drizzle/0016_charming_natasha_romanoff.sql +14 -0
- package/drizzle/0017_free_warbound.sql +1 -0
- package/drizzle/0018_confused_franklin_storm.sql +1 -0
- package/drizzle/0019_uneven_meggan.sql +22 -0
- package/drizzle/0020_chunky_meggan.sql +1 -0
- package/drizzle/meta/0013_snapshot.json +1487 -0
- package/drizzle/meta/0014_snapshot.json +1494 -0
- package/drizzle/meta/0015_snapshot.json +1502 -0
- package/drizzle/meta/0016_snapshot.json +1600 -0
- package/drizzle/meta/0017_snapshot.json +1607 -0
- package/drizzle/meta/0018_snapshot.json +1614 -0
- package/drizzle/meta/0019_snapshot.json +1773 -0
- package/drizzle/meta/0020_snapshot.json +1780 -0
- package/drizzle/meta/_journal.json +56 -0
- package/package.json +1 -1
- package/.next/standalone/.next/server/chunks/152.js +0 -8
- package/.next/standalone/.next/server/chunks/21.js +0 -1
- package/.next/standalone/.next/server/chunks/441.js +0 -1
- package/.next/standalone/.next/server/chunks/633.js +0 -1
- package/.next/standalone/.next/server/chunks/706.js +0 -8
- package/.next/standalone/.next/server/chunks/753.js +0 -1
- package/.next/standalone/.next/server/chunks/774.js +0 -62
- package/.next/standalone/.next/server/chunks/859.js +0 -1
- package/.next/standalone/.next/static/chunks/602-2da35213db585d76.js +0 -1
- package/.next/standalone/.next/static/chunks/66-2a5c080641376f3a.js +0 -1
- package/.next/standalone/.next/static/chunks/app/_global-error/page-94dd6bc2d60a8443.js +0 -1
- package/.next/standalone/.next/static/chunks/app/api/sse/jobs/[id]/route-94dd6bc2d60a8443.js +0 -1
- package/.next/standalone/.next/static/chunks/app/costs/page-7aeb8fd8816090f7.js +0 -1
- package/.next/standalone/.next/static/chunks/app/jobs/[id]/page-23dddd5f27bc7c02.js +0 -1
- package/.next/standalone/.next/static/chunks/app/layout-6ad777543855c715.js +0 -1
- package/.next/standalone/.next/static/chunks/app/needs-human/page-abedbafec351380c.js +0 -1
- package/.next/standalone/.next/static/chunks/app/page-ebe54ab85fbcf147.js +0 -1
- package/.next/standalone/.next/static/chunks/app/prompts/page-3b9006a4513a173a.js +0 -1
- package/.next/standalone/.next/static/chunks/app/repos/[id]/page-16e031b6eab3ee05.js +0 -1
- package/.next/standalone/.next/static/chunks/app/settings/page-54d0967b6d9f734f.js +0 -1
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-94dd6bc2d60a8443.js +0 -1
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-94dd6bc2d60a8443.js +0 -1
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-94dd6bc2d60a8443.js +0 -1
- package/.next/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-94dd6bc2d60a8443.js +0 -1
- package/.next/standalone/.next/static/css/b4ec7103106db91b.css +0 -3
- package/.next/standalone/.next/static/iEqTV0p0ZoW-sr0JEYokX/_buildManifest.js +0 -1
- package/.next/static/chunks/602-2da35213db585d76.js +0 -1
- package/.next/static/chunks/66-2a5c080641376f3a.js +0 -1
- package/.next/static/chunks/app/_global-error/page-94dd6bc2d60a8443.js +0 -1
- package/.next/static/chunks/app/api/sse/jobs/[id]/route-94dd6bc2d60a8443.js +0 -1
- package/.next/static/chunks/app/costs/page-7aeb8fd8816090f7.js +0 -1
- package/.next/static/chunks/app/jobs/[id]/page-23dddd5f27bc7c02.js +0 -1
- package/.next/static/chunks/app/layout-6ad777543855c715.js +0 -1
- package/.next/static/chunks/app/needs-human/page-abedbafec351380c.js +0 -1
- package/.next/static/chunks/app/page-ebe54ab85fbcf147.js +0 -1
- package/.next/static/chunks/app/prompts/page-3b9006a4513a173a.js +0 -1
- package/.next/static/chunks/app/repos/[id]/page-16e031b6eab3ee05.js +0 -1
- package/.next/static/chunks/app/settings/page-54d0967b6d9f734f.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-94dd6bc2d60a8443.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-94dd6bc2d60a8443.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-94dd6bc2d60a8443.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-94dd6bc2d60a8443.js +0 -1
- package/.next/static/css/b4ec7103106db91b.css +0 -3
- package/.next/static/iEqTV0p0ZoW-sr0JEYokX/_buildManifest.js +0 -1
- /package/.next/standalone/.next/static/{iEqTV0p0ZoW-sr0JEYokX → g7t6i0qpogjrDGpmNgFei}/_ssgManifest.js +0 -0
- /package/.next/static/{iEqTV0p0ZoW-sr0JEYokX → g7t6i0qpogjrDGpmNgFei}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ It's the difference between *driving* an agent and *operating a dock* of them.
|
|
|
71
71
|
|
|
72
72
|
🛂 **Opt-in autonomous triage** — per repo, let Drydock label incoming issues (deterministic keyword classifier, whitelist-only output) and auto-process the ones that are *ready* and not blocked. Off by default; gated by author association for public repos, a per-issue attempt limit, and all the usual cost/concurrency limits. Never auto-merges.
|
|
73
73
|
|
|
74
|
-
🔧 **CI babysitting & auto-merge** — polls `gh pr checks`, merges on green, and on red resumes the session with a CI-fix prompt (up to **3 retries**), then files a follow-up issue and hands off.
|
|
74
|
+
🔧 **CI babysitting & auto-merge** — polls `gh pr checks`, merges on green, and on red resumes the session with a CI-fix prompt (up to **3 retries**), then files a follow-up issue and hands off. The failed log is classified by failure type (test, type error, lint, build, dependency, timeout, flaky) and reduced to a focused, line-capped evidence slice so the fix prompt targets the actual failure.
|
|
75
75
|
|
|
76
76
|
🩹 **Opt-in CI auto-heal** — per repo, turn the failure path into a structured classify → fix → verify loop: failing checks are bucketed (healable / external / flaky / unknown), only healable ones get a targeted fix, and each attempt is verified for a real, improving change. External and AI-review checks are never code-healed. Hard budgets (per-session and per-fingerprint attempts, a cooldown, and a concurrency cap) keep it bounded. Off by default; never auto-merges.
|
|
77
77
|
|
|
@@ -79,17 +79,29 @@ It's the difference between *driving* an agent and *operating a dock* of them.
|
|
|
79
79
|
|
|
80
80
|
🧩 **Opt-in issue decomposition** — per repo, split a large issue ("fix these 5 bugs", "implement X with A/B/C") into ordered, tracked subtasks. A deterministic heuristic handles GitHub task lists (`- [ ]`) and "Bug N —" headings for free; prose falls back to a one-shot agent. Decomposition is idempotent (keyed on the issue body hash, redone only when the body changes), subtasks are surfaced in the agent prompt and worked in order, and progress is reflected on the issue and in the UI. Off by default. See [ADR 020](docs/adr/020-issue-decomposition.md).
|
|
81
81
|
|
|
82
|
+
🔎 **Opt-in post-PR verification** — per repo, run a **read-only** pass right after a PR opens that checks whether the diff actually satisfies the issue and each decomposed subtask. A one-shot agent is given the issue, its subtasks, and the (length-capped) diff and returns a strict JSON verdict (`done` / `pending` / `deferred`) per subtask; the result updates subtask status and posts a verification summary flagging what remains. It runs in a throwaway dir with a tight timeout, and on any failure (non-zero exit, non-JSON output, exception) leaves status unchanged — never corrupting state. Off by default; never auto-merges. See [ADR 027](docs/adr/027-post-pr-verification.md).
|
|
83
|
+
|
|
82
84
|
🚀 **Opt-in post-merge deployment healing** — per repo, watch a merged PR's deployment via pluggable platform adapters (Vercel and Railway today; adding Netlify/Fly/Render is one adapter, no core changes). The platform is auto-detected from repo config or set explicitly. The merged commit's deployment is polled with bounded delay/interval/timeout budgets, and on failure the logs are captured and a follow-up **fix PR** is opened for a human to review. Sessions are surfaced in the repo's Deployments panel. Off by default; never auto-merges. See [ADR 021](docs/adr/021-post-merge-deployment-healing.md).
|
|
83
85
|
|
|
86
|
+
🏷️ **Opt-in release management** — per repo, extend autonomy past merge to shipping: evaluate the PRs merged since the last tag, decide whether a release is warranted and the semver bump, generate notes, and publish a release. The auto path is **idempotent** (one run per merge commit, never a duplicate release for a tag) and a failed run is retryable; a **dry-run preview** shows the proposed version and included PRs with no side effects; a **manual publish** forces a release through the same evaluation pipeline. Gated by both a global kill-switch and a per-repo opt-in, off by default; releases at the default-branch tip and never auto-merges. See [ADR 028](docs/adr/028-release-management.md).
|
|
87
|
+
|
|
84
88
|
⚖️ **Rate-limit budgeting** — a priority-aware governor meters every GitHub call: the background sweep runs at *low* priority and yields once the budget drops below a reserve fraction, while interactive actions stay *high*; a hard floor stops anything from draining the budget to zero, a 429 backs off until reset, and unchanged issue lists are fetched with conditional ETag requests so they cost nothing. See [ADR 018](docs/adr/018-rate-limit-governor.md).
|
|
85
89
|
|
|
90
|
+
💬 **Ask about this PR** — on a job's detail view, ask a free-text question ("why did this change X?", "is the failing test related?", "what's left to do?") and a **read-only** agent answers from a length-capped context bundle Drydock already has: PR metadata, check pass/fail state, a review-feedback summary, the recent activity log, and the PR diff. Each question is persisted with a visible lifecycle (`answering → answered | error`), scoped to the PR it was asked about, and empty or failed responses are recorded as an error rather than crashing.
|
|
91
|
+
|
|
92
|
+
📝 **Per-repo agent instructions** — give each watched repo free-text guidance (coding conventions, "always run `pnpm test`", "don't touch `legacy/`", preferred PR style) from the automation panel. The text is injected into the issue-work prompt as a dedicated, length-capped section, so you steer agent behavior per project without editing global prompts or code. Empty by default; an unset value leaves the prompt unchanged.
|
|
93
|
+
|
|
86
94
|
📡 **Live logs over SSE** — the agent's NDJSON output is parsed incrementally, persisted, and streamed to the browser in real time.
|
|
87
95
|
|
|
88
|
-
💸 **Cost tracking** — per-job and aggregate spend from the agent's reported `total_cost_usd` (or estimated from tokens), with a **daily cost limit** that gates the driver loop.
|
|
96
|
+
💸 **Cost tracking** — per-job and aggregate spend from the agent's reported `total_cost_usd` (or estimated from tokens), with a **daily cost limit** that gates the driver loop and an optional **per-job cost ceiling** that aborts a single runaway session mid-stream (global default + per-repo override; off when unset). Spend is **exportable** to CSV or JSON from the cost dashboard — per-job line items or aggregates by repo/model, scoped to a date range and repo, with totals that reconcile with the dashboard.
|
|
89
97
|
|
|
90
98
|
⏯️ **Global pause & per-repo controls** — pause everything from the navbar, pick an agent and model per repo, toggle serial vs. parallel processing, and customize the queue label.
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
🪝 **Webhook-driven issue sync** — opt in per repo to receive issue events instead of waiting for the next poll. Set a secret on a repo and Drydock exposes a signature-verified receiver (`/api/webhooks/<id>`); a validated GitHub/GitLab issue event triggers a targeted, debounced sync so new issues surface near-instantly. Polling stays on as the default fallback and shares the same idempotent reconcile, so a change is never double-processed. Since Drydock binds `127.0.0.1`, expose the URL through a tunnel (e.g. `cloudflared`, `ngrok`). See [ADR 029](docs/adr/029-webhook-issue-sync.md).
|
|
101
|
+
|
|
102
|
+
🔔 **External notifications** — get pinged on Telegram, Slack (incoming webhook) and email (SMTP) for the lifecycle events you care about (job needs human, job failed, PR opened, PR merged, release published, daily cost limit reached, automation paused/draining). Each channel is configured independently, every event has a per-event opt-in, and a one-click test button verifies setup. Delivery is best-effort and never blocks the loop; secrets are redacted from logs. See [ADR 024](docs/adr/024-external-notifications.md).
|
|
103
|
+
|
|
104
|
+
🆙 **Update-available notice** — a passive, dismissible navbar banner appears when a newer Drydock release is published. The check queries the latest stable GitHub release (drafts/prereleases skipped), is cached for an hour, and dedupes concurrent checks onto a single upstream call; any network or parse error advertises no update, so a transient hiccup never raises a false alarm. Global installs get a `drydock update` hint.
|
|
93
105
|
|
|
94
106
|
📐 **ADR review queue** — a file watcher surfaces new `docs/adr/*.md` decisions for approve/reject.
|
|
95
107
|
|
|
@@ -114,7 +126,8 @@ flowchart LR
|
|
|
114
126
|
|
|
115
127
|
A single orchestrator boots with the server process (`src/instrumentation.ts`). On start it
|
|
116
128
|
runs crash recovery (requeue orphaned `working` jobs, park CI-babysitting states as
|
|
117
|
-
`interrupted
|
|
129
|
+
`interrupted`, and reap orphaned git worktrees left by a hard crash) and installs
|
|
130
|
+
graceful-shutdown handlers. The **driver loop** atomically claims
|
|
118
131
|
the next eligible queued job with a lease (respecting per-repo priority, the daily cost limit,
|
|
119
132
|
the global pause, and serial-vs-parallel settings), heartbeats it while it runs, then releases
|
|
120
133
|
the lease once it settles.
|
|
@@ -178,7 +191,7 @@ add a repository, and start queuing issues.
|
|
|
178
191
|
drydock --help # all flags
|
|
179
192
|
drydock --version # installed version
|
|
180
193
|
drydock --port 8080 --host 0.0.0.0 # bind elsewhere (defaults: 127.0.0.1:3737)
|
|
181
|
-
drydock update # update a global install
|
|
194
|
+
drydock update # update a global install (reports current → latest, skips if already current)
|
|
182
195
|
```
|
|
183
196
|
|
|
184
197
|
You still need the `claude` and (for GitHub) `gh` CLIs on `PATH` — see [Requirements](#requirements).
|
|
@@ -235,8 +248,8 @@ Drydock is configured at runtime from the **Settings** page and per-repo control
|
|
|
235
248
|
¹ A source checkout (`pnpm dev`/`pnpm start`) defaults `DRYDOCK_DB` to `data/drydock.db` in the
|
|
236
249
|
project; the `drydock` launcher defaults it to `~/.drydock/drydock.db`.
|
|
237
250
|
|
|
238
|
-
**Settings (global):** pause switch · daily cost limit · log retention (days) · `claude`/`gh` CLI paths · notification channels (Telegram / Slack / email) and per-event opt-in.
|
|
239
|
-
**Per repo:** platform (GitHub / GitLab, with base URL + token for GitLab) · default model · serial vs. parallel processing · queue label (default `drydock:queue`).
|
|
251
|
+
**Settings (global):** pause switch · release management kill-switch (master on/off for the opt-in release pipeline) · daily cost limit · max job cost (per-job USD ceiling that aborts a runaway session mid-stream; 0 = off) · log retention (days) · max job minutes (per-agent session timeout) · max CI wait minutes (how long the babysitter waits for checks to settle before escalating to needs-human) · `claude`/`gh` CLI paths · notification channels (Telegram / Slack / email) and per-event opt-in.
|
|
252
|
+
**Per repo:** platform (GitHub / GitLab, with base URL + token for GitLab) · default model · serial vs. parallel processing · queue label (default `drydock:queue`) · optional job/CI timeout overrides.
|
|
240
253
|
|
|
241
254
|
## Screens
|
|
242
255
|
|
|
@@ -247,7 +260,7 @@ project; the `drydock` launcher defaults it to `~/.drydock/drydock.db`.
|
|
|
247
260
|
| `/jobs/[id]` | Job detail — live streaming log, cost & tokens |
|
|
248
261
|
| `/prompts` | Versioned prompt editor |
|
|
249
262
|
| `/adrs` | ADR review queue |
|
|
250
|
-
| `/costs` | Cost dashboard — daily, by model, top jobs |
|
|
263
|
+
| `/costs` | Cost dashboard — daily, by model, top jobs, CSV/JSON export |
|
|
251
264
|
| `/settings` | Global settings |
|
|
252
265
|
|
|
253
266
|
## Project layout
|
|
@@ -348,10 +361,10 @@ UI: they refuse while draining, globally paused, or over the daily/per-repo cost
|
|
|
348
361
|
|
|
349
362
|
## Roadmap
|
|
350
363
|
|
|
351
|
-
- [
|
|
352
|
-
- [
|
|
353
|
-
- [
|
|
354
|
-
- [
|
|
364
|
+
- [x] Parallel multi-repo dashboards at a glance
|
|
365
|
+
- [x] Webhook-driven issue sync (vs. polling)
|
|
366
|
+
- [x] Richer CI failure classification & targeted fix prompts
|
|
367
|
+
- [x] Exportable cost reports
|
|
355
368
|
|
|
356
369
|
Have an idea? [Open an issue](https://github.com/NilsR0711/drydock/issues).
|
|
357
370
|
|
package/bin/drydock.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// step or TypeScript runtime so it works straight from the published tarball.
|
|
7
7
|
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
@@ -114,20 +114,122 @@ Options:
|
|
|
114
114
|
Data is stored in ~/.drydock (override with DRYDOCK_DATA_DIR); the database is
|
|
115
115
|
created and migrated automatically on first start.`;
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Classify how Drydock was installed from its package directory. Drives the
|
|
119
|
+
* in-dashboard update notice (issue #58): a global install can self-update via
|
|
120
|
+
* `drydock update`, whereas an npx run or a dev checkout cannot.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} packageRoot Absolute path to the installed package directory.
|
|
123
|
+
* @returns {"global" | "npx" | "local"}
|
|
124
|
+
*/
|
|
125
|
+
export function detectInstallKind(packageRoot) {
|
|
126
|
+
const normalized = packageRoot.replace(/\\/g, "/");
|
|
127
|
+
if (normalized.includes("/_npx/")) return "npx";
|
|
128
|
+
if (normalized.includes("/node_modules/")) return "global";
|
|
129
|
+
return "local";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The npm package name this CLI is published under. */
|
|
133
|
+
const PACKAGE_NAME = "@nilsr0711/drydock";
|
|
134
|
+
|
|
117
135
|
/** The command that updates a global install to the latest published version. */
|
|
118
136
|
export function updateCommand() {
|
|
119
|
-
return { command: "npm", args: ["install", "--global",
|
|
137
|
+
return { command: "npm", args: ["install", "--global", `${PACKAGE_NAME}@latest`] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compare two `x.y.z` versions (a leading `v` is tolerated). Returns 1 if `a` is
|
|
142
|
+
* newer, -1 if older, 0 if equal. Throws if either version is unparseable so
|
|
143
|
+
* callers can fall back rather than silently mis-ordering.
|
|
144
|
+
*/
|
|
145
|
+
export function compareVersions(a, b) {
|
|
146
|
+
const parse = (v) => {
|
|
147
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v).trim());
|
|
148
|
+
if (!m) throw new Error(`unparseable version: ${v}`);
|
|
149
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
150
|
+
};
|
|
151
|
+
const pa = parse(a);
|
|
152
|
+
const pb = parse(b);
|
|
153
|
+
for (let i = 0; i < 3; i++) {
|
|
154
|
+
if (pa[i] > pb[i]) return 1;
|
|
155
|
+
if (pa[i] < pb[i]) return -1;
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Decide what `drydock update` should do given the installed version and the
|
|
162
|
+
* latest published version (which may be null/unparseable when the check fails).
|
|
163
|
+
* Degrades gracefully: when the latest version can't be determined, still
|
|
164
|
+
* attempts the install rather than blocking the user.
|
|
165
|
+
*
|
|
166
|
+
* @returns {{ action: "skip" | "install", reason: string, latestVersion?: string }}
|
|
167
|
+
*/
|
|
168
|
+
export function planUpdate(currentVersion, latestVersion) {
|
|
169
|
+
if (!latestVersion) return { action: "install", reason: "unknown-latest" };
|
|
170
|
+
let cmp;
|
|
171
|
+
try {
|
|
172
|
+
cmp = compareVersions(latestVersion, currentVersion);
|
|
173
|
+
} catch {
|
|
174
|
+
return { action: "install", reason: "unknown-latest" };
|
|
175
|
+
}
|
|
176
|
+
if (cmp <= 0) return { action: "skip", reason: "up-to-date" };
|
|
177
|
+
return { action: "install", reason: "update-available", latestVersion };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve the latest published version from the npm registry's `latest`
|
|
182
|
+
* dist-tag. Never throws: returns null on any network/HTTP/parse failure so the
|
|
183
|
+
* caller can fall back to a blind install.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} pkg The (possibly scoped) package name.
|
|
186
|
+
* @param {{ fetchImpl?: typeof fetch }} [opts]
|
|
187
|
+
* @returns {Promise<string | null>}
|
|
188
|
+
*/
|
|
189
|
+
export async function resolveLatestVersion(pkg, { fetchImpl = fetch } = {}) {
|
|
190
|
+
try {
|
|
191
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`;
|
|
192
|
+
const res = await fetchImpl(url, {
|
|
193
|
+
headers: { Accept: "application/json", "User-Agent": "drydock-update" },
|
|
194
|
+
});
|
|
195
|
+
if (!res.ok) return null;
|
|
196
|
+
const body = await res.json();
|
|
197
|
+
return typeof body?.version === "string" ? body.version : null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
120
201
|
}
|
|
121
202
|
|
|
122
203
|
/** Run the self-update, inheriting stdio so npm's progress is visible. */
|
|
123
|
-
function runUpdate() {
|
|
204
|
+
async function runUpdate() {
|
|
205
|
+
const current = readVersion();
|
|
206
|
+
console.error(`Current version: drydock v${current}`);
|
|
207
|
+
|
|
208
|
+
const latest = await resolveLatestVersion(PACKAGE_NAME);
|
|
209
|
+
const plan = planUpdate(current, latest);
|
|
210
|
+
|
|
211
|
+
if (plan.action === "skip") {
|
|
212
|
+
console.error(`drydock is already up to date (v${current}).`);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (plan.reason === "unknown-latest") {
|
|
217
|
+
console.error("Could not determine the latest version; attempting update anyway…");
|
|
218
|
+
} else {
|
|
219
|
+
console.error(`Updating drydock ${current} → ${plan.latestVersion} …`);
|
|
220
|
+
}
|
|
221
|
+
|
|
124
222
|
const { command, args } = updateCommand();
|
|
125
|
-
console.error(`Updating drydock: ${command} ${args.join(" ")}`);
|
|
126
223
|
const child = spawn(command, args, {
|
|
127
224
|
shell: process.platform === "win32",
|
|
128
225
|
stdio: "inherit",
|
|
129
226
|
});
|
|
130
|
-
child.on("exit", (code) =>
|
|
227
|
+
child.on("exit", (code) => {
|
|
228
|
+
if (code === 0 && plan.reason === "update-available") {
|
|
229
|
+
console.error(`Updated to drydock v${plan.latestVersion}.`);
|
|
230
|
+
}
|
|
231
|
+
process.exit(code ?? 0);
|
|
232
|
+
});
|
|
131
233
|
child.on("error", (err) => {
|
|
132
234
|
console.error(`Update failed: ${err.message}`);
|
|
133
235
|
process.exit(1);
|
|
@@ -185,6 +287,10 @@ async function serve({ host, port, open }) {
|
|
|
185
287
|
PORT: String(port),
|
|
186
288
|
DRYDOCK_DB: resolveDbPath(),
|
|
187
289
|
DRYDOCK_MIGRATIONS: join(PACKAGE_ROOT, "drizzle"),
|
|
290
|
+
// Surface the running version and install kind to the dashboard so it can
|
|
291
|
+
// show an "update available" notice without bundling package.json (#58).
|
|
292
|
+
DRYDOCK_VERSION: readVersion(),
|
|
293
|
+
DRYDOCK_INSTALL_KIND: detectInstallKind(PACKAGE_ROOT),
|
|
188
294
|
};
|
|
189
295
|
|
|
190
296
|
const url = `http://${host}:${port}`;
|
|
@@ -225,15 +331,34 @@ async function main(argv) {
|
|
|
225
331
|
console.log(readVersion());
|
|
226
332
|
return;
|
|
227
333
|
case "update":
|
|
228
|
-
runUpdate();
|
|
334
|
+
await runUpdate();
|
|
229
335
|
return;
|
|
230
336
|
default:
|
|
231
337
|
await serve(directive);
|
|
232
338
|
}
|
|
233
339
|
}
|
|
234
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Decide whether this module is the program entry point. A plain string compare
|
|
343
|
+
* against `process.argv[1]` breaks for global/npx installs, where the bin is a
|
|
344
|
+
* symlink in a bin directory and argv[1] is that link rather than the real file.
|
|
345
|
+
* Resolving the entry path through symlinks (realpath) makes the two comparable,
|
|
346
|
+
* so `drydock`, `drydock --version`, etc. actually run when installed.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} modulePath Absolute path to this module (resolved import.meta.url).
|
|
349
|
+
* @param {string | undefined} entryPath The invoked entry path (process.argv[1]).
|
|
350
|
+
*/
|
|
351
|
+
export function isMainModule(modulePath, entryPath) {
|
|
352
|
+
if (!entryPath) return false;
|
|
353
|
+
try {
|
|
354
|
+
return modulePath === realpathSync(entryPath);
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
235
360
|
// Only run when executed directly, not when imported by tests.
|
|
236
|
-
if (
|
|
361
|
+
if (isMainModule(fileURLToPath(import.meta.url), process.argv[1])) {
|
|
237
362
|
main(process.argv.slice(2)).catch((err) => {
|
|
238
363
|
console.error(err instanceof Error ? err.message : String(err));
|
|
239
364
|
process.exit(1);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `repos` ADD `max_job_minutes` integer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `repos` ADD `max_ci_wait_minutes` integer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `repos` ADD `verify_pr` integer DEFAULT false NOT NULL;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE `pr_questions` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`job_id` integer NOT NULL,
|
|
4
|
+
`pr_number` integer NOT NULL,
|
|
5
|
+
`question` text NOT NULL,
|
|
6
|
+
`answer` text,
|
|
7
|
+
`status` text DEFAULT 'answering' NOT NULL,
|
|
8
|
+
`error_message` text,
|
|
9
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
10
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
11
|
+
FOREIGN KEY (`job_id`) REFERENCES `jobs`(`id`) ON UPDATE no action ON DELETE cascade
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE INDEX `pr_questions_job_idx` ON `pr_questions` (`job_id`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `repos` ADD `agent_instructions` text;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `repos` ADD `max_job_cost_usd` real;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
CREATE TABLE `release_runs` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`repo_id` integer NOT NULL,
|
|
4
|
+
`mode` text DEFAULT 'auto' NOT NULL,
|
|
5
|
+
`trigger_pr_number` integer,
|
|
6
|
+
`trigger_sha` text,
|
|
7
|
+
`status` text DEFAULT 'detected' NOT NULL,
|
|
8
|
+
`bump` text,
|
|
9
|
+
`from_tag` text,
|
|
10
|
+
`tag` text,
|
|
11
|
+
`title` text,
|
|
12
|
+
`notes` text,
|
|
13
|
+
`pr_numbers` text DEFAULT '[]' NOT NULL,
|
|
14
|
+
`error_message` text,
|
|
15
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
16
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
17
|
+
FOREIGN KEY (`repo_id`) REFERENCES `repos`(`id`) ON UPDATE no action ON DELETE cascade
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE INDEX `release_runs_repo_idx` ON `release_runs` (`repo_id`);--> statement-breakpoint
|
|
21
|
+
CREATE UNIQUE INDEX `release_runs_trigger_unique` ON `release_runs` (`repo_id`,`trigger_sha`) WHERE "release_runs"."trigger_sha" is not null;--> statement-breakpoint
|
|
22
|
+
ALTER TABLE `repos` ADD `release_enabled` integer DEFAULT false NOT NULL;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `repos` ADD `webhook_secret` text;
|