forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4

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 (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -335,27 +335,40 @@ function renderDayOneConfigYaml({ profile, classification, harnessTargets, rails
335
335
  config.workflow.classification = { default: classification };
336
336
  config.layer1Rails = {
337
337
  confirmed: railsConfirmed,
338
+ // The immutable Layer 1 floor: locked rails that cannot be toggled off.
338
339
  rails: [
339
- 'tdd_intent',
340
340
  'secret_scan',
341
341
  'branch_protection',
342
342
  'signed_commits',
343
343
  'schema_integrity',
344
344
  ],
345
+ // tdd_intent is a strong DEFAULT rail, not an immutable L1 floor: it is unlocked
346
+ // (locked:false) and honestly toggleable via `forge gate disable rail.tdd_intent`
347
+ // (the minimal profile ships it off). Listed separately so this display never
348
+ // implies it is a non-negotiable Layer 1 rail (issue eda6d866).
349
+ toggleableRails: ['tdd_intent'],
345
350
  };
346
351
  config.adapters = config.adapters || {};
347
352
  config.adapters.harness = {
348
353
  enabled: harnessTargets.length > 0,
349
354
  targets: harnessTargets,
350
355
  };
351
- config.protectedPaths = [
352
- ...new Set([
353
- ...(Array.isArray(config.protectedPaths) ? config.protectedPaths : []),
354
- '.forge/config.yaml',
355
- '.forge/patch.md',
356
- '.forge/protected-paths.yaml',
357
- ]),
358
- ];
356
+ // The adoption profile is authoritative for whether protected-path enforcement runs
357
+ // at all. When it resolves EMPTY (the `minimal` profile = zero active enforcement),
358
+ // the protected-path guard must stay genuinely inert — do NOT re-add the day-one
359
+ // paths, or `init --minimal` would silently reactivate the guard the profile just
360
+ // disabled (issue eda6d866). Standard/full keep their profile paths + the additions.
361
+ const profilePaths = Array.isArray(config.protectedPaths) ? config.protectedPaths : [];
362
+ config.protectedPaths = profilePaths.length === 0
363
+ ? []
364
+ : [
365
+ ...new Set([
366
+ ...profilePaths,
367
+ '.forge/config.yaml',
368
+ '.forge/patch.md',
369
+ '.forge/protected-paths.yaml',
370
+ ]),
371
+ ];
359
372
  return YAML.stringify(config);
360
373
  }
361
374
 
@@ -543,24 +556,17 @@ async function handler(args, flags, projectRoot = process.cwd(), deps = {}) {
543
556
  }
544
557
 
545
558
  // Close the onboarding loop that a bare `forge init` used to leave open:
546
- // 1. install git hooks so stage commands are not HOOKS_NOT_ACTIVE-blocked, and
547
- // 2. import any existing Beads store into the Kernel.
548
- // Both reuse setup's real implementations (overridable via deps for tests) and
549
- // degrade to a warning rather than failing init.
559
+ // install git hooks so stage commands are not HOOKS_NOT_ACTIVE-blocked. Reuses
560
+ // setup's real implementation (overridable via deps for tests) and degrades to a
561
+ // warning rather than failing init. Beads Kernel transfer is NOT done here: it
562
+ // is explicit-only via `forge migrate --from beads` (a7e1443c), never implicit.
550
563
  const installHooks = deps.installHooks
551
564
  || ((root) => require('./setup').ensureGitHooksInstalled(root));
552
- const autoMigrateBeads = deps.autoMigrateBeads
553
- || ((root) => require('./migrate').autoMigrateBeadsIfPresent(root));
554
565
  try {
555
566
  await installHooks(projectRoot);
556
567
  } catch (err) {
557
568
  console.warn(`Warning: git hook installation skipped: ${err.message}`);
558
569
  }
559
- try {
560
- await autoMigrateBeads(projectRoot);
561
- } catch (err) {
562
- console.warn(`Warning: Beads → Kernel migration skipped: ${err.message}`);
563
- }
564
570
 
565
571
  console.log(`Initialized Forge adoption profile '${profile}' at .forge/`);
566
572
  console.log(`Classification: ${choices.classification}`);
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const remember = require('./remember');
4
+ const recall = require('./recall');
5
+ const insights = require('./insights');
6
+ const { stripGlobalFlags } = require('../global-flags');
7
+
8
+ // One memorable surface over the EXISTING memory commands (kernel issue 25362344): every
9
+ // subcommand delegates to the standalone remember/recall/insights handlers — the same
10
+ // kernel-backed store, not a reimplementation. The standalone `forge remember`/`forge
11
+ // recall`/`forge insights` commands remain registered as back-compat aliases, so nothing
12
+ // that already calls them breaks.
13
+ const SUBCOMMANDS = {
14
+ add: {
15
+ handler: remember.handler,
16
+ summary: 'Persist a memory note (= forge remember; supports --type + What/Why/Where/Learned)',
17
+ },
18
+ recall: {
19
+ handler: recall.handler,
20
+ summary: 'Retrieve memory notes, newest first (= forge recall; filter with --type)',
21
+ },
22
+ search: {
23
+ // Search IS recall with a query — recall runs a BM25 token-AND search when a query is
24
+ // present, so the same handler serves both without a second code path.
25
+ handler: recall.handler,
26
+ summary: 'Search memory notes by query (recall with a query; filter with --type)',
27
+ },
28
+ insights: {
29
+ handler: insights.handler,
30
+ summary: 'Detect recurring evidence patterns and suggest follow-ups (= forge insights)',
31
+ },
32
+ };
33
+
34
+ const usage = 'Usage: forge memory <add|recall|search|insights> [args]';
35
+
36
+ function renderHelp() {
37
+ const width = Math.max(...Object.keys(SUBCOMMANDS).map(name => name.length));
38
+ const lines = [
39
+ usage,
40
+ '',
41
+ 'Subcommands:',
42
+ ...Object.entries(SUBCOMMANDS).map(
43
+ ([name, { summary }]) => ` ${name.padEnd(width)} ${summary}`
44
+ ),
45
+ '',
46
+ 'Back-compat: forge remember / forge recall / forge insights remain available as aliases.',
47
+ ];
48
+ return lines.join('\n');
49
+ }
50
+
51
+ async function handler(args, flags, projectRoot, opts) {
52
+ // The subcommand is the first positional token; global flags (e.g. `-p <dir>`) are stripped
53
+ // first so they never masquerade as the subcommand.
54
+ const positional = stripGlobalFlags(args).find(arg => !arg.startsWith('-'));
55
+
56
+ if (!positional || positional === 'help' || args.includes('--help') || args.includes('-h')) {
57
+ return { success: true, output: renderHelp() };
58
+ }
59
+
60
+ const sub = SUBCOMMANDS[positional];
61
+ if (!sub) {
62
+ return {
63
+ success: false,
64
+ error: `Unknown memory subcommand: ${positional}\n\n${renderHelp()}`,
65
+ };
66
+ }
67
+
68
+ // Forward everything EXCEPT the consumed subcommand token to the delegate, preserving any
69
+ // global flags the delegate re-parses (e.g. `-p <dir>`, `--all`).
70
+ const idx = args.indexOf(positional);
71
+ const childArgs = idx >= 0 ? [...args.slice(0, idx), ...args.slice(idx + 1)] : args;
72
+ return sub.handler(childArgs, flags, projectRoot, opts);
73
+ }
74
+
75
+ module.exports = {
76
+ name: 'memory',
77
+ description:
78
+ 'Unified memory surface: forge memory add|recall|search|insights (wraps remember/recall/insights)',
79
+ usage,
80
+ handler,
81
+ };
@@ -301,171 +301,10 @@ async function runBeadsMigration(options, projectRoot, opts) {
301
301
  }
302
302
  }
303
303
 
304
- /**
305
- * Detect a jsonl-backed Beads store under `<projectRoot>/.beads`. Returns the
306
- * directory path when it holds *.jsonl sidecars, else null.
307
- *
308
- * Onboarding (forge setup/init) uses this to decide whether to auto-migrate.
309
- * It deliberately never falls back to `bd export` (a Dolt-only store with no
310
- * jsonl returns null), so setup stays bd-free even when Dolt is down.
311
- */
312
- function detectBeadsJsonlSource(projectRoot, deps = {}) {
313
- const fsImpl = deps.fs || fs;
314
- const beadsDir = path.join(projectRoot || process.cwd(), '.beads');
315
- return fsImpl.existsSync(beadsDir) && directoryHasJsonl(beadsDir, fsImpl)
316
- ? beadsDir
317
- : null;
318
- }
319
-
320
- /**
321
- * Idempotently import an existing jsonl-backed Beads store into the Kernel as
322
- * part of onboarding. Reuses the exact `forge migrate --from beads` spine, so
323
- * gaps are surfaced honestly and a second run inserts nothing.
324
- *
325
- * No-op (`{ migrated: false, reason: 'no-beads-jsonl' }`) when there is no jsonl
326
- * `.beads/` present. Never requires the `bd` binary.
327
- *
328
- * @param {string} projectRoot
329
- * @param {object} [opts] - Passed through to runBeadsMigration (_broker/_now/_fs seams).
330
- * @returns {Promise<{ migrated: boolean, reason?: string, result?: object }>}
331
- */
332
- async function autoMigrateBeadsIfPresent(projectRoot, opts = {}) {
333
- if (!detectBeadsJsonlSource(projectRoot, { fs: opts._fs })) {
334
- return { migrated: false, reason: 'no-beads-jsonl' };
335
- }
336
- const result = await runBeadsMigration({ from: 'beads' }, projectRoot, opts);
337
- return { migrated: result.success === true, result };
338
- }
339
-
340
- const BEADS_MIGRATE_NUDGE =
341
- 'Forge could not import your Beads issues into the Kernel automatically. '
342
- + 'Run `forge migrate --from beads` to import them.';
343
-
344
- // The "already imported" marker lives as a row in the broker's kernel_migrations
345
- // ledger (created unconditionally by broker.initialize()), NOT as a file beside the
346
- // DB. Keeping it INSIDE the kernel DB means a DB reset drops the marker and the import
347
- // self-heals; a file sentinel would survive the reset and leave issues dark forever.
348
- // The id uses underscores so it passes the ledger charset /^[0-9a-z_]+$/i and stays
349
- // clear of the tokens the D20 retirement audit counts (see release-readiness.js).
350
- const IMPORT_MARKER_TABLE = 'kernel_migrations';
351
- const IMPORT_MARKER_ID = 'data_import_beads_jsonl';
352
-
353
- // Read/record the import marker via the DRIVER (the broker exposes no raw SQL). Both
354
- // calls are param-less (raw SQL + config); the id is a hardcoded constant and appliedAt
355
- // is an ISO string, so interpolation is injection-safe — mirrors broker.recordMigrationSql.
356
- async function importMarkerPresent(driver, config) {
357
- if (!driver || typeof driver.queryAll !== 'function') {
358
- return false;
359
- }
360
- try {
361
- const rows = await driver.queryAll(
362
- `SELECT 1 FROM ${IMPORT_MARKER_TABLE} WHERE id = '${IMPORT_MARKER_ID}' LIMIT 1;`,
363
- config,
364
- );
365
- return Array.isArray(rows) && rows.length > 0;
366
- } catch {
367
- // Ledger table absent / driver unavailable → treat as not-yet-imported.
368
- return false;
369
- }
370
- }
371
-
372
- async function recordImportMarker(driver, config, appliedAt) {
373
- if (!driver || typeof driver.exec !== 'function') {
374
- return;
375
- }
376
- try {
377
- await driver.exec(
378
- `INSERT OR IGNORE INTO ${IMPORT_MARKER_TABLE} (id, applied_at) VALUES ('${IMPORT_MARKER_ID}', '${appliedAt}');`,
379
- config,
380
- );
381
- } catch {
382
- // Best-effort marker; never break the command we ride on.
383
- }
384
- }
385
-
386
- /**
387
- * First-use safety net for the kernel default backend. Onboarding auto-migrate runs
388
- * only from `forge setup`/`init`; an existing repo whose user merely upgrades forge
389
- * reads an EMPTY kernel on the first issue command, so their existing Beads issues
390
- * appear to vanish. This imports them ONCE — gated by an in-DB marker row in the
391
- * kernel_migrations ledger, so the gate shares the DB lifecycle and a DB reset
392
- * self-heals — idempotently, announcing on stderr only so `--json` stdout stays a pure
393
- * contract. NEVER throws: the safety net must never break the command it rides on.
394
- *
395
- * Only a jsonl-backed store is auto-imported (the migration binary is never shelled).
396
- * A store with no jsonl export is skipped silently. The marker is SUCCESS-ONLY: a
397
- * failed import records nothing and is retried (with a nudge) on the next kernel
398
- * command, so transient failures and DB resets self-heal. Imported issues land in the
399
- * read model directly and arrive UNCLAIMED — they surface via `forge issue ready`.
400
- *
401
- * @param {object} params
402
- * @param {string} params.projectRoot
403
- * @param {string} params.databasePath - kernel DB path (used for the driver config).
404
- * @param {object} params.broker - an initialized kernel broker to import through.
405
- * @param {object} params.driver - kernel driver, for the ledger marker read/write.
406
- * @param {object} [deps] - { fs, warn, now, driver, _fs } seams for tests.
407
- * @returns {Promise<{action:'migrated'|'nudge'|'skip', reason?:string, inserted?:number}>}
408
- */
409
- async function autoMigrateBeadsAtRuntime({ projectRoot, databasePath, broker, driver } = {}, deps = {}) {
410
- const fsImpl = deps.fs || fs;
411
- const warn = deps.warn || ((msg) => process.stderr.write(`${msg}\n`));
412
- const appliedAt = deps.now || new Date().toISOString();
413
- const markerDriver = deps.driver || driver;
414
- const markerConfig = databasePath ? { databasePath } : {};
415
- try {
416
- if (!databasePath) {
417
- return { action: 'skip', reason: 'no-db-path' };
418
- }
419
-
420
- // One-time gate: the import already ran on THIS kernel DB (the marker dies with the
421
- // DB, so deleting kernel.sqlite to reset correctly re-triggers the import).
422
- if (await importMarkerPresent(markerDriver, markerConfig)) {
423
- return { action: 'skip', reason: 'already-imported' };
424
- }
425
-
426
- // Only a jsonl export can be imported automatically; anything else is skipped
427
- // silently (an empty or export-less store must never trigger a false nudge).
428
- if (!detectBeadsJsonlSource(projectRoot, { fs: fsImpl })) {
429
- return { action: 'skip', reason: 'no-jsonl' };
430
- }
431
-
432
- // Import (idempotent) through the already-initialized broker.
433
- let outcome;
434
- try {
435
- outcome = await autoMigrateBeadsIfPresent(projectRoot, { _broker: broker, _now: deps.now, _fs: deps._fs });
436
- } catch (err) {
437
- outcome = { migrated: false, result: { success: false, error: err.message } };
438
- }
439
-
440
- if (outcome.migrated) {
441
- const inserted = outcome.result?.imported?.issues?.inserted ?? 0;
442
- const skipped = outcome.result?.imported?.issues?.skipped ?? 0;
443
- // Record the marker LAST so a concurrent first run is a benign idempotent skip.
444
- await recordImportMarker(markerDriver, markerConfig, appliedAt);
445
- if (inserted > 0) {
446
- warn(`Forge: imported ${inserted} Beads issue(s) into the Kernel — your issues are here, not lost.`);
447
- }
448
- return { action: 'migrated', inserted, skipped };
449
- }
450
-
451
- // Success-only marker: a failed import records nothing and re-nudges next run,
452
- // so a transient failure self-heals once the underlying problem is resolved.
453
- const error = outcome.result?.error || outcome.reason || 'unknown error';
454
- warn(BEADS_MIGRATE_NUDGE);
455
- return { action: 'nudge', reason: 'migrate-failed', error };
456
- } catch (err) {
457
- // Absolutely never break the command we ride on.
458
- return { action: 'skip', reason: 'error', error: err.message };
459
- }
460
- }
461
-
462
304
  module.exports = {
463
305
  name: 'migrate',
464
306
  description: 'Migrate a Beads issue store into the Forge Kernel with --from beads, '
465
307
  + 'or preview the v2→v3 migration (preview only — --dry-run required; applying it is not yet available)',
466
- detectBeadsJsonlSource,
467
- autoMigrateBeadsIfPresent,
468
- autoMigrateBeadsAtRuntime,
469
308
  usage: 'forge migrate --from beads [--dry-run] [--source <dir>] [--json]',
470
309
  flags: {
471
310
  '--from <source>': 'Migration source. Use "beads" to import a Beads issue store into the Kernel.',