@unbrained/pm-cli 2026.3.12 → 2026.5.1

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 (285) hide show
  1. package/.agents/pm/extensions/.managed-extensions.json +42 -0
  2. package/.agents/pm/extensions/beads/index.js +109 -0
  3. package/.agents/pm/extensions/beads/manifest.json +7 -0
  4. package/{dist/cli/commands/beads.js → .agents/pm/extensions/beads/runtime.js} +31 -21
  5. package/.agents/pm/extensions/beads/runtime.ts +702 -0
  6. package/.agents/pm/extensions/todos/index.js +126 -0
  7. package/.agents/pm/extensions/todos/manifest.json +7 -0
  8. package/{dist/extensions/builtins/todos/import-export.js → .agents/pm/extensions/todos/runtime.js} +39 -29
  9. package/.agents/pm/extensions/todos/runtime.ts +568 -0
  10. package/AGENTS.md +196 -92
  11. package/CHANGELOG.md +399 -0
  12. package/CODE_OF_CONDUCT.md +42 -0
  13. package/CONTRIBUTING.md +144 -0
  14. package/PRD.md +512 -164
  15. package/README.md +1053 -2
  16. package/SECURITY.md +51 -0
  17. package/dist/cli/commands/activity.d.ts +5 -0
  18. package/dist/cli/commands/activity.js +66 -3
  19. package/dist/cli/commands/activity.js.map +1 -1
  20. package/dist/cli/commands/aggregate.d.ts +54 -0
  21. package/dist/cli/commands/aggregate.js +181 -0
  22. package/dist/cli/commands/aggregate.js.map +1 -0
  23. package/dist/cli/commands/append.js +4 -1
  24. package/dist/cli/commands/append.js.map +1 -1
  25. package/dist/cli/commands/calendar.d.ts +109 -0
  26. package/dist/cli/commands/calendar.js +797 -0
  27. package/dist/cli/commands/calendar.js.map +1 -0
  28. package/dist/cli/commands/claim.d.ts +5 -1
  29. package/dist/cli/commands/claim.js +42 -21
  30. package/dist/cli/commands/claim.js.map +1 -1
  31. package/dist/cli/commands/close.d.ts +1 -0
  32. package/dist/cli/commands/close.js +54 -5
  33. package/dist/cli/commands/close.js.map +1 -1
  34. package/dist/cli/commands/comments-audit.d.ts +91 -0
  35. package/dist/cli/commands/comments-audit.js +195 -0
  36. package/dist/cli/commands/comments-audit.js.map +1 -0
  37. package/dist/cli/commands/comments.d.ts +1 -0
  38. package/dist/cli/commands/comments.js +70 -21
  39. package/dist/cli/commands/comments.js.map +1 -1
  40. package/dist/cli/commands/completion.d.ts +10 -4
  41. package/dist/cli/commands/completion.js +1184 -137
  42. package/dist/cli/commands/completion.js.map +1 -1
  43. package/dist/cli/commands/config.d.ts +35 -3
  44. package/dist/cli/commands/config.js +968 -13
  45. package/dist/cli/commands/config.js.map +1 -1
  46. package/dist/cli/commands/context.d.ts +86 -0
  47. package/dist/cli/commands/context.js +299 -0
  48. package/dist/cli/commands/context.js.map +1 -0
  49. package/dist/cli/commands/contracts.d.ts +78 -0
  50. package/dist/cli/commands/contracts.js +920 -0
  51. package/dist/cli/commands/contracts.js.map +1 -0
  52. package/dist/cli/commands/create.d.ts +48 -14
  53. package/dist/cli/commands/create.js +1331 -160
  54. package/dist/cli/commands/create.js.map +1 -1
  55. package/dist/cli/commands/dedupe-audit.d.ts +81 -0
  56. package/dist/cli/commands/dedupe-audit.js +330 -0
  57. package/dist/cli/commands/dedupe-audit.js.map +1 -0
  58. package/dist/cli/commands/deps.d.ts +52 -0
  59. package/dist/cli/commands/deps.js +204 -0
  60. package/dist/cli/commands/deps.js.map +1 -0
  61. package/dist/cli/commands/docs.d.ts +19 -0
  62. package/dist/cli/commands/docs.js +212 -13
  63. package/dist/cli/commands/docs.js.map +1 -1
  64. package/dist/cli/commands/extension.d.ts +122 -0
  65. package/dist/cli/commands/extension.js +1850 -0
  66. package/dist/cli/commands/extension.js.map +1 -0
  67. package/dist/cli/commands/files.d.ts +52 -1
  68. package/dist/cli/commands/files.js +443 -13
  69. package/dist/cli/commands/files.js.map +1 -1
  70. package/dist/cli/commands/gc.d.ts +11 -1
  71. package/dist/cli/commands/gc.js +89 -11
  72. package/dist/cli/commands/gc.js.map +1 -1
  73. package/dist/cli/commands/get.d.ts +13 -0
  74. package/dist/cli/commands/get.js +35 -3
  75. package/dist/cli/commands/get.js.map +1 -1
  76. package/dist/cli/commands/health.d.ts +10 -2
  77. package/dist/cli/commands/health.js +774 -23
  78. package/dist/cli/commands/health.js.map +1 -1
  79. package/dist/cli/commands/history.d.ts +20 -0
  80. package/dist/cli/commands/history.js +152 -6
  81. package/dist/cli/commands/history.js.map +1 -1
  82. package/dist/cli/commands/index.d.ts +16 -3
  83. package/dist/cli/commands/index.js +16 -3
  84. package/dist/cli/commands/index.js.map +1 -1
  85. package/dist/cli/commands/init.d.ts +7 -2
  86. package/dist/cli/commands/init.js +137 -5
  87. package/dist/cli/commands/init.js.map +1 -1
  88. package/dist/cli/commands/learnings.d.ts +17 -0
  89. package/dist/cli/commands/learnings.js +129 -0
  90. package/dist/cli/commands/learnings.js.map +1 -0
  91. package/dist/cli/commands/list.d.ts +29 -1
  92. package/dist/cli/commands/list.js +289 -53
  93. package/dist/cli/commands/list.js.map +1 -1
  94. package/dist/cli/commands/normalize.d.ts +51 -0
  95. package/dist/cli/commands/normalize.js +298 -0
  96. package/dist/cli/commands/normalize.js.map +1 -0
  97. package/dist/cli/commands/notes.d.ts +17 -0
  98. package/dist/cli/commands/notes.js +129 -0
  99. package/dist/cli/commands/notes.js.map +1 -0
  100. package/dist/cli/commands/reindex.d.ts +1 -0
  101. package/dist/cli/commands/reindex.js +208 -32
  102. package/dist/cli/commands/reindex.js.map +1 -1
  103. package/dist/cli/commands/restore.js +164 -30
  104. package/dist/cli/commands/restore.js.map +1 -1
  105. package/dist/cli/commands/search.d.ts +14 -1
  106. package/dist/cli/commands/search.js +475 -81
  107. package/dist/cli/commands/search.js.map +1 -1
  108. package/dist/cli/commands/stats.js +26 -10
  109. package/dist/cli/commands/stats.js.map +1 -1
  110. package/dist/cli/commands/templates.d.ts +26 -0
  111. package/dist/cli/commands/templates.js +179 -0
  112. package/dist/cli/commands/templates.js.map +1 -0
  113. package/dist/cli/commands/test-all.d.ts +19 -1
  114. package/dist/cli/commands/test-all.js +161 -13
  115. package/dist/cli/commands/test-all.js.map +1 -1
  116. package/dist/cli/commands/test-runs.d.ts +63 -0
  117. package/dist/cli/commands/test-runs.js +179 -0
  118. package/dist/cli/commands/test-runs.js.map +1 -0
  119. package/dist/cli/commands/test.d.ts +75 -1
  120. package/dist/cli/commands/test.js +1360 -41
  121. package/dist/cli/commands/test.js.map +1 -1
  122. package/dist/cli/commands/update-many.d.ts +57 -0
  123. package/dist/cli/commands/update-many.js +631 -0
  124. package/dist/cli/commands/update-many.js.map +1 -0
  125. package/dist/cli/commands/update.d.ts +30 -0
  126. package/dist/cli/commands/update.js +1393 -84
  127. package/dist/cli/commands/update.js.map +1 -1
  128. package/dist/cli/commands/validate.d.ts +30 -0
  129. package/dist/cli/commands/validate.js +1140 -0
  130. package/dist/cli/commands/validate.js.map +1 -0
  131. package/dist/cli/error-guidance.d.ts +33 -0
  132. package/dist/cli/error-guidance.js +337 -0
  133. package/dist/cli/error-guidance.js.map +1 -0
  134. package/dist/cli/extension-command-options.d.ts +1 -0
  135. package/dist/cli/extension-command-options.js +92 -0
  136. package/dist/cli/extension-command-options.js.map +1 -1
  137. package/dist/cli/help-content.d.ts +20 -0
  138. package/dist/cli/help-content.js +543 -0
  139. package/dist/cli/help-content.js.map +1 -0
  140. package/dist/cli/main.js +3625 -445
  141. package/dist/cli/main.js.map +1 -1
  142. package/dist/core/extensions/index.d.ts +13 -1
  143. package/dist/core/extensions/index.js +108 -1
  144. package/dist/core/extensions/index.js.map +1 -1
  145. package/dist/core/extensions/item-fields.d.ts +2 -0
  146. package/dist/core/extensions/item-fields.js +79 -0
  147. package/dist/core/extensions/item-fields.js.map +1 -0
  148. package/dist/core/extensions/loader.d.ts +322 -9
  149. package/dist/core/extensions/loader.js +911 -20
  150. package/dist/core/extensions/loader.js.map +1 -1
  151. package/dist/core/extensions/runtime-registrations.d.ts +5 -0
  152. package/dist/core/extensions/runtime-registrations.js +51 -0
  153. package/dist/core/extensions/runtime-registrations.js.map +1 -0
  154. package/dist/core/history/history-stream-policy.d.ts +20 -0
  155. package/dist/core/history/history-stream-policy.js +53 -0
  156. package/dist/core/history/history-stream-policy.js.map +1 -0
  157. package/dist/core/history/history.js +90 -1
  158. package/dist/core/history/history.js.map +1 -1
  159. package/dist/core/item/id.js +4 -1
  160. package/dist/core/item/id.js.map +1 -1
  161. package/dist/core/item/index.d.ts +1 -0
  162. package/dist/core/item/index.js +1 -0
  163. package/dist/core/item/index.js.map +1 -1
  164. package/dist/core/item/item-format.d.ts +11 -5
  165. package/dist/core/item/item-format.js +507 -24
  166. package/dist/core/item/item-format.js.map +1 -1
  167. package/dist/core/item/parent-reference-policy.d.ts +6 -0
  168. package/dist/core/item/parent-reference-policy.js +32 -0
  169. package/dist/core/item/parent-reference-policy.js.map +1 -0
  170. package/dist/core/item/parse.d.ts +5 -0
  171. package/dist/core/item/parse.js +216 -19
  172. package/dist/core/item/parse.js.map +1 -1
  173. package/dist/core/item/sprint-release-format.d.ts +6 -0
  174. package/dist/core/item/sprint-release-format.js +33 -0
  175. package/dist/core/item/sprint-release-format.js.map +1 -0
  176. package/dist/core/item/status.d.ts +3 -0
  177. package/dist/core/item/status.js +24 -0
  178. package/dist/core/item/status.js.map +1 -0
  179. package/dist/core/item/type-registry.d.ts +37 -0
  180. package/dist/core/item/type-registry.js +706 -0
  181. package/dist/core/item/type-registry.js.map +1 -0
  182. package/dist/core/lock/lock.d.ts +1 -1
  183. package/dist/core/lock/lock.js +101 -12
  184. package/dist/core/lock/lock.js.map +1 -1
  185. package/dist/core/output/command-aware.d.ts +1 -0
  186. package/dist/core/output/command-aware.js +394 -0
  187. package/dist/core/output/command-aware.js.map +1 -0
  188. package/dist/core/output/output.d.ts +3 -0
  189. package/dist/core/output/output.js +124 -6
  190. package/dist/core/output/output.js.map +1 -1
  191. package/dist/core/schema/runtime-field-filters.d.ts +3 -0
  192. package/dist/core/schema/runtime-field-filters.js +39 -0
  193. package/dist/core/schema/runtime-field-filters.js.map +1 -0
  194. package/dist/core/schema/runtime-field-values.d.ts +8 -0
  195. package/dist/core/schema/runtime-field-values.js +154 -0
  196. package/dist/core/schema/runtime-field-values.js.map +1 -0
  197. package/dist/core/schema/runtime-schema.d.ts +68 -0
  198. package/dist/core/schema/runtime-schema.js +554 -0
  199. package/dist/core/schema/runtime-schema.js.map +1 -0
  200. package/dist/core/search/cache.d.ts +13 -1
  201. package/dist/core/search/cache.js +123 -14
  202. package/dist/core/search/cache.js.map +1 -1
  203. package/dist/core/search/semantic-defaults.d.ts +6 -0
  204. package/dist/core/search/semantic-defaults.js +120 -0
  205. package/dist/core/search/semantic-defaults.js.map +1 -0
  206. package/dist/core/search/vector-stores.js +3 -1
  207. package/dist/core/search/vector-stores.js.map +1 -1
  208. package/dist/core/shared/command-types.d.ts +2 -0
  209. package/dist/core/shared/conflict-markers.d.ts +7 -0
  210. package/dist/core/shared/conflict-markers.js +27 -0
  211. package/dist/core/shared/conflict-markers.js.map +1 -0
  212. package/dist/core/shared/constants.d.ts +15 -4
  213. package/dist/core/shared/constants.js +141 -1
  214. package/dist/core/shared/constants.js.map +1 -1
  215. package/dist/core/shared/errors.d.ts +10 -1
  216. package/dist/core/shared/errors.js +3 -1
  217. package/dist/core/shared/errors.js.map +1 -1
  218. package/dist/core/shared/text-normalization.d.ts +4 -0
  219. package/dist/core/shared/text-normalization.js +33 -0
  220. package/dist/core/shared/text-normalization.js.map +1 -0
  221. package/dist/core/shared/time.d.ts +1 -2
  222. package/dist/core/shared/time.js +98 -11
  223. package/dist/core/shared/time.js.map +1 -1
  224. package/dist/core/store/index.d.ts +1 -0
  225. package/dist/core/store/index.js +1 -0
  226. package/dist/core/store/index.js.map +1 -1
  227. package/dist/core/store/item-format-migration.d.ts +9 -0
  228. package/dist/core/store/item-format-migration.js +87 -0
  229. package/dist/core/store/item-format-migration.js.map +1 -0
  230. package/dist/core/store/item-store.d.ts +13 -4
  231. package/dist/core/store/item-store.js +238 -51
  232. package/dist/core/store/item-store.js.map +1 -1
  233. package/dist/core/store/paths.d.ts +21 -3
  234. package/dist/core/store/paths.js +59 -4
  235. package/dist/core/store/paths.js.map +1 -1
  236. package/dist/core/store/settings.d.ts +14 -1
  237. package/dist/core/store/settings.js +463 -7
  238. package/dist/core/store/settings.js.map +1 -1
  239. package/dist/core/telemetry/consent.d.ts +2 -0
  240. package/dist/core/telemetry/consent.js +79 -0
  241. package/dist/core/telemetry/consent.js.map +1 -0
  242. package/dist/core/telemetry/runtime.d.ts +38 -0
  243. package/dist/core/telemetry/runtime.js +733 -0
  244. package/dist/core/telemetry/runtime.js.map +1 -0
  245. package/dist/core/test/background-runs.d.ts +117 -0
  246. package/dist/core/test/background-runs.js +760 -0
  247. package/dist/core/test/background-runs.js.map +1 -0
  248. package/dist/core/test/item-test-run-tracking.d.ts +9 -0
  249. package/dist/core/test/item-test-run-tracking.js +50 -0
  250. package/dist/core/test/item-test-run-tracking.js.map +1 -0
  251. package/dist/sdk/cli-contracts.d.ts +92 -0
  252. package/dist/sdk/cli-contracts.js +2357 -0
  253. package/dist/sdk/cli-contracts.js.map +1 -0
  254. package/dist/sdk/index.d.ts +34 -0
  255. package/dist/sdk/index.js +23 -0
  256. package/dist/sdk/index.js.map +1 -0
  257. package/dist/types.d.ts +197 -3
  258. package/dist/types.js +48 -1
  259. package/dist/types.js.map +1 -1
  260. package/docs/ARCHITECTURE.md +368 -39
  261. package/docs/EXTENSIONS.md +454 -49
  262. package/docs/RELEASING.md +68 -19
  263. package/docs/SDK.md +123 -0
  264. package/docs/examples/starter-extension/README.md +48 -0
  265. package/docs/examples/starter-extension/index.js +191 -0
  266. package/docs/examples/starter-extension/manifest.json +17 -0
  267. package/docs/examples/starter-extension/package.json +10 -0
  268. package/package.json +33 -6
  269. package/.pi/extensions/pm-cli/index.ts +0 -778
  270. package/dist/cli/commands/beads.d.ts +0 -16
  271. package/dist/cli/commands/beads.js.map +0 -1
  272. package/dist/cli/commands/install.d.ts +0 -18
  273. package/dist/cli/commands/install.js +0 -87
  274. package/dist/cli/commands/install.js.map +0 -1
  275. package/dist/core/extensions/builtins.d.ts +0 -3
  276. package/dist/core/extensions/builtins.js +0 -47
  277. package/dist/core/extensions/builtins.js.map +0 -1
  278. package/dist/extensions/builtins/beads/index.d.ts +0 -8
  279. package/dist/extensions/builtins/beads/index.js +0 -33
  280. package/dist/extensions/builtins/beads/index.js.map +0 -1
  281. package/dist/extensions/builtins/todos/import-export.d.ts +0 -26
  282. package/dist/extensions/builtins/todos/import-export.js.map +0 -1
  283. package/dist/extensions/builtins/todos/index.d.ts +0 -8
  284. package/dist/extensions/builtins/todos/index.js +0 -38
  285. package/dist/extensions/builtins/todos/index.js.map +0 -1
@@ -0,0 +1,702 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathExists, removeFileIfExists, writeFileAtomic } from "../../../../src/core/fs/fs-utils.js";
4
+ import { getActiveExtensionRegistrations, runActiveOnReadHooks, runActiveOnWriteHooks } from "../../../../src/core/extensions/index.js";
5
+ import { appendHistoryEntry, createHistoryEntry } from "../../../../src/core/history/history.js";
6
+ import { generateItemId, normalizeItemId, normalizeRawItemId } from "../../../../src/core/item/id.js";
7
+ import { canonicalDocument, normalizeFrontMatter, serializeItemDocument } from "../../../../src/core/item/item-format.js";
8
+ import { normalizeStatusInput } from "../../../../src/core/item/status.js";
9
+ import { resolveItemTypeRegistry } from "../../../../src/core/item/type-registry.js";
10
+ import { parseTags } from "../../../../src/core/item/parse.js";
11
+ import { acquireLock } from "../../../../src/core/lock/lock.js";
12
+ import { EXIT_CODE } from "../../../../src/core/shared/constants.js";
13
+ import type { GlobalOptions } from "../../../../src/core/shared/command-types.js";
14
+ import { PmCliError } from "../../../../src/core/shared/errors.js";
15
+ import { isTimestampLiteral, nowIso } from "../../../../src/core/shared/time.js";
16
+ import { locateItem } from "../../../../src/core/store/item-store.js";
17
+ import { getHistoryPath, getItemPath, getSettingsPath, resolvePmRoot } from "../../../../src/core/store/paths.js";
18
+ import { readSettings } from "../../../../src/core/store/settings.js";
19
+ import type { Dependency, ItemDocument, ItemFrontMatter, ItemStatus, ItemType, LogNote, LinkedFile, LinkedTest, LinkedDoc } from "../../../../src/types/index.js";
20
+ import { DEPENDENCY_KIND_VALUES } from "../../../../src/types/index.js";
21
+
22
+ const PRIMARY_AUTO_DISCOVERY_FILES = [
23
+ ".beads/issues.jsonl",
24
+ "issues.jsonl",
25
+ ] as const;
26
+
27
+ const UNSAFE_AUTO_DISCOVERY_FILES = [
28
+ ".beads/sync_base.jsonl",
29
+ "sync_base.jsonl",
30
+ ] as const;
31
+
32
+ export interface BeadsImportOptions {
33
+ file?: string;
34
+ author?: string;
35
+ message?: string;
36
+ preserveSourceIds?: boolean;
37
+ }
38
+
39
+ export interface BeadsImportResult {
40
+ ok: boolean;
41
+ source: string;
42
+ imported: number;
43
+ skipped: number;
44
+ ids: string[];
45
+ warnings: string[];
46
+ }
47
+
48
+ interface BeadsRecord extends Record<string, unknown> {
49
+ id?: unknown;
50
+ title?: unknown;
51
+ description?: unknown;
52
+ issue_type?: unknown;
53
+ type?: unknown;
54
+ status?: unknown;
55
+ priority?: unknown;
56
+ tags?: unknown;
57
+ labels?: unknown;
58
+ body?: unknown;
59
+ created_at?: unknown;
60
+ updated_at?: unknown;
61
+ closed_at?: unknown;
62
+ due_at?: unknown;
63
+ deadline?: unknown;
64
+ assignee?: unknown;
65
+ owner?: unknown;
66
+ author?: unknown;
67
+ created_by?: unknown;
68
+ estimated_minutes?: unknown;
69
+ acceptance_criteria?: unknown;
70
+ design?: unknown;
71
+ external_ref?: unknown;
72
+ close_reason?: unknown;
73
+ dependencies?: unknown;
74
+ comments?: unknown;
75
+ notes?: unknown;
76
+ learnings?: unknown;
77
+ files?: unknown;
78
+ tests?: unknown;
79
+ docs?: unknown;
80
+ }
81
+
82
+ function toNonEmptyString(value: unknown): string | undefined {
83
+ if (typeof value !== "string") {
84
+ return undefined;
85
+ }
86
+ const trimmed = value.trim();
87
+ return trimmed.length > 0 ? trimmed : undefined;
88
+ }
89
+
90
+ function toIsoString(value: unknown): string | undefined {
91
+ const raw = toNonEmptyString(value);
92
+ if (!raw) {
93
+ return undefined;
94
+ }
95
+ if (!isTimestampLiteral(raw)) {
96
+ return undefined;
97
+ }
98
+ return raw;
99
+ }
100
+
101
+ function toEstimatedMinutes(value: unknown): number | undefined {
102
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
103
+ return value;
104
+ }
105
+ if (typeof value === "string" && value.trim().length > 0) {
106
+ const parsed = Number(value);
107
+ if (Number.isFinite(parsed) && parsed >= 0) {
108
+ return parsed;
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ function toPriority(value: unknown): 0 | 1 | 2 | 3 | 4 {
115
+ const fallback: 0 | 1 | 2 | 3 | 4 = 2;
116
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4) {
117
+ return value as 0 | 1 | 2 | 3 | 4;
118
+ }
119
+ if (typeof value === "string" && value.trim().length > 0) {
120
+ const parsed = Number(value);
121
+ if (Number.isInteger(parsed) && parsed >= 0 && parsed <= 4) {
122
+ return parsed as 0 | 1 | 2 | 3 | 4;
123
+ }
124
+ }
125
+ return fallback;
126
+ }
127
+
128
+ function toTags(value: unknown): string[] {
129
+ if (Array.isArray(value)) {
130
+ const tags = value
131
+ .filter((entry): entry is string => typeof entry === "string")
132
+ .map((entry) => entry.trim().toLowerCase())
133
+ .filter((entry) => entry.length > 0);
134
+ return Array.from(new Set(tags)).sort((left, right) => left.localeCompare(right));
135
+ }
136
+ if (typeof value === "string") {
137
+ return parseTags(value);
138
+ }
139
+ return [];
140
+ }
141
+
142
+ function toItemType(value: unknown): { type: ItemType; sourceType?: string } {
143
+ const raw = toNonEmptyString(value);
144
+ const normalized = raw?.toLowerCase();
145
+ switch (normalized) {
146
+ case "epic":
147
+ return { type: "Epic" };
148
+ case "feature":
149
+ return { type: "Feature" };
150
+ case "task":
151
+ return { type: "Task" };
152
+ case "chore":
153
+ return { type: "Chore" };
154
+ case "issue":
155
+ return { type: "Issue" };
156
+ case "bug":
157
+ return { type: "Issue", sourceType: raw };
158
+ case "event":
159
+ return { type: "Task", sourceType: raw };
160
+ default:
161
+ return { type: "Task", sourceType: raw };
162
+ }
163
+ }
164
+
165
+ function toStatus(value: unknown): ItemStatus {
166
+ const normalized = toNonEmptyString(value);
167
+ if (normalized) {
168
+ const canonical = normalizeStatusInput(normalized);
169
+ if (canonical) {
170
+ return canonical;
171
+ }
172
+ }
173
+ return "open";
174
+ }
175
+
176
+ function toDependencyKind(value: unknown): { kind: Dependency["kind"]; sourceKind?: string } {
177
+ const raw = toNonEmptyString(value);
178
+ const normalized = raw?.toLowerCase();
179
+ if (!normalized) {
180
+ return { kind: "related" };
181
+ }
182
+
183
+ const preserveIfChanged = (kind: Dependency["kind"]): { kind: Dependency["kind"]; sourceKind?: string } => ({
184
+ kind,
185
+ sourceKind: normalized === kind ? undefined : raw,
186
+ });
187
+
188
+ if (DEPENDENCY_KIND_VALUES.includes(normalized as Dependency["kind"])) {
189
+ return preserveIfChanged(normalized as Dependency["kind"]);
190
+ }
191
+
192
+ switch (normalized) {
193
+ case "parent-child":
194
+ return preserveIfChanged("parent_child");
195
+ case "child-of":
196
+ return preserveIfChanged("child_of");
197
+ case "related-to":
198
+ case "relates-to":
199
+ return preserveIfChanged("related_to");
200
+ case "discovered-from":
201
+ return preserveIfChanged("discovered_from");
202
+ case "blocked-by":
203
+ return preserveIfChanged("blocked_by");
204
+ case "incident-from":
205
+ return preserveIfChanged("incident_from");
206
+ default:
207
+ return {
208
+ kind: "related",
209
+ sourceKind: raw,
210
+ };
211
+ }
212
+ }
213
+
214
+ function normalizeImportedId(id: string, prefix: string, preserveSourceIds: boolean): string {
215
+ return preserveSourceIds ? normalizeRawItemId(id) : normalizeItemId(id, prefix);
216
+ }
217
+
218
+ function toDependencies(
219
+ value: unknown,
220
+ fallbackCreatedAt: string,
221
+ prefix: string,
222
+ preserveSourceIds: boolean,
223
+ ): Dependency[] | undefined {
224
+ if (!Array.isArray(value)) {
225
+ return undefined;
226
+ }
227
+
228
+ const dependencies: Dependency[] = [];
229
+ for (const entry of value) {
230
+ if (typeof entry === "string") {
231
+ const id = toNonEmptyString(entry);
232
+ if (!id) {
233
+ continue;
234
+ }
235
+ dependencies.push({
236
+ id: normalizeImportedId(id, prefix, preserveSourceIds),
237
+ kind: "related",
238
+ created_at: fallbackCreatedAt,
239
+ });
240
+ continue;
241
+ }
242
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
243
+ continue;
244
+ }
245
+ const candidate = entry as Record<string, unknown>;
246
+ const id = toNonEmptyString(candidate.id) ?? toNonEmptyString(candidate.item_id) ?? toNonEmptyString(candidate.depends_on_id);
247
+ if (!id) {
248
+ continue;
249
+ }
250
+ const dependencyKind = toDependencyKind(candidate.type ?? candidate.kind);
251
+ dependencies.push({
252
+ id: normalizeImportedId(id, prefix, preserveSourceIds),
253
+ kind: dependencyKind.kind,
254
+ created_at: toIsoString(candidate.created_at) ?? fallbackCreatedAt,
255
+ author: toNonEmptyString(candidate.author) ?? toNonEmptyString(candidate.created_by),
256
+ source_kind: dependencyKind.sourceKind,
257
+ });
258
+ }
259
+
260
+ return dependencies.length > 0 ? dependencies : undefined;
261
+ }
262
+
263
+ function toLogEntries(value: unknown, fallbackCreatedAt: string, fallbackAuthor: string): LogNote[] | undefined {
264
+ if (typeof value === "string") {
265
+ const text = toNonEmptyString(value);
266
+ if (!text) {
267
+ return undefined;
268
+ }
269
+ return [
270
+ {
271
+ created_at: fallbackCreatedAt,
272
+ author: fallbackAuthor,
273
+ text,
274
+ },
275
+ ];
276
+ }
277
+ if (!Array.isArray(value)) {
278
+ return undefined;
279
+ }
280
+
281
+ const entries: LogNote[] = [];
282
+ for (const entry of value) {
283
+ if (typeof entry === "string") {
284
+ const text = toNonEmptyString(entry);
285
+ if (!text) {
286
+ continue;
287
+ }
288
+ entries.push({
289
+ created_at: fallbackCreatedAt,
290
+ author: fallbackAuthor,
291
+ text,
292
+ });
293
+ continue;
294
+ }
295
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
296
+ continue;
297
+ }
298
+ const candidate = entry as Record<string, unknown>;
299
+ const text =
300
+ toNonEmptyString(candidate.text) ??
301
+ toNonEmptyString(candidate.comment) ??
302
+ toNonEmptyString(candidate.note) ??
303
+ toNonEmptyString(candidate.learning);
304
+ if (!text) {
305
+ continue;
306
+ }
307
+ entries.push({
308
+ created_at: toIsoString(candidate.created_at) ?? fallbackCreatedAt,
309
+ author: toNonEmptyString(candidate.author) ?? fallbackAuthor,
310
+ text,
311
+ });
312
+ }
313
+
314
+ return entries.length > 0 ? entries : undefined;
315
+ }
316
+
317
+ function toLinkedFiles(value: unknown): LinkedFile[] | undefined {
318
+ if (typeof value === "string") {
319
+ const p = toNonEmptyString(value);
320
+ if (!p) return undefined;
321
+ return [{ path: p, scope: "project" }];
322
+ }
323
+ if (!Array.isArray(value)) {
324
+ return undefined;
325
+ }
326
+
327
+ const files: LinkedFile[] = [];
328
+ for (const entry of value) {
329
+ if (typeof entry === "string") {
330
+ const p = toNonEmptyString(entry);
331
+ if (p) files.push({ path: p, scope: "project" });
332
+ continue;
333
+ }
334
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
335
+ continue;
336
+ }
337
+ const candidate = entry as Record<string, unknown>;
338
+ const p = toNonEmptyString(candidate.path) ?? toNonEmptyString(candidate.file);
339
+ if (!p) continue;
340
+
341
+ files.push({
342
+ path: p,
343
+ scope: toNonEmptyString(candidate.scope) === "global" ? "global" : "project",
344
+ note: toNonEmptyString(candidate.note),
345
+ });
346
+ }
347
+
348
+ return files.length > 0 ? files : undefined;
349
+ }
350
+
351
+ function toLinkedTests(value: unknown): LinkedTest[] | undefined {
352
+ if (typeof value === "string") {
353
+ const c = toNonEmptyString(value);
354
+ if (!c) return undefined;
355
+ return [{ command: c, scope: "project" }];
356
+ }
357
+ if (!Array.isArray(value)) {
358
+ return undefined;
359
+ }
360
+
361
+ const tests: LinkedTest[] = [];
362
+ for (const entry of value) {
363
+ if (typeof entry === "string") {
364
+ const c = toNonEmptyString(entry);
365
+ if (c) tests.push({ command: c, scope: "project" });
366
+ continue;
367
+ }
368
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
369
+ continue;
370
+ }
371
+ const candidate = entry as Record<string, unknown>;
372
+ const command = toNonEmptyString(candidate.command) ?? toNonEmptyString(candidate.test);
373
+ const p = toNonEmptyString(candidate.path);
374
+ if (!command && !p) continue;
375
+
376
+ let timeout: number | undefined;
377
+ if (typeof candidate.timeout_seconds === "number" && Number.isFinite(candidate.timeout_seconds)) {
378
+ timeout = candidate.timeout_seconds;
379
+ } else if (typeof candidate.timeout_seconds === "string") {
380
+ const parsed = Number(candidate.timeout_seconds);
381
+ if (Number.isFinite(parsed) && parsed >= 0) {
382
+ timeout = parsed;
383
+ }
384
+ }
385
+
386
+ tests.push({
387
+ command,
388
+ path: p,
389
+ scope: toNonEmptyString(candidate.scope) === "global" ? "global" : "project",
390
+ timeout_seconds: timeout,
391
+ note: toNonEmptyString(candidate.note),
392
+ });
393
+ }
394
+
395
+ return tests.length > 0 ? tests : undefined;
396
+ }
397
+
398
+ function toLinkedDocs(value: unknown): LinkedDoc[] | undefined {
399
+ if (typeof value === "string") {
400
+ const p = toNonEmptyString(value);
401
+ if (!p) return undefined;
402
+ return [{ path: p, scope: "project" }];
403
+ }
404
+ if (!Array.isArray(value)) {
405
+ return undefined;
406
+ }
407
+
408
+ const docs: LinkedDoc[] = [];
409
+ for (const entry of value) {
410
+ if (typeof entry === "string") {
411
+ const p = toNonEmptyString(entry);
412
+ if (p) docs.push({ path: p, scope: "project" });
413
+ continue;
414
+ }
415
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
416
+ continue;
417
+ }
418
+ const candidate = entry as Record<string, unknown>;
419
+ const p = toNonEmptyString(candidate.path) ?? toNonEmptyString(candidate.doc);
420
+ if (!p) continue;
421
+
422
+ docs.push({
423
+ path: p,
424
+ scope: toNonEmptyString(candidate.scope) === "global" ? "global" : "project",
425
+ note: toNonEmptyString(candidate.note),
426
+ });
427
+ }
428
+
429
+ return docs.length > 0 ? docs : undefined;
430
+ }
431
+
432
+ function selectAuthor(explicitAuthor: string | undefined, settingsAuthor: string): string {
433
+ const candidate = explicitAuthor ?? process.env.PM_AUTHOR ?? settingsAuthor;
434
+ const trimmed = candidate.trim();
435
+ return trimmed.length > 0 ? trimmed : "unknown";
436
+ }
437
+
438
+ function ensureInitHasRun(pmRoot: string): Promise<void> {
439
+ return pathExists(getSettingsPath(pmRoot)).then((exists) => {
440
+ if (!exists) {
441
+ throw new PmCliError(`Tracker is not initialized at ${pmRoot}. Run pm init first.`, EXIT_CODE.NOT_FOUND);
442
+ }
443
+ });
444
+ }
445
+
446
+ function emptyDocument(): ItemDocument {
447
+ return {
448
+ front_matter: {} as ItemFrontMatter,
449
+ body: "",
450
+ };
451
+ }
452
+
453
+ function resolveInputPath(rawPath: string): string {
454
+ return path.isAbsolute(rawPath) ? rawPath : path.resolve(process.cwd(), rawPath);
455
+ }
456
+
457
+ async function readStdin(): Promise<string> {
458
+ if (process.stdin.isTTY === true) {
459
+ throw new PmCliError(
460
+ '--file value "-" requires piped stdin input. Pipe JSONL content into the command, or end manual stdin with Ctrl+D (Unix/macOS) or Ctrl+Z then Enter (Windows).',
461
+ EXIT_CODE.USAGE,
462
+ );
463
+ }
464
+ return await new Promise<string>((resolve, reject) => {
465
+ let raw = "";
466
+ process.stdin.setEncoding("utf8");
467
+ process.stdin.on("data", (chunk) => {
468
+ raw += chunk;
469
+ });
470
+ process.stdin.on("end", () => resolve(raw));
471
+ process.stdin.on("error", reject);
472
+ });
473
+ }
474
+
475
+ async function resolveBeadsSource(rawPath: string | undefined): Promise<{
476
+ source: string;
477
+ sourcePath?: string;
478
+ raw: string;
479
+ warnings: string[];
480
+ }> {
481
+ const explicitSource = toNonEmptyString(rawPath);
482
+ if (explicitSource) {
483
+ if (explicitSource === "-") {
484
+ return {
485
+ source: "-",
486
+ raw: await readStdin(),
487
+ warnings: [],
488
+ };
489
+ }
490
+
491
+ const explicitPath = resolveInputPath(explicitSource);
492
+ if (!(await pathExists(explicitPath))) {
493
+ throw new PmCliError(`Beads source file not found at ${explicitPath}`, EXIT_CODE.NOT_FOUND);
494
+ }
495
+ return {
496
+ source: explicitSource,
497
+ sourcePath: explicitPath,
498
+ raw: await fs.readFile(explicitPath, "utf8"),
499
+ warnings: [],
500
+ };
501
+ }
502
+
503
+ for (const candidate of PRIMARY_AUTO_DISCOVERY_FILES) {
504
+ const candidatePath = resolveInputPath(candidate);
505
+ if (await pathExists(candidatePath)) {
506
+ return {
507
+ source: candidate,
508
+ sourcePath: candidatePath,
509
+ raw: await fs.readFile(candidatePath, "utf8"),
510
+ warnings: candidate === PRIMARY_AUTO_DISCOVERY_FILES[0] ? [] : [`beads_import_source_autodiscovered:${candidate}`],
511
+ };
512
+ }
513
+ }
514
+
515
+ for (const candidate of UNSAFE_AUTO_DISCOVERY_FILES) {
516
+ const candidatePath = resolveInputPath(candidate);
517
+ if (await pathExists(candidatePath)) {
518
+ throw new PmCliError(
519
+ `Beads auto-discovery found ${candidatePath}, but sync_base snapshots may be partial. Export a full Beads JSONL file and pass --file <path> (or --file - for stdin).`,
520
+ EXIT_CODE.NOT_FOUND,
521
+ );
522
+ }
523
+ }
524
+
525
+ throw new PmCliError(
526
+ `Beads source file not found. Checked ${PRIMARY_AUTO_DISCOVERY_FILES.join(", ")}. Use --file <path> or --file - for stdin.`,
527
+ EXIT_CODE.NOT_FOUND,
528
+ );
529
+ }
530
+
531
+ export async function runBeadsImport(options: BeadsImportOptions, global: GlobalOptions): Promise<BeadsImportResult> {
532
+ const pmRoot = resolvePmRoot(process.cwd(), global.path);
533
+ await ensureInitHasRun(pmRoot);
534
+
535
+ const settings = await readSettings(pmRoot);
536
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
537
+ const preserveSourceIds = options.preserveSourceIds === true;
538
+ const { source, sourcePath, raw, warnings: sourceWarnings } = await resolveBeadsSource(options.file);
539
+ const warnings: string[] = [
540
+ ...sourceWarnings,
541
+ ];
542
+ if (sourcePath) {
543
+ warnings.push(
544
+ ...(await runActiveOnReadHooks({
545
+ path: sourcePath,
546
+ scope: "project",
547
+ })),
548
+ );
549
+ }
550
+ const lines = raw.split(/\r?\n/);
551
+ const author = selectAuthor(toNonEmptyString(options.author), settings.author_default);
552
+ const message = toNonEmptyString(options.message) ?? "Import from Beads JSONL";
553
+ const ids: string[] = [];
554
+ let imported = 0;
555
+ let skipped = 0;
556
+
557
+ for (let index = 0; index < lines.length; index += 1) {
558
+ const lineNumber = index + 1;
559
+ const line = lines[index].trim();
560
+ if (line.length === 0) {
561
+ continue;
562
+ }
563
+
564
+ let parsed: unknown;
565
+ try {
566
+ parsed = JSON.parse(line) as unknown;
567
+ } catch {
568
+ warnings.push(`beads_import_invalid_jsonl_line:${lineNumber}`);
569
+ skipped += 1;
570
+ continue;
571
+ }
572
+
573
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
574
+ warnings.push(`beads_import_invalid_record:${lineNumber}`);
575
+ skipped += 1;
576
+ continue;
577
+ }
578
+ const record = parsed as BeadsRecord;
579
+
580
+ const title = toNonEmptyString(record.title);
581
+ if (!title) {
582
+ warnings.push(`beads_import_missing_title:${lineNumber}`);
583
+ skipped += 1;
584
+ continue;
585
+ }
586
+
587
+ const createdAt = toIsoString(record.created_at) ?? nowIso();
588
+ const updatedAt = toIsoString(record.updated_at) ?? createdAt;
589
+ const id = toNonEmptyString(record.id)
590
+ ? normalizeImportedId(toNonEmptyString(record.id) as string, settings.id_prefix, preserveSourceIds)
591
+ : await generateItemId(pmRoot, settings.id_prefix);
592
+ const typeMapping = toItemType(record.issue_type ?? record.type);
593
+ const type = typeMapping.type;
594
+ const closedAt = toIsoString(record.closed_at);
595
+ const assignee = toNonEmptyString(record.assignee) ?? toNonEmptyString(record.owner);
596
+ const frontMatter = normalizeFrontMatter({
597
+ id,
598
+ title,
599
+ description: toNonEmptyString(record.description) ?? "",
600
+ type,
601
+ source_type: typeMapping.sourceType,
602
+ status: toStatus(record.status),
603
+ priority: toPriority(record.priority),
604
+ tags: toTags(record.tags ?? record.labels),
605
+ created_at: createdAt,
606
+ updated_at: updatedAt,
607
+ deadline: toIsoString(record.due_at ?? record.deadline),
608
+ closed_at: closedAt,
609
+ assignee,
610
+ source_owner: toNonEmptyString(record.owner),
611
+ author: toNonEmptyString(record.author) ?? toNonEmptyString(record.created_by) ?? author,
612
+ estimated_minutes: toEstimatedMinutes(record.estimated_minutes),
613
+ acceptance_criteria: toNonEmptyString(record.acceptance_criteria),
614
+ design: toNonEmptyString(record.design),
615
+ external_ref: toNonEmptyString(record.external_ref),
616
+ close_reason: toNonEmptyString(record.close_reason),
617
+ dependencies: toDependencies(record.dependencies, createdAt, settings.id_prefix, preserveSourceIds),
618
+ comments: toLogEntries(record.comments, createdAt, author),
619
+ notes: toLogEntries(record.notes, createdAt, author),
620
+ learnings: toLogEntries(record.learnings, createdAt, author),
621
+ files: toLinkedFiles(record.files),
622
+ tests: toLinkedTests(record.tests),
623
+ docs: toLinkedDocs(record.docs),
624
+ });
625
+ const rawBody = toNonEmptyString(record.body) ?? "";
626
+ const design = toNonEmptyString(record.design);
627
+ const externalRef = toNonEmptyString(record.external_ref);
628
+ let finalBody = rawBody;
629
+ if (design) {
630
+ finalBody += (finalBody ? "\n\n" : "") + "## Design\n\n" + design;
631
+ }
632
+ if (externalRef) {
633
+ finalBody += (finalBody ? "\n\n" : "") + "## External Reference\n" + externalRef;
634
+ }
635
+ const afterDocument = canonicalDocument({
636
+ front_matter: frontMatter,
637
+ body: finalBody,
638
+ });
639
+ const existing = await locateItem(pmRoot, id, settings.id_prefix, settings.item_format, typeRegistry.type_to_folder);
640
+ if (existing) {
641
+ warnings.push(`beads_import_item_exists:${id}`);
642
+ skipped += 1;
643
+ continue;
644
+ }
645
+ const itemPath = getItemPath(pmRoot, type, id, settings.item_format, typeRegistry.type_to_folder);
646
+
647
+ const historyPath = getHistoryPath(pmRoot, id);
648
+ try {
649
+ const releaseLock = await acquireLock(pmRoot, id, settings.locks.ttl_seconds, author);
650
+ try {
651
+ await writeFileAtomic(itemPath, serializeItemDocument(afterDocument, { format: settings.item_format }));
652
+ try {
653
+ const entry = createHistoryEntry({
654
+ nowIso: nowIso(),
655
+ author,
656
+ op: "import",
657
+ before: emptyDocument(),
658
+ after: afterDocument,
659
+ message,
660
+ });
661
+ await appendHistoryEntry(historyPath, entry);
662
+ warnings.push(
663
+ ...(await runActiveOnWriteHooks({
664
+ path: itemPath,
665
+ scope: "project",
666
+ op: "import",
667
+ })),
668
+ ...(await runActiveOnWriteHooks({
669
+ path: historyPath,
670
+ scope: "project",
671
+ op: "import:history",
672
+ })),
673
+ );
674
+ } catch (error: unknown) {
675
+ await removeFileIfExists(itemPath);
676
+ throw error;
677
+ }
678
+ } finally {
679
+ await releaseLock();
680
+ }
681
+ } catch (error: unknown) {
682
+ if (error instanceof PmCliError && error.exitCode === EXIT_CODE.CONFLICT) {
683
+ warnings.push(`beads_import_lock_conflict:${id}`);
684
+ skipped += 1;
685
+ continue;
686
+ }
687
+ throw error;
688
+ }
689
+
690
+ ids.push(id);
691
+ imported += 1;
692
+ }
693
+
694
+ return {
695
+ ok: true,
696
+ source,
697
+ imported,
698
+ skipped,
699
+ ids,
700
+ warnings,
701
+ };
702
+ }