@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
@@ -2,21 +2,175 @@ import { pathExists, removeFileIfExists, writeFileAtomic } from "../../core/fs/f
2
2
  import { appendHistoryEntry, createHistoryEntry } from "../../core/history/history.js";
3
3
  import { generateItemId, normalizeItemId } from "../../core/item/id.js";
4
4
  import { canonicalDocument, normalizeFrontMatter, serializeItemDocument } from "../../core/item/item-format.js";
5
- import { parseCsvKv, parseOptionalNumber, parseTags } from "../../core/item/parse.js";
5
+ import { normalizeParentReferenceValue, validateMissingParentReference, } from "../../core/item/parent-reference-policy.js";
6
+ import { validateSprintOrReleaseValue } from "../../core/item/sprint-release-format.js";
7
+ import { createStdinTokenResolver, parseCsvKv, parseOptionalNumber, parseTags } from "../../core/item/parse.js";
8
+ import { normalizeStatusInput } from "../../core/item/status.js";
9
+ import { canonicalizeCommandOptionKey, commandOptionFlagLabel, resolveItemTypeRegistry, resolveCommandOptionPolicyState, resolveTypeDefinition, resolveTypeName, validateTypeOptions, } from "../../core/item/type-registry.js";
6
10
  import { acquireLock } from "../../core/lock/lock.js";
11
+ import { collectRuntimeCreateFieldValues } from "../../core/schema/runtime-field-values.js";
12
+ import { resolveRuntimeFieldRegistry, resolveRuntimeStatusRegistry, } from "../../core/schema/runtime-schema.js";
7
13
  import { EXIT_CODE, FRONT_MATTER_KEY_ORDER } from "../../core/shared/constants.js";
8
14
  import { PmCliError } from "../../core/shared/errors.js";
9
- import { isNoneToken, nowIso, resolveIsoOrRelative } from "../../core/shared/time.js";
10
- import { runActiveOnWriteHooks } from "../../core/extensions/index.js";
15
+ import { nowIso, resolveIsoOrRelative } from "../../core/shared/time.js";
16
+ import { getActiveExtensionRegistrations, runActiveOnWriteHooks } from "../../core/extensions/index.js";
17
+ import { applyRegisteredItemFieldDefaultsAndValidation } from "../../core/extensions/item-fields.js";
18
+ import { locateItem } from "../../core/store/item-store.js";
11
19
  import { getHistoryPath, getItemPath, getSettingsPath, resolvePmRoot } from "../../core/store/paths.js";
12
20
  import { readSettings } from "../../core/store/settings.js";
13
- import { CONFIDENCE_TEXT_VALUES, DEPENDENCY_KIND_VALUES, ISSUE_SEVERITY_VALUES, ITEM_TYPE_VALUES, RISK_VALUES, SCOPE_VALUES, STATUS_VALUES, } from "../../types/index.js";
21
+ import { loadCreateTemplateOptions } from "./templates.js";
22
+ import { CONFIDENCE_TEXT_VALUES, DEPENDENCY_KIND_VALUES, ISSUE_SEVERITY_VALUES, RECURRENCE_FREQUENCY_VALUES, RECURRENCE_WEEKDAY_VALUES, RISK_VALUES, SCOPE_VALUES, } from "../../types/index.js";
23
+ const CREATE_MODE_VALUES = ["strict", "progressive"];
24
+ const SCHEDULE_CREATE_PRESET_VALUES = ["lightweight"];
25
+ const SCHEDULE_CREATE_PRESET_TYPES = new Set(["Reminder", "Meeting", "Event"]);
26
+ const LOG_SEED_ALLOWED_KEYS = new Set(["author", "created_at", "text"]);
27
+ const LEGACY_NONE_TOKENS = new Set(["none", "null"]);
28
+ const CREATE_UNSET_FIELD_DEFINITIONS = [
29
+ { canonical: "tags", aliases: ["tags"], optionKey: "tags", frontMatterKey: "tags" },
30
+ { canonical: "deadline", aliases: ["deadline"], optionKey: "deadline", frontMatterKey: "deadline" },
31
+ {
32
+ canonical: "estimate",
33
+ aliases: ["estimate", "estimated_minutes", "estimated-minutes"],
34
+ optionKey: "estimatedMinutes",
35
+ frontMatterKey: "estimated_minutes",
36
+ },
37
+ {
38
+ canonical: "acceptance-criteria",
39
+ aliases: ["acceptance_criteria", "acceptance-criteria", "ac"],
40
+ optionKey: "acceptanceCriteria",
41
+ frontMatterKey: "acceptance_criteria",
42
+ },
43
+ {
44
+ canonical: "definition-of-ready",
45
+ aliases: ["definition_of_ready", "definition-of-ready"],
46
+ optionKey: "definitionOfReady",
47
+ frontMatterKey: "definition_of_ready",
48
+ },
49
+ { canonical: "order", aliases: ["order", "rank"], optionKey: "order", frontMatterKey: "order" },
50
+ { canonical: "goal", aliases: ["goal"], optionKey: "goal", frontMatterKey: "goal" },
51
+ { canonical: "objective", aliases: ["objective"], optionKey: "objective", frontMatterKey: "objective" },
52
+ { canonical: "value", aliases: ["value"], optionKey: "value", frontMatterKey: "value" },
53
+ { canonical: "impact", aliases: ["impact"], optionKey: "impact", frontMatterKey: "impact" },
54
+ { canonical: "outcome", aliases: ["outcome"], optionKey: "outcome", frontMatterKey: "outcome" },
55
+ { canonical: "why-now", aliases: ["why_now", "why-now"], optionKey: "whyNow", frontMatterKey: "why_now" },
56
+ { canonical: "author", aliases: ["author"], optionKey: "author", frontMatterKey: "author" },
57
+ { canonical: "assignee", aliases: ["assignee"], optionKey: "assignee", frontMatterKey: "assignee" },
58
+ { canonical: "parent", aliases: ["parent"], optionKey: "parent", frontMatterKey: "parent" },
59
+ { canonical: "reviewer", aliases: ["reviewer"], optionKey: "reviewer", frontMatterKey: "reviewer" },
60
+ { canonical: "risk", aliases: ["risk"], optionKey: "risk", frontMatterKey: "risk" },
61
+ { canonical: "confidence", aliases: ["confidence"], optionKey: "confidence", frontMatterKey: "confidence" },
62
+ { canonical: "sprint", aliases: ["sprint"], optionKey: "sprint", frontMatterKey: "sprint" },
63
+ { canonical: "release", aliases: ["release"], optionKey: "release", frontMatterKey: "release" },
64
+ {
65
+ canonical: "blocked-by",
66
+ aliases: ["blocked_by", "blocked-by"],
67
+ optionKey: "blockedBy",
68
+ frontMatterKey: "blocked_by",
69
+ },
70
+ {
71
+ canonical: "blocked-reason",
72
+ aliases: ["blocked_reason", "blocked-reason"],
73
+ optionKey: "blockedReason",
74
+ frontMatterKey: "blocked_reason",
75
+ },
76
+ {
77
+ canonical: "unblock-note",
78
+ aliases: ["unblock_note", "unblock-note"],
79
+ optionKey: "unblockNote",
80
+ frontMatterKey: "unblock_note",
81
+ },
82
+ { canonical: "reporter", aliases: ["reporter"], optionKey: "reporter", frontMatterKey: "reporter" },
83
+ { canonical: "severity", aliases: ["severity"], optionKey: "severity", frontMatterKey: "severity" },
84
+ {
85
+ canonical: "environment",
86
+ aliases: ["environment"],
87
+ optionKey: "environment",
88
+ frontMatterKey: "environment",
89
+ },
90
+ {
91
+ canonical: "repro-steps",
92
+ aliases: ["repro_steps", "repro-steps"],
93
+ optionKey: "reproSteps",
94
+ frontMatterKey: "repro_steps",
95
+ },
96
+ {
97
+ canonical: "resolution",
98
+ aliases: ["resolution"],
99
+ optionKey: "resolution",
100
+ frontMatterKey: "resolution",
101
+ },
102
+ {
103
+ canonical: "expected-result",
104
+ aliases: ["expected_result", "expected-result"],
105
+ optionKey: "expectedResult",
106
+ frontMatterKey: "expected_result",
107
+ },
108
+ {
109
+ canonical: "actual-result",
110
+ aliases: ["actual_result", "actual-result"],
111
+ optionKey: "actualResult",
112
+ frontMatterKey: "actual_result",
113
+ },
114
+ {
115
+ canonical: "affected-version",
116
+ aliases: ["affected_version", "affected-version"],
117
+ optionKey: "affectedVersion",
118
+ frontMatterKey: "affected_version",
119
+ },
120
+ {
121
+ canonical: "fixed-version",
122
+ aliases: ["fixed_version", "fixed-version"],
123
+ optionKey: "fixedVersion",
124
+ frontMatterKey: "fixed_version",
125
+ },
126
+ { canonical: "component", aliases: ["component"], optionKey: "component", frontMatterKey: "component" },
127
+ { canonical: "regression", aliases: ["regression"], optionKey: "regression", frontMatterKey: "regression" },
128
+ {
129
+ canonical: "customer-impact",
130
+ aliases: ["customer_impact", "customer-impact"],
131
+ optionKey: "customerImpact",
132
+ frontMatterKey: "customer_impact",
133
+ },
134
+ ];
135
+ const CREATE_UNSET_ALIAS_MAP = (() => {
136
+ const map = new Map();
137
+ for (const definition of CREATE_UNSET_FIELD_DEFINITIONS) {
138
+ for (const alias of definition.aliases) {
139
+ map.set(alias, {
140
+ optionKey: definition.optionKey,
141
+ frontMatterKey: definition.frontMatterKey,
142
+ });
143
+ }
144
+ }
145
+ return map;
146
+ })();
147
+ const CREATE_OPTION_KEY_TO_UNSET_CANONICAL = new Map(CREATE_UNSET_FIELD_DEFINITIONS.map((definition) => [definition.optionKey, definition.canonical]));
148
+ const CREATE_UNSET_SUPPORTED_CANONICAL_FIELDS = CREATE_UNSET_FIELD_DEFINITIONS.map((definition) => definition.canonical)
149
+ .sort((left, right) => left.localeCompare(right))
150
+ .join(", ");
151
+ function buildInvalidLogSeedKeysMessage(optionName, unsupportedKeys) {
152
+ const sortedUnsupported = [...unsupportedKeys].sort((left, right) => left.localeCompare(right));
153
+ const keyLabel = sortedUnsupported.length === 1 ? "key" : "keys";
154
+ return (`${optionName} supports only author, created_at, and text seed fields. ` +
155
+ `Found unsupported ${keyLabel}: ${sortedUnsupported.join(", ")}. ` +
156
+ `If text contains comma-separated key:value-like fragments, wrap text in quotes ` +
157
+ '(for example text="first,scope:project"), use markdown-style key/value input, ' +
158
+ `or pass ${optionName} - with piped stdin.`);
159
+ }
14
160
  function ensureEnumValue(value, allowed, label) {
15
161
  if (!allowed.includes(value)) {
16
162
  throw new PmCliError(`Invalid ${label} value "${value}". Allowed: ${allowed.join(", ")}`, EXIT_CODE.USAGE);
17
163
  }
18
164
  return value;
19
165
  }
166
+ function parseStatusValue(value, statusRegistry) {
167
+ const normalized = normalizeStatusInput(value, statusRegistry);
168
+ if (!normalized) {
169
+ const allowedStatuses = statusRegistry.definitions.map((definition) => definition.id);
170
+ throw new PmCliError(`Invalid status value "${value}". Allowed: ${allowedStatuses.join(", ")}`, EXIT_CODE.USAGE);
171
+ }
172
+ return normalized;
173
+ }
20
174
  function normalizeRiskInput(value) {
21
175
  const trimmed = value.trim();
22
176
  return trimmed.toLowerCase() === "med" ? "medium" : trimmed;
@@ -59,29 +213,101 @@ function parseCreatedAt(value, currentIso) {
59
213
  }
60
214
  return new Date(parsed).toISOString();
61
215
  }
216
+ function isLegacyNoneToken(value) {
217
+ if (value === undefined) {
218
+ return false;
219
+ }
220
+ return LEGACY_NONE_TOKENS.has(value.trim().toLowerCase());
221
+ }
222
+ function assertNoLegacyNoneToken(value, flag, replacementHint) {
223
+ if (!isLegacyNoneToken(value)) {
224
+ return;
225
+ }
226
+ const suffix = replacementHint ? ` ${replacementHint}` : "";
227
+ throw new PmCliError(`${flag} no longer accepts "none" or "null".${suffix}`.trim(), EXIT_CODE.USAGE);
228
+ }
229
+ function assertNoLegacyNoneTokens(values, flag, replacementHint) {
230
+ if (!values || values.length === 0) {
231
+ return;
232
+ }
233
+ const hasLegacyToken = values.some((value) => isLegacyNoneToken(value));
234
+ if (!hasLegacyToken) {
235
+ return;
236
+ }
237
+ const suffix = replacementHint ? ` ${replacementHint}` : "";
238
+ throw new PmCliError(`${flag} no longer accepts "none" or "null".${suffix}`.trim(), EXIT_CODE.USAGE);
239
+ }
62
240
  function parseOptionalString(value) {
63
241
  if (value === undefined)
64
242
  return undefined;
65
- if (isNoneToken(value))
66
- return undefined;
67
243
  return value;
68
244
  }
245
+ function resolveRuntimeCreateUnsetDefinition(token, runtimeFieldRegistry) {
246
+ if (!runtimeFieldRegistry) {
247
+ return undefined;
248
+ }
249
+ for (const definition of runtimeFieldRegistry.definitions) {
250
+ if (definition.allow_unset === false) {
251
+ continue;
252
+ }
253
+ const candidates = new Set([
254
+ definition.key,
255
+ definition.front_matter_key,
256
+ definition.cli_flag.replaceAll("-", "_"),
257
+ definition.cli_flag,
258
+ ...definition.cli_aliases.map((alias) => alias.replaceAll("-", "_")),
259
+ ...definition.cli_aliases,
260
+ ]);
261
+ if (!candidates.has(token)) {
262
+ continue;
263
+ }
264
+ return {
265
+ optionKey: definition.key,
266
+ frontMatterKey: definition.front_matter_key,
267
+ };
268
+ }
269
+ return undefined;
270
+ }
271
+ function parseCreateUnsetTargets(raw, runtimeFieldRegistry) {
272
+ const frontMatterKeys = new Set();
273
+ const optionKeys = new Set();
274
+ if (!raw || raw.length === 0) {
275
+ return { frontMatterKeys, optionKeys };
276
+ }
277
+ for (const entry of raw) {
278
+ const trimmed = entry.trim().toLowerCase();
279
+ if (!trimmed) {
280
+ throw new PmCliError("--unset values must not be empty", EXIT_CODE.USAGE);
281
+ }
282
+ if (isLegacyNoneToken(trimmed)) {
283
+ throw new PmCliError('--unset no longer accepts "none" or "null". Specify concrete field names such as --unset deadline', EXIT_CODE.USAGE);
284
+ }
285
+ const definition = CREATE_UNSET_ALIAS_MAP.get(trimmed) ?? resolveRuntimeCreateUnsetDefinition(trimmed, runtimeFieldRegistry);
286
+ if (!definition) {
287
+ throw new PmCliError(`Unsupported --unset field "${entry}". Supported fields: ${CREATE_UNSET_SUPPORTED_CANONICAL_FIELDS}`, EXIT_CODE.USAGE);
288
+ }
289
+ frontMatterKeys.add(definition.frontMatterKey);
290
+ optionKeys.add(definition.optionKey);
291
+ }
292
+ return { frontMatterKeys, optionKeys };
293
+ }
294
+ function weekdayOrderIndex(value) {
295
+ return RECURRENCE_WEEKDAY_VALUES.indexOf(value);
296
+ }
69
297
  function parseDependencies(raw, nowValue, prefix) {
70
298
  if (!raw || raw.length === 0)
71
299
  return { values: undefined, explicitEmpty: false };
72
- if (raw.some((entry) => isNoneToken(entry))) {
73
- if (raw.length > 1) {
74
- throw new PmCliError("--dep cannot mix 'none' with dependency values", EXIT_CODE.USAGE);
75
- }
76
- return { values: undefined, explicitEmpty: true };
77
- }
300
+ assertNoLegacyNoneTokens(raw, "--dep", "Use --clear-deps to clear dependencies.");
78
301
  const values = raw.map((entry) => {
79
302
  const kv = parseCsvKv(entry, "--dep");
80
- const id = kv.id;
81
- const kind = kv.kind;
303
+ const id = parseOptionalString(kv.id);
304
+ const kind = parseOptionalString(kv.kind);
82
305
  if (!id || !kind) {
83
306
  throw new PmCliError("--dep requires id and kind", EXIT_CODE.USAGE);
84
307
  }
308
+ if (id.trim().toLowerCase() === "undefined") {
309
+ throw new PmCliError(`--dep id must not use placeholder token "${id}". Use --clear-deps to clear dependencies.`, EXIT_CODE.USAGE);
310
+ }
85
311
  return {
86
312
  id: normalizeItemId(id, prefix),
87
313
  kind: ensureEnumValue(kind, DEPENDENCY_KIND_VALUES, "dependency kind"),
@@ -91,17 +317,44 @@ function parseDependencies(raw, nowValue, prefix) {
91
317
  });
92
318
  return { values, explicitEmpty: false };
93
319
  }
94
- function parseLogSeed(optionName, raw, nowValue, fallbackAuthor) {
320
+ export function parseLogSeed(optionName, raw, nowValue, fallbackAuthor) {
95
321
  if (!raw || raw.length === 0)
96
322
  return { values: undefined, explicitEmpty: false };
97
- if (raw.some((entry) => isNoneToken(entry))) {
98
- if (raw.length > 1) {
99
- throw new PmCliError(`${optionName} cannot mix 'none' with seed values`, EXIT_CODE.USAGE);
100
- }
101
- return { values: undefined, explicitEmpty: true };
102
- }
323
+ const clearHint = optionName === "--comment"
324
+ ? "Use --clear-comments to clear comments."
325
+ : optionName === "--note"
326
+ ? "Use --clear-notes to clear notes."
327
+ : "Use --clear-learnings to clear learnings.";
328
+ assertNoLegacyNoneTokens(raw, optionName, clearHint);
103
329
  const values = raw.map((entry) => {
104
- const kv = parseCsvKv(entry, optionName);
330
+ const trimmedEntry = entry.trim();
331
+ const buildPlainTextCommentSeed = () => {
332
+ if (trimmedEntry.length === 0) {
333
+ throw new PmCliError(`${optionName} requires text=<value>`, EXIT_CODE.USAGE);
334
+ }
335
+ return {
336
+ created_at: nowValue,
337
+ author: fallbackAuthor,
338
+ text: trimmedEntry,
339
+ };
340
+ };
341
+ let kv;
342
+ try {
343
+ kv = parseCsvKv(entry, optionName);
344
+ }
345
+ catch (error) {
346
+ if (optionName === "--comment") {
347
+ return buildPlainTextCommentSeed();
348
+ }
349
+ throw error;
350
+ }
351
+ const unsupportedKeys = Object.keys(kv).filter((key) => !LOG_SEED_ALLOWED_KEYS.has(key));
352
+ if (unsupportedKeys.length > 0) {
353
+ if (optionName === "--comment" && !trimmedEntry.includes(",") && !trimmedEntry.includes("\n")) {
354
+ return buildPlainTextCommentSeed();
355
+ }
356
+ throw new PmCliError(buildInvalidLogSeedKeysMessage(optionName, unsupportedKeys), EXIT_CODE.USAGE);
357
+ }
105
358
  const text = kv.text ?? "";
106
359
  if (text === "") {
107
360
  throw new PmCliError(`${optionName} requires text=<value>`, EXIT_CODE.USAGE);
@@ -114,15 +367,10 @@ function parseLogSeed(optionName, raw, nowValue, fallbackAuthor) {
114
367
  });
115
368
  return { values, explicitEmpty: false };
116
369
  }
117
- function parseFiles(raw) {
370
+ export function parseFiles(raw) {
118
371
  if (!raw || raw.length === 0)
119
372
  return { values: undefined, explicitEmpty: false };
120
- if (raw.some((entry) => isNoneToken(entry))) {
121
- if (raw.length > 1) {
122
- throw new PmCliError("--file cannot mix 'none' with file values", EXIT_CODE.USAGE);
123
- }
124
- return { values: undefined, explicitEmpty: true };
125
- }
373
+ assertNoLegacyNoneTokens(raw, "--file", "Use --clear-files to clear linked files.");
126
374
  const values = raw.map((entry) => {
127
375
  const kv = parseCsvKv(entry, "--file");
128
376
  if (!kv.path) {
@@ -136,21 +384,186 @@ function parseFiles(raw) {
136
384
  });
137
385
  return { values, explicitEmpty: false };
138
386
  }
139
- function parseTests(raw) {
140
- if (!raw || raw.length === 0)
141
- return { values: undefined, explicitEmpty: false };
142
- if (raw.some((entry) => isNoneToken(entry))) {
143
- if (raw.length > 1) {
144
- throw new PmCliError("--test cannot mix 'none' with test values", EXIT_CODE.USAGE);
387
+ const LINKED_TEST_PROTECTED_ENV_KEYS = new Set(["PM_PATH", "PM_GLOBAL_PATH", "FORCE_COLOR"]);
388
+ const LINKED_TEST_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
389
+ const LINKED_TEST_PM_CONTEXT_MODE_VALUES = ["schema", "tracker", "auto"];
390
+ function parseLinkedTestEnvSet(raw, optionName) {
391
+ const normalized = parseOptionalString(raw);
392
+ if (!normalized) {
393
+ return undefined;
394
+ }
395
+ const assignments = normalized
396
+ .split(/[;\n]/)
397
+ .map((entry) => entry.trim())
398
+ .filter((entry) => entry.length > 0);
399
+ if (assignments.length === 0) {
400
+ throw new PmCliError(`${optionName} env_set must include at least one KEY=VALUE assignment`, EXIT_CODE.USAGE);
401
+ }
402
+ const envSet = {};
403
+ for (const assignment of assignments) {
404
+ const separatorIndex = assignment.indexOf("=");
405
+ if (separatorIndex <= 0) {
406
+ throw new PmCliError(`${optionName} env_set entries must use KEY=VALUE and be separated by semicolons. Example: env_set=PORT=0;PLAYWRIGHT_BASE_URL=http://127.0.0.1:4173`, EXIT_CODE.USAGE);
407
+ }
408
+ const key = assignment.slice(0, separatorIndex).trim();
409
+ const value = assignment.slice(separatorIndex + 1);
410
+ if (!LINKED_TEST_ENV_NAME_PATTERN.test(key)) {
411
+ throw new PmCliError(`${optionName} env_set key "${key}" is invalid`, EXIT_CODE.USAGE);
412
+ }
413
+ if (LINKED_TEST_PROTECTED_ENV_KEYS.has(key.toUpperCase())) {
414
+ throw new PmCliError(`${optionName} env_set key "${key}" is reserved for sandbox safety`, EXIT_CODE.USAGE);
415
+ }
416
+ envSet[key] = value;
417
+ }
418
+ return Object.keys(envSet).length > 0 ? envSet : undefined;
419
+ }
420
+ function parseLinkedTestEnvClear(raw, optionName) {
421
+ const normalized = parseOptionalString(raw);
422
+ if (!normalized) {
423
+ return undefined;
424
+ }
425
+ const keys = [...new Set(normalized.split(/[;,\n]/).map((entry) => entry.trim()).filter((entry) => entry.length > 0))];
426
+ if (keys.length === 0) {
427
+ throw new PmCliError(`${optionName} env_clear must include at least one environment variable name`, EXIT_CODE.USAGE);
428
+ }
429
+ for (const key of keys) {
430
+ if (!LINKED_TEST_ENV_NAME_PATTERN.test(key)) {
431
+ throw new PmCliError(`${optionName} env_clear key "${key}" is invalid`, EXIT_CODE.USAGE);
432
+ }
433
+ if (LINKED_TEST_PROTECTED_ENV_KEYS.has(key.toUpperCase())) {
434
+ throw new PmCliError(`${optionName} env_clear key "${key}" is reserved for sandbox safety`, EXIT_CODE.USAGE);
435
+ }
436
+ }
437
+ return keys;
438
+ }
439
+ function parseLinkedTestBoolean(raw, optionName, fieldLabel) {
440
+ const normalized = parseOptionalString(raw);
441
+ if (!normalized) {
442
+ return undefined;
443
+ }
444
+ const value = normalized.trim().toLowerCase();
445
+ if (value === "true" || value === "1" || value === "yes") {
446
+ return true;
447
+ }
448
+ if (value === "false" || value === "0" || value === "no") {
449
+ return false;
450
+ }
451
+ throw new PmCliError(`${optionName} ${fieldLabel} must be one of true|false|1|0|yes|no`, EXIT_CODE.USAGE);
452
+ }
453
+ function parseLinkedTestContextMode(raw, optionName) {
454
+ const normalized = parseOptionalString(raw);
455
+ if (!normalized) {
456
+ return undefined;
457
+ }
458
+ const value = normalized.trim().toLowerCase();
459
+ if (LINKED_TEST_PM_CONTEXT_MODE_VALUES.includes(value)) {
460
+ return value;
461
+ }
462
+ throw new PmCliError(`${optionName} pm_context_mode must be one of: ${LINKED_TEST_PM_CONTEXT_MODE_VALUES.join(", ")}`, EXIT_CODE.USAGE);
463
+ }
464
+ function parseLinkedTestStringList(raw) {
465
+ const normalized = parseOptionalString(raw);
466
+ if (!normalized) {
467
+ return undefined;
468
+ }
469
+ const values = [...new Set(normalized.split(/[;\n]/).map((entry) => entry.trim()).filter((entry) => entry.length > 0))];
470
+ return values.length > 0 ? values : undefined;
471
+ }
472
+ function parseLinkedTestRegexList(raw, optionName, fieldLabel) {
473
+ const values = parseLinkedTestStringList(raw);
474
+ if (!values || values.length === 0) {
475
+ return undefined;
476
+ }
477
+ for (const pattern of values) {
478
+ try {
479
+ // Validate syntax early so malformed assertions fail at mutation time.
480
+ new RegExp(pattern, "m");
145
481
  }
146
- return { values: undefined, explicitEmpty: true };
482
+ catch (error) {
483
+ throw new PmCliError(`${optionName} ${fieldLabel} includes invalid regex "${pattern}": ${error instanceof Error ? error.message : String(error)}`, EXIT_CODE.USAGE);
484
+ }
485
+ }
486
+ return values;
487
+ }
488
+ function parseLinkedTestMinLines(raw, optionName) {
489
+ const normalized = parseOptionalString(raw);
490
+ if (!normalized) {
491
+ return undefined;
147
492
  }
493
+ const parsed = parseOptionalNumber(normalized, "assert_stdout_min_lines");
494
+ if (!Number.isInteger(parsed) || parsed < 0) {
495
+ throw new PmCliError(`${optionName} assert_stdout_min_lines must be an integer >= 0`, EXIT_CODE.USAGE);
496
+ }
497
+ return parsed;
498
+ }
499
+ function parseLinkedTestAssertionEqualsMap(raw, optionName) {
500
+ const normalized = parseOptionalString(raw);
501
+ if (!normalized) {
502
+ return undefined;
503
+ }
504
+ const assignments = normalized
505
+ .split(/[;\n]/)
506
+ .map((entry) => entry.trim())
507
+ .filter((entry) => entry.length > 0);
508
+ if (assignments.length === 0) {
509
+ throw new PmCliError(`${optionName} assert_json_field_equals must include at least one path=value assignment`, EXIT_CODE.USAGE);
510
+ }
511
+ const values = {};
512
+ for (const assignment of assignments) {
513
+ const separatorIndex = assignment.indexOf("=");
514
+ if (separatorIndex <= 0) {
515
+ throw new PmCliError(`${optionName} assert_json_field_equals entries must use path=value and be separated by semicolons`, EXIT_CODE.USAGE);
516
+ }
517
+ const key = assignment.slice(0, separatorIndex).trim();
518
+ const value = assignment.slice(separatorIndex + 1).trim();
519
+ if (key.length === 0 || value.length === 0) {
520
+ throw new PmCliError(`${optionName} assert_json_field_equals entries must include non-empty path and value`, EXIT_CODE.USAGE);
521
+ }
522
+ values[key] = value;
523
+ }
524
+ return Object.keys(values).length > 0 ? values : undefined;
525
+ }
526
+ function parseLinkedTestAssertionGteMap(raw, optionName) {
527
+ const normalized = parseOptionalString(raw);
528
+ if (!normalized) {
529
+ return undefined;
530
+ }
531
+ const assignments = normalized
532
+ .split(/[;\n]/)
533
+ .map((entry) => entry.trim())
534
+ .filter((entry) => entry.length > 0);
535
+ if (assignments.length === 0) {
536
+ throw new PmCliError(`${optionName} assert_json_field_gte must include at least one path=value assignment`, EXIT_CODE.USAGE);
537
+ }
538
+ const values = {};
539
+ for (const assignment of assignments) {
540
+ const separatorIndex = assignment.indexOf("=");
541
+ if (separatorIndex <= 0) {
542
+ throw new PmCliError(`${optionName} assert_json_field_gte entries must use path=value and be separated by semicolons`, EXIT_CODE.USAGE);
543
+ }
544
+ const key = assignment.slice(0, separatorIndex).trim();
545
+ const valueRaw = assignment.slice(separatorIndex + 1).trim();
546
+ if (key.length === 0 || valueRaw.length === 0) {
547
+ throw new PmCliError(`${optionName} assert_json_field_gte entries must include non-empty path and value`, EXIT_CODE.USAGE);
548
+ }
549
+ const value = Number.parseFloat(valueRaw);
550
+ if (!Number.isFinite(value)) {
551
+ throw new PmCliError(`${optionName} assert_json_field_gte value for "${key}" must be numeric`, EXIT_CODE.USAGE);
552
+ }
553
+ values[key] = value;
554
+ }
555
+ return Object.keys(values).length > 0 ? values : undefined;
556
+ }
557
+ export function parseTests(raw) {
558
+ if (!raw || raw.length === 0)
559
+ return { values: undefined, explicitEmpty: false };
560
+ assertNoLegacyNoneTokens(raw, "--test", "Use --clear-tests to clear linked tests.");
148
561
  const values = raw.map((entry) => {
149
562
  const kv = parseCsvKv(entry, "--test");
150
563
  const command = parseOptionalString(kv.command);
151
564
  const filePath = parseOptionalString(kv.path);
152
- if (!command && !filePath) {
153
- throw new PmCliError("--test requires command=<value> and/or path=<value>", EXIT_CODE.USAGE);
565
+ if (!command) {
566
+ throw new PmCliError("--test requires command=<value> (path=<value> is optional metadata)", EXIT_CODE.USAGE);
154
567
  }
155
568
  const timeoutSecondsRaw = parseOptionalString(kv.timeout_seconds);
156
569
  const timeoutAliasRaw = parseOptionalString(kv.timeout);
@@ -163,20 +576,26 @@ function parseTests(raw) {
163
576
  path: filePath,
164
577
  scope: ensureEnumValue(kv.scope ?? "project", SCOPE_VALUES, "test scope"),
165
578
  timeout_seconds: timeoutRaw ? parseOptionalNumber(timeoutRaw, "timeout_seconds") : undefined,
579
+ pm_context_mode: parseLinkedTestContextMode(kv.pm_context_mode, "--test"),
580
+ env_set: parseLinkedTestEnvSet(kv.env_set, "--test"),
581
+ env_clear: parseLinkedTestEnvClear(kv.env_clear, "--test"),
582
+ shared_host_safe: parseLinkedTestBoolean(kv.shared_host_safe, "--test", "shared_host_safe"),
583
+ assert_stdout_contains: parseLinkedTestStringList(kv.assert_stdout_contains),
584
+ assert_stdout_regex: parseLinkedTestRegexList(kv.assert_stdout_regex, "--test", "assert_stdout_regex"),
585
+ assert_stderr_contains: parseLinkedTestStringList(kv.assert_stderr_contains),
586
+ assert_stderr_regex: parseLinkedTestRegexList(kv.assert_stderr_regex, "--test", "assert_stderr_regex"),
587
+ assert_stdout_min_lines: parseLinkedTestMinLines(kv.assert_stdout_min_lines, "--test"),
588
+ assert_json_field_equals: parseLinkedTestAssertionEqualsMap(kv.assert_json_field_equals, "--test"),
589
+ assert_json_field_gte: parseLinkedTestAssertionGteMap(kv.assert_json_field_gte, "--test"),
166
590
  note: parseOptionalString(kv.note),
167
591
  };
168
592
  });
169
593
  return { values, explicitEmpty: false };
170
594
  }
171
- function parseDocs(raw) {
595
+ export function parseDocs(raw) {
172
596
  if (!raw || raw.length === 0)
173
597
  return { values: undefined, explicitEmpty: false };
174
- if (raw.some((entry) => isNoneToken(entry))) {
175
- if (raw.length > 1) {
176
- throw new PmCliError("--doc cannot mix 'none' with doc values", EXIT_CODE.USAGE);
177
- }
178
- return { values: undefined, explicitEmpty: true };
179
- }
598
+ assertNoLegacyNoneTokens(raw, "--doc", "Use --clear-docs to clear linked docs.");
180
599
  const values = raw.map((entry) => {
181
600
  const kv = parseCsvKv(entry, "--doc");
182
601
  if (!kv.path) {
@@ -190,6 +609,147 @@ function parseDocs(raw) {
190
609
  });
191
610
  return { values, explicitEmpty: false };
192
611
  }
612
+ function parseReminders(raw, nowValue) {
613
+ if (!raw || raw.length === 0)
614
+ return { values: undefined, explicitEmpty: false };
615
+ assertNoLegacyNoneTokens(raw, "--reminder", "Use --clear-reminders to clear reminders.");
616
+ const values = raw.map((entry) => {
617
+ const kv = parseCsvKv(entry, "--reminder");
618
+ const atRaw = parseOptionalString(kv.at);
619
+ const textRaw = parseOptionalString(kv.text);
620
+ if (!atRaw || !textRaw) {
621
+ throw new PmCliError("--reminder requires at=<iso|relative> and text=<value>", EXIT_CODE.USAGE);
622
+ }
623
+ const text = textRaw.trim();
624
+ if (!text) {
625
+ throw new PmCliError("--reminder text must not be empty", EXIT_CODE.USAGE);
626
+ }
627
+ return {
628
+ at: resolveIsoOrRelative(atRaw, new Date(nowValue), "reminder.at"),
629
+ text,
630
+ };
631
+ });
632
+ return { values, explicitEmpty: false };
633
+ }
634
+ function parseEventBoolean(value, flag) {
635
+ const normalized = value.trim().toLowerCase();
636
+ if (normalized === "true" || normalized === "1" || normalized === "yes") {
637
+ return true;
638
+ }
639
+ if (normalized === "false" || normalized === "0" || normalized === "no") {
640
+ return false;
641
+ }
642
+ throw new PmCliError(`${flag} must be one of true|false|1|0|yes|no`, EXIT_CODE.USAGE);
643
+ }
644
+ function parseDelimitedList(raw) {
645
+ if (!raw) {
646
+ return [];
647
+ }
648
+ return raw
649
+ .split("|")
650
+ .map((value) => value.trim())
651
+ .filter((value) => value.length > 0);
652
+ }
653
+ function parseRecurrenceRule(kv, startAt, nowValue) {
654
+ const freqRaw = parseOptionalString(kv.recur_freq)?.trim();
655
+ const intervalRaw = parseOptionalString(kv.recur_interval)?.trim();
656
+ const countRaw = parseOptionalString(kv.recur_count)?.trim();
657
+ const untilRaw = parseOptionalString(kv.recur_until)?.trim();
658
+ const byWeekdayRaw = parseOptionalString(kv.recur_by_weekday)?.trim();
659
+ const byMonthDayRaw = parseOptionalString(kv.recur_by_month_day)?.trim();
660
+ const exdatesRaw = parseOptionalString(kv.recur_exdates)?.trim();
661
+ const recurrenceInputsProvided = [freqRaw, intervalRaw, countRaw, untilRaw, byWeekdayRaw, byMonthDayRaw, exdatesRaw].some((value) => value !== undefined);
662
+ if (!recurrenceInputsProvided) {
663
+ return undefined;
664
+ }
665
+ if (!freqRaw) {
666
+ throw new PmCliError("--event recurrence fields require recur_freq=<daily|weekly|monthly|yearly>", EXIT_CODE.USAGE);
667
+ }
668
+ const freq = ensureEnumValue(freqRaw.toLowerCase(), RECURRENCE_FREQUENCY_VALUES, "event recurrence frequency");
669
+ const interval = intervalRaw !== undefined ? parseOptionalNumber(intervalRaw, "event recur_interval") : undefined;
670
+ if (interval !== undefined && (!Number.isInteger(interval) || interval < 1)) {
671
+ throw new PmCliError("--event recur_interval must be an integer >= 1", EXIT_CODE.USAGE);
672
+ }
673
+ const count = countRaw !== undefined ? parseOptionalNumber(countRaw, "event recur_count") : undefined;
674
+ if (count !== undefined && (!Number.isInteger(count) || count < 1)) {
675
+ throw new PmCliError("--event recur_count must be an integer >= 1", EXIT_CODE.USAGE);
676
+ }
677
+ const until = untilRaw ? resolveIsoOrRelative(untilRaw, nowValue, "event.recur_until") : undefined;
678
+ if (until && until < startAt) {
679
+ throw new PmCliError("--event recur_until must be at or after start", EXIT_CODE.USAGE);
680
+ }
681
+ const byWeekday = Array.from(new Set(parseDelimitedList(byWeekdayRaw).map((value) => ensureEnumValue(value.toLowerCase(), RECURRENCE_WEEKDAY_VALUES, "event weekday")))).sort((left, right) => weekdayOrderIndex(left) -
682
+ weekdayOrderIndex(right));
683
+ const byMonthDay = Array.from(new Set(parseDelimitedList(byMonthDayRaw).map((value) => {
684
+ const day = parseOptionalNumber(value, "event recur_by_month_day");
685
+ if (!Number.isInteger(day) || day < 1 || day > 31) {
686
+ throw new PmCliError("--event recur_by_month_day values must be integers 1..31", EXIT_CODE.USAGE);
687
+ }
688
+ return day;
689
+ }))).sort((left, right) => left - right);
690
+ const exdates = Array.from(new Set(parseDelimitedList(exdatesRaw).map((value) => resolveIsoOrRelative(value, nowValue, "event.recur_exdates")))).sort((left, right) => left.localeCompare(right));
691
+ return {
692
+ freq,
693
+ interval,
694
+ count,
695
+ until,
696
+ by_weekday: byWeekday.length > 0 ? byWeekday : undefined,
697
+ by_month_day: byMonthDay.length > 0 ? byMonthDay : undefined,
698
+ exdates: exdates.length > 0 ? exdates : undefined,
699
+ };
700
+ }
701
+ function parseEvents(raw, nowValue) {
702
+ if (!raw || raw.length === 0)
703
+ return { values: undefined, explicitEmpty: false };
704
+ assertNoLegacyNoneTokens(raw, "--event", "Use --clear-events to clear linked events.");
705
+ const referenceDate = new Date(nowValue);
706
+ const values = raw.map((entry) => {
707
+ const kv = parseCsvKv(entry, "--event");
708
+ const startRaw = parseOptionalString(kv.start)?.trim();
709
+ if (!startRaw) {
710
+ throw new PmCliError("--event requires start=<iso|relative>", EXIT_CODE.USAGE);
711
+ }
712
+ const startAt = resolveIsoOrRelative(startRaw, referenceDate, "event.start");
713
+ const endRaw = parseOptionalString(kv.end)?.trim();
714
+ const endAt = endRaw ? resolveIsoOrRelative(endRaw, referenceDate, "event.end") : undefined;
715
+ if (endAt && endAt <= startAt) {
716
+ throw new PmCliError("--event end must be after start", EXIT_CODE.USAGE);
717
+ }
718
+ const titleRaw = parseOptionalString(kv.title);
719
+ const descriptionRaw = parseOptionalString(kv.description);
720
+ const locationRaw = parseOptionalString(kv.location);
721
+ const timezoneRaw = parseOptionalString(kv.timezone);
722
+ const title = titleRaw?.trim();
723
+ const description = descriptionRaw?.trim();
724
+ const location = locationRaw?.trim();
725
+ const timezone = timezoneRaw?.trim();
726
+ if (titleRaw !== undefined && !title) {
727
+ throw new PmCliError("--event title must not be empty", EXIT_CODE.USAGE);
728
+ }
729
+ if (descriptionRaw !== undefined && !description) {
730
+ throw new PmCliError("--event description must not be empty", EXIT_CODE.USAGE);
731
+ }
732
+ if (locationRaw !== undefined && !location) {
733
+ throw new PmCliError("--event location must not be empty", EXIT_CODE.USAGE);
734
+ }
735
+ if (timezoneRaw !== undefined && !timezone) {
736
+ throw new PmCliError("--event timezone must not be empty", EXIT_CODE.USAGE);
737
+ }
738
+ const allDayRaw = parseOptionalString(kv.all_day)?.trim();
739
+ const recurrence = parseRecurrenceRule(kv, startAt, referenceDate);
740
+ return {
741
+ start_at: startAt,
742
+ end_at: endAt,
743
+ title,
744
+ description,
745
+ location,
746
+ all_day: allDayRaw !== undefined ? parseEventBoolean(allDayRaw, "--event all_day") : undefined,
747
+ timezone,
748
+ recurrence,
749
+ };
750
+ });
751
+ return { values, explicitEmpty: false };
752
+ }
193
753
  function buildChangedFields(frontMatter, explicitUnsets) {
194
754
  const changed = [
195
755
  ...FRONT_MATTER_KEY_ORDER.filter((key) => frontMatter[key] !== undefined),
@@ -206,6 +766,295 @@ function buildHistoryMessage(baseMessage, explicitUnsets) {
206
766
  const suffix = `explicit_unset=${explicitUnsets.join(",")}`;
207
767
  return trimmed ? `${trimmed} | ${suffix}` : suffix;
208
768
  }
769
+ function normalizeCreatePolicyOptionKey(raw, typeName, sourceLabel) {
770
+ const canonical = canonicalizeCommandOptionKey("create", raw);
771
+ if (!canonical) {
772
+ throw new PmCliError(`Unsupported ${sourceLabel} entry "${raw}" for type "${typeName}"`, EXIT_CODE.CONFLICT);
773
+ }
774
+ return canonical;
775
+ }
776
+ function parseTypeOptions(raw) {
777
+ if (!raw || raw.length === 0) {
778
+ return { values: undefined, explicitEmpty: false };
779
+ }
780
+ assertNoLegacyNoneTokens(raw, "--type-option", "Use --clear-type-options to clear existing type options.");
781
+ const values = {};
782
+ for (const entry of raw) {
783
+ const trimmedEntry = entry.trim();
784
+ if (trimmedEntry.length === 0) {
785
+ throw new PmCliError("--type-option values must not be empty", EXIT_CODE.USAGE);
786
+ }
787
+ let key;
788
+ let value;
789
+ const prefersStructuredKv = trimmedEntry.includes(",") ||
790
+ trimmedEntry.includes("\n") ||
791
+ trimmedEntry.startsWith("```") ||
792
+ /^(?:[-*+]\s+)?(?:key|value)\s*[:=]/i.test(trimmedEntry);
793
+ if (prefersStructuredKv) {
794
+ const kv = parseCsvKv(trimmedEntry, "--type-option");
795
+ key = parseOptionalString(kv.key)?.trim();
796
+ value = parseOptionalString(kv.value)?.trim();
797
+ }
798
+ else {
799
+ const equalsIndex = trimmedEntry.indexOf("=");
800
+ const colonIndex = trimmedEntry.indexOf(":");
801
+ let separatorIndex = equalsIndex;
802
+ if (equalsIndex <= 0 && colonIndex > 0) {
803
+ separatorIndex = colonIndex;
804
+ }
805
+ if (separatorIndex <= 0 || separatorIndex === trimmedEntry.length - 1) {
806
+ throw new PmCliError("--type-option requires key=value or key=<name>,value=<value> entries", EXIT_CODE.USAGE);
807
+ }
808
+ key = trimmedEntry.slice(0, separatorIndex).trim();
809
+ value = trimmedEntry.slice(separatorIndex + 1).trim();
810
+ }
811
+ if (!key || !value) {
812
+ throw new PmCliError("--type-option requires key and value", EXIT_CODE.USAGE);
813
+ }
814
+ values[key] = value;
815
+ }
816
+ const sortedEntries = Object.entries(values).sort((left, right) => left[0].localeCompare(right[0]));
817
+ return {
818
+ values: Object.fromEntries(sortedEntries),
819
+ explicitEmpty: false,
820
+ };
821
+ }
822
+ async function resolveCreateStdinInputs(options) {
823
+ const stdinResolver = createStdinTokenResolver();
824
+ return {
825
+ ...options,
826
+ body: await stdinResolver.resolveValue(options.body, "--body"),
827
+ dep: await stdinResolver.resolveList(options.dep, "--dep"),
828
+ comment: await stdinResolver.resolveList(options.comment, "--comment"),
829
+ note: await stdinResolver.resolveList(options.note, "--note"),
830
+ learning: await stdinResolver.resolveList(options.learning, "--learning"),
831
+ file: await stdinResolver.resolveList(options.file, "--file"),
832
+ test: await stdinResolver.resolveList(options.test, "--test"),
833
+ doc: await stdinResolver.resolveList(options.doc, "--doc"),
834
+ reminder: await stdinResolver.resolveList(options.reminder, "--reminder"),
835
+ event: await stdinResolver.resolveList(options.event, "--event"),
836
+ typeOption: await stdinResolver.resolveList(options.typeOption, "--type-option"),
837
+ };
838
+ }
839
+ function resolveCreateMode(createMode, defaultMode) {
840
+ if (createMode === undefined) {
841
+ return defaultMode;
842
+ }
843
+ const normalized = createMode.trim().toLowerCase();
844
+ if (normalized.length === 0) {
845
+ return defaultMode;
846
+ }
847
+ if (normalized === "strict" || normalized === "progressive") {
848
+ return normalized;
849
+ }
850
+ throw new PmCliError(`Invalid --create-mode value "${createMode}". Allowed: ${CREATE_MODE_VALUES.join(", ")}`, EXIT_CODE.USAGE);
851
+ }
852
+ function resolveScheduleCreatePreset(raw) {
853
+ if (raw === undefined) {
854
+ return undefined;
855
+ }
856
+ const normalized = raw.trim().toLowerCase();
857
+ if (normalized.length === 0) {
858
+ throw new PmCliError("--schedule-preset must not be empty", EXIT_CODE.USAGE);
859
+ }
860
+ if (normalized === "lightweight" || normalized === "lite" || normalized === "schedule-lite") {
861
+ return "lightweight";
862
+ }
863
+ throw new PmCliError(`Invalid --schedule-preset value "${raw}". Allowed: ${SCHEDULE_CREATE_PRESET_VALUES.join(", ")}`, EXIT_CODE.USAGE);
864
+ }
865
+ function resolveEffectiveCreateMode(createMode, schedulePreset, defaultMode) {
866
+ const resolvedMode = resolveCreateMode(createMode, defaultMode);
867
+ if (schedulePreset === undefined) {
868
+ return resolvedMode;
869
+ }
870
+ const createModeWasExplicit = typeof createMode === "string" && createMode.trim().length > 0;
871
+ if (createModeWasExplicit && resolvedMode === "strict") {
872
+ throw new PmCliError("--schedule-preset lightweight cannot be combined with --create-mode strict. Use --create-mode progressive or omit --create-mode.", EXIT_CODE.USAGE);
873
+ }
874
+ return "progressive";
875
+ }
876
+ function requireCreateOptionByType(typeDefinition, options, createMode, clearOptionKeys) {
877
+ const typeName = typeDefinition.name;
878
+ const scalarValues = {
879
+ title: options.title,
880
+ description: options.description,
881
+ type: options.type,
882
+ status: options.status,
883
+ priority: options.priority,
884
+ tags: options.tags,
885
+ body: options.body,
886
+ deadline: options.deadline,
887
+ estimatedMinutes: options.estimatedMinutes,
888
+ acceptanceCriteria: options.acceptanceCriteria,
889
+ definitionOfReady: options.definitionOfReady,
890
+ order: options.order ?? options.rank,
891
+ goal: options.goal,
892
+ objective: options.objective,
893
+ value: options.value,
894
+ impact: options.impact,
895
+ outcome: options.outcome,
896
+ whyNow: options.whyNow,
897
+ author: options.author,
898
+ message: options.message,
899
+ assignee: options.assignee,
900
+ parent: options.parent,
901
+ reviewer: options.reviewer,
902
+ risk: options.risk,
903
+ confidence: options.confidence,
904
+ sprint: options.sprint,
905
+ release: options.release,
906
+ blockedBy: options.blockedBy,
907
+ blockedReason: options.blockedReason,
908
+ unblockNote: options.unblockNote,
909
+ reporter: options.reporter,
910
+ severity: options.severity,
911
+ environment: options.environment,
912
+ reproSteps: options.reproSteps,
913
+ resolution: options.resolution,
914
+ expectedResult: options.expectedResult,
915
+ actualResult: options.actualResult,
916
+ affectedVersion: options.affectedVersion,
917
+ fixedVersion: options.fixedVersion,
918
+ component: options.component,
919
+ regression: options.regression,
920
+ customerImpact: options.customerImpact,
921
+ };
922
+ const repeatableValues = {
923
+ dep: options.dep,
924
+ comment: options.comment,
925
+ note: options.note,
926
+ learning: options.learning,
927
+ file: options.file,
928
+ test: options.test,
929
+ doc: options.doc,
930
+ reminder: options.reminder,
931
+ event: options.event,
932
+ typeOption: options.typeOption,
933
+ };
934
+ const hasOptionValue = (optionKey) => {
935
+ if (optionKey in scalarValues) {
936
+ return scalarValues[optionKey] !== undefined;
937
+ }
938
+ if (optionKey in repeatableValues) {
939
+ const value = repeatableValues[optionKey];
940
+ return Array.isArray(value) && value.length > 0;
941
+ }
942
+ return false;
943
+ };
944
+ const hasOptionMutation = (optionKey) => hasOptionValue(optionKey) || clearOptionKeys.has(optionKey);
945
+ const baseRequiredOptions = new Set(["title", "description", "type"]);
946
+ if (createMode === "strict") {
947
+ for (const field of typeDefinition.required_create_fields) {
948
+ baseRequiredOptions.add(normalizeCreatePolicyOptionKey(field, typeName, "required_create_fields"));
949
+ }
950
+ for (const field of typeDefinition.required_create_repeatables) {
951
+ baseRequiredOptions.add(normalizeCreatePolicyOptionKey(field, typeName, "required_create_repeatables"));
952
+ }
953
+ }
954
+ const policyState = resolveCommandOptionPolicyState(typeDefinition, "create", baseRequiredOptions);
955
+ if (policyState.errors.length > 0) {
956
+ throw new PmCliError(policyState.errors.join("; "), EXIT_CODE.CONFLICT);
957
+ }
958
+ for (const option of policyState.disabled) {
959
+ if (hasOptionMutation(option)) {
960
+ throw new PmCliError(`Option ${commandOptionFlagLabel("create", option)} is disabled for type "${typeName}" by command_option_policies`, EXIT_CODE.USAGE);
961
+ }
962
+ }
963
+ if (createMode === "strict") {
964
+ const strictRequiredClears = policyState.required.filter((required) => clearOptionKeys.has(required));
965
+ if (strictRequiredClears.length > 0) {
966
+ const requiredFlags = [...new Set(strictRequiredClears.map((required) => commandOptionFlagLabel("create", required)))].sort((left, right) => left.localeCompare(right));
967
+ throw new PmCliError(`Strict create mode requires concrete values for ${requiredFlags.join(", ")}; --unset/--clear-* directives cannot satisfy required options`, EXIT_CODE.USAGE);
968
+ }
969
+ }
970
+ const missingRequiredOptions = policyState.required.filter((required) => !hasOptionValue(required));
971
+ return [...new Set(missingRequiredOptions.map((required) => commandOptionFlagLabel("create", required)))].sort((left, right) => left.localeCompare(right));
972
+ }
973
+ const MISSING_REQUIRED_TYPE_OPTION_PATTERN = /^Missing required type option "([^"]+)" for type "([^"]+)"$/;
974
+ function collectMissingRequiredTypeOptionKeys(errors, typeName) {
975
+ const missingKeys = [];
976
+ for (const error of errors) {
977
+ const match = error.match(MISSING_REQUIRED_TYPE_OPTION_PATTERN);
978
+ if (!match) {
979
+ continue;
980
+ }
981
+ if (match[2] !== typeName) {
982
+ continue;
983
+ }
984
+ missingKeys.push(match[1]);
985
+ }
986
+ return [...new Set(missingKeys)].sort((left, right) => left.localeCompare(right));
987
+ }
988
+ function filterNonMissingTypeOptionErrors(errors, typeName) {
989
+ return errors.filter((error) => {
990
+ const match = error.match(MISSING_REQUIRED_TYPE_OPTION_PATTERN);
991
+ return !match || match[2] !== typeName;
992
+ });
993
+ }
994
+ function typeOptionExampleValue(typeDefinition, key) {
995
+ const optionDefinition = typeDefinition.options.find((option) => option.key === key);
996
+ const firstAllowed = optionDefinition?.values[0];
997
+ if (typeof firstAllowed === "string" && firstAllowed.trim().length > 0) {
998
+ return firstAllowed;
999
+ }
1000
+ return "<value>";
1001
+ }
1002
+ function createExampleTokensForFlag(flag, typeName, openStatus) {
1003
+ switch (flag) {
1004
+ case "--title":
1005
+ return ["--title", `"${typeName} example title"`];
1006
+ case "--description":
1007
+ return ["--description", `"${typeName} example description"`];
1008
+ case "--type":
1009
+ return ["--type", typeName];
1010
+ case "--status":
1011
+ return ["--status", openStatus];
1012
+ case "--priority":
1013
+ return ["--priority", "1"];
1014
+ case "--message":
1015
+ return ["--message", `"Create ${typeName} item"`];
1016
+ case "--dep":
1017
+ return ["--dep", "\"id=pm-xxxx,kind=related,author=maintainer,created_at=now\""];
1018
+ case "--comment":
1019
+ return ["--comment", "\"author=maintainer,created_at=now,text=Implementation context\""];
1020
+ case "--note":
1021
+ return ["--note", "\"author=maintainer,created_at=now,text=Design note\""];
1022
+ case "--learning":
1023
+ return ["--learning", "\"author=maintainer,created_at=now,text=Durable lesson\""];
1024
+ case "--file":
1025
+ return ["--file", "\"path=src/example.ts,scope=project,note=implementation file\""];
1026
+ case "--test":
1027
+ return ["--test", "\"command=node scripts/run-tests.mjs test,scope=project,timeout_seconds=240\""];
1028
+ case "--doc":
1029
+ return ["--doc", "\"path=README.md,scope=project,note=reference doc\""];
1030
+ default:
1031
+ return [flag, "\"<value>\""];
1032
+ }
1033
+ }
1034
+ function buildTypeSpecificCreateExample(typeDefinition, missingCreateFlags, missingTypeOptionKeys, openStatus) {
1035
+ const tokens = ["pm", "create", "--title", `"${typeDefinition.name} example title"`, "--description", `"${typeDefinition.name} example description"`, "--type", typeDefinition.name];
1036
+ const optionalRecommendationFlags = ["--status", "--priority", "--message"];
1037
+ const orderedFlags = [...new Set([...optionalRecommendationFlags, ...missingCreateFlags])];
1038
+ const includedFlags = new Set(["--title", "--description", "--type"]);
1039
+ for (const flag of orderedFlags) {
1040
+ if (includedFlags.has(flag)) {
1041
+ continue;
1042
+ }
1043
+ tokens.push(...createExampleTokensForFlag(flag, typeDefinition.name, openStatus));
1044
+ includedFlags.add(flag);
1045
+ }
1046
+ for (const key of missingTypeOptionKeys) {
1047
+ const value = typeOptionExampleValue(typeDefinition, key);
1048
+ tokens.push("--type-option", `${key}=${value}`);
1049
+ }
1050
+ return tokens.join(" ");
1051
+ }
1052
+ function requireStringOption(value, flag) {
1053
+ if (value === undefined) {
1054
+ throw new PmCliError(`Missing required option ${flag}`, EXIT_CODE.USAGE);
1055
+ }
1056
+ return value;
1057
+ }
209
1058
  function selectAuthor(explicitAuthor, settingsAuthor) {
210
1059
  const candidate = parseOptionalString(explicitAuthor) ?? process.env.PM_AUTHOR ?? settingsAuthor;
211
1060
  const trimmed = candidate.trim();
@@ -218,6 +1067,18 @@ function ensurePriority(rawPriority) {
218
1067
  }
219
1068
  return parsed;
220
1069
  }
1070
+ function mergeCreateOptionsWithTemplate(templateOptions, explicitOptions) {
1071
+ const merged = {};
1072
+ for (const [key, value] of Object.entries(templateOptions)) {
1073
+ merged[key] = Array.isArray(value) ? [...value] : value;
1074
+ }
1075
+ for (const [key, value] of Object.entries(explicitOptions)) {
1076
+ if (value !== undefined) {
1077
+ merged[key] = Array.isArray(value) ? [...value] : value;
1078
+ }
1079
+ }
1080
+ return merged;
1081
+ }
221
1082
  function ensureInitHasRun(pmRoot) {
222
1083
  return pathExists(getSettingsPath(pmRoot)).then((exists) => {
223
1084
  if (!exists) {
@@ -226,132 +1087,432 @@ function ensureInitHasRun(pmRoot) {
226
1087
  });
227
1088
  }
228
1089
  export async function runCreate(options, global) {
1090
+ let resolvedOptions = await resolveCreateStdinInputs(options);
229
1091
  const pmRoot = resolvePmRoot(process.cwd(), global.path);
230
1092
  await ensureInitHasRun(pmRoot);
231
1093
  const settings = await readSettings(pmRoot);
232
- const nowValue = nowIso();
233
- const author = selectAuthor(options.author, settings.author_default);
234
- const explicitUnsets = [];
235
- const dependencies = parseDependencies(options.dep, nowValue, settings.id_prefix);
236
- if (dependencies.explicitEmpty)
237
- explicitUnsets.push("dependencies");
238
- const comments = parseLogSeed("--comment", options.comment, nowValue, author);
239
- if (comments.explicitEmpty)
240
- explicitUnsets.push("comments");
241
- const notes = parseLogSeed("--note", options.note, nowValue, author);
242
- if (notes.explicitEmpty)
243
- explicitUnsets.push("notes");
244
- const learnings = parseLogSeed("--learning", options.learning, nowValue, author);
245
- if (learnings.explicitEmpty)
246
- explicitUnsets.push("learnings");
247
- const files = parseFiles(options.file);
248
- if (files.explicitEmpty)
249
- explicitUnsets.push("files");
250
- const tests = parseTests(options.test);
251
- if (tests.explicitEmpty)
252
- explicitUnsets.push("tests");
253
- const docs = parseDocs(options.doc);
254
- if (docs.explicitEmpty)
255
- explicitUnsets.push("docs");
256
- const scalarExplicitUnsetCandidates = [
257
- [options.deadline, "deadline"],
258
- [options.estimatedMinutes, "estimated_minutes"],
259
- [options.acceptanceCriteria, "acceptance_criteria"],
260
- [options.definitionOfReady, "definition_of_ready"],
261
- [options.order, "order"],
262
- [options.rank, "order"],
263
- [options.goal, "goal"],
264
- [options.objective, "objective"],
265
- [options.value, "value"],
266
- [options.impact, "impact"],
267
- [options.outcome, "outcome"],
268
- [options.whyNow, "why_now"],
269
- [options.assignee, "assignee"],
270
- [options.author, "author"],
271
- [options.parent, "parent"],
272
- [options.reviewer, "reviewer"],
273
- [options.risk, "risk"],
274
- [options.confidence, "confidence"],
275
- [options.sprint, "sprint"],
276
- [options.release, "release"],
277
- [options.blockedBy, "blocked_by"],
278
- [options.blockedReason, "blocked_reason"],
279
- [options.unblockNote, "unblock_note"],
280
- [options.reporter, "reporter"],
281
- [options.severity, "severity"],
282
- [options.environment, "environment"],
283
- [options.reproSteps, "repro_steps"],
284
- [options.resolution, "resolution"],
285
- [options.expectedResult, "expected_result"],
286
- [options.actualResult, "actual_result"],
287
- [options.affectedVersion, "affected_version"],
288
- [options.fixedVersion, "fixed_version"],
289
- [options.component, "component"],
290
- [options.regression, "regression"],
291
- [options.customerImpact, "customer_impact"],
1094
+ const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
1095
+ const runtimeFieldRegistry = resolveRuntimeFieldRegistry(settings.schema);
1096
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
1097
+ assertNoLegacyNoneToken(resolvedOptions.template, "--template", "Omit --template to skip template usage.");
1098
+ if (resolvedOptions.template !== undefined) {
1099
+ const templateName = resolvedOptions.template.trim();
1100
+ if (templateName.length === 0) {
1101
+ throw new PmCliError("--template must not be empty. Omit --template to disable template usage.", EXIT_CODE.USAGE);
1102
+ }
1103
+ const templateOptions = await loadCreateTemplateOptions(pmRoot, templateName);
1104
+ resolvedOptions = mergeCreateOptionsWithTemplate(templateOptions, resolvedOptions);
1105
+ }
1106
+ if (resolvedOptions.type === undefined) {
1107
+ throw new PmCliError("Missing required option --type <value>", EXIT_CODE.USAGE);
1108
+ }
1109
+ const resolvedTypeName = resolveTypeName(resolvedOptions.type, typeRegistry);
1110
+ if (!resolvedTypeName) {
1111
+ throw new PmCliError(`Invalid type value "${resolvedOptions.type}". Allowed: ${typeRegistry.types.join(", ")}`, EXIT_CODE.USAGE);
1112
+ }
1113
+ const typeDefinition = resolveTypeDefinition(resolvedTypeName, typeRegistry);
1114
+ if (!typeDefinition) {
1115
+ throw new PmCliError(`Invalid type value "${resolvedOptions.type}"`, EXIT_CODE.USAGE);
1116
+ }
1117
+ const type = typeDefinition.name;
1118
+ const schedulePreset = resolveScheduleCreatePreset(resolvedOptions.schedulePreset);
1119
+ if (schedulePreset !== undefined && !SCHEDULE_CREATE_PRESET_TYPES.has(type)) {
1120
+ throw new PmCliError(`--schedule-preset ${schedulePreset} is only supported for Reminder, Meeting, or Event types`, EXIT_CODE.USAGE);
1121
+ }
1122
+ const createMode = resolveEffectiveCreateMode(resolvedOptions.createMode, schedulePreset, settings.governance.create_mode_default);
1123
+ const unsetTargets = parseCreateUnsetTargets(resolvedOptions.unset, runtimeFieldRegistry);
1124
+ const explicitUnsets = new Set(unsetTargets.frontMatterKeys);
1125
+ const clearOptionKeys = new Set(unsetTargets.optionKeys);
1126
+ const clearCollectionDefinitions = [
1127
+ {
1128
+ enabled: resolvedOptions.clearDeps,
1129
+ optionKey: "dep",
1130
+ clearFlag: "--clear-deps",
1131
+ valueFlag: "--dep",
1132
+ values: resolvedOptions.dep,
1133
+ frontMatterKey: "dependencies",
1134
+ },
1135
+ {
1136
+ enabled: resolvedOptions.clearComments,
1137
+ optionKey: "comment",
1138
+ clearFlag: "--clear-comments",
1139
+ valueFlag: "--comment",
1140
+ values: resolvedOptions.comment,
1141
+ frontMatterKey: "comments",
1142
+ },
1143
+ {
1144
+ enabled: resolvedOptions.clearNotes,
1145
+ optionKey: "note",
1146
+ clearFlag: "--clear-notes",
1147
+ valueFlag: "--note",
1148
+ values: resolvedOptions.note,
1149
+ frontMatterKey: "notes",
1150
+ },
1151
+ {
1152
+ enabled: resolvedOptions.clearLearnings,
1153
+ optionKey: "learning",
1154
+ clearFlag: "--clear-learnings",
1155
+ valueFlag: "--learning",
1156
+ values: resolvedOptions.learning,
1157
+ frontMatterKey: "learnings",
1158
+ },
1159
+ {
1160
+ enabled: resolvedOptions.clearFiles,
1161
+ optionKey: "file",
1162
+ clearFlag: "--clear-files",
1163
+ valueFlag: "--file",
1164
+ values: resolvedOptions.file,
1165
+ frontMatterKey: "files",
1166
+ },
1167
+ {
1168
+ enabled: resolvedOptions.clearTests,
1169
+ optionKey: "test",
1170
+ clearFlag: "--clear-tests",
1171
+ valueFlag: "--test",
1172
+ values: resolvedOptions.test,
1173
+ frontMatterKey: "tests",
1174
+ },
1175
+ {
1176
+ enabled: resolvedOptions.clearDocs,
1177
+ optionKey: "doc",
1178
+ clearFlag: "--clear-docs",
1179
+ valueFlag: "--doc",
1180
+ values: resolvedOptions.doc,
1181
+ frontMatterKey: "docs",
1182
+ },
1183
+ {
1184
+ enabled: resolvedOptions.clearReminders,
1185
+ optionKey: "reminder",
1186
+ clearFlag: "--clear-reminders",
1187
+ valueFlag: "--reminder",
1188
+ values: resolvedOptions.reminder,
1189
+ frontMatterKey: "reminders",
1190
+ },
1191
+ {
1192
+ enabled: resolvedOptions.clearEvents,
1193
+ optionKey: "event",
1194
+ clearFlag: "--clear-events",
1195
+ valueFlag: "--event",
1196
+ values: resolvedOptions.event,
1197
+ frontMatterKey: "events",
1198
+ },
1199
+ {
1200
+ enabled: resolvedOptions.clearTypeOptions,
1201
+ optionKey: "typeOption",
1202
+ clearFlag: "--clear-type-options",
1203
+ valueFlag: "--type-option",
1204
+ values: resolvedOptions.typeOption,
1205
+ frontMatterKey: "type_options",
1206
+ },
292
1207
  ];
293
- for (const [value, key] of scalarExplicitUnsetCandidates) {
294
- if (isNoneToken(value)) {
295
- explicitUnsets.push(key);
1208
+ for (const definition of clearCollectionDefinitions) {
1209
+ if (!definition.enabled) {
1210
+ continue;
1211
+ }
1212
+ if (definition.values && definition.values.length > 0) {
1213
+ throw new PmCliError(`Cannot combine ${definition.clearFlag} with ${definition.valueFlag}`, EXIT_CODE.USAGE);
1214
+ }
1215
+ explicitUnsets.add(definition.frontMatterKey);
1216
+ clearOptionKeys.add(definition.optionKey);
1217
+ }
1218
+ const scalarOptionPresence = {
1219
+ tags: resolvedOptions.tags !== undefined,
1220
+ deadline: resolvedOptions.deadline !== undefined,
1221
+ estimatedMinutes: resolvedOptions.estimatedMinutes !== undefined,
1222
+ acceptanceCriteria: resolvedOptions.acceptanceCriteria !== undefined,
1223
+ definitionOfReady: resolvedOptions.definitionOfReady !== undefined,
1224
+ order: resolvedOptions.order !== undefined || resolvedOptions.rank !== undefined,
1225
+ goal: resolvedOptions.goal !== undefined,
1226
+ objective: resolvedOptions.objective !== undefined,
1227
+ value: resolvedOptions.value !== undefined,
1228
+ impact: resolvedOptions.impact !== undefined,
1229
+ outcome: resolvedOptions.outcome !== undefined,
1230
+ whyNow: resolvedOptions.whyNow !== undefined,
1231
+ author: resolvedOptions.author !== undefined,
1232
+ assignee: resolvedOptions.assignee !== undefined,
1233
+ parent: resolvedOptions.parent !== undefined,
1234
+ reviewer: resolvedOptions.reviewer !== undefined,
1235
+ risk: resolvedOptions.risk !== undefined,
1236
+ confidence: resolvedOptions.confidence !== undefined,
1237
+ sprint: resolvedOptions.sprint !== undefined,
1238
+ release: resolvedOptions.release !== undefined,
1239
+ blockedBy: resolvedOptions.blockedBy !== undefined,
1240
+ blockedReason: resolvedOptions.blockedReason !== undefined,
1241
+ unblockNote: resolvedOptions.unblockNote !== undefined,
1242
+ reporter: resolvedOptions.reporter !== undefined,
1243
+ severity: resolvedOptions.severity !== undefined,
1244
+ environment: resolvedOptions.environment !== undefined,
1245
+ reproSteps: resolvedOptions.reproSteps !== undefined,
1246
+ resolution: resolvedOptions.resolution !== undefined,
1247
+ expectedResult: resolvedOptions.expectedResult !== undefined,
1248
+ actualResult: resolvedOptions.actualResult !== undefined,
1249
+ affectedVersion: resolvedOptions.affectedVersion !== undefined,
1250
+ fixedVersion: resolvedOptions.fixedVersion !== undefined,
1251
+ component: resolvedOptions.component !== undefined,
1252
+ regression: resolvedOptions.regression !== undefined,
1253
+ customerImpact: resolvedOptions.customerImpact !== undefined,
1254
+ };
1255
+ for (const [optionKey, hasValue] of Object.entries(scalarOptionPresence)) {
1256
+ if (!hasValue || !unsetTargets.optionKeys.has(optionKey)) {
1257
+ continue;
1258
+ }
1259
+ const unsetField = CREATE_OPTION_KEY_TO_UNSET_CANONICAL.get(optionKey) ?? optionKey;
1260
+ throw new PmCliError(`Cannot combine --unset ${unsetField} with ${commandOptionFlagLabel("create", optionKey)}`, EXIT_CODE.USAGE);
1261
+ }
1262
+ const assertNoLegacyScalarToken = (value, optionKey) => {
1263
+ const unsetField = CREATE_OPTION_KEY_TO_UNSET_CANONICAL.get(optionKey);
1264
+ const hint = unsetField ? `Use --unset ${unsetField} to clear this field.` : undefined;
1265
+ assertNoLegacyNoneToken(value, commandOptionFlagLabel("create", optionKey), hint);
1266
+ };
1267
+ assertNoLegacyScalarToken(resolvedOptions.tags, "tags");
1268
+ assertNoLegacyScalarToken(resolvedOptions.deadline, "deadline");
1269
+ assertNoLegacyScalarToken(resolvedOptions.estimatedMinutes, "estimatedMinutes");
1270
+ assertNoLegacyScalarToken(resolvedOptions.acceptanceCriteria, "acceptanceCriteria");
1271
+ assertNoLegacyScalarToken(resolvedOptions.definitionOfReady, "definitionOfReady");
1272
+ assertNoLegacyScalarToken(resolvedOptions.order ?? resolvedOptions.rank, "order");
1273
+ assertNoLegacyScalarToken(resolvedOptions.goal, "goal");
1274
+ assertNoLegacyScalarToken(resolvedOptions.objective, "objective");
1275
+ assertNoLegacyScalarToken(resolvedOptions.value, "value");
1276
+ assertNoLegacyScalarToken(resolvedOptions.impact, "impact");
1277
+ assertNoLegacyScalarToken(resolvedOptions.outcome, "outcome");
1278
+ assertNoLegacyScalarToken(resolvedOptions.whyNow, "whyNow");
1279
+ assertNoLegacyScalarToken(resolvedOptions.author, "author");
1280
+ assertNoLegacyScalarToken(resolvedOptions.assignee, "assignee");
1281
+ assertNoLegacyScalarToken(resolvedOptions.parent, "parent");
1282
+ assertNoLegacyScalarToken(resolvedOptions.reviewer, "reviewer");
1283
+ assertNoLegacyScalarToken(resolvedOptions.risk, "risk");
1284
+ assertNoLegacyScalarToken(resolvedOptions.confidence, "confidence");
1285
+ assertNoLegacyScalarToken(resolvedOptions.sprint, "sprint");
1286
+ assertNoLegacyScalarToken(resolvedOptions.release, "release");
1287
+ assertNoLegacyScalarToken(resolvedOptions.blockedBy, "blockedBy");
1288
+ assertNoLegacyScalarToken(resolvedOptions.blockedReason, "blockedReason");
1289
+ assertNoLegacyScalarToken(resolvedOptions.unblockNote, "unblockNote");
1290
+ assertNoLegacyScalarToken(resolvedOptions.reporter, "reporter");
1291
+ assertNoLegacyScalarToken(resolvedOptions.severity, "severity");
1292
+ assertNoLegacyScalarToken(resolvedOptions.environment, "environment");
1293
+ assertNoLegacyScalarToken(resolvedOptions.reproSteps, "reproSteps");
1294
+ assertNoLegacyScalarToken(resolvedOptions.resolution, "resolution");
1295
+ assertNoLegacyScalarToken(resolvedOptions.expectedResult, "expectedResult");
1296
+ assertNoLegacyScalarToken(resolvedOptions.actualResult, "actualResult");
1297
+ assertNoLegacyScalarToken(resolvedOptions.affectedVersion, "affectedVersion");
1298
+ assertNoLegacyScalarToken(resolvedOptions.fixedVersion, "fixedVersion");
1299
+ assertNoLegacyScalarToken(resolvedOptions.component, "component");
1300
+ assertNoLegacyScalarToken(resolvedOptions.regression, "regression");
1301
+ assertNoLegacyScalarToken(resolvedOptions.customerImpact, "customerImpact");
1302
+ const missingRequiredCreateFlags = requireCreateOptionByType(typeDefinition, resolvedOptions, createMode, clearOptionKeys);
1303
+ const nowValue = nowIso();
1304
+ const author = selectAuthor(resolvedOptions.author, settings.author_default);
1305
+ const dependencies = parseDependencies(resolvedOptions.dep, nowValue, settings.id_prefix);
1306
+ const comments = parseLogSeed("--comment", resolvedOptions.comment, nowValue, author);
1307
+ const notes = parseLogSeed("--note", resolvedOptions.note, nowValue, author);
1308
+ const learnings = parseLogSeed("--learning", resolvedOptions.learning, nowValue, author);
1309
+ const files = parseFiles(resolvedOptions.file);
1310
+ const tests = parseTests(resolvedOptions.test);
1311
+ const docs = parseDocs(resolvedOptions.doc);
1312
+ const reminders = parseReminders(resolvedOptions.reminder, nowValue);
1313
+ const events = parseEvents(resolvedOptions.event, nowValue);
1314
+ const typeOptions = parseTypeOptions(resolvedOptions.typeOption);
1315
+ const validatedTypeOptions = validateTypeOptions(type, typeOptions.values, typeRegistry);
1316
+ const runtimeCreateFieldValues = collectRuntimeCreateFieldValues(resolvedOptions, runtimeFieldRegistry, type);
1317
+ for (const fieldKey of Object.keys(runtimeCreateFieldValues.values)) {
1318
+ if (!unsetTargets.frontMatterKeys.has(fieldKey)) {
1319
+ continue;
296
1320
  }
1321
+ throw new PmCliError(`Cannot combine --unset ${fieldKey.replaceAll("_", "-")} with its value flag`, EXIT_CODE.USAGE);
1322
+ }
1323
+ const missingRequiredTypeOptionKeys = collectMissingRequiredTypeOptionKeys(validatedTypeOptions.errors, type);
1324
+ const missingRequiredTypeOptionFlags = missingRequiredTypeOptionKeys.map((key) => `--type-option ${key}=<value>`);
1325
+ const combinedMissingFlags = [
1326
+ ...new Set([
1327
+ ...missingRequiredCreateFlags,
1328
+ ...missingRequiredTypeOptionFlags,
1329
+ ...runtimeCreateFieldValues.missing_required_flags,
1330
+ ]),
1331
+ ].sort((left, right) => left.localeCompare(right));
1332
+ if (combinedMissingFlags.length > 0) {
1333
+ const nextValidExample = buildTypeSpecificCreateExample(typeDefinition, missingRequiredCreateFlags, missingRequiredTypeOptionKeys, statusRegistry.open_status);
1334
+ const nextSteps = [`Run "pm create --help --type ${type}" for type-aware required option guidance.`];
1335
+ if (createMode === "strict") {
1336
+ nextSteps.push('For staged onboarding, retry with "--create-mode progressive".');
1337
+ if (SCHEDULE_CREATE_PRESET_TYPES.has(type)) {
1338
+ nextSteps.push('For minimal scheduling inputs, try "--schedule-preset lightweight".');
1339
+ }
1340
+ }
1341
+ const errorMessage = combinedMissingFlags.length === 1
1342
+ ? `Missing required option ${combinedMissingFlags[0]} for type "${type}"`
1343
+ : `Missing required options ${combinedMissingFlags.join(", ")} for type "${type}"`;
1344
+ throw new PmCliError(errorMessage, EXIT_CODE.USAGE, {
1345
+ code: "missing_required_option",
1346
+ required: `Provide all required create options and type options for type "${type}" in one invocation.`,
1347
+ examples: [nextValidExample],
1348
+ nextSteps,
1349
+ });
1350
+ }
1351
+ const nonMissingTypeOptionErrors = filterNonMissingTypeOptionErrors(validatedTypeOptions.errors, type);
1352
+ if (nonMissingTypeOptionErrors.length > 0) {
1353
+ const nextValidExample = buildTypeSpecificCreateExample(typeDefinition, [], [], statusRegistry.open_status);
1354
+ throw new PmCliError(nonMissingTypeOptionErrors.join("; "), EXIT_CODE.USAGE, {
1355
+ code: "invalid_argument_value",
1356
+ required: `Provide valid --type-option key/value pairs for type "${type}".`,
1357
+ examples: [nextValidExample],
1358
+ nextSteps: [`Run "pm create --help --type ${type}" to review allowed type-option keys and values.`],
1359
+ });
297
1360
  }
298
1361
  const id = await generateItemId(pmRoot, settings.id_prefix);
299
- const type = ensureEnumValue(options.type, ITEM_TYPE_VALUES, "type");
300
- const status = ensureEnumValue(options.status, STATUS_VALUES, "status");
301
- const priority = ensurePriority(options.priority);
302
- const tags = parseTags(options.tags);
303
- const deadline = isNoneToken(options.deadline) ? undefined : resolveIsoOrRelative(options.deadline, new Date(nowValue));
304
- const estimatedMinutes = isNoneToken(options.estimatedMinutes)
305
- ? undefined
306
- : parseOptionalNumber(options.estimatedMinutes, "estimated-minutes");
307
- const acceptanceCriteria = isNoneToken(options.acceptanceCriteria) ? undefined : options.acceptanceCriteria;
308
- const definitionOfReady = options.definitionOfReady !== undefined ? parseOptionalString(options.definitionOfReady) : undefined;
309
- if (options.order !== undefined && options.rank !== undefined && options.order !== options.rank) {
1362
+ const status = resolvedOptions.status !== undefined ? parseStatusValue(resolvedOptions.status, statusRegistry) : statusRegistry.open_status;
1363
+ const priority = resolvedOptions.priority !== undefined ? ensurePriority(resolvedOptions.priority) : 2;
1364
+ const tags = unsetTargets.frontMatterKeys.has("tags")
1365
+ ? []
1366
+ : resolvedOptions.tags !== undefined
1367
+ ? parseTags(resolvedOptions.tags)
1368
+ : [];
1369
+ const deadline = unsetTargets.frontMatterKeys.has("deadline")
1370
+ ? undefined
1371
+ : resolvedOptions.deadline === undefined
1372
+ ? undefined
1373
+ : resolveIsoOrRelative(resolvedOptions.deadline, new Date(nowValue), "deadline");
1374
+ const estimatedMinutes = unsetTargets.frontMatterKeys.has("estimated_minutes")
1375
+ ? undefined
1376
+ : resolvedOptions.estimatedMinutes === undefined
1377
+ ? undefined
1378
+ : parseOptionalNumber(resolvedOptions.estimatedMinutes, "estimated-minutes");
1379
+ const acceptanceCriteria = unsetTargets.frontMatterKeys.has("acceptance_criteria")
1380
+ ? undefined
1381
+ : resolvedOptions.acceptanceCriteria === undefined
1382
+ ? undefined
1383
+ : resolvedOptions.acceptanceCriteria;
1384
+ const definitionOfReady = unsetTargets.frontMatterKeys.has("definition_of_ready") || resolvedOptions.definitionOfReady === undefined
1385
+ ? undefined
1386
+ : parseOptionalString(resolvedOptions.definitionOfReady);
1387
+ if (resolvedOptions.order !== undefined &&
1388
+ resolvedOptions.rank !== undefined &&
1389
+ resolvedOptions.order !== resolvedOptions.rank) {
310
1390
  throw new PmCliError("--order and --rank must match when both are provided", EXIT_CODE.USAGE);
311
1391
  }
312
- const orderRaw = options.order ?? options.rank;
313
- const order = orderRaw === undefined || isNoneToken(orderRaw) ? undefined : parseOptionalNumber(orderRaw, "order");
1392
+ const orderRaw = resolvedOptions.order ?? resolvedOptions.rank;
1393
+ const order = unsetTargets.frontMatterKeys.has("order") || orderRaw === undefined ? undefined : parseOptionalNumber(orderRaw, "order");
314
1394
  if (order !== undefined && !Number.isInteger(order)) {
315
1395
  throw new PmCliError("Order must be an integer", EXIT_CODE.USAGE);
316
1396
  }
317
- const goal = options.goal !== undefined ? parseOptionalString(options.goal) : undefined;
318
- const objective = options.objective !== undefined ? parseOptionalString(options.objective) : undefined;
319
- const value = options.value !== undefined ? parseOptionalString(options.value) : undefined;
320
- const impact = options.impact !== undefined ? parseOptionalString(options.impact) : undefined;
321
- const outcome = options.outcome !== undefined ? parseOptionalString(options.outcome) : undefined;
322
- const whyNow = options.whyNow !== undefined ? parseOptionalString(options.whyNow) : undefined;
323
- const assignee = parseOptionalString(options.assignee);
324
- const authorValue = parseOptionalString(options.author) ?? author;
325
- const parent = options.parent !== undefined ? parseOptionalString(options.parent) : undefined;
326
- const reviewer = options.reviewer !== undefined ? parseOptionalString(options.reviewer) : undefined;
327
- const riskRaw = options.risk !== undefined ? parseOptionalString(options.risk) : undefined;
1397
+ const goal = unsetTargets.frontMatterKeys.has("goal") || resolvedOptions.goal === undefined ? undefined : parseOptionalString(resolvedOptions.goal);
1398
+ const objective = unsetTargets.frontMatterKeys.has("objective") || resolvedOptions.objective === undefined
1399
+ ? undefined
1400
+ : parseOptionalString(resolvedOptions.objective);
1401
+ const value = unsetTargets.frontMatterKeys.has("value") || resolvedOptions.value === undefined
1402
+ ? undefined
1403
+ : parseOptionalString(resolvedOptions.value);
1404
+ const impact = unsetTargets.frontMatterKeys.has("impact") || resolvedOptions.impact === undefined
1405
+ ? undefined
1406
+ : parseOptionalString(resolvedOptions.impact);
1407
+ const outcome = unsetTargets.frontMatterKeys.has("outcome") || resolvedOptions.outcome === undefined
1408
+ ? undefined
1409
+ : parseOptionalString(resolvedOptions.outcome);
1410
+ const whyNow = unsetTargets.frontMatterKeys.has("why_now") || resolvedOptions.whyNow === undefined
1411
+ ? undefined
1412
+ : parseOptionalString(resolvedOptions.whyNow);
1413
+ const assignee = unsetTargets.frontMatterKeys.has("assignee") || resolvedOptions.assignee === undefined
1414
+ ? undefined
1415
+ : parseOptionalString(resolvedOptions.assignee);
1416
+ const authorValue = unsetTargets.frontMatterKeys.has("author")
1417
+ ? undefined
1418
+ : parseOptionalString(resolvedOptions.author) ?? author;
1419
+ let parent = unsetTargets.frontMatterKeys.has("parent") || resolvedOptions.parent === undefined
1420
+ ? undefined
1421
+ : parseOptionalString(resolvedOptions.parent);
1422
+ const reviewer = unsetTargets.frontMatterKeys.has("reviewer") || resolvedOptions.reviewer === undefined
1423
+ ? undefined
1424
+ : parseOptionalString(resolvedOptions.reviewer);
1425
+ const riskRaw = unsetTargets.frontMatterKeys.has("risk") || resolvedOptions.risk === undefined
1426
+ ? undefined
1427
+ : parseOptionalString(resolvedOptions.risk);
328
1428
  const risk = riskRaw !== undefined ? ensureEnumValue(normalizeRiskInput(riskRaw), RISK_VALUES, "risk") : undefined;
329
- const confidenceRaw = options.confidence !== undefined ? parseOptionalString(options.confidence) : undefined;
1429
+ const confidenceRaw = unsetTargets.frontMatterKeys.has("confidence") || resolvedOptions.confidence === undefined
1430
+ ? undefined
1431
+ : parseOptionalString(resolvedOptions.confidence);
330
1432
  const confidence = confidenceRaw !== undefined ? parseConfidenceInput(confidenceRaw) : undefined;
331
- const sprint = options.sprint !== undefined ? parseOptionalString(options.sprint) : undefined;
332
- const release = options.release !== undefined ? parseOptionalString(options.release) : undefined;
333
- const blockedBy = options.blockedBy !== undefined ? parseOptionalString(options.blockedBy) : undefined;
334
- const blockedReason = options.blockedReason !== undefined ? parseOptionalString(options.blockedReason) : undefined;
335
- const unblockNote = options.unblockNote !== undefined ? parseOptionalString(options.unblockNote) : undefined;
336
- const reporter = options.reporter !== undefined ? parseOptionalString(options.reporter) : undefined;
337
- const severityRaw = options.severity !== undefined ? parseOptionalString(options.severity) : undefined;
1433
+ const parentReferencePolicy = settings.validation.parent_reference;
1434
+ const sprintReleasePolicy = settings.validation.sprint_release_format;
1435
+ const validationWarnings = [];
1436
+ if (parent !== undefined) {
1437
+ parent = normalizeParentReferenceValue(parent);
1438
+ const parentLocated = await locateItem(pmRoot, parent, settings.id_prefix, settings.item_format, typeRegistry.type_to_folder);
1439
+ if (!parentLocated) {
1440
+ const normalizedParentId = normalizeItemId(parent, settings.id_prefix);
1441
+ validationWarnings.push(...validateMissingParentReference(normalizedParentId, parentReferencePolicy).warnings);
1442
+ }
1443
+ }
1444
+ let sprint = unsetTargets.frontMatterKeys.has("sprint") || resolvedOptions.sprint === undefined
1445
+ ? undefined
1446
+ : parseOptionalString(resolvedOptions.sprint);
1447
+ if (sprint !== undefined) {
1448
+ const sprintValidation = validateSprintOrReleaseValue("sprint", sprint, sprintReleasePolicy);
1449
+ sprint = sprintValidation.value;
1450
+ validationWarnings.push(...sprintValidation.warnings);
1451
+ }
1452
+ let release = unsetTargets.frontMatterKeys.has("release") || resolvedOptions.release === undefined
1453
+ ? undefined
1454
+ : parseOptionalString(resolvedOptions.release);
1455
+ if (release !== undefined) {
1456
+ const releaseValidation = validateSprintOrReleaseValue("release", release, sprintReleasePolicy);
1457
+ release = releaseValidation.value;
1458
+ validationWarnings.push(...releaseValidation.warnings);
1459
+ }
1460
+ const blockedBy = unsetTargets.frontMatterKeys.has("blocked_by") || resolvedOptions.blockedBy === undefined
1461
+ ? undefined
1462
+ : parseOptionalString(resolvedOptions.blockedBy);
1463
+ const blockedReason = unsetTargets.frontMatterKeys.has("blocked_reason") || resolvedOptions.blockedReason === undefined
1464
+ ? undefined
1465
+ : parseOptionalString(resolvedOptions.blockedReason);
1466
+ const unblockNote = unsetTargets.frontMatterKeys.has("unblock_note") || resolvedOptions.unblockNote === undefined
1467
+ ? undefined
1468
+ : parseOptionalString(resolvedOptions.unblockNote);
1469
+ const reporter = unsetTargets.frontMatterKeys.has("reporter") || resolvedOptions.reporter === undefined
1470
+ ? undefined
1471
+ : parseOptionalString(resolvedOptions.reporter);
1472
+ const severityRaw = unsetTargets.frontMatterKeys.has("severity") || resolvedOptions.severity === undefined
1473
+ ? undefined
1474
+ : parseOptionalString(resolvedOptions.severity);
338
1475
  const severity = severityRaw !== undefined ? ensureEnumValue(normalizeSeverityInput(severityRaw), ISSUE_SEVERITY_VALUES, "severity") : undefined;
339
- const environment = options.environment !== undefined ? parseOptionalString(options.environment) : undefined;
340
- const reproSteps = options.reproSteps !== undefined ? parseOptionalString(options.reproSteps) : undefined;
341
- const resolution = options.resolution !== undefined ? parseOptionalString(options.resolution) : undefined;
342
- const expectedResult = options.expectedResult !== undefined ? parseOptionalString(options.expectedResult) : undefined;
343
- const actualResult = options.actualResult !== undefined ? parseOptionalString(options.actualResult) : undefined;
344
- const affectedVersion = options.affectedVersion !== undefined ? parseOptionalString(options.affectedVersion) : undefined;
345
- const fixedVersion = options.fixedVersion !== undefined ? parseOptionalString(options.fixedVersion) : undefined;
346
- const component = options.component !== undefined ? parseOptionalString(options.component) : undefined;
347
- const regressionRaw = options.regression !== undefined ? parseOptionalString(options.regression) : undefined;
1476
+ const environment = unsetTargets.frontMatterKeys.has("environment") || resolvedOptions.environment === undefined
1477
+ ? undefined
1478
+ : parseOptionalString(resolvedOptions.environment);
1479
+ const reproSteps = unsetTargets.frontMatterKeys.has("repro_steps") || resolvedOptions.reproSteps === undefined
1480
+ ? undefined
1481
+ : parseOptionalString(resolvedOptions.reproSteps);
1482
+ const resolution = unsetTargets.frontMatterKeys.has("resolution") || resolvedOptions.resolution === undefined
1483
+ ? undefined
1484
+ : parseOptionalString(resolvedOptions.resolution);
1485
+ const expectedResult = unsetTargets.frontMatterKeys.has("expected_result") || resolvedOptions.expectedResult === undefined
1486
+ ? undefined
1487
+ : parseOptionalString(resolvedOptions.expectedResult);
1488
+ const actualResult = unsetTargets.frontMatterKeys.has("actual_result") || resolvedOptions.actualResult === undefined
1489
+ ? undefined
1490
+ : parseOptionalString(resolvedOptions.actualResult);
1491
+ const affectedVersion = unsetTargets.frontMatterKeys.has("affected_version") || resolvedOptions.affectedVersion === undefined
1492
+ ? undefined
1493
+ : parseOptionalString(resolvedOptions.affectedVersion);
1494
+ const fixedVersion = unsetTargets.frontMatterKeys.has("fixed_version") || resolvedOptions.fixedVersion === undefined
1495
+ ? undefined
1496
+ : parseOptionalString(resolvedOptions.fixedVersion);
1497
+ const component = unsetTargets.frontMatterKeys.has("component") || resolvedOptions.component === undefined
1498
+ ? undefined
1499
+ : parseOptionalString(resolvedOptions.component);
1500
+ const regressionRaw = unsetTargets.frontMatterKeys.has("regression") || resolvedOptions.regression === undefined
1501
+ ? undefined
1502
+ : parseOptionalString(resolvedOptions.regression);
348
1503
  const regression = regressionRaw !== undefined ? parseRegressionInput(regressionRaw) : undefined;
349
- const customerImpact = options.customerImpact !== undefined ? parseOptionalString(options.customerImpact) : undefined;
1504
+ const customerImpact = unsetTargets.frontMatterKeys.has("customer_impact") || resolvedOptions.customerImpact === undefined
1505
+ ? undefined
1506
+ : parseOptionalString(resolvedOptions.customerImpact);
1507
+ const title = requireStringOption(resolvedOptions.title, "--title");
1508
+ const description = requireStringOption(resolvedOptions.description, "--description");
1509
+ const body = resolvedOptions.body ?? "";
350
1510
  const frontMatter = normalizeFrontMatter({
351
1511
  id,
352
- title: options.title,
353
- description: options.description,
1512
+ title,
1513
+ description,
354
1514
  type,
1515
+ type_options: validatedTypeOptions.normalized,
355
1516
  status,
356
1517
  priority,
357
1518
  tags,
@@ -398,22 +1559,32 @@ export async function runCreate(options, global) {
398
1559
  files: files.values,
399
1560
  tests: tests.values,
400
1561
  docs: docs.values,
1562
+ reminders: reminders.values,
1563
+ events: events.values,
1564
+ ...runtimeCreateFieldValues.values,
401
1565
  });
1566
+ try {
1567
+ applyRegisteredItemFieldDefaultsAndValidation(frontMatter, getActiveExtensionRegistrations());
1568
+ }
1569
+ catch (error) {
1570
+ throw new PmCliError(error instanceof Error ? error.message : "Invalid extension item field values", EXIT_CODE.USAGE);
1571
+ }
402
1572
  const afterDocument = canonicalDocument({
403
1573
  front_matter: frontMatter,
404
- body: options.body,
405
- });
1574
+ body,
1575
+ }, { schema: settings.schema });
406
1576
  const beforeDocument = {
407
1577
  front_matter: {},
408
1578
  body: "",
409
1579
  };
410
- const itemPath = getItemPath(pmRoot, type, id);
1580
+ const itemPath = getItemPath(pmRoot, type, id, settings.item_format, typeRegistry.type_to_folder);
411
1581
  const historyPath = getHistoryPath(pmRoot, id);
412
- const lockRelease = await acquireLock(pmRoot, id, settings.locks.ttl_seconds, author);
413
- const historyMessage = buildHistoryMessage(options.message, explicitUnsets);
1582
+ const lockRelease = await acquireLock(pmRoot, id, settings.locks.ttl_seconds, author, false, settings.governance.force_required_for_stale_lock);
1583
+ const explicitUnsetKeys = [...explicitUnsets].sort((left, right) => left.localeCompare(right));
1584
+ const historyMessage = buildHistoryMessage(resolvedOptions.message, explicitUnsetKeys);
414
1585
  let hookWarnings = [];
415
1586
  try {
416
- await writeFileAtomic(itemPath, serializeItemDocument(afterDocument));
1587
+ await writeFileAtomic(itemPath, serializeItemDocument(afterDocument, { format: settings.item_format, schema: settings.schema }));
417
1588
  try {
418
1589
  const entry = createHistoryEntry({
419
1590
  nowIso: nowValue,
@@ -445,12 +1616,12 @@ export async function runCreate(options, global) {
445
1616
  finally {
446
1617
  await lockRelease();
447
1618
  }
448
- const changedFields = buildChangedFields(frontMatter, explicitUnsets);
1619
+ const changedFields = buildChangedFields(frontMatter, explicitUnsetKeys);
449
1620
  const outputItem = structuredClone(frontMatter);
450
1621
  return {
451
1622
  item: outputItem,
452
1623
  changed_fields: changedFields,
453
- warnings: hookWarnings,
1624
+ warnings: [...validationWarnings, ...hookWarnings],
454
1625
  };
455
1626
  }
456
1627
  //# sourceMappingURL=create.js.map