@delegance/claude-autopilot 5.0.8 → 5.2.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +5 -1
  3. package/dist/src/cli/index.js +130 -2
  4. package/dist/src/cli/init-migrate.d.ts +35 -0
  5. package/dist/src/cli/init-migrate.js +299 -0
  6. package/dist/src/cli/migrate-doctor.d.ts +19 -0
  7. package/dist/src/cli/migrate-doctor.js +191 -0
  8. package/dist/src/core/migrate/alias-resolver.d.ts +18 -0
  9. package/dist/src/core/migrate/alias-resolver.js +150 -0
  10. package/dist/src/core/migrate/audit-log.d.ts +30 -0
  11. package/dist/src/core/migrate/audit-log.js +100 -0
  12. package/dist/src/core/migrate/contract.d.ts +27 -0
  13. package/dist/src/core/migrate/contract.js +35 -0
  14. package/dist/src/core/migrate/detector-rules.d.ts +26 -0
  15. package/dist/src/core/migrate/detector-rules.js +147 -0
  16. package/dist/src/core/migrate/detector.d.ts +16 -0
  17. package/dist/src/core/migrate/detector.js +105 -0
  18. package/dist/src/core/migrate/dispatcher.d.ts +19 -0
  19. package/dist/src/core/migrate/dispatcher.js +358 -0
  20. package/dist/src/core/migrate/doctor-checks.d.ts +19 -0
  21. package/dist/src/core/migrate/doctor-checks.js +304 -0
  22. package/dist/src/core/migrate/envelope.d.ts +25 -0
  23. package/dist/src/core/migrate/envelope.js +84 -0
  24. package/dist/src/core/migrate/executor.d.ts +33 -0
  25. package/dist/src/core/migrate/executor.js +102 -0
  26. package/dist/src/core/migrate/handshake.d.ts +17 -0
  27. package/dist/src/core/migrate/handshake.js +130 -0
  28. package/dist/src/core/migrate/migrator.d.ts +34 -0
  29. package/dist/src/core/migrate/migrator.js +302 -0
  30. package/dist/src/core/migrate/monorepo.d.ts +2 -0
  31. package/dist/src/core/migrate/monorepo.js +114 -0
  32. package/dist/src/core/migrate/policy-enforcer.d.ts +28 -0
  33. package/dist/src/core/migrate/policy-enforcer.js +111 -0
  34. package/dist/src/core/migrate/result-parser.d.ts +16 -0
  35. package/dist/src/core/migrate/result-parser.js +152 -0
  36. package/dist/src/core/migrate/schema-validator.d.ts +11 -0
  37. package/dist/src/core/migrate/schema-validator.js +103 -0
  38. package/dist/src/core/migrate/types.d.ts +49 -0
  39. package/dist/src/core/migrate/types.js +3 -0
  40. package/package.json +5 -1
  41. package/presets/aliases.lock.json +20 -0
  42. package/presets/schemas/migrate.schema.json +134 -0
  43. package/skills/autopilot/SKILL.md +29 -9
  44. package/skills/migrate/skill.manifest.json +7 -0
  45. package/skills/migrate-none/SKILL.md +40 -0
  46. package/skills/migrate-none/skill.manifest.json +7 -0
  47. package/skills/migrate-supabase/SKILL.md +126 -0
  48. package/skills/migrate-supabase/skill.manifest.json +7 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## v5.2.0 — Migrate skill generalization
4
+
5
+ ### Added
6
+
7
+ - **Generalized migrate phase** — `migrate@1` (thin orchestrator), `migrate.supabase@1` (rich Delegance runner, paths now parameterized via stack.md), `none@1` (no-op for `--skip-migrate`). Pipeline reads `.autopilot/stack.md` to dispatch the right skill.
8
+ - **Auto-detection at init** — `claude-autopilot init` walks the repo, sniffs for Prisma / Drizzle / Rails / Go-migrate / Flyway / dbmate / Alembic / Django / Ecto / TypeORM / Supabase signals, writes a stack.md non-interactively when there's a single high-confidence match. Prompts otherwise.
9
+ - **Stack.md schema validation** with security rules: shell metachars rejected in command args, env_file paths confined under project_root, dev_command-as-prod-command blocked.
10
+ - **Versioned alias map** (`presets/aliases.lock.json`) with stable IDs (`migrate@1`, `migrate.supabase@1`, `none@1`) so future renames don't break user configs.
11
+ - **Skill manifest version handshake** — every skill ships `skill.manifest.json` declaring `skill_runtime_api_version`, `min_runtime`, `max_runtime`. Dispatcher fails closed on incompatibility with explicit upgrade hints.
12
+ - **Hash-chained audit log** at `.autopilot/audit.log` (JSONL, monotonic seq + SHA-256 prev_hash chain) for every migrate dispatch. `claude-autopilot doctor` validates the chain.
13
+ - **Per-policy enforcement points** — `allow_prod_in_ci`, `require_clean_git`, `require_manual_approval`, `require_dry_run_first`. CI prod migrations require all 4 of: `--yes` flag, `AUTOPILOT_CI_POLICY=allow-prod`, `AUTOPILOT_TARGET_ENV=<env>`, stack.md `policy.allow_prod_in_ci: true`. Plus a recognized CI provider env (or `AUTOPILOT_CI_PROVIDER` override).
14
+ - **Structured argv execution** — commands stored as `{ exec, args[] }` and executed via `spawn(shell:false)`. No shell injection vector. Legacy string form deprecated, auto-migrated by `doctor --fix`.
15
+ - **`migrate doctor`** with read-only mode (default) and `--fix` mode for safe auto-corrections.
16
+ - **Monorepo support** — per-workspace `.autopilot/stack.md` plus root `.autopilot/manifest.yaml` for cross-workspace coordination.
17
+ - **Legacy migrate skill migrator** — automatically archives the existing `skills/migrate/` (legacy Delegance Supabase shape) to `skills/migrate.backup-<ISO>/` on `doctor --fix`, replaces with the thin shape.
18
+ - **Multi-CI provider detection** — GitHub Actions, GitLab CI, CircleCI, Buildkite, Jenkins recognized out of the box. `AUTOPILOT_CI_PROVIDER` override for self-hosted.
19
+ - **Delegance regression CI lane** — required GitHub Actions job that runs the full migrate-supabase flow against an anonymized fixture, asserting byte-for-byte ledger compatibility with pre-dispatcher behavior.
20
+
21
+ ### Changed
22
+
23
+ - `skills/autopilot/SKILL.md` Step 4 (Migrate) rewritten to describe the envelope-based dispatcher contract instead of invoking `/migrate` directly.
24
+
25
+ ### Backward-compat
26
+
27
+ - Delegance's existing `npx tsx scripts/supabase/migrate.ts` CLI invocation still works unchanged. The script now ALSO honors the autopilot dispatcher when invoked with `AUTOPILOT_ENVELOPE` + `AUTOPILOT_RESULT_PATH` env vars; falls back to legacy CLI parsing otherwise.
28
+ - The old `skills/migrate/` legacy SKILL.md is preserved (and will be auto-archived on first `doctor --fix` post-upgrade).
29
+
30
+ ### Migration guide for existing users
31
+
32
+ ```bash
33
+ # Upgrade
34
+ npm install -g @delegance/claude-autopilot@5.2.0
35
+
36
+ # Audit current state (read-only, exits non-zero if migration needed)
37
+ claude-autopilot doctor
38
+
39
+ # Apply auto-fixes (archives legacy /migrate skill, writes new stack.md)
40
+ claude-autopilot doctor --fix
41
+ ```
42
+
43
+ Existing `npx tsx scripts/supabase/migrate.ts <file> --env dev` workflows are unaffected.
44
+
3
45
  ## [5.0.0] — 2026-04-27
4
46
 
5
47
  First GA release after a five-alpha soak cycle. Promotes `5.0.0-alpha.5` to GA unchanged on the code side; the only diff is the version bump, README rebranding away from `@alpha` channel guidance, and a new "Reproducing the benchmark" section.
package/README.md CHANGED
@@ -78,12 +78,16 @@ Each phase is a Claude Code skill (`.claude/skills/<name>/SKILL.md`). You can in
78
78
  | **Plan** | `writing-plans` | Breaks spec into phased, checklist-shaped implementation plan | Claude |
79
79
  | **Plan review** | `codex-review` | Second model critiques the plan before you execute it | Codex / GPT-5 |
80
80
  | **Implement** | `subagent-driven-development` | Executes plan in a git worktree, one phase at a time, with per-phase tests | Claude |
81
- | **Migrate** | `migrate` | Runs database migrations dev → QA → prod with per-env validation | Deterministic |
81
+ | **Migrate** | `migrate` | Dispatches to the configured migration skill (see [Migrate phase](#migrate-phase)) — runs your migration tool dev → QA → prod with per-env validation | Deterministic |
82
82
  | **Validate** | `validate` | Static rules + tests + type check + security scan + LLM review | Any |
83
83
  | **PR** | `commit-push-pr` | Opens the PR with auto-generated title, summary, and test plan | Claude |
84
84
  | **Review** | `review-2pass` / `council` | Multi-model review of the diff (critical pass + informational pass) | Multiple |
85
85
  | **Triage** | `bugbot` | Fetches automated reviewer findings, auto-fixes real bugs, dismisses false positives | Claude |
86
86
 
87
+ ### Migrate phase
88
+
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.
90
+
87
91
  ## What's distinctive
88
92
 
89
93
  Features that are hard or impossible to find in the competitive set:
@@ -27,6 +27,9 @@ import { runWorker } from "./worker.js";
27
27
  import { runTestGen } from "./test-gen.js";
28
28
  import { runCouncilCmd } from "./council.js";
29
29
  import { runMigrateV4 } from "./migrate-v4.js";
30
+ import { runMigrateDoctor } from "./migrate-doctor.js";
31
+ import { initMigrate, NoMigrationToolDetectedError } from "./init-migrate.js";
32
+ import { dispatch as runMigrateDispatch } from "../core/migrate/dispatcher.js";
30
33
  import { findPackageRoot } from "./_pkg-root.js";
31
34
  import { GuardrailError } from "../core/errors.js";
32
35
  // Format unhandled errors as a one-line user-facing message instead of dumping a
@@ -156,7 +159,7 @@ These are aliases for the flat subcommands; they still work without the 'advance
156
159
  }
157
160
  args.shift(); // drop 'advanced'
158
161
  }
159
- const SUBCOMMANDS = ['init', 'run', 'scan', 'report', 'explain', 'ignore', 'ci', 'pr', 'fix', 'costs', 'watch', 'hook', 'autoregress', 'baseline', 'triage', 'lsp', 'worker', 'mcp', 'test-gen', 'pr-desc', 'doctor', 'preflight', 'setup', 'council', 'migrate-v4', 'brainstorm', 'help', '--help', '-h'];
162
+ const SUBCOMMANDS = ['init', 'run', 'scan', 'report', 'explain', 'ignore', 'ci', 'pr', 'fix', 'costs', 'watch', 'hook', 'autoregress', 'baseline', 'triage', 'lsp', 'worker', 'mcp', 'test-gen', 'pr-desc', 'doctor', 'preflight', 'setup', 'council', 'migrate-v4', 'migrate', 'migrate-doctor', 'brainstorm', 'help', '--help', '-h'];
160
163
  const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce', 'ask', 'focus', 'fail-on', 'note', 'reason', 'expires', 'profile', 'severity', 'prompt', 'context-file', 'path'];
161
164
  // Bare invocation — no subcommand, no flags → show welcome guide
162
165
  if (args.length === 0) {
@@ -210,6 +213,32 @@ function flag(name) {
210
213
  function boolFlag(name) {
211
214
  return args.includes(`--${name}`);
212
215
  }
216
+ /**
217
+ * Run the migrate-doctor with shared CLI formatting and exit handling.
218
+ *
219
+ * Both `migrate doctor` (two-word) and `migrate-doctor` (single-verb alias)
220
+ * resolve to this helper to keep their behavior locked together.
221
+ */
222
+ async function runMigrateDoctorCLI() {
223
+ const fix = args.includes('--fix');
224
+ const result = await runMigrateDoctor({ repoRoot: process.cwd(), fix });
225
+ for (const r of result.results) {
226
+ const mark = r.result.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
227
+ console.log(`${mark} ${r.name}${r.result.message ? ` — ${r.result.message}` : ''}`);
228
+ if (!r.result.ok && r.result.fixHint) {
229
+ console.log(` \x1b[2mhint: ${r.result.fixHint}\x1b[0m`);
230
+ }
231
+ }
232
+ if (result.mutations && result.mutations.length > 0) {
233
+ console.log(`\n\x1b[1mFixes applied:\x1b[0m`);
234
+ for (const m of result.mutations)
235
+ console.log(` - ${m}`);
236
+ }
237
+ if (result.migrationReportPath) {
238
+ console.log(`\n\x1b[2mMigration report: ${result.migrationReportPath}\x1b[0m`);
239
+ }
240
+ process.exit(result.allOk ? 0 : 1);
241
+ }
213
242
  function printUsage() {
214
243
  console.log(`
215
244
  Usage: claude-autopilot <command> [options] (legacy alias: guardrail)
@@ -225,7 +254,9 @@ Commands:
225
254
  fix Auto-fix cached findings using the configured LLM
226
255
  costs Show per-run cost summary
227
256
  ci Opinionated CI entrypoint (post comments + SARIF)
228
- init Scaffold guardrail.config.yaml from a preset
257
+ init Scaffold guardrail.config.yaml + auto-detect migrate stack (writes .autopilot/stack.md)
258
+ migrate Run database migrations via the stack-aware dispatcher
259
+ migrate doctor Validate .autopilot/stack.md and skill manifests (alias: migrate-doctor)
229
260
  setup Auto-detect stack, write config, install pre-push hook
230
261
  doctor Check prerequisites (alias: preflight)
231
262
  preflight Check prerequisites (alias: doctor)
@@ -280,6 +311,14 @@ Options (autoregress):
280
311
  --since <ref> Git ref for changed-files detection
281
312
  --snapshot <slug> Target a single snapshot
282
313
  --files <a,b,c> Explicit file list for generate (skips git detection)
314
+
315
+ Options (migrate):
316
+ --env <name> Target environment from .autopilot/stack.md (default: dev)
317
+ --dry-run Run skill in dry-run mode (no side effects)
318
+ --yes Required to apply prod migrations in CI
319
+
320
+ Options (migrate doctor / migrate-doctor):
321
+ --fix Apply auto-fixable mutations (legacy stack.md, skills/migrate/, schema_version)
283
322
  `);
284
323
  }
285
324
  switch (subcommand) {
@@ -310,6 +349,42 @@ switch (subcommand) {
310
349
  // `init` and `setup` are aliases. Keep both supported — no nag banner.
311
350
  const force = args.includes('--force');
312
351
  await runSetup({ force });
352
+ // After the existing init/setup logic, sniff for a migration tool and write
353
+ // .autopilot/stack.md. Non-interactive: high-confidence single matches are
354
+ // auto-selected; ambiguity / no-match downgrades to a TODO 'none@1' shape so
355
+ // we don't block the user. (Interactive prompts come from the autopilot skill,
356
+ // not the CLI.)
357
+ try {
358
+ const result = await initMigrate({
359
+ repoRoot: process.cwd(),
360
+ force,
361
+ });
362
+ for (const ws of result.workspaces) {
363
+ const rel = ws.workspace === process.cwd() ? '.' : ws.workspace;
364
+ console.log(`\x1b[2m[init-migrate] ${ws.action} ${rel}/.autopilot/stack.md (skill: ${ws.skill})\x1b[0m`);
365
+ }
366
+ }
367
+ catch (err) {
368
+ if (err instanceof NoMigrationToolDetectedError) {
369
+ // No high-confidence match — fall back to skipMigrate shape so the user
370
+ // can edit it later. This matches the auto-detection contract documented
371
+ // in the v5.2.0 CHANGELOG.
372
+ try {
373
+ await initMigrate({
374
+ repoRoot: process.cwd(),
375
+ force,
376
+ skipMigrate: true,
377
+ });
378
+ console.log(`\x1b[33m[init-migrate] No migration tool detected — wrote 'none@1' stack.md (edit .autopilot/stack.md to configure)\x1b[0m`);
379
+ }
380
+ catch (fallbackErr) {
381
+ console.error(`\x1b[31m[init-migrate] failed: ${fallbackErr.message}\x1b[0m`);
382
+ }
383
+ }
384
+ else {
385
+ console.error(`\x1b[31m[init-migrate] failed: ${err.message}\x1b[0m`);
386
+ }
387
+ }
313
388
  break;
314
389
  }
315
390
  case 'doctor':
@@ -577,6 +652,59 @@ switch (subcommand) {
577
652
  process.exit(code);
578
653
  break;
579
654
  }
655
+ case 'migrate': {
656
+ // Two-word `migrate doctor` is dispatched here before the generic migrate
657
+ // path so we don't try to read a stack.md or pick an env when the user is
658
+ // really asking for the doctor. `migrate-doctor` (single verb, below) is
659
+ // an equivalent alias.
660
+ if (args[1] === 'doctor') {
661
+ await runMigrateDoctorCLI();
662
+ break;
663
+ }
664
+ // Plain `migrate [--env <name>] [--dry-run] [--yes]` → dispatcher.
665
+ const envName = flag('env') ?? 'dev';
666
+ const dryRun = boolFlag('dry-run');
667
+ const yesFlag = boolFlag('yes');
668
+ // Read package version for the runtime handshake.
669
+ const root = findPackageRoot(import.meta.url);
670
+ let runtimeVersion = 'unknown';
671
+ if (root) {
672
+ try {
673
+ const nodeFs = await import('node:fs');
674
+ const nodePath = await import('node:path');
675
+ const pkg = JSON.parse(nodeFs.readFileSync(nodePath.join(root, 'package.json'), 'utf8'));
676
+ runtimeVersion = pkg.version;
677
+ }
678
+ catch {
679
+ /* fall through with 'unknown' — handshake will fail closed */
680
+ }
681
+ }
682
+ const result = await runMigrateDispatch({
683
+ repoRoot: process.cwd(),
684
+ env: envName,
685
+ yesFlag,
686
+ nonInteractive: !process.stdin.isTTY,
687
+ currentRuntimeVersion: runtimeVersion,
688
+ dryRun,
689
+ });
690
+ const ok = result.status === 'applied' || result.status === 'skipped';
691
+ const color = ok ? '\x1b[32m' : '\x1b[31m';
692
+ console.log(`${color}[migrate] status=${result.status} reason=${result.reasonCode}\x1b[0m`);
693
+ if (result.appliedMigrations.length > 0) {
694
+ console.log(` applied: ${result.appliedMigrations.join(', ')}`);
695
+ }
696
+ if (result.nextActions.length > 0) {
697
+ console.log(` next: ${result.nextActions.join('; ')}`);
698
+ }
699
+ process.exit(ok ? 0 : 1);
700
+ break;
701
+ }
702
+ case 'migrate-doctor': {
703
+ // Single-verb alias for `migrate doctor`. Documented for users whose shells
704
+ // or CI configs handle multi-word verbs awkwardly.
705
+ await runMigrateDoctorCLI();
706
+ break;
707
+ }
580
708
  case 'brainstorm': {
581
709
  // `brainstorm` is the front of the pipeline and is implemented as a Claude
582
710
  // Code skill (superpowers:brainstorming → autopilot), not a standalone CLI.
@@ -0,0 +1,35 @@
1
+ import { type DetectionMatch } from '../core/migrate/detector.ts';
2
+ export declare class NoMigrationToolDetectedError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export interface PrompterArgs {
6
+ workspace: string;
7
+ matches: DetectionMatch[];
8
+ }
9
+ export type Prompter = (args: PrompterArgs) => Promise<DetectionMatch>;
10
+ export interface InitMigrateOptions {
11
+ repoRoot: string;
12
+ skipMigrate?: boolean;
13
+ force?: boolean;
14
+ /**
15
+ * When true, computes what would be written but performs no write.
16
+ * Each workspace result includes a `diff` field showing the
17
+ * proposed change against the existing stack.md (or "would create
18
+ * new file" if missing). Used by `--force-rewrite` to preview
19
+ * changes before user confirmation.
20
+ */
21
+ dryRunPreview?: boolean;
22
+ prompter?: Prompter;
23
+ }
24
+ export interface WorkspaceResult {
25
+ workspace: string;
26
+ action: 'wrote' | 'updated' | 'skipped' | 'preview';
27
+ skill: string;
28
+ /** Populated only when dryRunPreview is true. */
29
+ diff?: string;
30
+ }
31
+ export interface InitMigrateResult {
32
+ workspaces: WorkspaceResult[];
33
+ }
34
+ export declare function initMigrate(opts: InitMigrateOptions): Promise<InitMigrateResult>;
35
+ //# sourceMappingURL=init-migrate.d.ts.map
@@ -0,0 +1,299 @@
1
+ // src/cli/init-migrate.ts
2
+ //
3
+ // Init flow extension for the migrate skill. Walks workspaces, runs
4
+ // detection per workspace, and writes a per-workspace
5
+ // `<workspace>/.autopilot/stack.md` (plus a root `<repoRoot>/.autopilot/
6
+ // manifest.yaml` for monorepos).
7
+ //
8
+ // Decision tree per workspace:
9
+ // - --skipMigrate: write `migrate.skill: "none@1"` shape with TODO
10
+ // - autoSelect (1 high-confidence match): write the rule's defaults
11
+ // - prompt required (>1 match or non-high): call injected prompter
12
+ // - zero matches: throw NoMigrationToolDetectedError
13
+ //
14
+ // Idempotent: existing stack.md is loaded and merged. User-edited fields
15
+ // (envs.*.command, custom env_file, etc.) are preserved; only
16
+ // `detected_at` is refreshed and missing default policy keys are added.
17
+ // `force: true` regenerates from scratch.
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import * as yaml from 'js-yaml';
21
+ import { detect } from "../core/migrate/detector.js";
22
+ import { findWorkspaces } from "../core/migrate/monorepo.js";
23
+ export class NoMigrationToolDetectedError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = 'NoMigrationToolDetectedError';
27
+ }
28
+ }
29
+ const DEFAULT_POLICY_GENERIC = {
30
+ allow_prod_in_ci: false,
31
+ require_clean_git: true,
32
+ require_manual_approval: true,
33
+ require_dry_run_first: false,
34
+ };
35
+ const DEFAULT_POLICY_SUPABASE = {
36
+ allow_prod_in_ci: false,
37
+ };
38
+ const DEFAULT_PROMPTER = async () => {
39
+ throw new Error('interactive prompt not available — pass `prompter` in initMigrate options or run with --skip-migrate');
40
+ };
41
+ function buildFreshStack(rule, skipMigrate) {
42
+ const detectedAt = new Date().toISOString();
43
+ if (skipMigrate) {
44
+ return {
45
+ schema_version: 1,
46
+ migrate: {
47
+ skill: 'none@1',
48
+ detected_at: detectedAt,
49
+ project_root: '.',
50
+ },
51
+ };
52
+ }
53
+ if (rule.defaultSkill === 'migrate.supabase@1') {
54
+ return {
55
+ schema_version: 1,
56
+ migrate: {
57
+ skill: 'migrate.supabase@1',
58
+ supabase: {
59
+ deltas_dir: 'data/deltas',
60
+ types_out: 'types/supabase.ts',
61
+ envs_file: '.claude/supabase-envs.json',
62
+ },
63
+ policy: { ...DEFAULT_POLICY_SUPABASE },
64
+ detected_at: detectedAt,
65
+ project_root: '.',
66
+ },
67
+ };
68
+ }
69
+ // Generic migrate@1 shape
70
+ const stack = {
71
+ schema_version: 1,
72
+ migrate: {
73
+ skill: 'migrate@1',
74
+ policy: { ...DEFAULT_POLICY_GENERIC },
75
+ detected_at: detectedAt,
76
+ project_root: '.',
77
+ },
78
+ };
79
+ if (rule.defaultCommand) {
80
+ stack.migrate.envs = {
81
+ dev: { command: { ...rule.defaultCommand, args: [...rule.defaultCommand.args] } },
82
+ };
83
+ }
84
+ else {
85
+ // No default command in the rule; init still writes envs.dev as a
86
+ // placeholder so schema validates. User is expected to fill it in.
87
+ stack.migrate.envs = {
88
+ dev: { command: { exec: 'TODO', args: ['configure-dev-command'] } },
89
+ };
90
+ }
91
+ return stack;
92
+ }
93
+ function mergePreserving(existing, fresh) {
94
+ // Preserve all user fields; only update detected_at and add missing
95
+ // defaults (schema_version, missing policy keys, project_root).
96
+ const merged = {
97
+ schema_version: existing.schema_version ?? fresh.schema_version,
98
+ migrate: { ...existing.migrate },
99
+ };
100
+ // Always refresh detected_at
101
+ merged.migrate.detected_at = fresh.migrate.detected_at;
102
+ // Backfill project_root if missing
103
+ if (!merged.migrate.project_root && fresh.migrate.project_root) {
104
+ merged.migrate.project_root = fresh.migrate.project_root;
105
+ }
106
+ // Merge missing default policy keys (do not overwrite user-set keys)
107
+ if (fresh.migrate.policy) {
108
+ const existingPolicy = (merged.migrate.policy ?? {});
109
+ const mergedPolicy = { ...existingPolicy };
110
+ for (const [k, v] of Object.entries(fresh.migrate.policy)) {
111
+ if (!(k in mergedPolicy))
112
+ mergedPolicy[k] = v;
113
+ }
114
+ merged.migrate.policy = mergedPolicy;
115
+ }
116
+ // Backfill supabase block if missing (preserve user values otherwise)
117
+ if (fresh.migrate.supabase && !merged.migrate.supabase) {
118
+ merged.migrate.supabase = { ...fresh.migrate.supabase };
119
+ }
120
+ // Backfill envs.dev if missing (preserve user-set commands otherwise)
121
+ if (fresh.migrate.envs?.dev && !merged.migrate.envs) {
122
+ merged.migrate.envs = {
123
+ dev: { ...fresh.migrate.envs.dev },
124
+ };
125
+ }
126
+ else if (fresh.migrate.envs?.dev && merged.migrate.envs && !merged.migrate.envs.dev) {
127
+ merged.migrate.envs.dev = { ...fresh.migrate.envs.dev };
128
+ }
129
+ return merged;
130
+ }
131
+ function readExistingStackMd(stackPath) {
132
+ try {
133
+ const content = fs.readFileSync(stackPath, 'utf8');
134
+ const parsed = yaml.load(content);
135
+ if (parsed &&
136
+ typeof parsed === 'object' &&
137
+ 'migrate' in parsed) {
138
+ return parsed;
139
+ }
140
+ return null;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ function serializeStackMd(stack, options) {
147
+ // YAML body
148
+ const body = yaml.dump(stack, { lineWidth: 120, noRefs: true });
149
+ if (options.skipMigrate) {
150
+ return (body +
151
+ '# TODO: configure your migration tool. See docs/skills/rich-migrate-contract.md\n');
152
+ }
153
+ return body;
154
+ }
155
+ /**
156
+ * Produce a unified-diff-style summary of the change from `oldText` to
157
+ * `newText`. Pure-line LCS — no external dependency. Output is for human
158
+ * review only; not intended to round-trip via `patch`.
159
+ */
160
+ function unifiedDiff(oldText, newText) {
161
+ if (oldText === newText)
162
+ return '';
163
+ const a = oldText.split('\n');
164
+ const b = newText.split('\n');
165
+ const m = a.length;
166
+ const n = b.length;
167
+ // LCS table
168
+ const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
169
+ for (let i = m - 1; i >= 0; i--) {
170
+ for (let j = n - 1; j >= 0; j--) {
171
+ if (a[i] === b[j]) {
172
+ lcs[i][j] = lcs[i + 1][j + 1] + 1;
173
+ }
174
+ else {
175
+ lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
176
+ }
177
+ }
178
+ }
179
+ const out = [];
180
+ let i = 0;
181
+ let j = 0;
182
+ while (i < m && j < n) {
183
+ if (a[i] === b[j]) {
184
+ out.push(` ${a[i]}`);
185
+ i++;
186
+ j++;
187
+ }
188
+ else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
189
+ out.push(`- ${a[i]}`);
190
+ i++;
191
+ }
192
+ else {
193
+ out.push(`+ ${b[j]}`);
194
+ j++;
195
+ }
196
+ }
197
+ while (i < m) {
198
+ out.push(`- ${a[i]}`);
199
+ i++;
200
+ }
201
+ while (j < n) {
202
+ out.push(`+ ${b[j]}`);
203
+ j++;
204
+ }
205
+ return out.join('\n');
206
+ }
207
+ function chooseRule(matches, autoSelect, prompter, workspace) {
208
+ if (autoSelect)
209
+ return Promise.resolve(matches[0]);
210
+ return prompter({ workspace, matches });
211
+ }
212
+ export async function initMigrate(opts) {
213
+ const repoRoot = path.resolve(opts.repoRoot);
214
+ const skipMigrate = opts.skipMigrate ?? false;
215
+ const force = opts.force ?? false;
216
+ const dryRunPreview = opts.dryRunPreview ?? false;
217
+ const prompter = opts.prompter ?? DEFAULT_PROMPTER;
218
+ const workspaces = findWorkspaces(repoRoot);
219
+ const results = [];
220
+ for (const workspace of workspaces) {
221
+ const stackDir = path.join(workspace, '.autopilot');
222
+ const stackPath = path.join(stackDir, 'stack.md');
223
+ const exists = fs.existsSync(stackPath);
224
+ let chosenRule = null;
225
+ if (!skipMigrate) {
226
+ const det = detect(workspace);
227
+ if (det.matches.length === 0) {
228
+ throw new NoMigrationToolDetectedError(`No migration tool detected in ${workspace}. Re-run with --skip-migrate to write a 'none@1' stack.md and configure later.`);
229
+ }
230
+ const chosen = await chooseRule(det.matches, det.autoSelect, prompter, workspace);
231
+ chosenRule = chosen.rule;
232
+ }
233
+ // For skipMigrate, chosenRule stays null — buildFreshStack ignores it.
234
+ const fresh = buildFreshStack(chosenRule ?? {}, skipMigrate);
235
+ let toWrite;
236
+ let action;
237
+ if (dryRunPreview) {
238
+ // Preview always reflects the *fresh* content (mirrors `force: true`).
239
+ // Merge-preserving previews can be added later if needed.
240
+ toWrite = fresh;
241
+ action = 'preview';
242
+ }
243
+ else if (exists && !force) {
244
+ const existing = readExistingStackMd(stackPath);
245
+ if (existing) {
246
+ toWrite = mergePreserving(existing, fresh);
247
+ action = 'updated';
248
+ }
249
+ else {
250
+ toWrite = fresh;
251
+ action = 'wrote';
252
+ }
253
+ }
254
+ else {
255
+ toWrite = fresh;
256
+ action = 'wrote';
257
+ }
258
+ const newContent = serializeStackMd(toWrite, { skipMigrate });
259
+ if (dryRunPreview) {
260
+ // Compute diff against existing on disk; do NOT write.
261
+ const oldContent = exists ? fs.readFileSync(stackPath, 'utf8') : '';
262
+ const diff = exists
263
+ ? unifiedDiff(oldContent, newContent)
264
+ : `would create new file ${path.relative(repoRoot, stackPath) || stackPath}\n${newContent
265
+ .split('\n')
266
+ .map(l => `+ ${l}`)
267
+ .join('\n')}`;
268
+ results.push({
269
+ workspace,
270
+ action,
271
+ skill: toWrite.migrate.skill,
272
+ diff,
273
+ });
274
+ continue;
275
+ }
276
+ fs.mkdirSync(stackDir, { recursive: true });
277
+ fs.writeFileSync(stackPath, newContent, 'utf8');
278
+ results.push({
279
+ workspace,
280
+ action,
281
+ skill: toWrite.migrate.skill,
282
+ });
283
+ }
284
+ // Multi-workspace repos: write a root manifest.yaml listing the workspaces.
285
+ if (workspaces.length > 1 && !dryRunPreview) {
286
+ const manifestDir = path.join(repoRoot, '.autopilot');
287
+ fs.mkdirSync(manifestDir, { recursive: true });
288
+ const manifest = {
289
+ schema_version: 1,
290
+ workspaces: results.map(r => ({
291
+ path: path.relative(repoRoot, r.workspace) || '.',
292
+ skill: r.skill,
293
+ })),
294
+ };
295
+ fs.writeFileSync(path.join(manifestDir, 'manifest.yaml'), yaml.dump(manifest, { lineWidth: 120, noRefs: true }), 'utf8');
296
+ }
297
+ return { workspaces: results };
298
+ }
299
+ //# sourceMappingURL=init-migrate.js.map
@@ -0,0 +1,19 @@
1
+ import { type NamedCheckResult } from '../core/migrate/doctor-checks.ts';
2
+ export interface RunMigrateDoctorOptions {
3
+ repoRoot: string;
4
+ fix?: boolean;
5
+ }
6
+ export interface RunMigrateDoctorResult {
7
+ allOk: boolean;
8
+ results: NamedCheckResult[];
9
+ /** Populated only when fix=true. Empty if no fixes were needed. */
10
+ mutations?: string[];
11
+ /**
12
+ * Absolute path of the migration report (Markdown) written under
13
+ * .autopilot/. Populated when fix=true and the legacy migrator ran. Read-
14
+ * only mode never writes a report.
15
+ */
16
+ migrationReportPath?: string;
17
+ }
18
+ export declare function runMigrateDoctor(opts: RunMigrateDoctorOptions): Promise<RunMigrateDoctorResult>;
19
+ //# sourceMappingURL=migrate-doctor.d.ts.map