@delegance/claude-autopilot 5.2.1 → 5.5.2

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +49 -17
  3. package/dist/src/adapters/council/claude.js +2 -1
  4. package/dist/src/adapters/council/openai.js +2 -1
  5. package/dist/src/adapters/deploy/generic.d.ts +39 -0
  6. package/dist/src/adapters/deploy/generic.js +98 -0
  7. package/dist/src/adapters/deploy/index.d.ts +13 -0
  8. package/dist/src/adapters/deploy/index.js +45 -0
  9. package/dist/src/adapters/deploy/types.d.ts +157 -0
  10. package/dist/src/adapters/deploy/types.js +15 -0
  11. package/dist/src/adapters/deploy/vercel.d.ts +127 -0
  12. package/dist/src/adapters/deploy/vercel.js +446 -0
  13. package/dist/src/adapters/review-engine/claude.js +2 -1
  14. package/dist/src/adapters/review-engine/codex.js +2 -1
  15. package/dist/src/adapters/review-engine/gemini.js +2 -1
  16. package/dist/src/adapters/review-engine/openai-compatible.js +2 -1
  17. package/dist/src/adapters/sdk-loader.d.ts +15 -0
  18. package/dist/src/adapters/sdk-loader.js +77 -0
  19. package/dist/src/cli/costs.js +4 -2
  20. package/dist/src/cli/deploy.d.ts +71 -0
  21. package/dist/src/cli/deploy.js +514 -0
  22. package/dist/src/cli/index.js +91 -3
  23. package/dist/src/cli/pr.js +8 -2
  24. package/dist/src/cli/preflight.js +76 -1
  25. package/dist/src/core/config/schema.d.ts +34 -0
  26. package/dist/src/core/config/schema.js +18 -0
  27. package/dist/src/core/config/types.d.ts +6 -0
  28. package/dist/src/core/errors.d.ts +1 -1
  29. package/dist/src/core/errors.js +1 -0
  30. package/dist/src/core/migrate/detector-rules.js +6 -0
  31. package/dist/src/core/migrate/schema-validator.js +7 -0
  32. package/dist/src/core/persist/cost-log.js +8 -0
  33. package/package.json +8 -5
  34. package/scripts/autoregress.ts +2 -1
  35. package/skills/migrate/SKILL.md +193 -47
package/CHANGELOG.md CHANGED
@@ -1,5 +1,102 @@
1
1
  # Changelog
2
2
 
3
+ ## v5.3.0 — Deploy phase (in flight, not yet shipped)
4
+
5
+ ### Added
6
+
7
+ - **`deploy` phase** — adapter-agnostic deploy step that runs your existing deploy command, extracts the URL from stdout, optionally polls a `healthCheckUrl`, and (optionally) posts result to a PR. Closes the loop from "PR merged" to "PR merged + deployed + smoke-tested + URL on the PR".
8
+ - **`deployCommand` + `healthCheckUrl` config keys** — anything that works in your terminal works as `deployCommand` (`vercel --prod`, `flyctl deploy`, `kubectl apply`, `gh workflow run`, `make deploy`).
9
+ - **`claude-autopilot deploy [--dry-run|--command|--health-url|--pr <n>]`** — CLI surface. PR comment integration via `gh pr comment`.
10
+
11
+ First-class provider adapters (Vercel/Fly/Render with API-level deploy IDs + rollback hooks) are queued for v5.4.
12
+
13
+ ## v5.2.2 — Demo polish
14
+
15
+ ### Fixed
16
+
17
+ - **Cost log skips zero-token entries.** Setup-flow scans, dry-runs, and no-findings paths were polluting the log with empty rows that drowned real review entries in `claude-autopilot costs` output.
18
+ - **`costs` shows scope.** Output now explicitly notes "per-project — scoped to `<cwd>/.guardrail-cache/costs.jsonl`" so users understand it's not a global aggregate.
19
+ - **`pr` no longer hard-fails on missing config.** First-run on a fresh repo now auto-detects + prints a remediation line pointing at `setup`.
20
+
21
+ ### Added
22
+
23
+ - **DEMO.md committed at repo root.** Real end-to-end pipeline run on randai-johnson (multi-file Python integration, 12 min wall clock, $2.20 spend, 5 new tests, zero manual intervention). Linkable from external docs / pitch material.
24
+
25
+ ## v5.2.1 — Stress-test polish
26
+
27
+ ### Fixed
28
+
29
+ - **venv detection in tests phase.** `pytest -q` now resolves to `<project>/.venv/bin/pytest` (or `venv/`, `env/`) when present, so `claude-autopilot pr` no longer reports "tests failed" on Python repos with venv-installed pytest.
30
+ - **`autoregress` 100% broken on global install** — the bridge resolved `SCRIPT` to `dist/scripts/autoregress.ts` under the compiled layout, but `scripts/` ships at the package root. Every invocation threw `ERR_MODULE_NOT_FOUND`. Now uses `findPackageRoot` + existence check.
31
+ - **Council in python preset.** Python preset now ships a commented `council:` template (mirrors the generic preset). Out-of-the-box `init --preset python` no longer requires manual schema discovery.
32
+ - **Regression-lane fixture top-level await.** CI workflow's `npx tsx -e "..."` blocks wrapped in `async () => {...}` so esbuild's CJS output accepts them. Plus expected-ledger.json updated to match v5.2.0's new version format.
33
+
34
+ ## v5.0.8 — Line extraction + fix gate
35
+
36
+ ### Fixed
37
+
38
+ - **Parser extracts "line N" / "on line N" / "at line N" from prose** when not adjacent to a file ref. Previously findings shipped with file but no line, so `fix --dry-run` reported "no fixable findings" on a non-empty findings list.
39
+ - **`fix` distinguishes actionable (file present) from fixable (file + line).** Dry-run surfaces actionable findings even when line-less, with a clear message about why the LLM-fix loop can't act on them.
40
+
41
+ ## v5.0.7 — File backfill + cost ledger consolidation
42
+
43
+ ### Fixed
44
+
45
+ - **Single-file scan unconditionally backfills the file path.** The 5.0.6 fallback only triggered on `<unspecified>`, so prose-noise like `"n.r"` slipped through and broke `fix`.
46
+ - **`pr-desc` and `council` now persist to the cost ledger.** Previously only `scan` and `run` were tracked, so `claude-autopilot costs` showed misleadingly low totals after multi-call sessions.
47
+ - **Single-letter code extensions removed from bare-reference parser** (c/d/h/m/r/s) — they still match when backtick-wrapped, but bare matches like "n.r" no longer slip through.
48
+ - **`appendCostLog` swallows write errors centrally.** Cost log is observability, not a contract — a read-only FS or full disk no longer crashes commands that already succeeded.
49
+
50
+ ## v5.0.6 — Setup YAML + branch fallback
51
+
52
+ ### Fixed
53
+
54
+ - **`setup` no longer writes duplicate `testCommand` keys.** Several presets (go, python, python-fastapi, rails-postgres) ship with their own `testCommand:` line; `cli/setup.ts` was unconditionally appending another, producing invalid YAML that hard-failed every command after `setup` on those stacks.
55
+ - **Single-file scan backfills file path** (initial fix; superseded by v5.0.7's unconditional version).
56
+ - **Branch-derived PR titles default to `chore:` for unknown prefixes.** `autopilot-test/validate-weights` → `chore: validate weights` instead of `autopilot test validate weights` (which fails commitlint).
57
+
58
+ ## v5.0.5 — Python detect + parser hardening
59
+
60
+ ### Added
61
+
62
+ - **`presets/python/`** — general Python config (pytest, ruff, hardcoded-secrets, common protected paths). Detector now picks it for any `pyproject.toml` or `requirements.txt` without FastAPI signals (was falling through to the JS/Generic preset).
63
+
64
+ ### Fixed
65
+
66
+ - **Parser rejects "e.g" / "i.e" / "etc" prose as file refs.** The prior regex `\.[a-z]{1,6}` accepted any 1-6 letter suffix, so prose like "(e.g. dict, list)" was matched. Bare references now require a known code-file extension.
67
+ - **`pr-desc` real titles.** Prompt now explicitly asks for a Title line; parser falls through to a branch-derived conventional-commit title (`fix/cost-tracker` → `fix: cost tracker`), then first summary bullet, then `chore: update` only as a last resort.
68
+ - **`runReviewOnTestFail` default flipped to `true`.** Failed/missing test commands no longer silently kill the LLM review phase. Strict gating still available via explicit `false`.
69
+
70
+ ## v5.0.4 — Council Responses API
71
+
72
+ ### Fixed
73
+
74
+ - **Council 404s on `gpt-5.3-codex` resolved.** Codex variants and o-series reasoning models are Responses-API-only — the council adapter only used `client.chat.completions`. Now branches by model name (`/codex|^o[1-9]|^gpt-5\.3-/`) to use `client.responses.create()` for those models. Fixes the multi-model differentiator for any user with only `OPENAI_API_KEY`.
75
+ - **Generic preset ships a working council template.**
76
+
77
+ ## v5.0.3 — Cost tracker
78
+
79
+ ### Fixed
80
+
81
+ - **Codex adapter computes `costUSD`** (was returning `usage` without a cost field, so every codex run logged $0).
82
+ - **`scan` now persists to cost log** (was only `run` writing entries).
83
+
84
+ ## v5.0.2 — Post-install friction
85
+
86
+ ### Fixed
87
+
88
+ - **preflight `tsx` false-positive eliminated.** Every fresh global install reported `✗ tsx available` blocker because the bundled tsx wasn't checked. Now uses `findPackageRoot(import.meta.url)`.
89
+ - **Top-level `unhandledRejection` + `uncaughtException` handlers** format `GuardrailError` as a single-line red message instead of a Node stack trace. `CLAUDE_AUTOPILOT_DEBUG=1` re-enables stack.
90
+ - **Tarball trimmed:** dropped `src/` + `*.map` from `files` array → 319 files / 182 kB packed (was 726 / 382 kB), -56% / -52%.
91
+ - **Stale strings:** `@alpha` install hint → `@latest`; `npx guardrail run` blocker text → `claude-autopilot run`; init deprecation banner removed (both verbs work).
92
+
93
+ ## v5.0.1 — Types + tombstone
94
+
95
+ ### Fixed
96
+
97
+ - **Ships `dist/src/index.d.ts`** for TypeScript consumers.
98
+ - **Tombstone `@delegance/guardrail` package** publishes a forwarder pointing at the renamed package; pre-rename versions deprecated with migration message.
99
+
3
100
  ## v5.2.0 — Migrate skill generalization
4
101
 
5
102
  ### Added
package/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # @delegance/claude-autopilot
2
2
 
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![GitHub](https://img.shields.io/badge/GitHub-axledbetter%2Fclaude--autopilot-181717?logo=github)](https://github.com/axledbetter/claude-autopilot) [![npm](https://img.shields.io/npm/v/@delegance/claude-autopilot.svg)](https://www.npmjs.com/package/@delegance/claude-autopilot)
4
+
3
5
  **Autonomous development pipeline for Claude Code. Brainstorm → spec → plan → implement → migrate → validate → PR → review → merge — all from your terminal, on your codebase, with your test suite.**
4
6
 
7
+ **Open source, MIT-licensed, runs on your machine with your API keys.** No hosted agent, no per-seat subscription — `npm install -g @delegance/claude-autopilot` and you're done.
8
+
5
9
  ```bash
6
10
  claude-autopilot brainstorm "add SSO with SAML for enterprise tenants"
7
11
  # → writes spec (reviewed by Codex) → writes plan (reviewed by Codex) →
@@ -13,6 +17,8 @@ claude-autopilot brainstorm "add SSO with SAML for enterprise tenants"
13
17
 
14
18
  *No hosted agent. No per-seat subscription. Runs locally on your machine, against your real repo, using your API keys. Every phase is a Claude Code skill you can intervene in, rewire, or run by itself.*
15
19
 
20
+ **See it work end-to-end:** [DEMO.md](DEMO.md) — one real autonomous run on a Python codebase. 12 minutes wall clock, $2.20 spend, 5 new tests, multi-file integration, zero manual intervention. Honest about what's bounded today.
21
+
16
22
  ---
17
23
 
18
24
  ## Benchmark
@@ -29,24 +35,31 @@ Every finding came with a concrete remediation (often a code patch or named libr
29
35
 
30
36
  ## Why this vs the alternatives
31
37
 
32
- AI coding tools fall into three buckets. Here's where claude-autopilot sits.
33
-
34
- | Tool | Shape | Hosted? | Model lock-in | Pipeline structure | You can intervene mid-flow? |
38
+ | Tool | Where code lives | Pricing model | Models | Pipeline | Intervenable? |
35
39
  |---|---|---|---|---|---|
36
- | **Devin** (Cognition) | Autonomous agent | Yes (SaaS, $500/mo) | Cognition's stack | Opaque | No — watch a dashboard |
37
- | **GitHub Copilot Workspace** | Spec plan PR | Yes | Copilot only | Fixed, non-extensible | Edit the plan, that's it |
38
- | **Factory Droids** | Multi-agent workflow | Yes (per-seat) | Factory's stack | Fixed | Limited |
39
- | **Cursor BugBot / Copilot Review / CodeRabbit** | Async PR reviewer | Yes | Vendor's model | Single phase (review only) | N/A — post-hoc only |
40
- | **Aider / Cline / Cursor agent mode** | Interactive pair programming | Local | User's choice | None single-shot prompts | Continuous |
41
- | **OpenHands / SWE-agent** | Open-ended agent framework | Local | User's choice | None — agent decides | Rare, research-grade |
42
- | **claude-autopilot** | **Opinionated local pipeline** | **Local** | **Any LLM (Claude / GPT / Gemini / Groq / Ollama)** | **Fixed but rewireable, skill-per-phase** | **Every phase. All state on disk.** |
40
+ | **Devin** (Cognition) | Hosted sandbox | Per-ACU (cloud markup) | Cognition's stack | Opaque | No — dashboard only |
41
+ | **Factory Droids** | Hosted | Per-task + seat | Factory's stack | Fixed | Limited |
42
+ | **GitHub Copilot Workspace** | GitHub-hosted | Per-seat ($) | Copilot only | Fixed, non-extensible | Edit the plan |
43
+ | **Cursor / Copilot agent mode** | Local IDE | Per-seat ($) | Vendor's model | None single-shot | Continuous |
44
+ | **Cursor BugBot / CodeRabbit** | Hosted | Per-PR or seat | Vendor's model | Review only | Post-hoc |
45
+ | **Aider / Cline** | Local CLI | Free + your API key | User's choice | None | Continuous |
46
+ | **OpenHands / SWE-agent** | Local research | Free | User's choice | Agent decides | Rare |
47
+ | **claude-autopilot** | **Local CLI, your repo** | **Free + your existing Claude subscription** | **Multi-model per role (Claude + Codex + Gemini)** | **Skill-per-phase, rewireable** | **Every phase, all state on disk** |
48
+
49
+ Three things only this product gives you:
50
+
51
+ 1. **Multi-model council.** Same design question goes to Claude + Codex + Gemini in parallel; a fourth model synthesizes the consensus. Different blind spots, different recommendations, one merged answer. **No other tool dispatches multi-model on a per-decision basis.**
52
+ 2. **Your code never leaves your machine.** No cloud sandbox. No SaaS markup. The `git push` that happens at the end is from your laptop. For private repos, regulated industries, or anyone who doesn't want their unfinished code on someone else's servers — this is the only autonomous-agent shape that fits.
53
+ 3. **Ships as a Claude Code skill, not a competing IDE.** `/brainstorm`, `/autopilot`, `/migrate`, `/validate` are first-class Claude Code commands. As Claude Code grows, autopilot rides that adoption. You don't switch tools to use it; it's already there.
54
+
55
+ Plus the four practical differences:
43
56
 
44
- The architectural differences that matter most in practice:
57
+ - **Multi-model by role.** Claude writes code, Codex reviews the plan, bugbot triages PR findings. Swap any of them.
58
+ - **Your stack, not a sandbox.** Runs your `npm test`, your `prisma migrate`, your `gh pr create`. If it works in your terminal, it works in the pipeline.
59
+ - **Phase artifacts on disk, editable.** Every phase writes to a file you can open — `docs/specs/*.md`, `docs/plans/*.md`, a branch, a PR. Stop, edit by hand, resume, or re-run any phase in isolation.
60
+ - **Test-gated auto-revert.** `claude-autopilot fix --verify` patches a file, runs your tests, reverts on failure. Built into the CLI, not a wrapper.
45
61
 
46
- 1. **Multi-model by design.** Claude writes code, Codex reviews the plan, bugbot triages PR findings. Different model for each role, swap any of them. The pipeline's phases are explicit contracts, not one opaque API call.
47
- 2. **Your stack, not a sandbox.** It runs your `npm test`, your `prisma migrate`, your `gh pr create`, your `ruff check`. If it works in your terminal, it works in the pipeline.
48
- 3. **Phase artifacts on disk, editable.** Every phase writes to a file you can open — `docs/specs/*.md`, `docs/plans/*.md`, a branch, a PR. Stop, edit by hand, resume, or re-run any phase in isolation.
49
- 4. **Test-gated auto-revert as a first-class command.** `claude-autopilot fix --verify` patches a file, runs your full test suite, and reverts on failure. Built into the CLI, not a wrapper you write yourself.
62
+ **Real numbers from a real run:** [DEMO.md](DEMO.md) autonomous multi-file change on a Python codebase, **12 minutes, $2.20, zero manual intervention.**
50
63
 
51
64
  ## 30-second quickstart
52
65
 
@@ -86,7 +99,22 @@ Each phase is a Claude Code skill (`.claude/skills/<name>/SKILL.md`). You can in
86
99
 
87
100
  ### Migrate phase
88
101
 
89
- Configure your migration tool in `.autopilot/stack.md`. The pipeline reads stack.md, dispatches to the configured skill (`migrate@1` for generic; `migrate.supabase@1` for rich Supabase ledger; `none@1` to skip), and runs your tool with full safety: structured argv (no shell injection), 4-flag CI prod gate, hash-chained audit log. Run `claude-autopilot init` to auto-detect your stack. See [docs/skills/rich-migrate-contract.md](docs/skills/rich-migrate-contract.md) for the skill contract and [docs/skills/version-compatibility.md](docs/skills/version-compatibility.md) for the version model.
102
+ Configure your migration tool in `.autopilot/stack.md`. The pipeline reads stack.md, dispatches to the configured skill (`migrate@1` for generic; `migrate.supabase@1` for rich Supabase ledger; `none@1` to skip), and runs your tool with full safety: structured argv (no shell injection), 4-flag CI prod gate, hash-chained audit log. Run `claude-autopilot init` to auto-detect your stack — the detector recognizes Rails, Alembic, Django, Prisma, Drizzle, golang-migrate, dbmate, flyway, supabase-cli, ecto, typeorm, and falls back to a "configure manually" path. See [docs/skills/rich-migrate-contract.md](docs/skills/rich-migrate-contract.md) for the skill contract and [docs/skills/version-compatibility.md](docs/skills/version-compatibility.md) for the version model.
103
+
104
+ Generic example (Rails):
105
+
106
+ ```yaml
107
+ migrate:
108
+ skill: "migrate@1"
109
+ envs:
110
+ dev:
111
+ command: { exec: "rails", args: ["db:migrate"] }
112
+ env_file: ".env.development"
113
+ prod:
114
+ command: { exec: "rails", args: ["db:migrate", "RAILS_ENV=production"] }
115
+ ```
116
+
117
+ See `skills/migrate/SKILL.md` for examples covering Alembic, Django, Prisma, Drizzle, golang-migrate, dbmate, flyway, and custom scripts.
90
118
 
91
119
  ## What's distinctive
92
120
 
@@ -315,6 +343,10 @@ ANTHROPIC_API_KEY=sk-ant-... claude-autopilot scan --all
315
343
 
316
344
  We do not claim 13/13 reflects every real-world repo — it's a reproducible upper bound on a fixture that exercises the categories we explicitly target.
317
345
 
346
+ ## Contributing
347
+
348
+ Issues and PRs welcome — https://github.com/axledbetter/claude-autopilot/issues. The pipeline literally builds itself; many features in this repo were implemented by autopilot running against autopilot ([DEMO.md](DEMO.md) walks through six self-eat PRs with cost trajectory $10 → ~$2.50). Read [CONTRIBUTING.md](CONTRIBUTING.md) if it exists, otherwise: clone, `npm install`, `npm test`, open a PR.
349
+
318
350
  ## License
319
351
 
320
- MIT
352
+ MIT — see [LICENSE](LICENSE).
@@ -1,6 +1,6 @@
1
- import Anthropic from '@anthropic-ai/sdk';
2
1
  import { GuardrailError } from "../../core/errors.js";
3
2
  import { classifyError } from "../review-engine/prompt-builder.js";
3
+ import { loadAnthropic } from "../sdk-loader.js";
4
4
  const SYSTEM_PROMPT = `You are a technical advisor reviewing a software design decision. Evaluate the provided context and question critically. Be direct and specific. Surface tradeoffs, risks, and your recommendation.`;
5
5
  const MAX_OUTPUT_TOKENS = 2048;
6
6
  // Default Opus 4.7 rates — env override for other models.
@@ -14,6 +14,7 @@ export function makeClaudeCouncilAdapter(model, label) {
14
14
  if (!apiKey) {
15
15
  throw new GuardrailError('ANTHROPIC_API_KEY not set', { code: 'auth', provider: 'claude' });
16
16
  }
17
+ const Anthropic = await loadAnthropic();
17
18
  const client = new Anthropic({ apiKey });
18
19
  let response;
19
20
  try {
@@ -1,6 +1,6 @@
1
- import OpenAI from 'openai';
2
1
  import { GuardrailError } from "../../core/errors.js";
3
2
  import { classifyError } from "../review-engine/prompt-builder.js";
3
+ import { loadOpenAI } from "../sdk-loader.js";
4
4
  const SYSTEM_PROMPT = `You are a technical advisor reviewing a software design decision. Evaluate the provided context and question critically. Be direct and specific. Surface tradeoffs, risks, and your recommendation.`;
5
5
  const MAX_OUTPUT_TOKENS = 2048;
6
6
  // Models that ONLY work via the Responses API (not chat.completions).
@@ -25,6 +25,7 @@ export function makeOpenAICouncilAdapter(model, label) {
25
25
  if (!apiKey) {
26
26
  throw new GuardrailError('OPENAI_API_KEY not set', { code: 'auth', provider: 'openai' });
27
27
  }
28
+ const OpenAI = await loadOpenAI();
28
29
  const client = new OpenAI({ apiKey });
29
30
  const userInput = `## Context\n\n${context}\n\n## Question\n\n${prompt}`;
30
31
  try {
@@ -0,0 +1,39 @@
1
+ import type { ChildProcessByStdio } from 'node:child_process';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ import type { DeployAdapter, DeployInput, DeployResult } from './types.ts';
4
+ /**
5
+ * Function signature for the spawn dependency. Tests inject a fake spawn that
6
+ * emits canned stdout/exit events without touching the real OS process API.
7
+ */
8
+ export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
9
+ shell: boolean | string;
10
+ signal?: AbortSignal;
11
+ }) => ChildProcessByStdio<Writable | null, Readable | null, Readable | null>;
12
+ export interface GenericDeployAdapterOptions {
13
+ /** Free-form shell command (e.g. `vercel --prod`). Required. */
14
+ deployCommand: string;
15
+ /** Optional health-check URL — accepted for forward-compat with Phase 4; unused in Phase 1. */
16
+ healthCheckUrl?: string;
17
+ /** Injected spawn implementation (defaults to `node:child_process` spawn). */
18
+ spawnImpl?: SpawnFn;
19
+ /** When true, suppress teeing child stdout/stderr to the parent's process streams. Tests pass true. */
20
+ quiet?: boolean;
21
+ /** Wall-clock source. Tests pass a controllable counter. */
22
+ nowImpl?: () => number;
23
+ }
24
+ /**
25
+ * Generic shell-command deploy adapter.
26
+ *
27
+ * Captures stdout, looks for the first http(s) URL, returns it as `deployUrl`.
28
+ * Exit code 0 → `pass`; non-zero → `fail`.
29
+ */
30
+ export declare class GenericDeployAdapter implements DeployAdapter {
31
+ readonly name = "generic";
32
+ private readonly deployCommand;
33
+ private readonly spawnImpl;
34
+ private readonly quiet;
35
+ private readonly now;
36
+ constructor(opts: GenericDeployAdapterOptions);
37
+ deploy(input: DeployInput): Promise<DeployResult>;
38
+ }
39
+ //# sourceMappingURL=generic.d.ts.map
@@ -0,0 +1,98 @@
1
+ // src/adapters/deploy/generic.ts
2
+ //
3
+ // Generic deploy adapter — runs an arbitrary shell command and reports back.
4
+ //
5
+ // Wraps the v5.3 "deployCommand" approach as a DeployAdapter so the same
6
+ // CLI surface (`claude-autopilot deploy`) works whether you have a Vercel
7
+ // project, a Fly app with `flyctl deploy`, a custom `make deploy`, or anything
8
+ // else that prints a URL to stdout.
9
+ //
10
+ // `status()` and `rollback()` are deliberately omitted — without platform-API
11
+ // state we have no way to answer "is the build still going" or "promote the
12
+ // previous deploy". Callers that need those can switch to a platform adapter.
13
+ import { spawn as defaultSpawn } from 'node:child_process';
14
+ import { GuardrailError } from "../../core/errors.js";
15
+ const URL_RE = /https?:\/\/[^\s)>"']+/i;
16
+ /**
17
+ * Generic shell-command deploy adapter.
18
+ *
19
+ * Captures stdout, looks for the first http(s) URL, returns it as `deployUrl`.
20
+ * Exit code 0 → `pass`; non-zero → `fail`.
21
+ */
22
+ export class GenericDeployAdapter {
23
+ name = 'generic';
24
+ deployCommand;
25
+ spawnImpl;
26
+ quiet;
27
+ now;
28
+ constructor(opts) {
29
+ if (!opts.deployCommand || opts.deployCommand.trim() === '') {
30
+ throw new GuardrailError('Generic deploy adapter requires `deployCommand`', { code: 'invalid_config', provider: 'generic' });
31
+ }
32
+ this.deployCommand = opts.deployCommand;
33
+ this.spawnImpl = opts.spawnImpl ?? defaultSpawn;
34
+ this.quiet = opts.quiet ?? process.env.AUTOPILOT_DEPLOY_QUIET === '1';
35
+ this.now = opts.nowImpl ?? Date.now;
36
+ }
37
+ async deploy(input) {
38
+ const start = this.now();
39
+ return new Promise((resolve, reject) => {
40
+ let stdoutBuf = '';
41
+ let stderrBuf = '';
42
+ const child = this.spawnImpl(this.deployCommand, [], {
43
+ shell: true,
44
+ signal: input.signal,
45
+ });
46
+ child.stdout?.on('data', (chunk) => {
47
+ const s = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
48
+ stdoutBuf += s;
49
+ if (!this.quiet)
50
+ process.stdout.write(s);
51
+ });
52
+ child.stderr?.on('data', (chunk) => {
53
+ const s = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
54
+ stderrBuf += s;
55
+ if (!this.quiet)
56
+ process.stderr.write(s);
57
+ });
58
+ child.on('error', (err) => {
59
+ // AbortError from a passed signal — surface as an aborted in-progress
60
+ // result rather than a hard reject.
61
+ if (err.name === 'AbortError') {
62
+ resolve({
63
+ status: 'in-progress',
64
+ durationMs: this.now() - start,
65
+ output: 'Deploy aborted by caller.',
66
+ });
67
+ return;
68
+ }
69
+ reject(new GuardrailError(`Generic deploy adapter failed to spawn: ${err.message}`, { code: 'adapter_bug', provider: 'generic', details: { errno: err.code } }));
70
+ });
71
+ child.on('close', (code) => {
72
+ const durationMs = this.now() - start;
73
+ const tail = lastNLines(stdoutBuf + stderrBuf, 20);
74
+ if (code === 0) {
75
+ const match = stdoutBuf.match(URL_RE);
76
+ resolve({
77
+ status: 'pass',
78
+ deployUrl: match?.[0],
79
+ durationMs,
80
+ output: tail,
81
+ });
82
+ }
83
+ else {
84
+ resolve({
85
+ status: 'fail',
86
+ durationMs,
87
+ output: tail,
88
+ });
89
+ }
90
+ });
91
+ });
92
+ }
93
+ }
94
+ function lastNLines(s, n) {
95
+ const lines = s.split(/\r?\n/);
96
+ return lines.slice(Math.max(0, lines.length - n)).join('\n');
97
+ }
98
+ //# sourceMappingURL=generic.js.map
@@ -0,0 +1,13 @@
1
+ import type { DeployAdapter, DeployConfig } from './types.ts';
2
+ export * from './types.ts';
3
+ export { VercelDeployAdapter } from './vercel.ts';
4
+ export { GenericDeployAdapter } from './generic.ts';
5
+ /**
6
+ * Construct the right deploy adapter for the supplied config.
7
+ *
8
+ * Throws `GuardrailError` (code: invalid_config) when required adapter-specific
9
+ * fields are missing — failing fast at construction beats silently dropping
10
+ * the deploy step.
11
+ */
12
+ export declare function createDeployAdapter(config: DeployConfig): DeployAdapter;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,45 @@
1
+ // src/adapters/deploy/index.ts
2
+ //
3
+ // Public surface for the deploy-adapter package + factory.
4
+ import { GuardrailError } from "../../core/errors.js";
5
+ import { GenericDeployAdapter } from "./generic.js";
6
+ import { VercelDeployAdapter } from "./vercel.js";
7
+ export * from "./types.js";
8
+ export { VercelDeployAdapter } from "./vercel.js";
9
+ export { GenericDeployAdapter } from "./generic.js";
10
+ /**
11
+ * Construct the right deploy adapter for the supplied config.
12
+ *
13
+ * Throws `GuardrailError` (code: invalid_config) when required adapter-specific
14
+ * fields are missing — failing fast at construction beats silently dropping
15
+ * the deploy step.
16
+ */
17
+ export function createDeployAdapter(config) {
18
+ switch (config.adapter) {
19
+ case 'vercel': {
20
+ if (!config.project) {
21
+ throw new GuardrailError('deploy.adapter=vercel requires deploy.project (Vercel project ID or slug)', { code: 'invalid_config', provider: 'vercel' });
22
+ }
23
+ return new VercelDeployAdapter({
24
+ project: config.project,
25
+ team: config.team,
26
+ target: config.target,
27
+ });
28
+ }
29
+ case 'generic': {
30
+ if (!config.deployCommand) {
31
+ throw new GuardrailError('deploy.adapter=generic requires deploy.deployCommand (shell command)', { code: 'invalid_config', provider: 'generic' });
32
+ }
33
+ return new GenericDeployAdapter({
34
+ deployCommand: config.deployCommand,
35
+ healthCheckUrl: config.healthCheckUrl,
36
+ });
37
+ }
38
+ default: {
39
+ // exhaustiveness guard — TS narrows config.adapter to never here
40
+ const exhaustive = config.adapter;
41
+ throw new GuardrailError(`Unknown deploy adapter: ${String(exhaustive)}`, { code: 'invalid_config' });
42
+ }
43
+ }
44
+ }
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Common input to a deploy operation.
3
+ *
4
+ * Adapters are free to ignore fields they don't need. The Vercel adapter uses
5
+ * `commitSha` (and the env-derived git source) to choose what to build; the
6
+ * generic adapter uses neither — it just runs the configured deploy command.
7
+ */
8
+ export interface DeployInput {
9
+ /** Symbolic git ref (branch / tag). Optional — adapters fall back to the configured target. */
10
+ ref?: string;
11
+ /** Specific commit SHA to deploy. Takes precedence over `ref` when both are set. */
12
+ commitSha?: string;
13
+ /** Free-form metadata propagated to the platform when supported (Vercel attaches as deployment meta). */
14
+ meta?: Record<string, string>;
15
+ /** Abort signal — adapters MUST honor this for any in-flight HTTP / spawn work. */
16
+ signal?: AbortSignal;
17
+ /**
18
+ * Fired exactly once with the platform-native deploy ID as soon as it's
19
+ * known. Adapters that obtain the ID synchronously (Vercel returns it from
20
+ * the create-deployment POST) MUST call this immediately after the POST
21
+ * resolves but before polling begins. Adapters with no discrete ID (the
22
+ * generic shell adapter) do NOT call it.
23
+ *
24
+ * Consumers use this to start side-channel work in parallel with the
25
+ * deploy — most notably log streaming via `--watch`.
26
+ */
27
+ onDeployStart?: (deployId: string) => void;
28
+ }
29
+ /**
30
+ * Outcome of a deploy operation.
31
+ *
32
+ * `status: 'in-progress'` is reserved for the case where polling timed out
33
+ * before the platform reached a terminal state — the deploy may still finish
34
+ * later. The adapter does NOT auto-resume in Phase 1; the caller can re-poll
35
+ * via `status({ deployId })`.
36
+ */
37
+ export interface DeployResult {
38
+ status: 'pass' | 'fail' | 'in-progress';
39
+ /** Adapter-native deploy ID. Vercel uses `dpl_xxx`. Empty for generic when stdout has no extractable URL. */
40
+ deployId?: string;
41
+ /** Public URL of the deploy (e.g. `https://my-app-abc.vercel.app`). */
42
+ deployUrl?: string;
43
+ /** URL to the build logs / dashboard for human follow-up. */
44
+ buildLogsUrl?: string;
45
+ /** Wall-clock duration of the adapter call, in milliseconds. */
46
+ durationMs: number;
47
+ /** Human-readable summary suitable for the PR comment (last 50 log lines, status line, etc.). */
48
+ output?: string;
49
+ /** Populated when the adapter auto-rolled back to a previous deploy. Phase 3+. */
50
+ rolledBackTo?: string;
51
+ }
52
+ /**
53
+ * Input to a one-shot status query (no polling). Used by the future
54
+ * `claude-autopilot deploy status <id>` CLI subcommand and by the polling
55
+ * loop inside `deploy()`.
56
+ */
57
+ export interface DeployStatusInput {
58
+ deployId: string;
59
+ signal?: AbortSignal;
60
+ }
61
+ /**
62
+ * Result of a one-shot status query. Same shape as DeployResult with the
63
+ * deployId required for traceability. Adapters that don't support status
64
+ * (e.g. generic) leave the `status` method unimplemented.
65
+ */
66
+ export interface DeployStatusResult extends Omit<DeployResult, 'deployId'> {
67
+ deployId: string;
68
+ }
69
+ /**
70
+ * Input to a rollback operation. Reserved for Phase 3.
71
+ *
72
+ * `to` is optional: when omitted the adapter rolls back to the previous
73
+ * production deploy (looked up via the platform API).
74
+ */
75
+ export interface DeployRollbackInput {
76
+ /** Specific deploy ID to roll back to. When omitted, the previous prod deploy is used. */
77
+ to?: string;
78
+ signal?: AbortSignal;
79
+ }
80
+ /**
81
+ * Input to a one-shot log-streaming subscription.
82
+ *
83
+ * Returned `AsyncIterable` yields `DeployLogLine`s as the platform emits
84
+ * them. Consumers iterate with `for await ... of`. Cancellation is via the
85
+ * `signal` — once aborted, the underlying transport is torn down and the
86
+ * iterator finishes (or throws `AbortError`, depending on adapter).
87
+ */
88
+ export interface DeployStreamLogsInput {
89
+ deployId: string;
90
+ signal?: AbortSignal;
91
+ }
92
+ /**
93
+ * A single log line surfaced from the platform.
94
+ *
95
+ * Fields beyond `timestamp` and `text` are best-effort — adapters populate
96
+ * what they have. Consumers MUST NOT rely on `level` or `source` being set.
97
+ */
98
+ export interface DeployLogLine {
99
+ /** Milliseconds since epoch — from the platform if provided, else when received locally. */
100
+ timestamp: number;
101
+ /** Build phase or component (e.g. 'build', 'deploy'). Optional. */
102
+ source?: string;
103
+ /** 'info' | 'warn' | 'error' | 'stdout' | 'stderr' — adapter-defined. Optional. */
104
+ level?: string;
105
+ /** Log text, no trailing newline. */
106
+ text: string;
107
+ }
108
+ /**
109
+ * The DeployAdapter contract.
110
+ *
111
+ * `deploy` is required. `status` and `rollback` are optional so adapters that
112
+ * don't expose them (the generic shell adapter being the canonical example)
113
+ * can omit the methods rather than throwing at runtime.
114
+ */
115
+ export interface DeployAdapter {
116
+ /** Stable identifier — surfaced in CLI output and logs. */
117
+ readonly name: string;
118
+ deploy(input: DeployInput): Promise<DeployResult>;
119
+ status?(input: DeployStatusInput): Promise<DeployStatusResult>;
120
+ rollback?(input: DeployRollbackInput): Promise<DeployResult>;
121
+ /**
122
+ * Subscribe to real-time build logs. Optional — adapters without a
123
+ * platform API for log streaming (e.g. the generic shell adapter) omit
124
+ * this method, and the `undefined` is the canonical "not supported"
125
+ * signal for callers.
126
+ */
127
+ streamLogs?(input: DeployStreamLogsInput): AsyncIterable<DeployLogLine>;
128
+ }
129
+ /**
130
+ * Configuration block for the `deploy` phase. Lives under `deploy:` in
131
+ * `guardrail.config.yaml`.
132
+ *
133
+ * Fields are conditionally required based on `adapter`:
134
+ * - `vercel` requires `project`
135
+ * - `generic` requires `deployCommand`
136
+ *
137
+ * The factory in `./index.ts` enforces these rules at construction time.
138
+ */
139
+ export interface DeployConfig {
140
+ /** Which adapter to use. Phase 1 ships `vercel` + `generic`. */
141
+ adapter: 'vercel' | 'generic';
142
+ /** Vercel project ID or slug. Required when `adapter === 'vercel'`. */
143
+ project?: string;
144
+ /** Vercel team ID for team accounts. Optional. */
145
+ team?: string;
146
+ /** Deploy target. Default: `production`. */
147
+ target?: 'production' | 'preview';
148
+ /** Shell command to run for the deploy (e.g. `vercel --prod`). Required when `adapter === 'generic'`. */
149
+ deployCommand?: string;
150
+ /** Stream build logs to stderr in real time. Phase 2. */
151
+ watchBuildLogs?: boolean;
152
+ /** Auto-rollback triggers. Phase 3 / 4. */
153
+ rollbackOn?: Array<'healthCheckFailure' | 'smokeTestFailure'>;
154
+ /** URL polled after deploy succeeds to confirm app health. Used by both adapters once Phase 4 lands. */
155
+ healthCheckUrl?: string;
156
+ }
157
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,15 @@
1
+ // src/adapters/deploy/types.ts
2
+ //
3
+ // DeployAdapter contract — Phase 1 of the v5.4 Vercel adapter spec.
4
+ //
5
+ // A DeployAdapter abstracts over the "deploy this code somewhere" step of the
6
+ // pipeline. Adapters can be platform-specific (vercel, fly, render) or generic
7
+ // (a free-form shell command à la `vercel --prod` from v5.3).
8
+ //
9
+ // Phase 1 implements `deploy()` and `status()`. `rollback()` is reserved for
10
+ // Phase 3 and intentionally optional on the interface so generic adapters that
11
+ // don't support it can omit the method entirely.
12
+ //
13
+ // Spec: docs/specs/v5.4-vercel-adapter.md
14
+ export {};
15
+ //# sourceMappingURL=types.js.map