@unbrained/pm-cli 2026.8.26 → 2026.8.27

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 (161) hide show
  1. package/.agents/skills/HARNESS_COMPATIBILITY.md +32 -0
  2. package/.agents/skills/README.md +47 -0
  3. package/.agents/skills/pm-developer/SKILL.md +117 -0
  4. package/.agents/skills/pm-developer/references/COMMAND_PLAYBOOK.md +49 -0
  5. package/.agents/skills/pm-developer/references/GRAPH_AND_RELATIONSHIPS.md +91 -0
  6. package/.agents/skills/pm-developer/references/MULTI_AGENT_MERGE.md +72 -0
  7. package/.agents/skills/pm-developer/references/PROMPTS.md +17 -0
  8. package/.agents/skills/pm-developer/references/SCRIPTING_COMPOSITION.md +82 -0
  9. package/.agents/skills/pm-developer/references/TOKEN_BUDGETS.md +85 -0
  10. package/.agents/skills/pm-extensions/SKILL.md +106 -0
  11. package/.agents/skills/pm-extensions/references/AUTHORING.md +95 -0
  12. package/.agents/skills/pm-extensions/references/LIFECYCLE.md +40 -0
  13. package/.agents/skills/pm-extensions/references/TROUBLESHOOTING.md +25 -0
  14. package/.agents/skills/pm-sdk/SKILL.md +107 -0
  15. package/.agents/skills/pm-sdk/references/DOMAIN_MODELING.md +78 -0
  16. package/.agents/skills/pm-sdk/references/INTEGRATION_CHECKLIST.md +31 -0
  17. package/.agents/skills/pm-sdk/references/PROMPTS.md +13 -0
  18. package/.agents/skills/pm-sdk/references/SURFACE_MAP.md +82 -0
  19. package/.agents/skills/pm-user/SKILL.md +111 -0
  20. package/.agents/skills/pm-user/references/BACKLOG_SHAPING.md +105 -0
  21. package/.agents/skills/pm-user/references/PROMPTS.md +17 -0
  22. package/.agents/skills/pm-user/references/WORKFLOWS.md +35 -0
  23. package/.claude-plugin/marketplace.json +2 -2
  24. package/CHANGELOG.md +25 -3
  25. package/README.md +8 -5
  26. package/dist/cli/commander-usage.js +11 -7
  27. package/dist/cli/error-guidance.js +3 -3
  28. package/dist/cli/help-content.d.ts +2 -0
  29. package/dist/cli/help-content.js +53 -17
  30. package/dist/cli/help-json-payload.d.ts +8 -2
  31. package/dist/cli/help-json-payload.js +46 -12
  32. package/dist/cli/main.js +52 -74
  33. package/dist/cli/register-annotations.js +27 -21
  34. package/dist/cli/register-setup.js +98 -57
  35. package/dist/cli-bundle/bundle-manifest.json +152 -152
  36. package/dist/cli-bundle/chunks/chunk-3OO3W6FW.js +202 -0
  37. package/dist/cli-bundle/chunks/chunk-52EKTW6V.js +3 -0
  38. package/dist/cli-bundle/chunks/{chunk-KBFP3E4E.js → chunk-CVBBGWW5.js} +62 -44
  39. package/dist/cli-bundle/chunks/chunk-MFNTKMTI.js +13 -0
  40. package/dist/cli-bundle/chunks/{chunk-E2GCFJSU.js → chunk-OS27HHBN.js} +31 -31
  41. package/dist/cli-bundle/chunks/chunk-QTO7USTH.js +2 -0
  42. package/dist/cli-bundle/chunks/{chunk-S4U76VZF.js → chunk-R4ETAOJC.js} +2 -2
  43. package/dist/cli-bundle/chunks/{chunk-UKBCRPA2.js → chunk-SH6P7FXI.js} +2 -2
  44. package/dist/cli-bundle/chunks/{chunk-WRHJ3MB6.js → chunk-SHMDY36D.js} +2 -2
  45. package/dist/cli-bundle/chunks/{chunk-ZNRLJ54C.js → chunk-SKXLJIEK.js} +2 -2
  46. package/dist/cli-bundle/chunks/chunk-TNX6HC54.js +3 -0
  47. package/dist/cli-bundle/chunks/{register-list-query-EMCPMICY.js → register-list-query-EUWM6VII.js} +2 -2
  48. package/dist/cli-bundle/chunks/{register-mutation-OJ67ABCB.js → register-mutation-FD4HSAVU.js} +4 -4
  49. package/dist/cli-bundle/chunks/{register-operations-H2GLP7LT.js → register-operations-HRMNFEC3.js} +2 -2
  50. package/dist/cli-bundle/chunks/register-setup-33GNICLX.js +2 -0
  51. package/dist/cli-bundle/focused-chunks/{chunk-4XNH2HM7.js → chunk-2AGZ5BRT.js} +2 -2
  52. package/dist/cli-bundle/focused-chunks/{chunk-72T6JGAE.js → chunk-4BR5UU52.js} +45 -45
  53. package/dist/cli-bundle/focused-chunks/{chunk-OHIHZ7HS.js → chunk-6GCRSLPG.js} +2 -2
  54. package/dist/cli-bundle/focused-chunks/{chunk-UYBA57GY.js → chunk-AD6ULRAF.js} +2 -2
  55. package/dist/cli-bundle/focused-chunks/{chunk-7YCDTCBC.js → chunk-AQ5IYEZZ.js} +2 -2
  56. package/dist/cli-bundle/focused-chunks/{chunk-YO3ZF3FI.js → chunk-EKX37ZHA.js} +2 -2
  57. package/dist/cli-bundle/focused-chunks/{chunk-LV5N3LK5.js → chunk-FC2AXLB5.js} +2 -2
  58. package/dist/cli-bundle/focused-chunks/{chunk-IBHXMFE7.js → chunk-HC7ODMH3.js} +2 -2
  59. package/dist/cli-bundle/focused-chunks/{chunk-4K2II4TV.js → chunk-HVQ22RC4.js} +2 -2
  60. package/dist/cli-bundle/focused-chunks/{chunk-A644DUFQ.js → chunk-MEASX544.js} +2 -2
  61. package/dist/cli-bundle/focused-chunks/{chunk-FXDLT6FL.js → chunk-MXTYGECH.js} +2 -2
  62. package/dist/cli-bundle/focused-chunks/{chunk-MMXUPDDJ.js → chunk-SUBSWYW3.js} +2 -2
  63. package/dist/cli-bundle/focused-chunks/{chunk-57XY346D.js → chunk-XDPYBQCF.js} +9 -9
  64. package/dist/cli-bundle/focused-chunks/{chunk-TMJDFHVD.js → chunk-Y3JJXRVK.js} +2 -2
  65. package/dist/cli-bundle/focused-chunks/{chunk-66VGB23P.js → chunk-Y5A7SJJ7.js} +2 -2
  66. package/dist/cli-bundle/focused-chunks/chunk-YHWHX6YY.js +2 -0
  67. package/dist/cli-bundle/focused-chunks/{chunk-P2E6LDAE.js → chunk-YVVZ3LQ6.js} +3 -3
  68. package/dist/cli-bundle/focused-chunks/chunk-Z2USIBR2.js +5 -0
  69. package/dist/cli-bundle/main.js +15 -14
  70. package/dist/cli-bundle/sdk-authoring.js +1 -1
  71. package/dist/cli-bundle/sdk-contracts.js +2 -2
  72. package/dist/cli-bundle/sdk-core.js +31 -31
  73. package/dist/cli-bundle/sdk-governance.js +1 -1
  74. package/dist/cli-bundle/sdk-graph.js +1 -1
  75. package/dist/cli-bundle/sdk-merge.js +31 -31
  76. package/dist/cli-bundle/sdk-query.js +1 -1
  77. package/dist/cli-bundle/sdk-runtime.js +1 -1
  78. package/dist/cli-bundle/sdk-testing.js +1 -1
  79. package/dist/cli-bundle/sdk.js +32 -7
  80. package/dist/core/governance/issue-codes.d.ts +11 -2
  81. package/dist/core/governance/issue-codes.js +29 -10
  82. package/dist/core/item/item-format.js +3 -3
  83. package/dist/core/store/item-store.js +12 -5
  84. package/dist/mcp/server.js +123 -9
  85. package/dist/mcp/tool-definitions.d.ts +2 -0
  86. package/dist/mcp/tool-definitions.js +2 -2
  87. package/dist/sdk/agent/closed-domain-contracts.d.ts +1 -1
  88. package/dist/sdk/agent/closed-domain-contracts.js +24 -2
  89. package/dist/sdk/agent-capability-contracts.js +6 -2
  90. package/dist/sdk/cli-bootstrap.js +3 -2
  91. package/dist/sdk/cli-contracts/command-aliases.js +15 -2
  92. package/dist/sdk/cli-contracts/enum-contracts.d.ts +4 -1
  93. package/dist/sdk/cli-contracts/enum-contracts.js +7 -2
  94. package/dist/sdk/cli-contracts/flag-contracts.js +9 -5
  95. package/dist/sdk/cli-contracts/grammar-contracts.d.ts +3 -3
  96. package/dist/sdk/cli-contracts/grammar-contracts.js +24 -17
  97. package/dist/sdk/cli-contracts/runtime-contracts.js +13 -11
  98. package/dist/sdk/cli-contracts/tool-schema.js +18 -15
  99. package/dist/sdk/cli-contracts.d.ts +1 -1
  100. package/dist/sdk/cli-contracts.js +3 -3
  101. package/dist/sdk/cli-program.js +3 -2
  102. package/dist/sdk/completion.js +40 -13
  103. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +14 -2
  104. package/dist/sdk/governance/upgrade.d.ts +2 -0
  105. package/dist/sdk/governance/upgrade.js +30 -8
  106. package/dist/sdk/governance/validate.js +8 -6
  107. package/dist/sdk/guide-topics.js +6 -6
  108. package/dist/sdk/index.d.ts +4 -2
  109. package/dist/sdk/index.js +5 -3
  110. package/dist/sdk/mcp/apps.d.ts +70 -0
  111. package/dist/sdk/mcp/apps.js +154 -0
  112. package/dist/sdk/mcp/skills.d.ts +127 -0
  113. package/dist/sdk/mcp/skills.js +390 -0
  114. package/dist/sdk/read-output-contracts.js +16 -3
  115. package/dist/sdk/runtime-action-aliases.js +7 -3
  116. package/dist/sdk/runtime-input.js +12 -4
  117. package/dist/sdk/runtime-primitives.d.ts +1 -1
  118. package/dist/sdk/runtime-primitives.js +3 -3
  119. package/docs/CLI_GRAMMAR.md +7 -1
  120. package/docs/COMMANDS.md +2 -2
  121. package/docs/EXTENSIONS.md +33 -32
  122. package/docs/MCP_2026_07_28.md +24 -2
  123. package/docs/MCP_2026_07_28_CONFORMANCE.md +4 -4
  124. package/docs/MCP_SKILLS_AND_APPS.md +107 -0
  125. package/docs/QUICKSTART.md +15 -15
  126. package/docs/README.md +1 -0
  127. package/docs/RELEASING.md +11 -3
  128. package/docs/SDK_CONTEXT_INTEGRITY.md +6 -0
  129. package/docs/SDK_EVIDENCE_TRACEABILITY.md +9 -1
  130. package/docs/generated/AGENT_CAPABILITY_ROUTING.md +1 -1
  131. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +7 -6
  132. package/marketplace.json +2 -2
  133. package/package.json +9 -7
  134. package/packages/pm-beads/README.md +12 -6
  135. package/packages/pm-beads/docs/MIGRATION.md +53 -0
  136. package/packages/pm-beads/extensions/beads/index.ts +8 -0
  137. package/packages/pm-beads/extensions/beads/runtime.ts +671 -112
  138. package/packages/pm-beads/package.json +1 -1
  139. package/packages/pm-calendar/package.json +1 -1
  140. package/packages/pm-command-kit/package.json +1 -1
  141. package/packages/pm-digital-twin/package.json +1 -1
  142. package/packages/pm-governance-audit/package.json +1 -1
  143. package/packages/pm-guide-shell/package.json +1 -1
  144. package/packages/pm-kanban/package.json +1 -1
  145. package/packages/pm-lifecycle-hooks/package.json +1 -1
  146. package/packages/pm-linked-test-adapters/package.json +1 -1
  147. package/packages/pm-search-advanced/package.json +1 -1
  148. package/packages/pm-templates/package.json +1 -1
  149. package/packages/pm-todos/package.json +1 -1
  150. package/packages/pm-vcs/package.json +1 -1
  151. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  152. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  153. package/sdk/public-surface.json +235 -13
  154. package/dist/cli-bundle/chunks/chunk-ES25LX3D.js +0 -202
  155. package/dist/cli-bundle/chunks/chunk-FRDWWB6R.js +0 -3
  156. package/dist/cli-bundle/chunks/chunk-ICQ3RVIY.js +0 -2
  157. package/dist/cli-bundle/chunks/chunk-IV64RJVE.js +0 -13
  158. package/dist/cli-bundle/chunks/chunk-MVYLQ67M.js +0 -3
  159. package/dist/cli-bundle/chunks/register-setup-GLZAHLVI.js +0 -2
  160. package/dist/cli-bundle/focused-chunks/chunk-7I23XGWO.js +0 -2
  161. package/dist/cli-bundle/focused-chunks/chunk-LMKG3DFE.js +0 -5
@@ -11,7 +11,9 @@ import type {
11
11
  GlobalOptions,
12
12
  ItemStatus,
13
13
  ItemType,
14
+ LogNote,
14
15
  PmSettings,
16
+ StructuredJsonValue,
15
17
  ToImportLinkedArtifactsOptions,
16
18
  ToImportLinkedTestsOptions,
17
19
  ToImportLogEntriesOptions,
@@ -27,10 +29,16 @@ const UNSAFE_AUTO_DISCOVERY_FILES = [
27
29
  "sync_base.jsonl",
28
30
  ] as const;
29
31
 
32
+ const AUTO_DISCOVERY_WARNINGS = new Map<string, string[]>([
33
+ ["issues.jsonl", ["beads_import_source_autodiscovered:issues.jsonl"]],
34
+ ]);
35
+
30
36
  /** Inputs that customize the beads import operation. */
31
37
  export interface BeadsImportOptions {
32
38
  /** Value that configures or reports file for this contract. */
33
39
  file?: string;
40
+ /** Path to a complete `bd backup` JSONL directory. */
41
+ backupDir?: string;
34
42
  /** Value that configures or reports author for this contract. */
35
43
  author?: string;
36
44
  /** Human-readable explanation suitable for logs and agent-facing output. */
@@ -53,6 +61,36 @@ export interface BeadsImportResult {
53
61
  ids: string[];
54
62
  /** Value that configures or reports warnings for this contract. */
55
63
  warnings: string[];
64
+ /** Whether source and imported relational counts prove lossless parity. */
65
+ complete?: boolean;
66
+ /** Source rows counted across a portable backup. */
67
+ source_counts?: BeadsPortableCounts;
68
+ /** Rows attached to imported pm items from a portable backup. */
69
+ imported_counts?: BeadsPortableCounts;
70
+ /** Complete source-to-imported identity receipt when source IDs are preserved. */
71
+ id_mapping?: BeadsIdMapping[];
72
+ }
73
+
74
+ /** Relational row counts carried by a Beads portable-backup receipt. */
75
+ export interface BeadsPortableCounts {
76
+ /** Issue rows in `issues.jsonl`. */
77
+ issues: number;
78
+ /** Event rows in `events.jsonl`. */
79
+ events: number;
80
+ /** Comment rows in `comments.jsonl`. */
81
+ comments: number;
82
+ /** Dependency rows in `dependencies.jsonl`. */
83
+ dependencies: number;
84
+ /** Label rows in `labels.jsonl`. */
85
+ labels: number;
86
+ }
87
+
88
+ /** One exact source-to-imported ID mapping. */
89
+ export interface BeadsIdMapping {
90
+ /** Authoritative source identifier. */
91
+ source_id: string;
92
+ /** Identifier committed to pm storage. */
93
+ imported_id: string;
56
94
  }
57
95
 
58
96
  interface BeadsRecord extends Record<string, unknown> {
@@ -80,6 +118,12 @@ interface BeadsRecord extends Record<string, unknown> {
80
118
  design?: unknown;
81
119
  external_ref?: unknown;
82
120
  close_reason?: unknown;
121
+ resolution?: unknown;
122
+ expected_result?: unknown;
123
+ actual_result?: unknown;
124
+ parent?: unknown;
125
+ comment_count?: unknown;
126
+ source_events?: unknown;
83
127
  dependencies?: unknown;
84
128
  comments?: unknown;
85
129
  notes?: unknown;
@@ -96,6 +140,7 @@ interface BeadsImportRuntime {
96
140
  | "commitImportedItem"
97
141
  | "generateItemId"
98
142
  | "getItemPath"
143
+ | "listAllItemMetadataLight"
99
144
  | "locateItem"
100
145
  | "normalizeItemMetadata"
101
146
  >;
@@ -107,6 +152,19 @@ interface BeadsImportRuntime {
107
152
  message: string;
108
153
  }
109
154
 
155
+ interface ResolvedBeadsSource {
156
+ source: string;
157
+ sourcePaths: string[];
158
+ raw: string;
159
+ warnings: string[];
160
+ records?: BeadsRecord[];
161
+ counts?: BeadsPortableCounts;
162
+ }
163
+
164
+ interface BeadsPortableBackupState {
165
+ counts?: Partial<BeadsPortableCounts> & { config?: number };
166
+ }
167
+
110
168
  type BeadsImportLineResult =
111
169
  | { id: string; writeWarnings: string[] }
112
170
  | { warning: string };
@@ -120,7 +178,6 @@ const {
120
178
  getActiveExtensionRegistrations,
121
179
  isTimestampLiteral,
122
180
  normalizeItemId,
123
- normalizeRawItemId,
124
181
  nowIso,
125
182
  pathExists,
126
183
  readSettings,
@@ -256,10 +313,24 @@ function normalizeImportedId(
256
313
  preserveSourceIds: boolean,
257
314
  ): string {
258
315
  return preserveSourceIds
259
- ? normalizeRawItemId(id)
316
+ ? preserveBeadsSourceId(id)
260
317
  : normalizeItemId(id, prefix);
261
318
  }
262
319
 
320
+ function preserveBeadsSourceId(id: string): string {
321
+ if (
322
+ id.length === 0 ||
323
+ id !== id.trim() ||
324
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(id)
325
+ ) {
326
+ throw new PmCliError(
327
+ `Beads source ID ${JSON.stringify(id)} cannot be preserved safely; IDs must contain only letters, digits, dots, underscores, and hyphens with no surrounding whitespace.`,
328
+ EXIT_CODE.USAGE,
329
+ );
330
+ }
331
+ return id;
332
+ }
333
+
263
334
  function toDependencies(
264
335
  value: unknown,
265
336
  fallbackCreatedAt: string,
@@ -284,31 +355,39 @@ function toDependencies(
284
355
  return dependencies.length > 0 ? dependencies : undefined;
285
356
  }
286
357
 
287
- function toDependency(
288
- value: unknown,
289
- fallbackCreatedAt: string,
290
- prefix: string,
291
- preserveSourceIds: boolean,
292
- ): Dependency | undefined {
358
+ function toBeadsDependencyId(value: unknown): string | undefined {
293
359
  if (typeof value === "string") {
294
- return toDependencyFromString(
295
- value,
296
- fallbackCreatedAt,
297
- prefix,
298
- preserveSourceIds,
299
- );
360
+ return toNonEmptyString(value);
300
361
  }
301
362
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
302
363
  return undefined;
303
364
  }
304
365
  const candidate = value as Record<string, unknown>;
305
- const id =
366
+ return (
367
+ toNonEmptyString(candidate.depends_on_id) ??
306
368
  toNonEmptyString(candidate.id) ??
307
- toNonEmptyString(candidate.item_id) ??
308
- toNonEmptyString(candidate.depends_on_id);
369
+ toNonEmptyString(candidate.item_id)
370
+ );
371
+ }
372
+
373
+ function toDependency(
374
+ value: unknown,
375
+ fallbackCreatedAt: string,
376
+ prefix: string,
377
+ preserveSourceIds: boolean,
378
+ ): Dependency | undefined {
379
+ const id = toBeadsDependencyId(value);
309
380
  if (!id) {
310
381
  return undefined;
311
382
  }
383
+ if (typeof value === "string") {
384
+ return {
385
+ id: normalizeImportedId(id, prefix, preserveSourceIds),
386
+ kind: "related",
387
+ created_at: fallbackCreatedAt,
388
+ };
389
+ }
390
+ const candidate = value as Record<string, unknown>;
312
391
  const dependencyKind = toDependencyKind(candidate.type ?? candidate.kind);
313
392
  return {
314
393
  id: normalizeImportedId(id, prefix, preserveSourceIds),
@@ -321,23 +400,6 @@ function toDependency(
321
400
  };
322
401
  }
323
402
 
324
- function toDependencyFromString(
325
- value: string,
326
- fallbackCreatedAt: string,
327
- prefix: string,
328
- preserveSourceIds: boolean,
329
- ): Dependency | undefined {
330
- const id = toNonEmptyString(value);
331
- if (!id) {
332
- return undefined;
333
- }
334
- return {
335
- id: normalizeImportedId(id, prefix, preserveSourceIds),
336
- kind: "related",
337
- created_at: fallbackCreatedAt,
338
- };
339
- }
340
-
341
403
  const selectAuthor = selectImportAuthor;
342
404
  const ensureInitHasRun = ensureTrackerInitialized;
343
405
 
@@ -365,17 +427,26 @@ async function readStdin(): Promise<string> {
365
427
  });
366
428
  }
367
429
 
368
- async function resolveBeadsSource(rawPath: string | undefined): Promise<{
369
- source: string;
370
- sourcePath?: string;
371
- raw: string;
372
- warnings: string[];
373
- }> {
430
+ async function resolveBeadsSource(
431
+ rawPath: string | undefined,
432
+ rawBackupDir: string | undefined,
433
+ ): Promise<ResolvedBeadsSource> {
434
+ const backupDir = toNonEmptyString(rawBackupDir);
374
435
  const explicitSource = toNonEmptyString(rawPath);
436
+ if (backupDir && explicitSource) {
437
+ throw new PmCliError(
438
+ 'Options "--file" and "--backup-dir" are mutually exclusive.',
439
+ EXIT_CODE.USAGE,
440
+ );
441
+ }
442
+ if (backupDir) {
443
+ return resolvePortableBackup(backupDir);
444
+ }
375
445
  if (explicitSource) {
376
446
  if (explicitSource === "-") {
377
447
  return {
378
448
  source: "-",
449
+ sourcePaths: [],
379
450
  raw: await readStdin(),
380
451
  warnings: [],
381
452
  };
@@ -390,7 +461,7 @@ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
390
461
  }
391
462
  return {
392
463
  source: explicitSource,
393
- sourcePath: explicitPath,
464
+ sourcePaths: [explicitPath],
394
465
  raw: await fs.readFile(explicitPath, "utf8"),
395
466
  warnings: [],
396
467
  };
@@ -401,12 +472,9 @@ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
401
472
  if (await pathExists(candidatePath)) {
402
473
  return {
403
474
  source: candidate,
404
- sourcePath: candidatePath,
475
+ sourcePaths: [candidatePath],
405
476
  raw: await fs.readFile(candidatePath, "utf8"),
406
- warnings:
407
- candidate === PRIMARY_AUTO_DISCOVERY_FILES[0]
408
- ? []
409
- : [`beads_import_source_autodiscovered:${candidate}`],
477
+ warnings: AUTO_DISCOVERY_WARNINGS.get(candidate) ?? [],
410
478
  };
411
479
  }
412
480
  }
@@ -427,6 +495,275 @@ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
427
495
  );
428
496
  }
429
497
 
498
+ async function readPortableJsonl(
499
+ backupPath: string,
500
+ filename: string,
501
+ ): Promise<{ path: string; records: Record<string, unknown>[] }> {
502
+ const filePath = path.join(backupPath, filename);
503
+ if (!(await pathExists(filePath))) {
504
+ throw new PmCliError(
505
+ `Beads portable backup is incomplete: missing ${filePath}. Run bd backup --force and pass its backup directory with --backup-dir.`,
506
+ EXIT_CODE.NOT_FOUND,
507
+ );
508
+ }
509
+ const records: Record<string, unknown>[] = [];
510
+ const lines = (await fs.readFile(filePath, "utf8")).split(/\r?\n/u);
511
+ for (let index = 0; index < lines.length; index += 1) {
512
+ const line = lines[index]!.trim();
513
+ if (line.length === 0) continue;
514
+ let parsed: unknown;
515
+ try {
516
+ parsed = JSON.parse(line) as unknown;
517
+ } catch {
518
+ throw new PmCliError(
519
+ `Beads portable backup contains invalid JSON in ${filename}:${index + 1}.`,
520
+ EXIT_CODE.USAGE,
521
+ );
522
+ }
523
+ if (
524
+ typeof parsed !== "object" ||
525
+ parsed === null ||
526
+ Array.isArray(parsed)
527
+ ) {
528
+ throw new PmCliError(
529
+ `Beads portable backup contains a non-record row in ${filename}:${index + 1}.`,
530
+ EXIT_CODE.USAGE,
531
+ );
532
+ }
533
+ records.push(parsed as Record<string, unknown>);
534
+ }
535
+ return { path: filePath, records };
536
+ }
537
+
538
+ function portableRelationIssueId(
539
+ record: Record<string, unknown>,
540
+ filename: string,
541
+ ): string {
542
+ const issueId = toNonEmptyString(record.issue_id);
543
+ if (!issueId) {
544
+ throw new PmCliError(
545
+ `Beads portable backup relation in ${filename} is missing issue_id.`,
546
+ EXIT_CODE.USAGE,
547
+ );
548
+ }
549
+ return issueId;
550
+ }
551
+
552
+ function attachPortableRelations(
553
+ issues: BeadsRecord[],
554
+ comments: Record<string, unknown>[],
555
+ events: Record<string, unknown>[],
556
+ dependencies: Record<string, unknown>[],
557
+ labels: Record<string, unknown>[],
558
+ ): void {
559
+ const byId = new Map<string, BeadsRecord>();
560
+ for (const issue of issues) {
561
+ const id = toNonEmptyString(issue.id);
562
+ if (!id) {
563
+ throw new PmCliError(
564
+ "Beads portable backup issue is missing id.",
565
+ EXIT_CODE.USAGE,
566
+ );
567
+ }
568
+ if (byId.has(id)) {
569
+ throw new PmCliError(
570
+ `Beads portable backup contains duplicate issue ID ${id}.`,
571
+ EXIT_CODE.CONFLICT,
572
+ );
573
+ }
574
+ if (!toNonEmptyString(issue.title)) {
575
+ throw new PmCliError(
576
+ `Beads portable backup issue ${id} is missing title.`,
577
+ EXIT_CODE.USAGE,
578
+ );
579
+ }
580
+ byId.set(id, issue);
581
+ }
582
+ attachPortableRelationRows(
583
+ byId,
584
+ comments,
585
+ "comments.jsonl",
586
+ "comments",
587
+ (row, issueId) => {
588
+ if (!toNonEmptyString(row.text)) {
589
+ throw new PmCliError(
590
+ `Beads portable backup comment for ${issueId} is missing text.`,
591
+ EXIT_CODE.USAGE,
592
+ );
593
+ }
594
+ return row;
595
+ },
596
+ );
597
+ attachPortableRelationRows(
598
+ byId,
599
+ events,
600
+ "events.jsonl",
601
+ "source_events",
602
+ (row, issueId) => {
603
+ if (!toNonEmptyString(row.event_type)) {
604
+ throw new PmCliError(
605
+ `Beads portable backup event for ${issueId} is missing event_type.`,
606
+ EXIT_CODE.USAGE,
607
+ );
608
+ }
609
+ return row;
610
+ },
611
+ );
612
+ attachPortableRelationRows(
613
+ byId,
614
+ dependencies,
615
+ "dependencies.jsonl",
616
+ "dependencies",
617
+ (row, issueId) => {
618
+ const targetId = toNonEmptyString(row.depends_on_id);
619
+ if (!targetId) {
620
+ throw new PmCliError(
621
+ `Beads portable backup dependency for ${issueId} is missing depends_on_id.`,
622
+ EXIT_CODE.USAGE,
623
+ );
624
+ }
625
+ if (!byId.has(targetId)) {
626
+ throw new PmCliError(
627
+ `Beads portable backup dependency for ${issueId} references missing target ${targetId}.`,
628
+ EXIT_CODE.CONFLICT,
629
+ );
630
+ }
631
+ return {
632
+ ...row,
633
+ id: undefined,
634
+ item_id: undefined,
635
+ depends_on_id: targetId,
636
+ };
637
+ },
638
+ );
639
+ attachPortableRelationRows(
640
+ byId,
641
+ labels,
642
+ "labels.jsonl",
643
+ "labels",
644
+ (row, issueId) => {
645
+ const label = toNonEmptyString(row.label);
646
+ if (!label) {
647
+ throw new PmCliError(
648
+ `Beads portable backup label for ${issueId} is missing label.`,
649
+ EXIT_CODE.USAGE,
650
+ );
651
+ }
652
+ return label;
653
+ },
654
+ );
655
+ }
656
+
657
+ function attachPortableRelationRows(
658
+ byId: ReadonlyMap<string, BeadsRecord>,
659
+ rows: Record<string, unknown>[],
660
+ filename: string,
661
+ field: "comments" | "dependencies" | "labels" | "source_events",
662
+ normalize: (row: Record<string, unknown>, issueId: string) => unknown,
663
+ ): void {
664
+ for (const row of rows) {
665
+ const issueId = portableRelationIssueId(row, filename);
666
+ const issue = byId.get(issueId);
667
+ if (!issue) {
668
+ throw new PmCliError(
669
+ `Beads portable backup ${filename} references missing issue ${issueId}.`,
670
+ EXIT_CODE.CONFLICT,
671
+ );
672
+ }
673
+ const current = Array.isArray(issue[field]) ? issue[field] : [];
674
+ issue[field] = [...current, normalize(row, issueId)];
675
+ }
676
+ }
677
+
678
+ function verifyPortableCounts(
679
+ expected: BeadsPortableBackupState["counts"],
680
+ actual: BeadsPortableCounts,
681
+ ): void {
682
+ if (!expected) {
683
+ throw new PmCliError(
684
+ "Beads portable backup_state.json is missing counts.",
685
+ EXIT_CODE.USAGE,
686
+ );
687
+ }
688
+ for (const key of Object.keys(actual) as Array<keyof BeadsPortableCounts>) {
689
+ if (expected[key] !== actual[key]) {
690
+ throw new PmCliError(
691
+ `Beads portable backup count mismatch for ${key}: backup_state=${String(expected[key])}, observed=${actual[key]}.`,
692
+ EXIT_CODE.CONFLICT,
693
+ );
694
+ }
695
+ }
696
+ }
697
+
698
+ async function resolvePortableBackup(
699
+ rawBackupDir: string,
700
+ ): Promise<ResolvedBeadsSource> {
701
+ const backupPath = resolveInputPath(rawBackupDir);
702
+ if (!(await pathExists(backupPath))) {
703
+ throw new PmCliError(
704
+ `Beads portable backup directory not found at ${backupPath}`,
705
+ EXIT_CODE.NOT_FOUND,
706
+ );
707
+ }
708
+ const [issuesFile, eventsFile, commentsFile, dependenciesFile, labelsFile] =
709
+ await Promise.all([
710
+ readPortableJsonl(backupPath, "issues.jsonl"),
711
+ readPortableJsonl(backupPath, "events.jsonl"),
712
+ readPortableJsonl(backupPath, "comments.jsonl"),
713
+ readPortableJsonl(backupPath, "dependencies.jsonl"),
714
+ readPortableJsonl(backupPath, "labels.jsonl"),
715
+ ]);
716
+ const statePath = path.join(backupPath, "backup_state.json");
717
+ if (!(await pathExists(statePath))) {
718
+ throw new PmCliError(
719
+ `Beads portable backup is incomplete: missing ${statePath}.`,
720
+ EXIT_CODE.NOT_FOUND,
721
+ );
722
+ }
723
+ let state: BeadsPortableBackupState;
724
+ try {
725
+ state = JSON.parse(
726
+ await fs.readFile(statePath, "utf8"),
727
+ ) as BeadsPortableBackupState;
728
+ } catch {
729
+ throw new PmCliError(
730
+ `Beads portable backup contains invalid JSON in ${statePath}.`,
731
+ EXIT_CODE.USAGE,
732
+ );
733
+ }
734
+ const records = issuesFile.records.map((record) => ({ ...record }));
735
+ attachPortableRelations(
736
+ records,
737
+ commentsFile.records,
738
+ eventsFile.records,
739
+ dependenciesFile.records,
740
+ labelsFile.records,
741
+ );
742
+ const counts: BeadsPortableCounts = {
743
+ issues: records.length,
744
+ events: eventsFile.records.length,
745
+ comments: commentsFile.records.length,
746
+ dependencies: dependenciesFile.records.length,
747
+ labels: labelsFile.records.length,
748
+ };
749
+ verifyPortableCounts(state.counts, counts);
750
+ return {
751
+ source: backupPath,
752
+ sourcePaths: [
753
+ issuesFile.path,
754
+ eventsFile.path,
755
+ commentsFile.path,
756
+ dependenciesFile.path,
757
+ labelsFile.path,
758
+ statePath,
759
+ ],
760
+ raw: "",
761
+ warnings: [],
762
+ records,
763
+ counts,
764
+ };
765
+ }
766
+
430
767
  function parseBeadsLine(line: string, lineNumber: number): ParsedBeadsLine {
431
768
  const trimmed = line.trim();
432
769
  if (trimmed.length === 0) {
@@ -444,11 +781,144 @@ function parseBeadsLine(line: string, lineNumber: number): ParsedBeadsLine {
444
781
  return { record: parsed as BeadsRecord };
445
782
  }
446
783
 
784
+ function parseBeadsRecords(source: ResolvedBeadsSource): {
785
+ records: Array<{ record: BeadsRecord; lineNumber: number }>;
786
+ warnings: string[];
787
+ skipped: number;
788
+ } {
789
+ if (source.records) {
790
+ return {
791
+ records: source.records.map((record, index) => ({
792
+ record,
793
+ lineNumber: index + 1,
794
+ })),
795
+ warnings: [],
796
+ skipped: 0,
797
+ };
798
+ }
799
+ const records: Array<{ record: BeadsRecord; lineNumber: number }> = [];
800
+ const warnings: string[] = [];
801
+ let skipped = 0;
802
+ const lines = source.raw.split(/\r?\n/u);
803
+ for (let index = 0; index < lines.length; index += 1) {
804
+ const lineNumber = index + 1;
805
+ const parsed = parseBeadsLine(lines[index]!, lineNumber);
806
+ if (!parsed) continue;
807
+ if ("warning" in parsed) {
808
+ warnings.push(parsed.warning);
809
+ skipped += 1;
810
+ } else {
811
+ records.push({ record: parsed.record, lineNumber });
812
+ }
813
+ }
814
+ return { records, warnings, skipped };
815
+ }
816
+
817
+ function assertLosslessSourceFormat(
818
+ source: ResolvedBeadsSource,
819
+ records: Array<{ record: BeadsRecord }>,
820
+ ): void {
821
+ if (
822
+ source.counts === undefined &&
823
+ records.some(
824
+ ({ record }) =>
825
+ typeof record.comment_count === "number" && record.comment_count > 0,
826
+ )
827
+ ) {
828
+ throw new PmCliError(
829
+ "This Beads issue export advertises comment counts but omits the relational comment/event tables. Run `bd backup --force` and import the resulting directory with `pm beads import --backup-dir <path>`; no pm items were written.",
830
+ EXIT_CODE.USAGE,
831
+ );
832
+ }
833
+ }
834
+
835
+ function assertPreservedRelationshipIdSafety(record: BeadsRecord): void {
836
+ const parent = toNonEmptyString(record.parent);
837
+ if (parent) preserveBeadsSourceId(parent);
838
+ if (!Array.isArray(record.dependencies)) return;
839
+ for (const dependency of record.dependencies) {
840
+ const dependencyId = toBeadsDependencyId(dependency);
841
+ if (dependencyId) preserveBeadsSourceId(dependencyId);
842
+ }
843
+ }
844
+
845
+ async function assertPreservedIdSafety(
846
+ records: Array<{ record: BeadsRecord; lineNumber: number }>,
847
+ runtime: BeadsImportRuntime,
848
+ ): Promise<void> {
849
+ if (!runtime.preserveSourceIds) return;
850
+ const sourceByFoldedId = new Map<
851
+ string,
852
+ { id: string; lineNumber: number }
853
+ >();
854
+ for (const { record, lineNumber } of records) {
855
+ assertPreservedRelationshipIdSafety(record);
856
+ if (typeof record.id !== "string" || record.id.trim().length === 0) {
857
+ continue;
858
+ }
859
+ const id = preserveBeadsSourceId(record.id);
860
+ const folded = id.toLocaleLowerCase("en-US");
861
+ const previous = sourceByFoldedId.get(folded);
862
+ if (previous) {
863
+ throw new PmCliError(
864
+ `Beads preserve-source-ids case-insensitive ID collision: ${previous.id} (line ${previous.lineNumber}) conflicts with ${id} (line ${lineNumber}); no pm items were written.`,
865
+ EXIT_CODE.CONFLICT,
866
+ );
867
+ }
868
+ sourceByFoldedId.set(folded, { id, lineNumber });
869
+ }
870
+ const existingItems = await runtime.sdk.listAllItemMetadataLight(
871
+ runtime.pmRoot,
872
+ runtime.settings.item_format,
873
+ runtime.typeRegistry.type_to_folder,
874
+ );
875
+ const existingByFoldedId = new Map(
876
+ existingItems.map(({ id }) => [id.toLocaleLowerCase("en-US"), id]),
877
+ );
878
+ for (const { id } of sourceByFoldedId.values()) {
879
+ const existingId = existingByFoldedId.get(id.toLocaleLowerCase("en-US"));
880
+ if (existingId) {
881
+ throw new PmCliError(
882
+ `Beads preserve-source-ids target collision: source ${id} conflicts case-insensitively with existing pm item ${existingId}; no pm items were written.`,
883
+ EXIT_CODE.CONFLICT,
884
+ );
885
+ }
886
+ }
887
+ }
888
+
889
+ function countImportedPortableRelations(
890
+ records: Array<{ record: BeadsRecord }>,
891
+ importedSourceIds: ReadonlySet<string>,
892
+ ): BeadsPortableCounts {
893
+ const importedRecords = records
894
+ .map(({ record }) => record)
895
+ .filter(
896
+ (record) =>
897
+ typeof record.id === "string" && importedSourceIds.has(record.id),
898
+ );
899
+ const count = (field: keyof BeadsRecord): number =>
900
+ importedRecords.reduce(
901
+ (total, record) =>
902
+ total + (Array.isArray(record[field]) ? record[field].length : 0),
903
+ 0,
904
+ );
905
+ return {
906
+ issues: importedRecords.length,
907
+ events: count("source_events"),
908
+ comments: count("comments"),
909
+ dependencies: count("dependencies"),
910
+ labels: count("labels"),
911
+ };
912
+ }
913
+
447
914
  async function resolveBeadsImportId(
448
915
  record: BeadsRecord,
449
916
  runtime: BeadsImportRuntime,
450
917
  ): Promise<string> {
451
- const rawId = toNonEmptyString(record.id);
918
+ const rawId =
919
+ typeof record.id === "string" && record.id.trim().length > 0
920
+ ? record.id
921
+ : undefined;
452
922
  return rawId
453
923
  ? normalizeImportedId(
454
924
  rawId,
@@ -461,6 +931,53 @@ async function resolveBeadsImportId(
461
931
  );
462
932
  }
463
933
 
934
+ function toBeadsEventNotes(
935
+ value: unknown,
936
+ fallbackCreatedAt: string,
937
+ fallbackAuthor: string,
938
+ ): LogNote[] {
939
+ if (!Array.isArray(value)) return [];
940
+ return value.flatMap((entry): LogNote[] => {
941
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
942
+ return [];
943
+ }
944
+ const record = entry as Record<string, unknown>;
945
+ const eventType = toNonEmptyString(record.event_type);
946
+ if (!eventType) {
947
+ return [];
948
+ }
949
+ const data = {
950
+ source: "beads-portable-backup",
951
+ ...record,
952
+ } as Record<string, StructuredJsonValue>;
953
+ return [
954
+ {
955
+ created_at: toIsoString(record.created_at) ?? fallbackCreatedAt,
956
+ author: toNonEmptyString(record.actor) ?? fallbackAuthor,
957
+ text: toNonEmptyString(record.comment) ?? eventType,
958
+ format: "json",
959
+ event_type: `beads:${eventType}`,
960
+ data,
961
+ },
962
+ ];
963
+ });
964
+ }
965
+
966
+ function collectBeadsNotes(
967
+ record: BeadsRecord,
968
+ createdAt: string,
969
+ author: string,
970
+ ): LogNote[] | undefined {
971
+ const notes =
972
+ toImportLogEntries(record.notes, {
973
+ ...BEADS_LOG_ENTRY_OPTIONS,
974
+ fallbackCreatedAt: createdAt,
975
+ fallbackAuthor: author,
976
+ }) ?? [];
977
+ notes.push(...toBeadsEventNotes(record.source_events, createdAt, author));
978
+ return notes.length > 0 ? notes : undefined;
979
+ }
980
+
464
981
  function buildBeadsImportedBody(record: BeadsRecord): string {
465
982
  const rawBody = toNonEmptyString(record.body) ?? "";
466
983
  const design = toNonEmptyString(record.design);
@@ -486,21 +1003,66 @@ async function importBeadsRecord(
486
1003
  return { warning: `beads_import_missing_title:${lineNumber}` };
487
1004
  }
488
1005
 
1006
+ const id = await resolveBeadsImportId(record, runtime);
1007
+ const itemMetadata = buildBeadsItemMetadata(record, id, title, runtime);
1008
+ const afterDocument = runtime.sdk.canonicalDocument({
1009
+ metadata: itemMetadata,
1010
+ body: buildBeadsImportedBody(record),
1011
+ });
1012
+ const existing = await runtime.sdk.locateItem(
1013
+ runtime.pmRoot,
1014
+ id,
1015
+ runtime.settings.id_prefix,
1016
+ runtime.settings.item_format,
1017
+ runtime.typeRegistry.type_to_folder,
1018
+ );
1019
+ if (existing) {
1020
+ return { warning: `beads_import_item_exists:${id}` };
1021
+ }
1022
+ const itemPath = runtime.sdk.getItemPath(
1023
+ runtime.pmRoot,
1024
+ itemMetadata.type,
1025
+ id,
1026
+ "toon",
1027
+ runtime.typeRegistry.type_to_folder,
1028
+ );
1029
+ const commit = await runtime.sdk.commitImportedItem({
1030
+ pmRoot: runtime.pmRoot,
1031
+ id,
1032
+ itemPath,
1033
+ document: afterDocument,
1034
+ author: runtime.author,
1035
+ message: runtime.message,
1036
+ settings: runtime.settings,
1037
+ conflictWarningPrefix: "beads_import_lock_conflict",
1038
+ });
1039
+ return commit.committed
1040
+ ? { id, writeWarnings: commit.writeWarnings }
1041
+ : { warning: commit.conflictWarning };
1042
+ }
1043
+
1044
+ function buildBeadsItemMetadata(
1045
+ record: BeadsRecord,
1046
+ id: string,
1047
+ title: string,
1048
+ runtime: BeadsImportRuntime,
1049
+ ): ReturnType<typeof pmSdk.normalizeItemMetadata> {
489
1050
  const createdAt = toIsoString(record.created_at) ?? nowIso();
490
1051
  const updatedAt = toIsoString(record.updated_at) ?? createdAt;
491
- const id = await resolveBeadsImportId(record, runtime);
492
1052
  const typeMapping = toItemType(record.issue_type ?? record.type);
493
1053
  const type = typeMapping.type;
1054
+ const status = toStatus(record.status);
494
1055
  const closedAt = toIsoString(record.closed_at);
1056
+ const closeReason = toNonEmptyString(record.close_reason);
495
1057
  const assignee =
496
1058
  toNonEmptyString(record.assignee) ?? toNonEmptyString(record.owner);
497
- const itemMetadata = runtime.sdk.normalizeItemMetadata({
1059
+ return runtime.sdk.normalizeItemMetadata({
498
1060
  id,
499
1061
  title,
500
1062
  description: toNonEmptyString(record.description) ?? "",
501
1063
  type,
502
1064
  source_type: typeMapping.sourceType,
503
- status: toStatus(record.status),
1065
+ status,
504
1066
  priority: toPriority(record.priority),
505
1067
  tags: toTags(record.tags ?? record.labels),
506
1068
  created_at: createdAt,
@@ -517,7 +1079,20 @@ async function importBeadsRecord(
517
1079
  acceptance_criteria: toNonEmptyString(record.acceptance_criteria),
518
1080
  design: toNonEmptyString(record.design),
519
1081
  external_ref: toNonEmptyString(record.external_ref),
520
- close_reason: toNonEmptyString(record.close_reason),
1082
+ parent:
1083
+ typeof record.parent === "string" && record.parent.trim().length > 0
1084
+ ? normalizeImportedId(
1085
+ record.parent,
1086
+ runtime.settings.id_prefix,
1087
+ runtime.preserveSourceIds,
1088
+ )
1089
+ : undefined,
1090
+ close_reason: closeReason,
1091
+ resolution:
1092
+ toNonEmptyString(record.resolution) ??
1093
+ (status === "closed" || status === "canceled" ? closeReason : undefined),
1094
+ expected_result: toNonEmptyString(record.expected_result),
1095
+ actual_result: toNonEmptyString(record.actual_result),
521
1096
  dependencies: toDependencies(
522
1097
  record.dependencies,
523
1098
  createdAt,
@@ -529,11 +1104,7 @@ async function importBeadsRecord(
529
1104
  fallbackCreatedAt: createdAt,
530
1105
  fallbackAuthor: runtime.author,
531
1106
  }),
532
- notes: toImportLogEntries(record.notes, {
533
- ...BEADS_LOG_ENTRY_OPTIONS,
534
- fallbackCreatedAt: createdAt,
535
- fallbackAuthor: runtime.author,
536
- }),
1107
+ notes: collectBeadsNotes(record, createdAt, runtime.author),
537
1108
  learnings: toImportLogEntries(record.learnings, {
538
1109
  ...BEADS_LOG_ENTRY_OPTIONS,
539
1110
  fallbackCreatedAt: createdAt,
@@ -543,40 +1114,6 @@ async function importBeadsRecord(
543
1114
  tests: toImportLinkedTests(record.tests, BEADS_TEST_OPTIONS),
544
1115
  docs: toImportLinkedDocs(record.docs, BEADS_DOC_OPTIONS),
545
1116
  });
546
- const afterDocument = runtime.sdk.canonicalDocument({
547
- metadata: itemMetadata,
548
- body: buildBeadsImportedBody(record),
549
- });
550
- const existing = await runtime.sdk.locateItem(
551
- runtime.pmRoot,
552
- id,
553
- runtime.settings.id_prefix,
554
- runtime.settings.item_format,
555
- runtime.typeRegistry.type_to_folder,
556
- );
557
- if (existing) {
558
- return { warning: `beads_import_item_exists:${id}` };
559
- }
560
- const itemPath = runtime.sdk.getItemPath(
561
- runtime.pmRoot,
562
- type,
563
- id,
564
- "toon",
565
- runtime.typeRegistry.type_to_folder,
566
- );
567
- const commit = await runtime.sdk.commitImportedItem({
568
- pmRoot: runtime.pmRoot,
569
- id,
570
- itemPath,
571
- document: afterDocument,
572
- author: runtime.author,
573
- message: runtime.message,
574
- settings: runtime.settings,
575
- conflictWarningPrefix: "beads_import_lock_conflict",
576
- });
577
- return commit.committed
578
- ? { id, writeWarnings: commit.writeWarnings }
579
- : { warning: commit.conflictWarning };
580
1117
  }
581
1118
 
582
1119
  /** Executes the beads import operation through the package runtime. */
@@ -593,14 +1130,18 @@ export async function runBeadsImport(
593
1130
  getActiveExtensionRegistrations(),
594
1131
  );
595
1132
  const preserveSourceIds = options.preserveSourceIds === true;
1133
+ const resolvedSource = await resolveBeadsSource(
1134
+ options.file,
1135
+ options.backupDir,
1136
+ );
596
1137
  const {
597
1138
  source,
598
- sourcePath,
599
- raw,
1139
+ sourcePaths,
600
1140
  warnings: sourceWarnings,
601
- } = await resolveBeadsSource(options.file);
1141
+ counts: sourceCounts,
1142
+ } = resolvedSource;
602
1143
  const warnings: string[] = [...sourceWarnings];
603
- if (sourcePath) {
1144
+ for (const sourcePath of sourcePaths) {
604
1145
  warnings.push(
605
1146
  ...(await runActiveOnReadHooks({
606
1147
  path: sourcePath,
@@ -608,7 +1149,9 @@ export async function runBeadsImport(
608
1149
  })),
609
1150
  );
610
1151
  }
611
- const lines = raw.split(/\r?\n/);
1152
+ const parsed = parseBeadsRecords(resolvedSource);
1153
+ warnings.push(...parsed.warnings);
1154
+ assertLosslessSourceFormat(resolvedSource, parsed.records);
612
1155
  const author = selectAuthor(
613
1156
  toNonEmptyString(options.author),
614
1157
  settings.author_default,
@@ -616,8 +1159,10 @@ export async function runBeadsImport(
616
1159
  const message =
617
1160
  toNonEmptyString(options.message) ?? "Import from Beads JSONL";
618
1161
  const ids: string[] = [];
1162
+ const idMapping: BeadsIdMapping[] = [];
1163
+ const importedSourceIds = new Set<string>();
619
1164
  let imported = 0;
620
- let skipped = 0;
1165
+ let skipped = parsed.skipped;
621
1166
  const runtime: BeadsImportRuntime = {
622
1167
  sdk: pmSdk,
623
1168
  pmRoot,
@@ -627,23 +1172,10 @@ export async function runBeadsImport(
627
1172
  author,
628
1173
  message,
629
1174
  };
1175
+ await assertPreservedIdSafety(parsed.records, runtime);
630
1176
 
631
- for (let index = 0; index < lines.length; index += 1) {
632
- const lineNumber = index + 1;
633
- const parsed = parseBeadsLine(lines[index], lineNumber);
634
- if (!parsed) {
635
- continue;
636
- }
637
- if ("warning" in parsed) {
638
- warnings.push(parsed.warning);
639
- skipped += 1;
640
- continue;
641
- }
642
- const importedLine = await importBeadsRecord(
643
- parsed.record,
644
- lineNumber,
645
- runtime,
646
- );
1177
+ for (const { record, lineNumber } of parsed.records) {
1178
+ const importedLine = await importBeadsRecord(record, lineNumber, runtime);
647
1179
  if ("warning" in importedLine) {
648
1180
  warnings.push(importedLine.warning);
649
1181
  skipped += 1;
@@ -651,9 +1183,22 @@ export async function runBeadsImport(
651
1183
  }
652
1184
  warnings.push(...importedLine.writeWarnings);
653
1185
  ids.push(importedLine.id);
1186
+ if (typeof record.id === "string" && record.id.trim().length > 0) {
1187
+ importedSourceIds.add(record.id);
1188
+ if (preserveSourceIds) {
1189
+ idMapping.push({
1190
+ source_id: record.id,
1191
+ imported_id: importedLine.id,
1192
+ });
1193
+ }
1194
+ }
654
1195
  imported += 1;
655
1196
  }
656
1197
 
1198
+ const importedCounts = sourceCounts
1199
+ ? countImportedPortableRelations(parsed.records, importedSourceIds)
1200
+ : undefined;
1201
+
657
1202
  return {
658
1203
  ok: true,
659
1204
  source,
@@ -661,5 +1206,19 @@ export async function runBeadsImport(
661
1206
  skipped,
662
1207
  ids,
663
1208
  warnings,
1209
+ ...(sourceCounts
1210
+ ? {
1211
+ complete:
1212
+ skipped === 0 &&
1213
+ Object.keys(sourceCounts).every(
1214
+ (key) =>
1215
+ sourceCounts[key as keyof BeadsPortableCounts] ===
1216
+ importedCounts?.[key as keyof BeadsPortableCounts],
1217
+ ),
1218
+ source_counts: sourceCounts,
1219
+ imported_counts: importedCounts,
1220
+ }
1221
+ : {}),
1222
+ ...(preserveSourceIds ? { id_mapping: idMapping } : {}),
664
1223
  };
665
1224
  }