@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
package/dist/cli/main.js CHANGED
@@ -3,29 +3,143 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
- import { runAppend, runActivity, runClaim, runClose, runComments, runCompletion, runConfig, runCreate, runDelete, runDocs, runFiles, runGet, runGc, runHealth, runHistory, runInit, runInstall, runList, runSearch, runReindex, runRestore, runRelease, runStats, runTest, runTestAll, runUpdate, } from "./commands/index.js";
7
- import { activateExtensions, clearActiveExtensionHooks, createEmptyExtensionCommandRegistry, createEmptyExtensionHookRegistry, createEmptyExtensionRendererRegistry, loadExtensions, runActiveCommandHandler, runAfterCommandHooks, runBeforeCommandHooks, setActiveCommandContext, setActiveExtensionCommands, setActiveExtensionHooks, setActiveExtensionRenderers, } from "../core/extensions/index.js";
6
+ import { runAggregate, runAppend, runActivity, runCalendar, runClaim, runClose, runComments, runCommentsAudit, runCompletion, runConfig, runContracts, runCreate, runDelete, runDedupeAudit, runDeps, runDocs, runExtension, runFiles, runFilesDiscover, runGet, runGc, runHealth, runHistory, runInit, runLearnings, runList, runNormalize, runNotes, runSearch, runReindex, runRestore, renderCalendarMarkdown, renderContextMarkdown, runRelease, resolveCalendarOutputFormat, resolveContextOutputFormat, runStats, runStartBackgroundRun, runTest, runTestAll, runTestRunsList, runTestRunsLogs, runTestRunsResume, runTestRunsStatus, runTestRunsStop, runTestRunsWorker, runTemplatesList, runTemplatesSave, runTemplatesShow, runUpdate, runUpdateMany, runValidate, runContext, } from "./commands/index.js";
7
+ import { activateExtensions, clearActiveExtensionHooks, getActiveCommandResult, getActiveExtensionRegistrations, loadExtensions, runActiveCommandHandler, runActiveParserOverride, runActivePreflightOverride, runActiveServiceOverride, runAfterCommandHooks, runBeforeCommandHooks, setActiveCommandResult, setActiveCommandContext, setActiveExtensionCommands, setActiveExtensionHooks, setActiveExtensionParsers, setActiveExtensionPreflight, setActiveExtensionRegistrations, setActiveExtensionRenderers, setActiveExtensionServices, } from "../core/extensions/index.js";
8
8
  import { pathExists } from "../core/fs/fs-utils.js";
9
+ import { commandOptionFlagLabel, resolveCommandOptionPolicyState, resolveItemTypeRegistry, resolveTypeDefinition, } from "../core/item/type-registry.js";
10
+ import { normalizeStatusInput } from "../core/item/status.js";
11
+ import { resolveRuntimeFieldRegistry, resolveRuntimeStatusRegistry, } from "../core/schema/runtime-schema.js";
9
12
  import { refreshSearchArtifactsForMutation } from "../core/search/cache.js";
10
13
  import { EXIT_CODE } from "../core/shared/constants.js";
11
14
  import { PmCliError } from "../core/shared/errors.js";
12
- import { printError, printResult } from "../core/output/output.js";
15
+ import { printError, printResult, writeStdout } from "../core/output/output.js";
16
+ import { maybeRunFirstUseTelemetryPrompt } from "../core/telemetry/consent.js";
17
+ import { finishTelemetryCommand, startTelemetryCommand, } from "../core/telemetry/runtime.js";
18
+ import { migrateItemFilesToFormat } from "../core/store/item-format-migration.js";
19
+ import { listAllFrontMatter } from "../core/store/item-store.js";
13
20
  import { getSettingsPath, resolvePmRoot } from "../core/store/paths.js";
14
- import { readSettings } from "../core/store/settings.js";
15
- import { getEnabledBuiltInExtensions } from "../core/extensions/builtins.js";
16
- import { parseLooseCommandOptions } from "./extension-command-options.js";
21
+ import { readSettings, readSettingsWithMetadata, writeSettings } from "../core/store/settings.js";
22
+ import { BUILTIN_ITEM_TYPE_VALUES } from "../types/index.js";
23
+ import { coerceLooseCommandOptionsWithFlagDefinitions, parseLooseCommandOptions } from "./extension-command-options.js";
24
+ import { attachRichHelpText, normalizeHelpCommandPath, resolveHelpDetailMode, resolveHelpNarrative } from "./help-content.js";
25
+ import { formatCommanderErrorForDisplay, formatCommanderErrorForJson, formatPmCliErrorForDisplay, formatPmCliErrorForJson, formatUnknownErrorForJson, } from "./error-guidance.js";
26
+ import { CALENDAR_COMMANDER_STRING_OPTION_CONTRACTS, ACTIVITY_COMMANDER_STRING_OPTION_CONTRACTS, CONTEXT_COMMANDER_STRING_OPTION_CONTRACTS, CREATE_COMMANDER_REPEATABLE_OPTION_CONTRACTS, CREATE_COMMANDER_STRING_OPTION_CONTRACTS, LIST_COMMANDER_STRING_OPTION_CONTRACTS, SEARCH_COMMANDER_STRING_OPTION_CONTRACTS, UPDATE_COMMANDER_REPEATABLE_OPTION_CONTRACTS, UPDATE_COMMANDER_STRING_OPTION_CONTRACTS, readFirstStringFromCommanderOptions, readStringArrayFromCommanderOptions, } from "../sdk/cli-contracts.js";
27
+ const PM_PACKAGE_ROOT_ENV = "PM_CLI_PACKAGE_ROOT";
28
+ function resolvePmPackageRoot() {
29
+ const mainPath = fileURLToPath(import.meta.url);
30
+ return path.resolve(path.dirname(mainPath), "../..");
31
+ }
32
+ if (typeof process.env[PM_PACKAGE_ROOT_ENV] !== "string" || process.env[PM_PACKAGE_ROOT_ENV]?.trim().length === 0) {
33
+ process.env[PM_PACKAGE_ROOT_ENV] = resolvePmPackageRoot();
34
+ }
17
35
  function collect(value, previous) {
18
36
  const next = previous ?? [];
19
37
  next.push(value);
20
38
  return next;
21
39
  }
40
+ function pushOptionalValueFlag(args, flag, value) {
41
+ if (typeof value !== "string") {
42
+ return;
43
+ }
44
+ const trimmed = value.trim();
45
+ if (trimmed.length === 0) {
46
+ return;
47
+ }
48
+ args.push(flag, trimmed);
49
+ }
50
+ function pushOptionalBooleanFlag(args, flag, value) {
51
+ if (value === true) {
52
+ args.push(flag);
53
+ }
54
+ }
55
+ function pushRepeatableValueFlag(args, flag, values) {
56
+ if (!Array.isArray(values)) {
57
+ return;
58
+ }
59
+ for (const value of values) {
60
+ if (typeof value !== "string") {
61
+ continue;
62
+ }
63
+ const trimmed = value.trim();
64
+ if (trimmed.length === 0) {
65
+ continue;
66
+ }
67
+ args.push(flag, trimmed);
68
+ }
69
+ }
70
+ function buildBackgroundTestCommandArgs(id, options) {
71
+ const args = ["test", id, "--run", "--json", "--progress"];
72
+ pushRepeatableValueFlag(args, "--add", options.add);
73
+ pushRepeatableValueFlag(args, "--remove", options.remove);
74
+ pushOptionalValueFlag(args, "--timeout", options.timeout);
75
+ pushRepeatableValueFlag(args, "--env-set", options.envSet);
76
+ pushRepeatableValueFlag(args, "--env-clear", options.envClear);
77
+ pushOptionalBooleanFlag(args, "--shared-host-safe", options.sharedHostSafe);
78
+ pushOptionalValueFlag(args, "--pm-context", options.pmContext);
79
+ pushOptionalBooleanFlag(args, "--override-linked-pm-context", options.overrideLinkedPmContext);
80
+ pushOptionalBooleanFlag(args, "--fail-on-context-mismatch", options.failOnContextMismatch);
81
+ pushOptionalBooleanFlag(args, "--fail-on-skipped", options.failOnSkipped);
82
+ pushOptionalBooleanFlag(args, "--fail-on-empty-test-run", options.failOnEmptyTestRun);
83
+ pushOptionalBooleanFlag(args, "--require-assertions-for-pm", options.requireAssertionsForPm);
84
+ pushOptionalBooleanFlag(args, "--check-context", options.checkContext);
85
+ pushOptionalBooleanFlag(args, "--auto-pm-context", options.autoPmContext);
86
+ pushOptionalValueFlag(args, "--author", options.author);
87
+ pushOptionalValueFlag(args, "--message", options.message);
88
+ pushOptionalBooleanFlag(args, "--force", options.force);
89
+ return args;
90
+ }
91
+ function buildBackgroundTestAllCommandArgs(options) {
92
+ const args = ["test-all", "--json", "--progress"];
93
+ pushOptionalValueFlag(args, "--status", options.status);
94
+ pushOptionalValueFlag(args, "--limit", options.limit);
95
+ pushOptionalValueFlag(args, "--offset", options.offset);
96
+ pushOptionalValueFlag(args, "--timeout", options.timeout);
97
+ pushRepeatableValueFlag(args, "--env-set", options.envSet);
98
+ pushRepeatableValueFlag(args, "--env-clear", options.envClear);
99
+ pushOptionalBooleanFlag(args, "--shared-host-safe", options.sharedHostSafe);
100
+ pushOptionalValueFlag(args, "--pm-context", options.pmContext);
101
+ pushOptionalBooleanFlag(args, "--override-linked-pm-context", options.overrideLinkedPmContext);
102
+ pushOptionalBooleanFlag(args, "--fail-on-context-mismatch", options.failOnContextMismatch);
103
+ pushOptionalBooleanFlag(args, "--fail-on-skipped", options.failOnSkipped);
104
+ pushOptionalBooleanFlag(args, "--fail-on-empty-test-run", options.failOnEmptyTestRun);
105
+ pushOptionalBooleanFlag(args, "--require-assertions-for-pm", options.requireAssertionsForPm);
106
+ pushOptionalBooleanFlag(args, "--check-context", options.checkContext);
107
+ pushOptionalBooleanFlag(args, "--auto-pm-context", options.autoPmContext);
108
+ return args;
109
+ }
110
+ const RESOLVED_GLOBAL_OPTIONS = Symbol("pm.resolvedGlobalOptions");
111
+ function setResolvedGlobalOptions(command, globalOptions) {
112
+ command[RESOLVED_GLOBAL_OPTIONS] = { ...globalOptions };
113
+ }
114
+ function clearResolvedGlobalOptions(command) {
115
+ delete command[RESOLVED_GLOBAL_OPTIONS];
116
+ }
117
+ async function applyDefaultOutputFormat(globalOptions) {
118
+ if (globalOptions.json === true) {
119
+ return globalOptions;
120
+ }
121
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
122
+ if (!(await pathExists(getSettingsPath(pmRoot)))) {
123
+ return globalOptions;
124
+ }
125
+ const settings = await readSettings(pmRoot);
126
+ return {
127
+ ...globalOptions,
128
+ defaultOutputFormat: settings.output.default_format,
129
+ };
130
+ }
22
131
  function getGlobalOptions(command) {
132
+ const resolved = command[RESOLVED_GLOBAL_OPTIONS];
133
+ if (resolved) {
134
+ return { ...resolved };
135
+ }
23
136
  const opts = command.optsWithGlobals();
24
137
  return {
25
- json: Boolean(opts.json),
138
+ json: opts.json === true ? true : undefined,
26
139
  quiet: Boolean(opts.quiet),
27
140
  path: typeof opts.path === "string" ? opts.path : undefined,
28
141
  noExtensions: opts.extensions === false,
142
+ noPager: Boolean(opts.noPager),
29
143
  profile: Boolean(opts.profile),
30
144
  };
31
145
  }
@@ -53,7 +167,14 @@ function toNonEmptyFlagString(value) {
53
167
  const trimmed = value.trim();
54
168
  return trimmed.length > 0 ? trimmed : null;
55
169
  }
170
+ function toOptionalBoolean(value) {
171
+ return typeof value === "boolean" ? value : undefined;
172
+ }
56
173
  function formatDynamicExtensionFlagHelpLine(definition) {
174
+ const visible = toOptionalBoolean(definition.visible);
175
+ if (visible === false) {
176
+ return null;
177
+ }
57
178
  const longName = toNonEmptyFlagString(definition.long);
58
179
  if (!longName || !longName.startsWith("--") || longName.length < 3) {
59
180
  return null;
@@ -63,7 +184,15 @@ function formatDynamicExtensionFlagHelpLine(definition) {
63
184
  const valueName = toNonEmptyFlagString(definition.value_name);
64
185
  const valueSuffix = valueName ? ` <${valueName}>` : "";
65
186
  const description = toNonEmptyFlagString(definition.description) ?? "Extension-provided option.";
66
- return `${shortPrefix}${longName}${valueSuffix} ${description}`;
187
+ const markers = [];
188
+ if (toOptionalBoolean(definition.required) === true) {
189
+ markers.push("required");
190
+ }
191
+ if (toOptionalBoolean(definition.enabled) === false) {
192
+ markers.push("disabled");
193
+ }
194
+ const markerSuffix = markers.length > 0 ? ` [${markers.join(", ")}]` : "";
195
+ return `${shortPrefix}${longName}${valueSuffix} ${description}${markerSuffix}`;
67
196
  }
68
197
  function buildDynamicExtensionFlagHelp(definitions) {
69
198
  const lines = [
@@ -98,8 +227,284 @@ function collectDynamicExtensionFlagHelpByCommand(registrations) {
98
227
  }
99
228
  return helpByCommand;
100
229
  }
230
+ function normalizeExtensionCommandAction(commandPath, action) {
231
+ if (typeof action !== "string" || action.trim().length === 0) {
232
+ return commandPath.replace(/\s+/g, "-");
233
+ }
234
+ return action.trim().toLowerCase();
235
+ }
236
+ function normalizeExtensionCommandStringList(values) {
237
+ if (!Array.isArray(values)) {
238
+ return [];
239
+ }
240
+ const normalized = [];
241
+ const seen = new Set();
242
+ for (const value of values) {
243
+ if (typeof value !== "string") {
244
+ continue;
245
+ }
246
+ const trimmed = value.trim();
247
+ if (trimmed.length === 0 || seen.has(trimmed)) {
248
+ continue;
249
+ }
250
+ seen.add(trimmed);
251
+ normalized.push(trimmed);
252
+ }
253
+ return normalized;
254
+ }
255
+ function normalizeExtensionCommandArguments(values) {
256
+ if (!Array.isArray(values)) {
257
+ return [];
258
+ }
259
+ return values
260
+ .map((value) => {
261
+ const name = typeof value.name === "string" ? value.name.trim() : "";
262
+ if (name.length === 0) {
263
+ return null;
264
+ }
265
+ const normalized = {
266
+ name,
267
+ required: value.required === true,
268
+ variadic: value.variadic === true,
269
+ };
270
+ if (typeof value.description === "string" && value.description.trim().length > 0) {
271
+ normalized.description = value.description.trim();
272
+ }
273
+ return normalized;
274
+ })
275
+ .filter((entry) => entry !== null);
276
+ }
277
+ function collectExtensionCommandHelpDescriptors(commandHandlers, commandDefinitions, flagRegistrations) {
278
+ const definitionsByCommand = new Map();
279
+ for (const definition of commandDefinitions) {
280
+ const commandPath = normalizeExtensionCommandPath(definition.command);
281
+ if (commandPath.length === 0) {
282
+ continue;
283
+ }
284
+ const description = typeof definition.description === "string" && definition.description.trim().length > 0
285
+ ? definition.description.trim()
286
+ : undefined;
287
+ const intent = typeof definition.intent === "string" && definition.intent.trim().length > 0
288
+ ? definition.intent.trim()
289
+ : undefined;
290
+ definitionsByCommand.set(commandPath, {
291
+ command: commandPath,
292
+ action: normalizeExtensionCommandAction(commandPath, definition.action),
293
+ description,
294
+ intent,
295
+ examples: normalizeExtensionCommandStringList(definition.examples),
296
+ failure_hints: normalizeExtensionCommandStringList(definition.failure_hints),
297
+ arguments: normalizeExtensionCommandArguments(definition.arguments),
298
+ flags: [],
299
+ source: {
300
+ layer: definition.layer,
301
+ name: definition.name,
302
+ },
303
+ });
304
+ }
305
+ const flagsByCommand = new Map();
306
+ for (const registration of flagRegistrations) {
307
+ const commandPath = normalizeExtensionCommandPath(registration.target_command);
308
+ if (commandPath.length === 0) {
309
+ continue;
310
+ }
311
+ const existing = flagsByCommand.get(commandPath) ?? [];
312
+ existing.push(...registration.flags);
313
+ flagsByCommand.set(commandPath, existing);
314
+ }
315
+ const commandSet = new Set();
316
+ for (const commandPath of commandHandlers) {
317
+ const normalized = normalizeExtensionCommandPath(commandPath);
318
+ if (normalized.length > 0) {
319
+ commandSet.add(normalized);
320
+ }
321
+ }
322
+ for (const commandPath of definitionsByCommand.keys()) {
323
+ commandSet.add(commandPath);
324
+ }
325
+ for (const commandPath of flagsByCommand.keys()) {
326
+ commandSet.add(commandPath);
327
+ }
328
+ const descriptors = new Map();
329
+ const sortedCommands = [...commandSet].sort((left, right) => left.localeCompare(right));
330
+ for (const commandPath of sortedCommands) {
331
+ const definition = definitionsByCommand.get(commandPath);
332
+ const flags = flagsByCommand.get(commandPath) ?? [];
333
+ if (definition) {
334
+ descriptors.set(commandPath, {
335
+ ...definition,
336
+ flags,
337
+ });
338
+ continue;
339
+ }
340
+ descriptors.set(commandPath, {
341
+ command: commandPath,
342
+ action: normalizeExtensionCommandAction(commandPath, undefined),
343
+ examples: [],
344
+ failure_hints: [],
345
+ arguments: [],
346
+ flags,
347
+ });
348
+ }
349
+ return descriptors;
350
+ }
351
+ function buildExtensionArgumentToken(argument) {
352
+ const variadicSuffix = argument.variadic ? "..." : "";
353
+ if (argument.required) {
354
+ return `<${argument.name}${variadicSuffix}>`;
355
+ }
356
+ return `[${argument.name}${variadicSuffix}]`;
357
+ }
358
+ function applyDynamicExtensionArguments(command, descriptor) {
359
+ for (const argument of descriptor.arguments) {
360
+ command.argument(buildExtensionArgumentToken(argument), argument.description ?? "Extension argument.");
361
+ }
362
+ }
363
+ function formatDynamicExtensionOptionFlags(definition) {
364
+ const visible = toOptionalBoolean(definition.visible);
365
+ if (visible === false) {
366
+ return null;
367
+ }
368
+ const longName = toNonEmptyFlagString(definition.long);
369
+ const shortName = toNonEmptyFlagString(definition.short);
370
+ const normalizedShort = shortName && shortName.startsWith("-") && !shortName.startsWith("--") ? shortName : null;
371
+ const normalizedLong = longName && longName.startsWith("--") && longName.length > 2 ? longName : null;
372
+ if (!normalizedLong && !normalizedShort) {
373
+ return null;
374
+ }
375
+ const optionValueName = toNonEmptyFlagString(definition.value_name);
376
+ const optionValueSuffix = optionValueName ? ` <${optionValueName}>` : "";
377
+ const optionNames = [normalizedShort, normalizedLong].filter((entry) => entry !== null);
378
+ return `${optionNames.join(", ")}${optionValueSuffix}`;
379
+ }
380
+ function formatDynamicExtensionOptionDescription(definition) {
381
+ const description = toNonEmptyFlagString(definition.description) ?? "Extension-provided option.";
382
+ const markers = [];
383
+ if (toOptionalBoolean(definition.required) === true) {
384
+ markers.push("required");
385
+ }
386
+ if (toOptionalBoolean(definition.enabled) === false) {
387
+ markers.push("disabled");
388
+ }
389
+ const markerSuffix = markers.length > 0 ? ` [${markers.join(", ")}]` : "";
390
+ return `${description}${markerSuffix}`;
391
+ }
392
+ function applyDynamicExtensionOptions(command, descriptor) {
393
+ const seen = new Set();
394
+ for (const definition of descriptor.flags) {
395
+ const optionFlags = formatDynamicExtensionOptionFlags(definition);
396
+ if (!optionFlags || seen.has(optionFlags)) {
397
+ continue;
398
+ }
399
+ seen.add(optionFlags);
400
+ command.option(optionFlags, formatDynamicExtensionOptionDescription(definition));
401
+ }
402
+ }
403
+ function buildDynamicExtensionHelpOptionSummary(definition) {
404
+ const flags = formatDynamicExtensionOptionFlags(definition);
405
+ if (!flags) {
406
+ return null;
407
+ }
408
+ const longName = toNonEmptyFlagString(definition.long);
409
+ const shortName = toNonEmptyFlagString(definition.short);
410
+ const normalizedLong = longName && longName.startsWith("--") && longName.length > 2 ? longName : null;
411
+ const normalizedShort = shortName && shortName.startsWith("-") && !shortName.startsWith("--") ? shortName : null;
412
+ const valueName = toNonEmptyFlagString(definition.value_name);
413
+ const required = toOptionalBoolean(definition.required) === true;
414
+ return {
415
+ flags,
416
+ long: normalizedLong,
417
+ short: normalizedShort,
418
+ description: formatDynamicExtensionOptionDescription(definition),
419
+ takes_value: valueName !== null,
420
+ value_required: valueName !== null,
421
+ value_name: valueName,
422
+ variadic: false,
423
+ required,
424
+ aliases: [],
425
+ alias_for: null,
426
+ };
427
+ }
428
+ function buildDynamicExtensionHelpOptionSummaries(descriptor) {
429
+ if (!descriptor) {
430
+ return [];
431
+ }
432
+ const summaries = [];
433
+ const seen = new Set();
434
+ for (const definition of descriptor.flags) {
435
+ const summary = buildDynamicExtensionHelpOptionSummary(definition);
436
+ if (!summary || seen.has(summary.flags)) {
437
+ continue;
438
+ }
439
+ seen.add(summary.flags);
440
+ summaries.push(summary);
441
+ }
442
+ return summaries;
443
+ }
444
+ function mergeHelpOptionSummaries(base, extension) {
445
+ if (extension.length === 0) {
446
+ return base;
447
+ }
448
+ const merged = [...base];
449
+ const seen = new Set(base.map((entry) => entry.flags));
450
+ for (const entry of extension) {
451
+ if (seen.has(entry.flags)) {
452
+ continue;
453
+ }
454
+ seen.add(entry.flags);
455
+ merged.push(entry);
456
+ }
457
+ return merged;
458
+ }
459
+ function buildDynamicExtensionCommandMetadataHelp(descriptor) {
460
+ const lines = [];
461
+ if (descriptor.intent) {
462
+ lines.push(`Intent: ${descriptor.intent}`);
463
+ }
464
+ if (descriptor.action) {
465
+ lines.push(`Action contract: ${descriptor.action}`);
466
+ }
467
+ if (descriptor.examples.length > 0) {
468
+ lines.push("Examples:");
469
+ for (const example of descriptor.examples) {
470
+ lines.push(` - ${example}`);
471
+ }
472
+ }
473
+ if (descriptor.failure_hints.length > 0) {
474
+ lines.push("Common failure hints:");
475
+ for (const hint of descriptor.failure_hints) {
476
+ lines.push(` - ${hint}`);
477
+ }
478
+ }
479
+ if (lines.length === 0) {
480
+ return null;
481
+ }
482
+ return `\nExtension command metadata:\n ${lines.join("\n ")}`;
483
+ }
484
+ function commandAliases(command) {
485
+ const commandRecord = command;
486
+ if (typeof commandRecord.aliases === "function") {
487
+ return commandRecord.aliases().map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0);
488
+ }
489
+ if (typeof commandRecord.alias === "function") {
490
+ const alias = commandRecord.alias();
491
+ if (typeof alias === "string" && alias.trim().length > 0) {
492
+ return [alias.trim().toLowerCase()];
493
+ }
494
+ }
495
+ if (Array.isArray(commandRecord._aliases)) {
496
+ return commandRecord._aliases.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0);
497
+ }
498
+ return [];
499
+ }
101
500
  function findDirectChildCommand(parent, name) {
102
- return parent.commands.find((entry) => entry.name() === name) ?? null;
501
+ const normalizedTarget = name.trim().toLowerCase();
502
+ return (parent.commands.find((entry) => {
503
+ if (entry.name().trim().toLowerCase() === normalizedTarget) {
504
+ return true;
505
+ }
506
+ return commandAliases(entry).includes(normalizedTarget);
507
+ }) ?? null);
103
508
  }
104
509
  function findCommandByPath(root, pathParts) {
105
510
  let current = root;
@@ -164,6 +569,9 @@ function parseBootstrapPathToken(token, next) {
164
569
  function parseBootstrapGlobalOptions(argv) {
165
570
  let pathValue;
166
571
  let noExtensions = false;
572
+ let noPager = false;
573
+ let json = false;
574
+ let quiet = false;
167
575
  let index = 0;
168
576
  while (index < argv.length) {
169
577
  const token = argv[index];
@@ -175,6 +583,21 @@ function parseBootstrapGlobalOptions(argv) {
175
583
  index += 1;
176
584
  continue;
177
585
  }
586
+ if (token === "--no-pager") {
587
+ noPager = true;
588
+ index += 1;
589
+ continue;
590
+ }
591
+ if (token === "--json") {
592
+ json = true;
593
+ index += 1;
594
+ continue;
595
+ }
596
+ if (token === "--quiet") {
597
+ quiet = true;
598
+ index += 1;
599
+ continue;
600
+ }
178
601
  const parsedPath = parseBootstrapPathToken(token, argv[index + 1]);
179
602
  if (parsedPath) {
180
603
  if (parsedPath.pathValue !== undefined) {
@@ -188,9 +611,436 @@ function parseBootstrapGlobalOptions(argv) {
188
611
  return {
189
612
  path: pathValue,
190
613
  noExtensions,
614
+ noPager,
615
+ json,
616
+ quiet,
617
+ };
618
+ }
619
+ function stripGlobalBootstrapTokens(argv) {
620
+ const remaining = [];
621
+ let index = 0;
622
+ while (index < argv.length) {
623
+ const token = argv[index];
624
+ if (token === "--") {
625
+ break;
626
+ }
627
+ if (token === "--json" ||
628
+ token === "--quiet" ||
629
+ token === "--no-extensions" ||
630
+ token === "--no-pager" ||
631
+ token === "--profile" ||
632
+ token === "--explain") {
633
+ index += 1;
634
+ continue;
635
+ }
636
+ if (token === "--path") {
637
+ index += 2;
638
+ continue;
639
+ }
640
+ if (token.startsWith("--path=")) {
641
+ index += 1;
642
+ continue;
643
+ }
644
+ remaining.push(token);
645
+ index += 1;
646
+ }
647
+ return remaining;
648
+ }
649
+ function shouldDisablePagerForInvocation(argv, bootstrapGlobal) {
650
+ if (bootstrapGlobal.noPager) {
651
+ return true;
652
+ }
653
+ if (process.stdout.isTTY === true) {
654
+ return false;
655
+ }
656
+ const helpRequest = parseBootstrapHelpRequest(argv);
657
+ return helpRequest.requested;
658
+ }
659
+ function applyBootstrapPagerPolicy(argv) {
660
+ const bootstrapGlobal = parseBootstrapGlobalOptions(argv);
661
+ if (!shouldDisablePagerForInvocation(argv, bootstrapGlobal)) {
662
+ return;
663
+ }
664
+ process.env.PAGER = "cat";
665
+ process.env.MANPAGER = "cat";
666
+ process.env.GIT_PAGER = "cat";
667
+ if (typeof process.env.LESS !== "string" || process.env.LESS.trim().length === 0) {
668
+ process.env.LESS = "FRX";
669
+ }
670
+ }
671
+ function parseBootstrapHelpRequest(argv) {
672
+ const stripped = stripGlobalBootstrapTokens(argv);
673
+ const first = stripped[0]?.trim().toLowerCase();
674
+ if (first === "help") {
675
+ const commandPathTokens = [];
676
+ for (let index = 1; index < stripped.length; index += 1) {
677
+ const token = stripped[index];
678
+ if (token.startsWith("-")) {
679
+ break;
680
+ }
681
+ commandPathTokens.push(token.trim().toLowerCase());
682
+ }
683
+ return {
684
+ requested: true,
685
+ commandPathTokens,
686
+ };
687
+ }
688
+ const helpFlagIndex = stripped.findIndex((token) => token === "--help" || token === "-h");
689
+ if (helpFlagIndex < 0) {
690
+ return {
691
+ requested: false,
692
+ commandPathTokens: [],
693
+ };
694
+ }
695
+ const commandPathTokens = [];
696
+ for (const token of stripped) {
697
+ if (token.startsWith("-")) {
698
+ break;
699
+ }
700
+ commandPathTokens.push(token.trim().toLowerCase());
701
+ }
702
+ return {
703
+ requested: true,
704
+ commandPathTokens,
705
+ };
706
+ }
707
+ function parseBootstrapCommandName(argv) {
708
+ for (let index = 0; index < argv.length; index += 1) {
709
+ const token = argv[index];
710
+ if (token === "--") {
711
+ break;
712
+ }
713
+ if (token === "--path") {
714
+ index += 1;
715
+ continue;
716
+ }
717
+ if (token.startsWith("--path=") ||
718
+ token === "--json" ||
719
+ token === "--quiet" ||
720
+ token === "--no-extensions" ||
721
+ token === "--no-pager" ||
722
+ token === "--profile" ||
723
+ token === "--explain") {
724
+ continue;
725
+ }
726
+ if (token.startsWith("-")) {
727
+ continue;
728
+ }
729
+ return token.trim().toLowerCase();
730
+ }
731
+ return undefined;
732
+ }
733
+ const EXTENSION_ACTION_SYNTAX_TOKENS = new Set([
734
+ "install",
735
+ "uninstall",
736
+ "explore",
737
+ "manage",
738
+ "doctor",
739
+ "adopt",
740
+ "adopt-all",
741
+ "activate",
742
+ "deactivate",
743
+ ]);
744
+ function normalizeLegacyExtensionActionSyntax(argv) {
745
+ const extensionIndex = argv.findIndex((token) => token === "extension");
746
+ if (extensionIndex < 0) {
747
+ return [...argv];
748
+ }
749
+ const actionToken = argv[extensionIndex + 1];
750
+ if (!actionToken || actionToken.startsWith("-")) {
751
+ return [...argv];
752
+ }
753
+ if (!EXTENSION_ACTION_SYNTAX_TOKENS.has(actionToken)) {
754
+ return [...argv];
755
+ }
756
+ if (argv.includes("--help") || argv.includes("-h")) {
757
+ return [...argv];
758
+ }
759
+ const forcedActionFlag = `--${actionToken}`;
760
+ if (argv.includes(forcedActionFlag)) {
761
+ return [...argv];
762
+ }
763
+ return [...argv.slice(0, extensionIndex + 1), forcedActionFlag, ...argv.slice(extensionIndex + 2)];
764
+ }
765
+ const BUILTIN_TYPE_HELP_VALUES = BUILTIN_ITEM_TYPE_VALUES.join("|");
766
+ function resolveCommandFromPathTokens(root, pathTokens) {
767
+ if (pathTokens.length === 0) {
768
+ return root;
769
+ }
770
+ return findCommandByPath(root, pathTokens);
771
+ }
772
+ function extractOptionValueName(flags) {
773
+ const match = flags.match(/[<[]([^>\]]+)[>\]]/);
774
+ if (!match) {
775
+ return null;
776
+ }
777
+ const value = match[1]?.trim();
778
+ return value && value.length > 0 ? value : null;
779
+ }
780
+ function readOptionAttributeName(option) {
781
+ const optionRecord = option;
782
+ if (typeof optionRecord.attributeName === "function") {
783
+ const value = optionRecord.attributeName();
784
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
785
+ }
786
+ if (typeof optionRecord.attributeName === "string" && optionRecord.attributeName.trim().length > 0) {
787
+ return optionRecord.attributeName.trim();
788
+ }
789
+ return null;
790
+ }
791
+ function buildOptionAliasMap(options) {
792
+ const aliasMap = new Map();
793
+ for (const option of options) {
794
+ const optionRecord = option;
795
+ const attributeName = readOptionAttributeName(option);
796
+ if (!attributeName || typeof optionRecord.long !== "string" || optionRecord.long.trim().length === 0) {
797
+ continue;
798
+ }
799
+ const existing = aliasMap.get(attributeName) ?? [];
800
+ existing.push(optionRecord.long.trim());
801
+ aliasMap.set(attributeName, existing);
802
+ }
803
+ for (const [attributeName, values] of aliasMap.entries()) {
804
+ aliasMap.set(attributeName, [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))].sort((left, right) => left.localeCompare(right)));
805
+ }
806
+ return aliasMap;
807
+ }
808
+ function buildHelpOptionSummaries(command) {
809
+ const options = (command.options ?? []);
810
+ const optionAliasMap = buildOptionAliasMap(options);
811
+ return options.map((option) => {
812
+ const optionRecord = option;
813
+ const flags = typeof optionRecord.flags === "string" ? optionRecord.flags.trim() : "";
814
+ const description = typeof optionRecord.description === "string" ? optionRecord.description.trim() : "";
815
+ const attributeName = readOptionAttributeName(option);
816
+ const aliasCandidates = attributeName ? optionAliasMap.get(attributeName) ?? [] : [];
817
+ const aliases = aliasCandidates
818
+ .filter((entry) => entry !== optionRecord.long)
819
+ .map((entry) => entry.trim())
820
+ .filter((entry) => entry.length > 0);
821
+ const aliasForMatch = description.match(/^Alias for ([^ ]+)/i);
822
+ const aliasFor = aliasForMatch && aliasForMatch[1] ? aliasForMatch[1].trim() : null;
823
+ const required = optionRecord.mandatory === true || description.includes("[required]") || description.toLowerCase().includes("required;");
824
+ const valueRequired = flags.includes("<");
825
+ const takesValue = valueRequired || flags.includes("[");
826
+ const summary = {
827
+ flags,
828
+ long: typeof optionRecord.long === "string" ? optionRecord.long : null,
829
+ short: typeof optionRecord.short === "string" ? optionRecord.short : null,
830
+ description,
831
+ takes_value: takesValue,
832
+ value_required: valueRequired,
833
+ value_name: extractOptionValueName(flags),
834
+ variadic: optionRecord.variadic === true,
835
+ required,
836
+ aliases,
837
+ alias_for: aliasFor,
838
+ };
839
+ if (optionRecord.defaultValue !== undefined) {
840
+ summary.default_value = optionRecord.defaultValue;
841
+ }
842
+ return summary;
843
+ });
844
+ }
845
+ function buildHelpArgumentSummaries(command) {
846
+ const commandRecord = command;
847
+ const argumentsList = Array.isArray(commandRecord.registeredArguments)
848
+ ? commandRecord.registeredArguments
849
+ : Array.isArray(commandRecord._args)
850
+ ? commandRecord._args
851
+ : [];
852
+ return argumentsList.map((argument) => {
853
+ const rawName = typeof argument.name === "function"
854
+ ? argument.name()
855
+ : typeof argument.name === "string"
856
+ ? argument.name
857
+ : "argument";
858
+ const description = typeof argument.description === "string" && argument.description.trim().length > 0
859
+ ? argument.description.trim()
860
+ : null;
861
+ return {
862
+ name: rawName.trim(),
863
+ required: argument.required === true,
864
+ variadic: argument.variadic === true,
865
+ description,
866
+ };
867
+ });
868
+ }
869
+ function buildHelpSubcommandSummaries(command) {
870
+ return command.commands
871
+ .map((entry) => ({
872
+ name: entry.name().trim(),
873
+ aliases: commandAliases(entry),
874
+ description: entry.description().trim(),
875
+ }))
876
+ .sort((left, right) => left.name.localeCompare(right.name));
877
+ }
878
+ function buildJsonHelpPayload(rootProgram, targetCommand, argv, requestedPath) {
879
+ const detailMode = resolveHelpDetailMode(argv);
880
+ const resolvedPath = normalizeHelpCommandPath(getCommandPath(targetCommand));
881
+ const commandPath = resolvedPath.length > 0 ? resolvedPath : undefined;
882
+ const fallbackNarrative = resolveHelpNarrative(commandPath, detailMode);
883
+ const extensionDescriptor = commandPath ? activeRuntimeExtensionCommandDescriptors.get(commandPath) : undefined;
884
+ const extensionExamples = extensionDescriptor?.examples ?? [];
885
+ const extensionFailureHints = extensionDescriptor?.failure_hints ?? [];
886
+ const narrative = extensionDescriptor
887
+ ? {
888
+ intent: extensionDescriptor.intent ?? extensionDescriptor.description ?? fallbackNarrative.intent,
889
+ examples: detailMode === "detailed"
890
+ ? extensionExamples.length > 0
891
+ ? [...extensionExamples]
892
+ : [...fallbackNarrative.examples]
893
+ : extensionExamples.length > 0
894
+ ? [extensionExamples[0]]
895
+ : [...fallbackNarrative.examples],
896
+ tips: detailMode === "detailed"
897
+ ? extensionFailureHints.length > 0
898
+ ? [...extensionFailureHints]
899
+ : [...fallbackNarrative.tips]
900
+ : [],
901
+ detail_mode: detailMode,
902
+ }
903
+ : fallbackNarrative;
904
+ const optionSummaries = mergeHelpOptionSummaries(buildHelpOptionSummaries(targetCommand), buildDynamicExtensionHelpOptionSummaries(extensionDescriptor));
905
+ const subcommands = buildHelpSubcommandSummaries(targetCommand);
906
+ return {
907
+ format: "pm_help_v1",
908
+ detail_mode: detailMode,
909
+ root_command: rootProgram.name(),
910
+ requested_path: requestedPath,
911
+ resolved_path: resolvedPath.length > 0 ? resolvedPath : rootProgram.name(),
912
+ description: targetCommand.description(),
913
+ usage: targetCommand.usage(),
914
+ intent: narrative.intent,
915
+ examples: narrative.examples,
916
+ tips: narrative.tips,
917
+ arguments: buildHelpArgumentSummaries(targetCommand),
918
+ options: optionSummaries,
919
+ subcommands,
920
+ has_subcommands: subcommands.length > 0,
191
921
  };
192
922
  }
923
+ async function maybeRenderBootstrapJsonHelp(rootProgram, argv) {
924
+ const bootstrapGlobal = parseBootstrapGlobalOptions(argv);
925
+ if (!bootstrapGlobal.json) {
926
+ return false;
927
+ }
928
+ const helpRequest = parseBootstrapHelpRequest(argv);
929
+ if (!helpRequest.requested) {
930
+ return false;
931
+ }
932
+ const targetCommand = resolveCommandFromPathTokens(rootProgram, helpRequest.commandPathTokens);
933
+ if (!targetCommand) {
934
+ if (!bootstrapGlobal.quiet) {
935
+ const unknownMessage = `unknown command '${helpRequest.commandPathTokens.join(" ")}'`;
936
+ const envelope = formatCommanderErrorForJson(unknownMessage, "help", BUILTIN_TYPE_HELP_VALUES, EXIT_CODE.USAGE, buildUnknownCommandGuidanceFromRuntime(unknownMessage, rootProgram));
937
+ printError(JSON.stringify(envelope, null, 2));
938
+ }
939
+ process.exitCode = EXIT_CODE.USAGE;
940
+ return true;
941
+ }
942
+ if (!bootstrapGlobal.quiet) {
943
+ const payload = buildJsonHelpPayload(rootProgram, targetCommand, argv, helpRequest.commandPathTokens);
944
+ writeStdout(`${JSON.stringify(payload, null, 2)}\n`);
945
+ }
946
+ process.exitCode = EXIT_CODE.SUCCESS;
947
+ return true;
948
+ }
949
+ function parseBootstrapTypeValue(argv) {
950
+ for (let index = 0; index < argv.length; index += 1) {
951
+ const token = argv[index];
952
+ if (token === "--type") {
953
+ const candidate = argv[index + 1];
954
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
955
+ return candidate.trim();
956
+ }
957
+ continue;
958
+ }
959
+ if (token.startsWith("--type=")) {
960
+ const candidate = token.slice("--type=".length).trim();
961
+ if (candidate.length > 0) {
962
+ return candidate;
963
+ }
964
+ }
965
+ }
966
+ return undefined;
967
+ }
968
+ function buildCreateUpdatePolicyHelpText(commandName, typeRegistry, argv) {
969
+ const selectedTypeRaw = parseBootstrapTypeValue(argv);
970
+ if (!selectedTypeRaw) {
971
+ const allowed = typeRegistry.types.join("|");
972
+ const lines = [
973
+ "",
974
+ "Type-aware option policies:",
975
+ " pass --type <value> with --help to render required/disabled/hidden option policy details for that type.",
976
+ ` active type values: ${allowed}`,
977
+ ];
978
+ if (commandName === "create") {
979
+ lines.push(" scheduling shortcut: use --schedule-preset lightweight for Reminder/Meeting/Event minimal create flows.");
980
+ }
981
+ return lines.join("\n");
982
+ }
983
+ const typeDefinition = resolveTypeDefinition(selectedTypeRaw, typeRegistry);
984
+ if (!typeDefinition) {
985
+ const allowed = typeRegistry.types.join("|");
986
+ return [
987
+ "",
988
+ `Type-aware option policies: type "${selectedTypeRaw}" is not in the active registry.`,
989
+ ` active type values: ${allowed}`,
990
+ ].join("\n");
991
+ }
992
+ const baseRequired = commandName === "create"
993
+ ? new Set(["title", "description", "type", ...typeDefinition.required_create_fields, ...typeDefinition.required_create_repeatables])
994
+ : new Set();
995
+ const policyState = resolveCommandOptionPolicyState(typeDefinition, commandName, baseRequired);
996
+ const toFlags = (options) => options.length > 0 ? options.map((option) => commandOptionFlagLabel(commandName, option)).join(", ") : "none";
997
+ const lines = [
998
+ "",
999
+ `Type-aware option policies for ${typeDefinition.name}:`,
1000
+ ` required: ${toFlags(policyState.required)}`,
1001
+ ` disabled: ${toFlags(policyState.disabled)}`,
1002
+ ` hidden: ${toFlags(policyState.hidden)}`,
1003
+ ];
1004
+ if (commandName === "create" && ["Reminder", "Meeting", "Event"].includes(typeDefinition.name)) {
1005
+ lines.push(" schedule preset: --schedule-preset lightweight switches schedule artifacts to progressive required-option policy.");
1006
+ lines.push(" strict parity remains available via --create-mode strict.");
1007
+ }
1008
+ if (typeDefinition.options.length === 0) {
1009
+ lines.push(" type options: none");
1010
+ }
1011
+ else {
1012
+ lines.push(" type options:");
1013
+ for (const option of typeDefinition.options) {
1014
+ const requiredLabel = option.required ? " (required)" : "";
1015
+ const aliases = option.aliases ?? [];
1016
+ lines.push(` - ${option.key}${requiredLabel}`);
1017
+ lines.push(` values: ${option.values.length > 0 ? option.values.join("|") : "any non-empty string"}`);
1018
+ lines.push(` aliases: ${aliases.length > 0 ? aliases.join("|") : "none"}`);
1019
+ if (option.description && option.description.trim().length > 0) {
1020
+ lines.push(` description: ${option.description.trim()}`);
1021
+ }
1022
+ }
1023
+ }
1024
+ if (policyState.errors.length > 0) {
1025
+ lines.push(` config errors: ${policyState.errors.join("; ")}`);
1026
+ }
1027
+ return lines.join("\n");
1028
+ }
1029
+ function attachCreateUpdatePolicyHelpText(rootProgram, typeRegistry, argv) {
1030
+ const bootstrapCommand = parseBootstrapCommandName(argv);
1031
+ if (bootstrapCommand !== "create" && bootstrapCommand !== "update") {
1032
+ return;
1033
+ }
1034
+ const command = findDirectChildCommand(rootProgram, bootstrapCommand);
1035
+ if (!command) {
1036
+ return;
1037
+ }
1038
+ command.addHelpText("after", buildCreateUpdatePolicyHelpText(bootstrapCommand, typeRegistry, argv));
1039
+ }
193
1040
  let activeExtensionHookContext = null;
1041
+ let activeTelemetryCommandContext = null;
1042
+ let runtimeExtensionSnapshotCache = null;
1043
+ let activeRuntimeExtensionCommandDescriptors = new Map();
194
1044
  function formatHookWarnings(warnings) {
195
1045
  return warnings.join(",");
196
1046
  }
@@ -273,6 +1123,8 @@ function decideWriteGate(commandPath, options) {
273
1123
  forceRequested,
274
1124
  };
275
1125
  case "comments":
1126
+ case "notes":
1127
+ case "learnings":
276
1128
  return {
277
1129
  isMutation: typeof options.add === "string",
278
1130
  forceCapable: true,
@@ -311,11 +1163,34 @@ function enforceMandatoryMigrationWriteGate(commandPath, options, blockers) {
311
1163
  : "This command path does not support --force bypass.";
312
1164
  throw new PmCliError(`Write command "${commandPath}" blocked by unresolved mandatory extension migrations (${codes.join(",")}). ${forceGuidance}`, EXIT_CODE.CONFLICT);
313
1165
  }
314
- function describeUnknownError(error) {
315
- if (error instanceof PmCliError) {
316
- return error.message;
1166
+ async function enforceItemFormatWriteGateAndPreflightMigration(commandPath, options, pmRoot, decision) {
1167
+ const writeGate = decideWriteGate(commandPath, options);
1168
+ if (!writeGate.isMutation) {
1169
+ return;
317
1170
  }
318
- if (error instanceof Error) {
1171
+ if (!decision.enforce_item_format_gate && !decision.run_preflight_item_format_sync) {
1172
+ return;
1173
+ }
1174
+ if (!(await pathExists(getSettingsPath(pmRoot)))) {
1175
+ return;
1176
+ }
1177
+ const { settings, metadata, warnings } = await readSettingsWithMetadata(pmRoot);
1178
+ for (const warning of warnings) {
1179
+ printError(`warning:${warning}`);
1180
+ }
1181
+ if (decision.enforce_item_format_gate && !metadata.has_explicit_item_format) {
1182
+ await writeSettings(pmRoot, settings, "item_format:auto_select_default");
1183
+ }
1184
+ if (decision.run_preflight_item_format_sync) {
1185
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
1186
+ await migrateItemFilesToFormat(pmRoot, settings.item_format, "item_format:pre_mutation_sync", typeRegistry.type_to_folder, settings.schema);
1187
+ }
1188
+ }
1189
+ function describeUnknownError(error) {
1190
+ if (error instanceof PmCliError) {
1191
+ return error.message;
1192
+ }
1193
+ if (error instanceof Error) {
319
1194
  return error.message;
320
1195
  }
321
1196
  return "Unknown failure";
@@ -366,31 +1241,221 @@ async function invalidateSearchCachesForMutation(globalOptions, result) {
366
1241
  }
367
1242
  }
368
1243
  async function runAndClearAfterCommandHooks(outcome) {
1244
+ const telemetryRuntime = activeTelemetryCommandContext;
1245
+ activeTelemetryCommandContext = null;
1246
+ await finishTelemetryCommand(telemetryRuntime, {
1247
+ ok: outcome.ok,
1248
+ error: outcome.error,
1249
+ result: getActiveCommandResult(),
1250
+ });
369
1251
  const runtime = activeExtensionHookContext;
370
1252
  activeExtensionHookContext = null;
371
- clearActiveExtensionHooks();
372
1253
  if (!runtime) {
1254
+ clearActiveExtensionHooks();
373
1255
  return;
374
1256
  }
375
1257
  const hookWarnings = await runAfterCommandHooks(runtime.hooks, {
376
1258
  command: runtime.commandName,
377
1259
  args: runtime.commandArgs,
1260
+ options: { ...runtime.commandOptions },
1261
+ global: { ...runtime.globalOptions },
378
1262
  pm_root: runtime.pmRoot,
379
1263
  ok: outcome.ok,
380
1264
  error: outcome.error,
1265
+ result: getActiveCommandResult(),
381
1266
  });
1267
+ clearActiveExtensionHooks();
382
1268
  if (runtime.profileEnabled && hookWarnings.length > 0) {
383
1269
  printError(`profile:extensions hook_warnings=${formatHookWarnings(hookWarnings)}`);
384
1270
  }
385
1271
  }
386
- async function maybeLoadRuntimeExtensions(command) {
387
- const globalOptions = getGlobalOptions(command);
388
- if (globalOptions.noExtensions) {
389
- return null;
1272
+ function extractCommandScopedOptions(command, commandArgs, extensionFlagDefinitions = []) {
1273
+ const allOptions = command.optsWithGlobals();
1274
+ const scoped = { ...allOptions };
1275
+ delete scoped.json;
1276
+ delete scoped.quiet;
1277
+ delete scoped.path;
1278
+ delete scoped.noExtensions;
1279
+ delete scoped.extensions;
1280
+ delete scoped.profile;
1281
+ delete scoped.pager;
1282
+ const looseOptions = parseLooseCommandOptions(commandArgs);
1283
+ for (const [key, value] of Object.entries(looseOptions)) {
1284
+ if (scoped[key] === undefined) {
1285
+ scoped[key] = value;
1286
+ }
1287
+ }
1288
+ if (extensionFlagDefinitions.length > 0) {
1289
+ return coerceLooseCommandOptionsWithFlagDefinitions(scoped, extensionFlagDefinitions);
1290
+ }
1291
+ return scoped;
1292
+ }
1293
+ function collectExtensionFlagDefinitionsForCommand(registrations, commandPath) {
1294
+ const normalizedCommandPath = normalizeExtensionCommandPath(commandPath);
1295
+ if (normalizedCommandPath.length === 0) {
1296
+ return [];
1297
+ }
1298
+ return registrations.flags
1299
+ .filter((entry) => normalizeExtensionCommandPath(entry.target_command) === normalizedCommandPath)
1300
+ .flatMap((entry) => entry.flags);
1301
+ }
1302
+ const RUNTIME_FIELD_COMMAND_BY_COMMAND_PATH = {
1303
+ create: "create",
1304
+ update: "update",
1305
+ "update-many": "update_many",
1306
+ list: "list",
1307
+ "list-all": "list",
1308
+ "list-draft": "list",
1309
+ "list-open": "list",
1310
+ "list-in-progress": "list",
1311
+ "list-blocked": "list",
1312
+ "list-closed": "list",
1313
+ "list-canceled": "list",
1314
+ search: "search",
1315
+ calendar: "calendar",
1316
+ context: "context",
1317
+ "templates save": "create",
1318
+ };
1319
+ const runtimeFieldLooseFlagDefinitionCache = new Map();
1320
+ function toLooseFieldDefinitionType(fieldType) {
1321
+ if (fieldType === "number") {
1322
+ return "number";
1323
+ }
1324
+ if (fieldType === "boolean") {
1325
+ return "boolean";
1326
+ }
1327
+ return "string";
1328
+ }
1329
+ function commandHasLongOption(command, longFlag) {
1330
+ return command.options.some((option) => option.long === longFlag);
1331
+ }
1332
+ function commandHasShortOption(command, shortFlag) {
1333
+ return command.options.some((option) => option.short === shortFlag);
1334
+ }
1335
+ function addRuntimeFieldOption(command, flagToken, description, repeatable) {
1336
+ const normalizedToken = flagToken.trim();
1337
+ if (!normalizedToken) {
1338
+ return;
1339
+ }
1340
+ const helpText = description.length > 0 ? description : `Runtime schema field (${flagToken})`;
1341
+ if (normalizedToken.startsWith("-") && !normalizedToken.startsWith("--")) {
1342
+ if (commandHasShortOption(command, normalizedToken)) {
1343
+ return;
1344
+ }
1345
+ if (repeatable) {
1346
+ command.option(`${normalizedToken} <value>`, `${helpText} (repeatable)`, collect);
1347
+ return;
1348
+ }
1349
+ command.option(`${normalizedToken} <value>`, helpText);
1350
+ return;
1351
+ }
1352
+ const longFlag = normalizedToken.startsWith("--") ? normalizedToken : `--${normalizedToken}`;
1353
+ if (commandHasLongOption(command, longFlag)) {
1354
+ return;
1355
+ }
1356
+ if (repeatable) {
1357
+ command.option(`${longFlag} <value>`, `${helpText} (repeatable)`, collect);
1358
+ return;
1359
+ }
1360
+ command.option(`${longFlag} <value>`, helpText);
1361
+ }
1362
+ async function collectRuntimeFieldLooseFlagDefinitionsForCommand(commandPath, pmRoot) {
1363
+ const runtimeCommand = RUNTIME_FIELD_COMMAND_BY_COMMAND_PATH[commandPath];
1364
+ if (!runtimeCommand) {
1365
+ return [];
1366
+ }
1367
+ const cacheKey = `${pmRoot}:${runtimeCommand}`;
1368
+ const cached = runtimeFieldLooseFlagDefinitionCache.get(cacheKey);
1369
+ if (cached) {
1370
+ return cached;
1371
+ }
1372
+ if (!(await pathExists(getSettingsPath(pmRoot)))) {
1373
+ runtimeFieldLooseFlagDefinitionCache.set(cacheKey, []);
1374
+ return [];
1375
+ }
1376
+ const settings = await readSettings(pmRoot);
1377
+ const fieldRegistry = resolveRuntimeFieldRegistry(settings.schema);
1378
+ const definitions = (fieldRegistry.command_to_fields.get(runtimeCommand) ?? []).flatMap((field) => {
1379
+ const flagTokens = [field.cli_flag, ...field.cli_aliases];
1380
+ return flagTokens.map((token) => ({
1381
+ long: `--${token}`,
1382
+ type: toLooseFieldDefinitionType(field.type),
1383
+ value_type: toLooseFieldDefinitionType(field.type),
1384
+ }));
1385
+ });
1386
+ runtimeFieldLooseFlagDefinitionCache.set(cacheKey, definitions);
1387
+ return definitions;
1388
+ }
1389
+ async function registerRuntimeSchemaFieldFlags(rootProgram) {
1390
+ const bootstrapGlobalOptions = parseBootstrapGlobalOptions(process.argv.slice(2));
1391
+ const pmRoot = resolvePmRoot(process.cwd(), bootstrapGlobalOptions.path);
1392
+ if (!(await pathExists(getSettingsPath(pmRoot)))) {
1393
+ return;
1394
+ }
1395
+ const settings = await readSettings(pmRoot);
1396
+ const fieldRegistry = resolveRuntimeFieldRegistry(settings.schema);
1397
+ const mappings = [
1398
+ { path: "create", command: "create" },
1399
+ { path: "update", command: "update" },
1400
+ { path: "update-many", command: "update_many" },
1401
+ { path: "list", command: "list" },
1402
+ { path: "list-all", command: "list" },
1403
+ { path: "list-draft", command: "list" },
1404
+ { path: "list-open", command: "list" },
1405
+ { path: "list-in-progress", command: "list" },
1406
+ { path: "list-blocked", command: "list" },
1407
+ { path: "list-closed", command: "list" },
1408
+ { path: "list-canceled", command: "list" },
1409
+ { path: "search", command: "search" },
1410
+ { path: "calendar", command: "calendar" },
1411
+ { path: "context", command: "context" },
1412
+ { path: "templates save", command: "create" },
1413
+ ];
1414
+ for (const mapping of mappings) {
1415
+ const command = findCommandByPath(rootProgram, mapping.path.split(" "));
1416
+ if (!command) {
1417
+ continue;
1418
+ }
1419
+ for (const field of fieldRegistry.command_to_fields.get(mapping.command) ?? []) {
1420
+ const description = field.description ?? "";
1421
+ addRuntimeFieldOption(command, field.cli_flag, description, field.repeatable);
1422
+ for (const alias of field.cli_aliases) {
1423
+ addRuntimeFieldOption(command, alias, `Alias for --${field.cli_flag}`, field.repeatable);
1424
+ }
1425
+ }
1426
+ }
1427
+ }
1428
+ function defaultPreflightDecision() {
1429
+ return {
1430
+ enforce_item_format_gate: true,
1431
+ run_preflight_item_format_sync: true,
1432
+ run_extension_migrations: true,
1433
+ enforce_mandatory_migration_gate: true,
1434
+ };
1435
+ }
1436
+ function buildRuntimeExtensionSnapshotCacheKey(pmRoot) {
1437
+ return `pm-root:${pmRoot}`;
1438
+ }
1439
+ function emitExtensionProfile(globalOptions, snapshot) {
1440
+ if (!globalOptions.profile) {
1441
+ return;
1442
+ }
1443
+ printError(`profile:extensions loaded=${snapshot.loadedCount} failed=${snapshot.loadFailedCount} warnings=${snapshot.loadWarnings.length} activation_failed=${snapshot.activationFailedCount} hook_counts=before:${snapshot.hooks.beforeCommand.length}|after:${snapshot.hooks.afterCommand.length}|write:${snapshot.hooks.onWrite.length}|read:${snapshot.hooks.onRead.length}|index:${snapshot.hooks.onIndex.length} command_overrides=${snapshot.commands.overrides.length} command_handlers=${snapshot.commands.handlers.length} parser_overrides=${snapshot.parsers.overrides.length} preflight_overrides=${snapshot.preflight.overrides.length} service_overrides=${snapshot.services.overrides.length} renderer_overrides=${snapshot.renderers.overrides.length}`);
1444
+ if (snapshot.activationWarnings.length > 0) {
1445
+ printError(`profile:extensions activation_warnings=${formatHookWarnings(snapshot.activationWarnings)}`);
1446
+ }
1447
+ }
1448
+ async function loadRuntimeExtensionSnapshot(pmRoot) {
1449
+ const cacheKey = buildRuntimeExtensionSnapshotCacheKey(pmRoot);
1450
+ if (runtimeExtensionSnapshotCache?.key === cacheKey) {
1451
+ return runtimeExtensionSnapshotCache.snapshot;
390
1452
  }
391
- const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
392
1453
  const settingsPath = getSettingsPath(pmRoot);
393
1454
  if (!(await pathExists(settingsPath))) {
1455
+ runtimeExtensionSnapshotCache = {
1456
+ key: cacheKey,
1457
+ snapshot: null,
1458
+ };
394
1459
  return null;
395
1460
  }
396
1461
  try {
@@ -399,61 +1464,143 @@ async function maybeLoadRuntimeExtensions(command) {
399
1464
  pmRoot,
400
1465
  settings,
401
1466
  cwd: process.cwd(),
402
- noExtensions: globalOptions.noExtensions,
1467
+ noExtensions: false,
403
1468
  });
404
- const loadedWithBuiltins = [...getEnabledBuiltInExtensions(settings), ...loadResult.loaded];
405
1469
  const activationResult = await activateExtensions({
406
1470
  ...loadResult,
407
- loaded: loadedWithBuiltins,
1471
+ loaded: loadResult.loaded,
408
1472
  });
409
- if (globalOptions.profile) {
410
- printError(`profile:extensions loaded=${loadedWithBuiltins.length} failed=${loadResult.failed.length} warnings=${loadResult.warnings.length} activation_failed=${activationResult.failed.length} hook_counts=before:${activationResult.hook_counts.before_command}|after:${activationResult.hook_counts.after_command}|write:${activationResult.hook_counts.on_write}|read:${activationResult.hook_counts.on_read}|index:${activationResult.hook_counts.on_index} command_overrides=${activationResult.command_override_count} command_handlers=${activationResult.command_handler_count} renderer_overrides=${activationResult.renderer_override_count}`);
411
- if (activationResult.warnings.length > 0) {
412
- printError(`profile:extensions activation_warnings=${formatHookWarnings(activationResult.warnings)}`);
413
- }
414
- }
415
- const migrationBlockers = collectMandatoryMigrationBlockers(activationResult.registrations.migrations);
416
- return {
1473
+ const commandHandlers = [...new Set(activationResult.commands.handlers.map((entry) => normalizeExtensionCommandPath(entry.command)))]
1474
+ .filter((entry) => entry.length > 0)
1475
+ .sort((left, right) => left.localeCompare(right));
1476
+ const commandFlagHelp = collectDynamicExtensionFlagHelpByCommand(activationResult.registrations.flags);
1477
+ const commandDescriptors = collectExtensionCommandHelpDescriptors(commandHandlers, activationResult.registrations.commands, activationResult.registrations.flags);
1478
+ const snapshot = {
417
1479
  hooks: activationResult.hooks,
418
1480
  commands: activationResult.commands,
1481
+ parsers: activationResult.parsers,
1482
+ preflight: activationResult.preflight,
1483
+ services: activationResult.services,
419
1484
  renderers: activationResult.renderers,
1485
+ registrations: activationResult.registrations,
420
1486
  pmRoot,
421
- migrationBlockers,
1487
+ settings,
1488
+ commandHandlers,
1489
+ commandFlagHelp,
1490
+ commandDescriptors,
1491
+ loadWarnings: [...loadResult.warnings],
1492
+ activationWarnings: [...activationResult.warnings],
1493
+ loadedCount: loadResult.loaded.length,
1494
+ loadFailedCount: loadResult.failed.length,
1495
+ activationFailedCount: activationResult.failed.length,
422
1496
  };
1497
+ runtimeExtensionSnapshotCache = {
1498
+ key: cacheKey,
1499
+ snapshot,
1500
+ };
1501
+ return snapshot;
423
1502
  }
424
- catch (error) {
425
- if (globalOptions.profile) {
426
- const message = error instanceof Error ? error.message : String(error);
427
- printError(`profile:extensions load_error=${message}`);
428
- }
429
- return {
430
- hooks: createEmptyExtensionHookRegistry(),
431
- commands: createEmptyExtensionCommandRegistry(),
432
- renderers: createEmptyExtensionRendererRegistry(),
433
- pmRoot,
434
- migrationBlockers: [],
1503
+ catch {
1504
+ runtimeExtensionSnapshotCache = {
1505
+ key: cacheKey,
1506
+ snapshot: null,
435
1507
  };
1508
+ return null;
436
1509
  }
437
1510
  }
1511
+ async function maybeLoadRuntimeExtensions(command) {
1512
+ const globalOptions = getGlobalOptions(command);
1513
+ if (globalOptions.noExtensions) {
1514
+ return null;
1515
+ }
1516
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
1517
+ const snapshot = await loadRuntimeExtensionSnapshot(pmRoot);
1518
+ if (!snapshot) {
1519
+ return null;
1520
+ }
1521
+ emitExtensionProfile(globalOptions, snapshot);
1522
+ return {
1523
+ hooks: snapshot.hooks,
1524
+ commands: snapshot.commands,
1525
+ parsers: snapshot.parsers,
1526
+ preflight: snapshot.preflight,
1527
+ services: snapshot.services,
1528
+ renderers: snapshot.renderers,
1529
+ registrations: snapshot.registrations,
1530
+ pmRoot,
1531
+ };
1532
+ }
1533
+ async function executeRegisteredRuntimeMigrations(migrations, pmRoot) {
1534
+ const warnings = [];
1535
+ for (let index = 0; index < migrations.length; index += 1) {
1536
+ const migration = migrations[index];
1537
+ const status = resolveNormalizedMigrationStatus(migration.definition);
1538
+ if (status === "applied") {
1539
+ continue;
1540
+ }
1541
+ const runtimeDefinition = migration.runtime_definition ?? migration.definition;
1542
+ const run = runtimeDefinition.run;
1543
+ if (typeof run !== "function") {
1544
+ continue;
1545
+ }
1546
+ const migrationId = resolveMigrationId(migration.definition, index);
1547
+ try {
1548
+ await Promise.resolve(run({
1549
+ id: migrationId,
1550
+ command: "migration",
1551
+ layer: migration.layer,
1552
+ extension: migration.name,
1553
+ pm_root: pmRoot,
1554
+ status,
1555
+ }));
1556
+ migration.definition.status = "applied";
1557
+ delete migration.definition.reason;
1558
+ delete migration.definition.error;
1559
+ delete migration.definition.message;
1560
+ }
1561
+ catch (error) {
1562
+ migration.definition.status = "failed";
1563
+ migration.definition.reason = describeUnknownError(error);
1564
+ warnings.push(`extension_migration_failed:${migration.layer}:${migration.name}:${migrationId}`);
1565
+ }
1566
+ }
1567
+ return warnings;
1568
+ }
438
1569
  async function runRequiredExtensionCommand(command, options, globalOptions) {
439
1570
  const commandPath = getCommandPath(command);
440
- const commandArgs = command.args.map(String);
1571
+ let commandArgs = command.args.map(String);
1572
+ let commandOptions = { ...options };
1573
+ let resolvedGlobalOptions = { ...globalOptions };
441
1574
  const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
1575
+ const parserOverride = await runActiveParserOverride({
1576
+ command: commandPath,
1577
+ args: commandArgs,
1578
+ options: commandOptions,
1579
+ global: resolvedGlobalOptions,
1580
+ pm_root: pmRoot,
1581
+ });
1582
+ if (globalOptions.profile && parserOverride.warnings.length > 0) {
1583
+ printError(`profile:extensions parser_warnings=${formatHookWarnings(parserOverride.warnings)}`);
1584
+ }
1585
+ commandArgs = parserOverride.context.args;
1586
+ commandOptions = parserOverride.context.options;
1587
+ resolvedGlobalOptions = parserOverride.context.global;
1588
+ setActiveCommandResult(undefined);
442
1589
  setActiveCommandContext({
443
1590
  command: commandPath,
444
1591
  args: commandArgs,
445
- options: { ...options },
446
- global: { ...globalOptions },
1592
+ options: { ...commandOptions },
1593
+ global: { ...resolvedGlobalOptions },
447
1594
  pm_root: pmRoot,
448
1595
  });
449
1596
  const extensionCommandResult = await runActiveCommandHandler({
450
1597
  command: commandPath,
451
1598
  args: commandArgs,
452
- options,
453
- global: globalOptions,
1599
+ options: commandOptions,
1600
+ global: resolvedGlobalOptions,
454
1601
  pm_root: pmRoot,
455
1602
  });
456
- if (globalOptions.profile && extensionCommandResult.warnings.length > 0) {
1603
+ if (resolvedGlobalOptions.profile && extensionCommandResult.warnings.length > 0) {
457
1604
  printError(`profile:extensions command_handler_warnings=${formatHookWarnings(extensionCommandResult.warnings)}`);
458
1605
  }
459
1606
  if (!extensionCommandResult.handled) {
@@ -463,71 +1610,159 @@ async function runRequiredExtensionCommand(command, options, globalOptions) {
463
1610
  }
464
1611
  throw new PmCliError(`Command "${commandPath}" is provided by extensions and is not currently available.`, EXIT_CODE.NOT_FOUND);
465
1612
  }
1613
+ setActiveCommandResult(extensionCommandResult.result);
466
1614
  return extensionCommandResult.result;
467
1615
  }
1616
+ const WRAPPED_ACTION_HANDLER = Symbol("pm.wrappedActionHandler");
1617
+ function wrapProgramActionsForExtensionHandlers(rootProgram) {
1618
+ const visit = (entry) => {
1619
+ const actionEntry = entry;
1620
+ if (typeof actionEntry._actionHandler === "function" && actionEntry[WRAPPED_ACTION_HANDLER] !== true) {
1621
+ const originalAction = actionEntry._actionHandler;
1622
+ actionEntry._actionHandler = async function wrappedActionHandler(...actionArgs) {
1623
+ const possibleCommand = actionArgs[actionArgs.length - 1];
1624
+ const actionCommand = possibleCommand instanceof Command ? possibleCommand : entry;
1625
+ const startedAt = Date.now();
1626
+ clearResolvedGlobalOptions(actionCommand);
1627
+ let globalOptions = getGlobalOptions(actionCommand);
1628
+ const commandPath = getCommandPath(actionCommand);
1629
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
1630
+ let commandArgs = actionCommand.args.map(String);
1631
+ const activeRegistrations = getActiveExtensionRegistrations();
1632
+ const extensionFlagDefinitions = activeRegistrations
1633
+ ? collectExtensionFlagDefinitionsForCommand(activeRegistrations, commandPath)
1634
+ : [];
1635
+ const runtimeFieldFlagDefinitions = await collectRuntimeFieldLooseFlagDefinitionsForCommand(commandPath, pmRoot);
1636
+ let commandOptions = extractCommandScopedOptions(actionCommand, commandArgs, [
1637
+ ...extensionFlagDefinitions,
1638
+ ...runtimeFieldFlagDefinitions,
1639
+ ]);
1640
+ const parserOverride = await runActiveParserOverride({
1641
+ command: commandPath,
1642
+ args: commandArgs,
1643
+ options: commandOptions,
1644
+ global: globalOptions,
1645
+ pm_root: pmRoot,
1646
+ });
1647
+ if (globalOptions.profile && parserOverride.warnings.length > 0) {
1648
+ printError(`profile:extensions parser_warnings=${formatHookWarnings(parserOverride.warnings)}`);
1649
+ }
1650
+ commandArgs = parserOverride.context.args;
1651
+ commandOptions = parserOverride.context.options;
1652
+ globalOptions = parserOverride.context.global;
1653
+ globalOptions = await applyDefaultOutputFormat(globalOptions);
1654
+ setResolvedGlobalOptions(actionCommand, globalOptions);
1655
+ actionCommand.args = [...commandArgs];
1656
+ if ("_processArguments" in actionCommand && typeof actionCommand._processArguments === "function") {
1657
+ actionCommand._processArguments();
1658
+ }
1659
+ if (actionArgs.length > 0 && Array.isArray(actionArgs[0])) {
1660
+ actionArgs[0] = [...actionCommand.processedArgs];
1661
+ }
1662
+ for (const [key, value] of Object.entries(commandOptions)) {
1663
+ actionCommand.setOptionValueWithSource(key, value, "cli");
1664
+ }
1665
+ setActiveCommandResult(undefined);
1666
+ setActiveCommandContext({
1667
+ command: commandPath,
1668
+ args: commandArgs,
1669
+ options: { ...commandOptions },
1670
+ global: { ...globalOptions },
1671
+ pm_root: pmRoot,
1672
+ });
1673
+ const extensionCommandResult = await runActiveCommandHandler({
1674
+ command: commandPath,
1675
+ args: commandArgs,
1676
+ options: commandOptions,
1677
+ global: globalOptions,
1678
+ pm_root: pmRoot,
1679
+ });
1680
+ if (globalOptions.profile && extensionCommandResult.warnings.length > 0) {
1681
+ printError(`profile:extensions command_handler_warnings=${formatHookWarnings(extensionCommandResult.warnings)}`);
1682
+ }
1683
+ if (extensionCommandResult.handled) {
1684
+ setActiveCommandResult(extensionCommandResult.result);
1685
+ printResult(extensionCommandResult.result, globalOptions);
1686
+ if (globalOptions.profile) {
1687
+ printError(`profile:command=${commandPath} took_ms=${Date.now() - startedAt}`);
1688
+ }
1689
+ return;
1690
+ }
1691
+ return await originalAction.apply(this, actionArgs);
1692
+ };
1693
+ actionEntry[WRAPPED_ACTION_HANDLER] = true;
1694
+ }
1695
+ for (const child of entry.commands) {
1696
+ visit(child);
1697
+ }
1698
+ };
1699
+ visit(rootProgram);
1700
+ }
468
1701
  async function registerDynamicExtensionCommandPaths(rootProgram) {
469
1702
  const bootstrapGlobalOptions = parseBootstrapGlobalOptions(process.argv.slice(2));
470
1703
  if (bootstrapGlobalOptions.noExtensions) {
1704
+ activeRuntimeExtensionCommandDescriptors = new Map();
1705
+ setActiveExtensionServices({ overrides: [] });
471
1706
  return;
472
1707
  }
473
1708
  const pmRoot = resolvePmRoot(process.cwd(), bootstrapGlobalOptions.path);
474
- const settingsPath = getSettingsPath(pmRoot);
475
- if (!(await pathExists(settingsPath))) {
1709
+ const snapshot = await loadRuntimeExtensionSnapshot(pmRoot);
1710
+ if (!snapshot) {
1711
+ activeRuntimeExtensionCommandDescriptors = new Map();
1712
+ setActiveExtensionServices({ overrides: [] });
476
1713
  return;
477
1714
  }
478
- let settings;
479
- try {
480
- settings = await readSettings(pmRoot);
481
- }
482
- catch {
483
- return;
484
- }
485
- let commandHandlers;
486
- let commandFlagHelp;
487
- try {
488
- const loadResult = await loadExtensions({
489
- pmRoot,
490
- settings,
491
- cwd: process.cwd(),
492
- noExtensions: false,
493
- });
494
- const loadedWithBuiltins = [...getEnabledBuiltInExtensions(settings), ...loadResult.loaded];
495
- const activationResult = await activateExtensions({
496
- ...loadResult,
497
- loaded: loadedWithBuiltins,
498
- });
499
- commandHandlers = [...new Set(activationResult.commands.handlers.map((entry) => normalizeExtensionCommandPath(entry.command)))]
500
- .filter((entry) => entry.length > 0)
501
- .sort((left, right) => left.localeCompare(right));
502
- commandFlagHelp = collectDynamicExtensionFlagHelpByCommand(activationResult.registrations.flags);
503
- }
504
- catch {
505
- return;
506
- }
507
- for (const commandPath of commandHandlers) {
1715
+ // Ensure usage/help/error formatting overrides are available even when parse
1716
+ // errors occur before preAction hooks initialize full runtime extension state.
1717
+ setActiveExtensionServices(snapshot.services);
1718
+ activeRuntimeExtensionCommandDescriptors = new Map(snapshot.commandDescriptors);
1719
+ const typeRegistry = resolveItemTypeRegistry(snapshot.settings, snapshot.registrations);
1720
+ attachCreateUpdatePolicyHelpText(rootProgram, typeRegistry, process.argv.slice(2));
1721
+ const commandPaths = [...new Set([...snapshot.commandHandlers, ...snapshot.commandDescriptors.keys()])].sort((left, right) => left.localeCompare(right));
1722
+ for (const commandPath of commandPaths) {
508
1723
  const pathParts = commandPath.split(" ").filter((part) => part.length > 0);
509
1724
  if (pathParts.length === 0) {
510
1725
  continue;
511
1726
  }
512
- if (findCommandByPath(rootProgram, pathParts)) {
1727
+ const descriptor = snapshot.commandDescriptors.get(commandPath);
1728
+ const existingCommand = findCommandByPath(rootProgram, pathParts);
1729
+ const flagHelp = snapshot.commandFlagHelp.get(commandPath);
1730
+ const metadataHelp = descriptor ? buildDynamicExtensionCommandMetadataHelp(descriptor) : null;
1731
+ if (existingCommand) {
1732
+ if (flagHelp) {
1733
+ existingCommand.addHelpText("after", flagHelp);
1734
+ }
1735
+ if (metadataHelp) {
1736
+ existingCommand.addHelpText("after", metadataHelp);
1737
+ }
513
1738
  continue;
514
1739
  }
515
1740
  const dynamicCommand = ensureCommandPath(rootProgram, pathParts);
516
1741
  if (!dynamicCommand) {
517
1742
  continue;
518
1743
  }
519
- const flagHelp = commandFlagHelp.get(commandPath);
1744
+ if (descriptor?.description) {
1745
+ dynamicCommand.description(descriptor.description);
1746
+ }
1747
+ if (descriptor) {
1748
+ applyDynamicExtensionArguments(dynamicCommand, descriptor);
1749
+ }
520
1750
  if (flagHelp) {
521
1751
  dynamicCommand.addHelpText("after", flagHelp);
522
1752
  }
1753
+ if (metadataHelp) {
1754
+ dynamicCommand.addHelpText("after", metadataHelp);
1755
+ }
523
1756
  dynamicCommand
524
1757
  .allowUnknownOption(true)
525
1758
  .allowExcessArguments(true)
526
1759
  .action(async (_options, command) => {
527
1760
  const globalOptions = getGlobalOptions(command);
528
1761
  const startedAt = Date.now();
529
- const looseOptions = parseLooseCommandOptions(command.args.map(String));
530
- const result = await runRequiredExtensionCommand(command, looseOptions, globalOptions);
1762
+ const extensionFlagDefinitions = collectExtensionFlagDefinitionsForCommand(snapshot.registrations, commandPath);
1763
+ const scopedOptions = extractCommandScopedOptions(command, command.args.map(String), extensionFlagDefinitions);
1764
+ const result = await runRequiredExtensionCommand(command, scopedOptions, globalOptions);
1765
+ await invalidateSearchCachesForMutation(globalOptions, result);
531
1766
  printResult(result, globalOptions);
532
1767
  if (globalOptions.profile) {
533
1768
  printError(`profile:command=${commandPath} took_ms=${Date.now() - startedAt}`);
@@ -535,179 +1770,483 @@ async function registerDynamicExtensionCommandPaths(rootProgram) {
535
1770
  });
536
1771
  }
537
1772
  }
538
- function normalizeCreateOptions(commandOptions) {
539
- const estimatedMinutes = (typeof commandOptions.estimate === "string" ? commandOptions.estimate : undefined) ??
540
- (typeof commandOptions.estimatedMinutes === "string" ? commandOptions.estimatedMinutes : undefined) ??
541
- (typeof commandOptions.estimated_minutes === "string" ? commandOptions.estimated_minutes : undefined);
542
- if (estimatedMinutes === undefined) {
543
- throw new PmCliError("Missing required option --estimate/--estimated-minutes/--estimated_minutes", EXIT_CODE.USAGE);
1773
+ function normalizeCreateOptions(commandOptions, options = {}) {
1774
+ const readCreateString = (target) => readFirstStringFromCommanderOptions(commandOptions, CREATE_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
1775
+ target,
1776
+ keys: [target],
1777
+ });
1778
+ const readCreateList = (target) => readStringArrayFromCommanderOptions(commandOptions, CREATE_COMMANDER_REPEATABLE_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
1779
+ target,
1780
+ keys: [target],
1781
+ });
1782
+ const type = readCreateString("type");
1783
+ if (options.requireType !== false && type === undefined) {
1784
+ throw new PmCliError("Missing required option --type <value>", EXIT_CODE.USAGE);
544
1785
  }
545
- const requiredString = (key, display) => {
546
- const value = commandOptions[key];
547
- if (typeof value !== "string") {
548
- throw new PmCliError(`Missing required option ${display}`, EXIT_CODE.USAGE);
549
- }
550
- return value;
1786
+ const normalized = {
1787
+ title: readCreateString("title"),
1788
+ description: readCreateString("description"),
1789
+ type,
1790
+ template: readCreateString("template"),
1791
+ createMode: readCreateString("createMode"),
1792
+ schedulePreset: readCreateString("schedulePreset"),
1793
+ status: readCreateString("status"),
1794
+ priority: readCreateString("priority"),
1795
+ tags: readCreateString("tags"),
1796
+ body: readCreateString("body"),
1797
+ deadline: readCreateString("deadline"),
1798
+ estimatedMinutes: readCreateString("estimatedMinutes"),
1799
+ acceptanceCriteria: readCreateString("acceptanceCriteria"),
1800
+ definitionOfReady: readCreateString("definitionOfReady"),
1801
+ order: readCreateString("order"),
1802
+ rank: readCreateString("rank"),
1803
+ goal: readCreateString("goal"),
1804
+ objective: readCreateString("objective"),
1805
+ value: readCreateString("value"),
1806
+ impact: readCreateString("impact"),
1807
+ outcome: readCreateString("outcome"),
1808
+ whyNow: readCreateString("whyNow"),
1809
+ author: readCreateString("author"),
1810
+ message: readCreateString("message"),
1811
+ assignee: readCreateString("assignee"),
1812
+ parent: readCreateString("parent"),
1813
+ reviewer: readCreateString("reviewer"),
1814
+ risk: readCreateString("risk"),
1815
+ confidence: readCreateString("confidence"),
1816
+ sprint: readCreateString("sprint"),
1817
+ release: readCreateString("release"),
1818
+ blockedBy: readCreateString("blockedBy"),
1819
+ blockedReason: readCreateString("blockedReason"),
1820
+ unblockNote: readCreateString("unblockNote"),
1821
+ reporter: readCreateString("reporter"),
1822
+ severity: readCreateString("severity"),
1823
+ environment: readCreateString("environment"),
1824
+ reproSteps: readCreateString("reproSteps"),
1825
+ resolution: readCreateString("resolution"),
1826
+ expectedResult: readCreateString("expectedResult"),
1827
+ actualResult: readCreateString("actualResult"),
1828
+ affectedVersion: readCreateString("affectedVersion"),
1829
+ fixedVersion: readCreateString("fixedVersion"),
1830
+ component: readCreateString("component"),
1831
+ regression: readCreateString("regression"),
1832
+ customerImpact: readCreateString("customerImpact"),
1833
+ dep: readCreateList("dep"),
1834
+ comment: readCreateList("comment"),
1835
+ note: readCreateList("note"),
1836
+ learning: readCreateList("learning"),
1837
+ file: readCreateList("file"),
1838
+ test: readCreateList("test"),
1839
+ doc: readCreateList("doc"),
1840
+ reminder: readCreateList("reminder"),
1841
+ event: readCreateList("event"),
1842
+ typeOption: readCreateList("typeOption"),
1843
+ unset: readCreateList("unset"),
1844
+ clearDeps: commandOptions.clearDeps === true ? true : undefined,
1845
+ clearComments: commandOptions.clearComments === true ? true : undefined,
1846
+ clearNotes: commandOptions.clearNotes === true ? true : undefined,
1847
+ clearLearnings: commandOptions.clearLearnings === true ? true : undefined,
1848
+ clearFiles: commandOptions.clearFiles === true ? true : undefined,
1849
+ clearTests: commandOptions.clearTests === true ? true : undefined,
1850
+ clearDocs: commandOptions.clearDocs === true ? true : undefined,
1851
+ clearReminders: commandOptions.clearReminders === true ? true : undefined,
1852
+ clearEvents: commandOptions.clearEvents === true ? true : undefined,
1853
+ clearTypeOptions: commandOptions.clearTypeOptions === true ? true : undefined,
551
1854
  };
552
- const requiredRepeatable = (key, display) => {
553
- const value = commandOptions[key];
554
- if (!Array.isArray(value) || value.length === 0) {
555
- throw new PmCliError(`Missing required option ${display} (use 'none' for explicit empty)`, EXIT_CODE.USAGE);
1855
+ for (const [key, value] of Object.entries(commandOptions)) {
1856
+ if (Object.hasOwn(normalized, key)) {
1857
+ continue;
556
1858
  }
557
- return value;
558
- };
559
- return {
560
- title: requiredString("title", "--title"),
561
- description: requiredString("description", "--description"),
562
- type: requiredString("type", "--type"),
563
- status: requiredString("status", "--status"),
564
- priority: requiredString("priority", "--priority"),
565
- tags: requiredString("tags", "--tags"),
566
- body: requiredString("body", "--body"),
567
- deadline: requiredString("deadline", "--deadline"),
568
- estimatedMinutes,
569
- acceptanceCriteria: (typeof commandOptions.acceptanceCriteria === "string" ? commandOptions.acceptanceCriteria : undefined) ??
570
- (typeof commandOptions.acceptance_criteria === "string" ? commandOptions.acceptance_criteria : undefined) ??
571
- requiredString("ac", "--acceptance-criteria/--ac"),
572
- definitionOfReady: (typeof commandOptions.definitionOfReady === "string" ? commandOptions.definitionOfReady : undefined) ??
573
- (typeof commandOptions.definition_of_ready === "string" ? commandOptions.definition_of_ready : undefined),
574
- order: typeof commandOptions.order === "string" ? commandOptions.order : undefined,
575
- rank: typeof commandOptions.rank === "string" ? commandOptions.rank : undefined,
576
- goal: typeof commandOptions.goal === "string" ? commandOptions.goal : undefined,
577
- objective: typeof commandOptions.objective === "string" ? commandOptions.objective : undefined,
578
- value: typeof commandOptions.value === "string" ? commandOptions.value : undefined,
579
- impact: typeof commandOptions.impact === "string" ? commandOptions.impact : undefined,
580
- outcome: typeof commandOptions.outcome === "string" ? commandOptions.outcome : undefined,
581
- whyNow: (typeof commandOptions.whyNow === "string" ? commandOptions.whyNow : undefined) ??
582
- (typeof commandOptions.why_now === "string" ? commandOptions.why_now : undefined),
583
- author: requiredString("author", "--author"),
584
- message: requiredString("message", "--message"),
585
- assignee: requiredString("assignee", "--assignee"),
586
- parent: typeof commandOptions.parent === "string" ? commandOptions.parent : undefined,
587
- reviewer: typeof commandOptions.reviewer === "string" ? commandOptions.reviewer : undefined,
588
- risk: typeof commandOptions.risk === "string" ? commandOptions.risk : undefined,
589
- confidence: typeof commandOptions.confidence === "string" ? commandOptions.confidence : undefined,
590
- sprint: typeof commandOptions.sprint === "string" ? commandOptions.sprint : undefined,
591
- release: typeof commandOptions.release === "string" ? commandOptions.release : undefined,
592
- blockedBy: (typeof commandOptions.blockedBy === "string" ? commandOptions.blockedBy : undefined) ??
593
- (typeof commandOptions.blocked_by === "string" ? commandOptions.blocked_by : undefined),
594
- blockedReason: (typeof commandOptions.blockedReason === "string" ? commandOptions.blockedReason : undefined) ??
595
- (typeof commandOptions.blocked_reason === "string" ? commandOptions.blocked_reason : undefined),
596
- unblockNote: (typeof commandOptions.unblockNote === "string" ? commandOptions.unblockNote : undefined) ??
597
- (typeof commandOptions.unblock_note === "string" ? commandOptions.unblock_note : undefined),
598
- reporter: typeof commandOptions.reporter === "string" ? commandOptions.reporter : undefined,
599
- severity: typeof commandOptions.severity === "string" ? commandOptions.severity : undefined,
600
- environment: typeof commandOptions.environment === "string" ? commandOptions.environment : undefined,
601
- reproSteps: (typeof commandOptions.reproSteps === "string" ? commandOptions.reproSteps : undefined) ??
602
- (typeof commandOptions.repro_steps === "string" ? commandOptions.repro_steps : undefined),
603
- resolution: typeof commandOptions.resolution === "string" ? commandOptions.resolution : undefined,
604
- expectedResult: (typeof commandOptions.expectedResult === "string" ? commandOptions.expectedResult : undefined) ??
605
- (typeof commandOptions.expected_result === "string" ? commandOptions.expected_result : undefined),
606
- actualResult: (typeof commandOptions.actualResult === "string" ? commandOptions.actualResult : undefined) ??
607
- (typeof commandOptions.actual_result === "string" ? commandOptions.actual_result : undefined),
608
- affectedVersion: (typeof commandOptions.affectedVersion === "string" ? commandOptions.affectedVersion : undefined) ??
609
- (typeof commandOptions.affected_version === "string" ? commandOptions.affected_version : undefined),
610
- fixedVersion: (typeof commandOptions.fixedVersion === "string" ? commandOptions.fixedVersion : undefined) ??
611
- (typeof commandOptions.fixed_version === "string" ? commandOptions.fixed_version : undefined),
612
- component: typeof commandOptions.component === "string" ? commandOptions.component : undefined,
613
- regression: typeof commandOptions.regression === "string" ? commandOptions.regression : undefined,
614
- customerImpact: (typeof commandOptions.customerImpact === "string" ? commandOptions.customerImpact : undefined) ??
615
- (typeof commandOptions.customer_impact === "string" ? commandOptions.customer_impact : undefined),
616
- dep: requiredRepeatable("dep", "--dep"),
617
- comment: requiredRepeatable("comment", "--comment"),
618
- note: requiredRepeatable("note", "--note"),
619
- learning: requiredRepeatable("learning", "--learning"),
620
- file: requiredRepeatable("file", "--file"),
621
- test: requiredRepeatable("test", "--test"),
622
- doc: requiredRepeatable("doc", "--doc"),
623
- };
1859
+ normalized[key] = value;
1860
+ }
1861
+ return normalized;
624
1862
  }
625
1863
  function normalizeUpdateOptions(commandOptions) {
626
- const estimatedMinutes = (typeof commandOptions.estimate === "string" ? commandOptions.estimate : undefined) ??
627
- (typeof commandOptions.estimatedMinutes === "string" ? commandOptions.estimatedMinutes : undefined) ??
628
- (typeof commandOptions.estimated_minutes === "string" ? commandOptions.estimated_minutes : undefined);
629
- return {
630
- description: typeof commandOptions.description === "string" ? commandOptions.description : undefined,
631
- status: typeof commandOptions.status === "string" ? commandOptions.status : undefined,
632
- priority: typeof commandOptions.priority === "string" ? commandOptions.priority : undefined,
633
- type: typeof commandOptions.type === "string" ? commandOptions.type : undefined,
634
- tags: typeof commandOptions.tags === "string" ? commandOptions.tags : undefined,
635
- deadline: typeof commandOptions.deadline === "string" ? commandOptions.deadline : undefined,
636
- estimatedMinutes,
637
- acceptanceCriteria: (typeof commandOptions.acceptanceCriteria === "string" ? commandOptions.acceptanceCriteria : undefined) ??
638
- (typeof commandOptions.acceptance_criteria === "string" ? commandOptions.acceptance_criteria : undefined) ??
639
- (typeof commandOptions.ac === "string" ? commandOptions.ac : undefined),
640
- definitionOfReady: (typeof commandOptions.definitionOfReady === "string" ? commandOptions.definitionOfReady : undefined) ??
641
- (typeof commandOptions.definition_of_ready === "string" ? commandOptions.definition_of_ready : undefined),
642
- order: typeof commandOptions.order === "string" ? commandOptions.order : undefined,
643
- rank: typeof commandOptions.rank === "string" ? commandOptions.rank : undefined,
644
- goal: typeof commandOptions.goal === "string" ? commandOptions.goal : undefined,
645
- objective: typeof commandOptions.objective === "string" ? commandOptions.objective : undefined,
646
- value: typeof commandOptions.value === "string" ? commandOptions.value : undefined,
647
- impact: typeof commandOptions.impact === "string" ? commandOptions.impact : undefined,
648
- outcome: typeof commandOptions.outcome === "string" ? commandOptions.outcome : undefined,
649
- whyNow: (typeof commandOptions.whyNow === "string" ? commandOptions.whyNow : undefined) ??
650
- (typeof commandOptions.why_now === "string" ? commandOptions.why_now : undefined),
651
- author: typeof commandOptions.author === "string" ? commandOptions.author : undefined,
652
- message: typeof commandOptions.message === "string" ? commandOptions.message : undefined,
1864
+ const readUpdateString = (target) => readFirstStringFromCommanderOptions(commandOptions, UPDATE_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
1865
+ target,
1866
+ keys: [target],
1867
+ });
1868
+ const readUpdateList = (target) => readStringArrayFromCommanderOptions(commandOptions, UPDATE_COMMANDER_REPEATABLE_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
1869
+ target,
1870
+ keys: [target],
1871
+ });
1872
+ const normalized = {
1873
+ title: readUpdateString("title"),
1874
+ description: readUpdateString("description"),
1875
+ body: readUpdateString("body"),
1876
+ status: readUpdateString("status"),
1877
+ closeReason: readUpdateString("closeReason"),
1878
+ priority: readUpdateString("priority"),
1879
+ type: readUpdateString("type"),
1880
+ tags: readUpdateString("tags"),
1881
+ deadline: readUpdateString("deadline"),
1882
+ estimatedMinutes: readUpdateString("estimatedMinutes"),
1883
+ acceptanceCriteria: readUpdateString("acceptanceCriteria"),
1884
+ definitionOfReady: readUpdateString("definitionOfReady"),
1885
+ order: readUpdateString("order"),
1886
+ rank: readUpdateString("rank"),
1887
+ goal: readUpdateString("goal"),
1888
+ objective: readUpdateString("objective"),
1889
+ value: readUpdateString("value"),
1890
+ impact: readUpdateString("impact"),
1891
+ outcome: readUpdateString("outcome"),
1892
+ whyNow: readUpdateString("whyNow"),
1893
+ author: readUpdateString("author"),
1894
+ message: readUpdateString("message"),
653
1895
  force: Boolean(commandOptions.force),
654
- assignee: typeof commandOptions.assignee === "string" ? commandOptions.assignee : undefined,
655
- parent: typeof commandOptions.parent === "string" ? commandOptions.parent : undefined,
656
- reviewer: typeof commandOptions.reviewer === "string" ? commandOptions.reviewer : undefined,
657
- risk: typeof commandOptions.risk === "string" ? commandOptions.risk : undefined,
658
- confidence: typeof commandOptions.confidence === "string" ? commandOptions.confidence : undefined,
659
- sprint: typeof commandOptions.sprint === "string" ? commandOptions.sprint : undefined,
660
- release: typeof commandOptions.release === "string" ? commandOptions.release : undefined,
661
- blockedBy: (typeof commandOptions.blockedBy === "string" ? commandOptions.blockedBy : undefined) ??
662
- (typeof commandOptions.blocked_by === "string" ? commandOptions.blocked_by : undefined),
663
- blockedReason: (typeof commandOptions.blockedReason === "string" ? commandOptions.blockedReason : undefined) ??
664
- (typeof commandOptions.blocked_reason === "string" ? commandOptions.blocked_reason : undefined),
665
- unblockNote: (typeof commandOptions.unblockNote === "string" ? commandOptions.unblockNote : undefined) ??
666
- (typeof commandOptions.unblock_note === "string" ? commandOptions.unblock_note : undefined),
667
- reporter: typeof commandOptions.reporter === "string" ? commandOptions.reporter : undefined,
668
- severity: typeof commandOptions.severity === "string" ? commandOptions.severity : undefined,
669
- environment: typeof commandOptions.environment === "string" ? commandOptions.environment : undefined,
670
- reproSteps: (typeof commandOptions.reproSteps === "string" ? commandOptions.reproSteps : undefined) ??
671
- (typeof commandOptions.repro_steps === "string" ? commandOptions.repro_steps : undefined),
672
- resolution: typeof commandOptions.resolution === "string" ? commandOptions.resolution : undefined,
673
- expectedResult: (typeof commandOptions.expectedResult === "string" ? commandOptions.expectedResult : undefined) ??
674
- (typeof commandOptions.expected_result === "string" ? commandOptions.expected_result : undefined),
675
- actualResult: (typeof commandOptions.actualResult === "string" ? commandOptions.actualResult : undefined) ??
676
- (typeof commandOptions.actual_result === "string" ? commandOptions.actual_result : undefined),
677
- affectedVersion: (typeof commandOptions.affectedVersion === "string" ? commandOptions.affectedVersion : undefined) ??
678
- (typeof commandOptions.affected_version === "string" ? commandOptions.affected_version : undefined),
679
- fixedVersion: (typeof commandOptions.fixedVersion === "string" ? commandOptions.fixedVersion : undefined) ??
680
- (typeof commandOptions.fixed_version === "string" ? commandOptions.fixed_version : undefined),
681
- component: typeof commandOptions.component === "string" ? commandOptions.component : undefined,
682
- regression: typeof commandOptions.regression === "string" ? commandOptions.regression : undefined,
683
- customerImpact: (typeof commandOptions.customerImpact === "string" ? commandOptions.customerImpact : undefined) ??
684
- (typeof commandOptions.customer_impact === "string" ? commandOptions.customer_impact : undefined),
1896
+ allowAuditUpdate: commandOptions.allowAuditUpdate === true || commandOptions.allow_audit_update === true ? true : undefined,
1897
+ allowAuditDepUpdate: commandOptions.allowAuditDepUpdate === true || commandOptions.allow_audit_dep_update === true ? true : undefined,
1898
+ assignee: readUpdateString("assignee"),
1899
+ parent: readUpdateString("parent"),
1900
+ reviewer: readUpdateString("reviewer"),
1901
+ risk: readUpdateString("risk"),
1902
+ confidence: readUpdateString("confidence"),
1903
+ sprint: readUpdateString("sprint"),
1904
+ release: readUpdateString("release"),
1905
+ blockedBy: readUpdateString("blockedBy"),
1906
+ blockedReason: readUpdateString("blockedReason"),
1907
+ unblockNote: readUpdateString("unblockNote"),
1908
+ reporter: readUpdateString("reporter"),
1909
+ severity: readUpdateString("severity"),
1910
+ environment: readUpdateString("environment"),
1911
+ reproSteps: readUpdateString("reproSteps"),
1912
+ resolution: readUpdateString("resolution"),
1913
+ expectedResult: readUpdateString("expectedResult"),
1914
+ actualResult: readUpdateString("actualResult"),
1915
+ affectedVersion: readUpdateString("affectedVersion"),
1916
+ fixedVersion: readUpdateString("fixedVersion"),
1917
+ component: readUpdateString("component"),
1918
+ regression: readUpdateString("regression"),
1919
+ customerImpact: readUpdateString("customerImpact"),
1920
+ dep: readUpdateList("dep"),
1921
+ depRemove: readUpdateList("depRemove"),
1922
+ replaceDeps: commandOptions.replaceDeps === true ? true : undefined,
1923
+ replaceTests: commandOptions.replaceTests === true ? true : undefined,
1924
+ comment: readUpdateList("comment"),
1925
+ note: readUpdateList("note"),
1926
+ learning: readUpdateList("learning"),
1927
+ file: readUpdateList("file"),
1928
+ test: readUpdateList("test"),
1929
+ doc: readUpdateList("doc"),
1930
+ reminder: readUpdateList("reminder"),
1931
+ event: readUpdateList("event"),
1932
+ typeOption: readUpdateList("typeOption"),
1933
+ unset: readUpdateList("unset"),
1934
+ clearDeps: commandOptions.clearDeps === true ? true : undefined,
1935
+ clearComments: commandOptions.clearComments === true ? true : undefined,
1936
+ clearNotes: commandOptions.clearNotes === true ? true : undefined,
1937
+ clearLearnings: commandOptions.clearLearnings === true ? true : undefined,
1938
+ clearFiles: commandOptions.clearFiles === true ? true : undefined,
1939
+ clearTests: commandOptions.clearTests === true ? true : undefined,
1940
+ clearDocs: commandOptions.clearDocs === true ? true : undefined,
1941
+ clearReminders: commandOptions.clearReminders === true ? true : undefined,
1942
+ clearEvents: commandOptions.clearEvents === true ? true : undefined,
1943
+ clearTypeOptions: commandOptions.clearTypeOptions === true ? true : undefined,
685
1944
  };
1945
+ for (const [key, value] of Object.entries(commandOptions)) {
1946
+ if (Object.hasOwn(normalized, key)) {
1947
+ continue;
1948
+ }
1949
+ normalized[key] = value;
1950
+ }
1951
+ return normalized;
1952
+ }
1953
+ const UPDATE_MANY_CONTROL_OPTION_KEYS = new Set([
1954
+ "filterStatus",
1955
+ "filterType",
1956
+ "filterTag",
1957
+ "filterPriority",
1958
+ "filterDeadlineBefore",
1959
+ "filterDeadlineAfter",
1960
+ "filterAssignee",
1961
+ "filterAssigneeFilter",
1962
+ "filterAssignee_filter",
1963
+ "filterParent",
1964
+ "filterSprint",
1965
+ "filterRelease",
1966
+ "limit",
1967
+ "offset",
1968
+ "dryRun",
1969
+ "rollback",
1970
+ "checkpoint",
1971
+ ]);
1972
+ function extractUpdateManyMutationOptionSource(commandOptions) {
1973
+ const mutationOptions = {};
1974
+ for (const [key, value] of Object.entries(commandOptions)) {
1975
+ if (UPDATE_MANY_CONTROL_OPTION_KEYS.has(key)) {
1976
+ continue;
1977
+ }
1978
+ mutationOptions[key] = value;
1979
+ }
1980
+ return mutationOptions;
1981
+ }
1982
+ function readListOptionString(options, target) {
1983
+ return readFirstStringFromCommanderOptions(options, LIST_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
1984
+ target,
1985
+ keys: [target],
1986
+ });
686
1987
  }
687
1988
  function normalizeListOptions(options) {
1989
+ const normalized = {
1990
+ status: readListOptionString(options, "status"),
1991
+ type: readListOptionString(options, "type"),
1992
+ tag: readListOptionString(options, "tag"),
1993
+ priority: readListOptionString(options, "priority"),
1994
+ deadlineBefore: readListOptionString(options, "deadlineBefore"),
1995
+ deadlineAfter: readListOptionString(options, "deadlineAfter"),
1996
+ assignee: readListOptionString(options, "assignee"),
1997
+ assigneeFilter: readListOptionString(options, "assigneeFilter"),
1998
+ parent: readListOptionString(options, "parent"),
1999
+ sprint: readListOptionString(options, "sprint"),
2000
+ release: readListOptionString(options, "release"),
2001
+ limit: readListOptionString(options, "limit"),
2002
+ offset: readListOptionString(options, "offset"),
2003
+ includeBody: options.includeBody === true ? true : undefined,
2004
+ compact: options.compact === true ? true : undefined,
2005
+ fields: readListOptionString(options, "fields"),
2006
+ sort: readListOptionString(options, "sort"),
2007
+ order: readListOptionString(options, "order"),
2008
+ };
2009
+ for (const [key, value] of Object.entries(options)) {
2010
+ if (Object.hasOwn(normalized, key)) {
2011
+ continue;
2012
+ }
2013
+ normalized[key] = value;
2014
+ }
2015
+ return normalized;
2016
+ }
2017
+ function normalizeAggregateOptions(options) {
688
2018
  return {
689
- type: typeof options.type === "string" ? options.type : undefined,
690
- tag: typeof options.tag === "string" ? options.tag : undefined,
691
- priority: typeof options.priority === "string" ? options.priority : undefined,
692
- deadlineBefore: typeof options.deadlineBefore === "string" ? options.deadlineBefore : undefined,
693
- deadlineAfter: typeof options.deadlineAfter === "string" ? options.deadlineAfter : undefined,
694
- assignee: typeof options.assignee === "string" ? options.assignee : undefined,
695
- sprint: typeof options.sprint === "string" ? options.sprint : undefined,
696
- release: typeof options.release === "string" ? options.release : undefined,
697
- limit: typeof options.limit === "string" ? options.limit : undefined,
2019
+ groupBy: typeof options.groupBy === "string" ? options.groupBy : undefined,
2020
+ count: options.count === true ? true : undefined,
2021
+ includeUnparented: options.includeUnparented === true || options.include_unparented === true,
2022
+ status: typeof options.status === "string" ? options.status : undefined,
2023
+ type: readListOptionString(options, "type"),
2024
+ tag: readListOptionString(options, "tag"),
2025
+ priority: readListOptionString(options, "priority"),
2026
+ deadlineBefore: readListOptionString(options, "deadlineBefore"),
2027
+ deadlineAfter: readListOptionString(options, "deadlineAfter"),
2028
+ assignee: readListOptionString(options, "assignee"),
2029
+ assigneeFilter: readListOptionString(options, "assigneeFilter"),
2030
+ parent: readListOptionString(options, "parent"),
2031
+ sprint: readListOptionString(options, "sprint"),
2032
+ release: readListOptionString(options, "release"),
698
2033
  };
699
2034
  }
700
- function normalizeSearchOptions(options) {
2035
+ function normalizeDedupeAuditOptions(options) {
701
2036
  return {
702
2037
  mode: typeof options.mode === "string" ? options.mode : undefined,
2038
+ status: typeof options.status === "string" ? options.status : undefined,
2039
+ type: readListOptionString(options, "type"),
2040
+ tag: readListOptionString(options, "tag"),
2041
+ priority: readListOptionString(options, "priority"),
2042
+ deadlineBefore: readListOptionString(options, "deadlineBefore"),
2043
+ deadlineAfter: readListOptionString(options, "deadlineAfter"),
2044
+ assignee: readListOptionString(options, "assignee"),
2045
+ assigneeFilter: readListOptionString(options, "assigneeFilter"),
2046
+ parent: readListOptionString(options, "parent"),
2047
+ sprint: readListOptionString(options, "sprint"),
2048
+ release: readListOptionString(options, "release"),
2049
+ limit: readListOptionString(options, "limit"),
2050
+ threshold: typeof options.threshold === "string" ? options.threshold : undefined,
2051
+ };
2052
+ }
2053
+ function printListJsonStream(commandName, result, globalOptions) {
2054
+ setActiveCommandResult(result);
2055
+ if (globalOptions.quiet) {
2056
+ return;
2057
+ }
2058
+ const warnings = Array.isArray(result.warnings) ? result.warnings : [];
2059
+ const metaPayload = {
2060
+ type: "meta",
2061
+ command: commandName,
2062
+ count: result.count,
2063
+ now: result.now,
2064
+ filters: result.filters,
2065
+ };
2066
+ if (warnings.length > 0) {
2067
+ metaPayload.warnings = warnings;
2068
+ }
2069
+ if (!writeStdout(`${JSON.stringify(metaPayload)}\n`)) {
2070
+ return;
2071
+ }
2072
+ for (const item of result.items) {
2073
+ if (!writeStdout(`${JSON.stringify({ type: "item", command: commandName, item })}\n`)) {
2074
+ return;
2075
+ }
2076
+ }
2077
+ writeStdout(`${JSON.stringify({ type: "end", command: commandName, count: result.count })}\n`);
2078
+ }
2079
+ function printActivityJsonStream(result, options, globalOptions) {
2080
+ setActiveCommandResult(result);
2081
+ if (globalOptions.quiet) {
2082
+ return;
2083
+ }
2084
+ const metaPayload = {
2085
+ type: "meta",
2086
+ command: "activity",
2087
+ count: result.count,
2088
+ filters: {
2089
+ id: options.id ?? null,
2090
+ op: options.op ?? null,
2091
+ author: options.author ?? null,
2092
+ from: options.from ?? null,
2093
+ to: options.to ?? null,
2094
+ limit: options.limit ?? null,
2095
+ },
2096
+ };
2097
+ if (!writeStdout(`${JSON.stringify(metaPayload)}\n`)) {
2098
+ return;
2099
+ }
2100
+ for (const entry of result.activity) {
2101
+ if (!writeStdout(`${JSON.stringify({ type: "entry", command: "activity", entry })}\n`)) {
2102
+ return;
2103
+ }
2104
+ }
2105
+ writeStdout(`${JSON.stringify({ type: "end", command: "activity", count: result.count })}\n`);
2106
+ }
2107
+ function normalizeSearchOptions(options) {
2108
+ const readSearchString = (target) => readFirstStringFromCommanderOptions(options, SEARCH_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
2109
+ target,
2110
+ keys: [target],
2111
+ });
2112
+ const fields = readSearchString("fields");
2113
+ const compactRequested = options.compact === true;
2114
+ const fullRequested = options.full === true;
2115
+ const defaultCompact = !compactRequested && !fullRequested && fields === undefined;
2116
+ const normalized = {
2117
+ mode: readSearchString("mode"),
703
2118
  includeLinked: options.includeLinked === true ? true : undefined,
704
- type: typeof options.type === "string" ? options.type : undefined,
705
- tag: typeof options.tag === "string" ? options.tag : undefined,
706
- priority: typeof options.priority === "string" ? options.priority : undefined,
707
- deadlineBefore: typeof options.deadlineBefore === "string" ? options.deadlineBefore : undefined,
708
- deadlineAfter: typeof options.deadlineAfter === "string" ? options.deadlineAfter : undefined,
709
- limit: typeof options.limit === "string" ? options.limit : undefined,
2119
+ titleExact: options.titleExact === true ? true : undefined,
2120
+ phraseExact: options.phraseExact === true ? true : undefined,
2121
+ type: readSearchString("type"),
2122
+ tag: readSearchString("tag"),
2123
+ priority: readSearchString("priority"),
2124
+ deadlineBefore: readSearchString("deadlineBefore"),
2125
+ deadlineAfter: readSearchString("deadlineAfter"),
2126
+ limit: readSearchString("limit"),
2127
+ fields,
2128
+ compact: compactRequested || defaultCompact ? true : undefined,
2129
+ full: fullRequested ? true : undefined,
710
2130
  };
2131
+ for (const [key, value] of Object.entries(options)) {
2132
+ if (Object.hasOwn(normalized, key)) {
2133
+ continue;
2134
+ }
2135
+ normalized[key] = value;
2136
+ }
2137
+ return normalized;
2138
+ }
2139
+ function normalizeSearchKeywordsInput(keywords) {
2140
+ const query = keywords
2141
+ .map((entry) => entry.trim())
2142
+ .filter((entry) => entry.length > 0)
2143
+ .join(" ");
2144
+ if (query.length === 0) {
2145
+ throw new PmCliError("Search query must not be empty", EXIT_CODE.USAGE);
2146
+ }
2147
+ return query;
2148
+ }
2149
+ function normalizeCalendarOptions(options) {
2150
+ const readCalendarString = (target) => readFirstStringFromCommanderOptions(options, CALENDAR_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
2151
+ target,
2152
+ keys: [target],
2153
+ });
2154
+ const normalized = {
2155
+ view: readCalendarString("view"),
2156
+ date: readCalendarString("date"),
2157
+ from: readCalendarString("from"),
2158
+ to: readCalendarString("to"),
2159
+ past: options.past === true ? true : undefined,
2160
+ fullPeriod: options.fullPeriod === true || options.full_period === true ? true : undefined,
2161
+ limit: readCalendarString("limit"),
2162
+ type: readCalendarString("type"),
2163
+ tag: readCalendarString("tag"),
2164
+ priority: readCalendarString("priority"),
2165
+ status: readCalendarString("status"),
2166
+ assignee: readCalendarString("assignee"),
2167
+ assigneeFilter: readCalendarString("assigneeFilter"),
2168
+ sprint: readCalendarString("sprint"),
2169
+ release: readCalendarString("release"),
2170
+ include: readCalendarString("include"),
2171
+ recurrenceLookaheadDays: readCalendarString("recurrenceLookaheadDays"),
2172
+ recurrenceLookbackDays: readCalendarString("recurrenceLookbackDays"),
2173
+ occurrenceLimit: readCalendarString("occurrenceLimit"),
2174
+ format: readCalendarString("format"),
2175
+ };
2176
+ for (const [key, value] of Object.entries(options)) {
2177
+ if (Object.hasOwn(normalized, key)) {
2178
+ continue;
2179
+ }
2180
+ normalized[key] = value;
2181
+ }
2182
+ return normalized;
2183
+ }
2184
+ function normalizeActivityOptions(options) {
2185
+ const readActivityString = (target) => readFirstStringFromCommanderOptions(options, ACTIVITY_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
2186
+ target,
2187
+ keys: [target],
2188
+ });
2189
+ return {
2190
+ id: readActivityString("id"),
2191
+ op: readActivityString("op"),
2192
+ author: readActivityString("author"),
2193
+ from: readActivityString("from"),
2194
+ to: readActivityString("to"),
2195
+ limit: readActivityString("limit"),
2196
+ };
2197
+ }
2198
+ function resolveActivityStreamMode(raw) {
2199
+ if (raw === true) {
2200
+ return true;
2201
+ }
2202
+ if (raw === false || raw === undefined || raw === null) {
2203
+ return false;
2204
+ }
2205
+ if (typeof raw === "string") {
2206
+ const normalized = raw.trim().toLowerCase();
2207
+ if (normalized.length === 0 ||
2208
+ normalized === "rows" ||
2209
+ normalized === "ndjson" ||
2210
+ normalized === "jsonl" ||
2211
+ normalized === "true" ||
2212
+ normalized === "1" ||
2213
+ normalized === "yes" ||
2214
+ normalized === "on") {
2215
+ return true;
2216
+ }
2217
+ if (normalized === "false" || normalized === "off" || normalized === "none" || normalized === "0") {
2218
+ return false;
2219
+ }
2220
+ }
2221
+ throw new PmCliError("Activity --stream accepts rows|ndjson|jsonl (or no value)", EXIT_CODE.USAGE);
2222
+ }
2223
+ function normalizeContextOptions(options) {
2224
+ const readContextString = (target) => readFirstStringFromCommanderOptions(options, CONTEXT_COMMANDER_STRING_OPTION_CONTRACTS.find((entry) => entry.target === target) ?? {
2225
+ target,
2226
+ keys: [target],
2227
+ });
2228
+ const normalized = {
2229
+ date: readContextString("date"),
2230
+ from: readContextString("from"),
2231
+ to: readContextString("to"),
2232
+ past: options.past === true ? true : undefined,
2233
+ type: readContextString("type"),
2234
+ tag: readContextString("tag"),
2235
+ priority: readContextString("priority"),
2236
+ assignee: readContextString("assignee"),
2237
+ assigneeFilter: readContextString("assigneeFilter"),
2238
+ sprint: readContextString("sprint"),
2239
+ release: readContextString("release"),
2240
+ limit: readContextString("limit"),
2241
+ format: readContextString("format"),
2242
+ };
2243
+ for (const [key, value] of Object.entries(options)) {
2244
+ if (Object.hasOwn(normalized, key)) {
2245
+ continue;
2246
+ }
2247
+ normalized[key] = value;
2248
+ }
2249
+ return normalized;
711
2250
  }
712
2251
  function resolveCliVersion() {
713
2252
  try {
@@ -721,42 +2260,111 @@ function resolveCliVersion() {
721
2260
  return "0.0.0";
722
2261
  }
723
2262
  }
2263
+ const CLI_VERSION = resolveCliVersion();
724
2264
  const program = new Command();
725
2265
  program
726
2266
  .name("pm")
727
2267
  .description("Universal, flexible, extensible, agent-optimized project management CLI for any project or programming language.")
728
- .version(resolveCliVersion())
729
- .showHelpAfterError()
2268
+ .version(CLI_VERSION)
2269
+ .showHelpAfterError(false)
730
2270
  .allowExcessArguments(false)
731
2271
  .allowUnknownOption(false)
2272
+ .configureOutput({
2273
+ writeOut: (str) => {
2274
+ writeStdout(str);
2275
+ },
2276
+ // Commander errors are rendered in our own catch path.
2277
+ writeErr: () => { },
2278
+ })
732
2279
  .option("--json", "Output JSON instead of TOON")
733
2280
  .option("--quiet", "Suppress stdout output")
734
2281
  .option("--path <dir>", "Override PM path for this command")
735
2282
  .option("--no-extensions", "Disable extension loading")
2283
+ .option("--no-pager", "Disable pager integration for help and long output")
2284
+ .option("--explain", "Render extended rationale and examples in help output")
736
2285
  .option("--profile", "Print deterministic timing diagnostics")
737
2286
  .exitOverride();
738
2287
  program.hook("preAction", async (_thisCommand, actionCommand) => {
739
2288
  activeExtensionHookContext = null;
2289
+ activeTelemetryCommandContext = null;
740
2290
  clearActiveExtensionHooks();
2291
+ clearResolvedGlobalOptions(actionCommand);
2292
+ const bootstrapGlobalOptions = getGlobalOptions(actionCommand);
2293
+ const commandPath = getCommandPath(actionCommand);
2294
+ let commandArgs = actionCommand.args.map(String);
2295
+ let commandOptions = extractCommandScopedOptions(actionCommand, commandArgs);
2296
+ let globalOptions = { ...bootstrapGlobalOptions };
2297
+ await maybeRunFirstUseTelemetryPrompt(commandPath, globalOptions);
2298
+ const fallbackPmRoot = resolvePmRoot(process.cwd(), bootstrapGlobalOptions.path);
741
2299
  const runtimeExtensions = await maybeLoadRuntimeExtensions(actionCommand);
742
2300
  if (!runtimeExtensions) {
2301
+ activeTelemetryCommandContext = await startTelemetryCommand({
2302
+ command: commandPath,
2303
+ pm_version: CLI_VERSION,
2304
+ args: commandArgs,
2305
+ options: commandOptions,
2306
+ global: globalOptions,
2307
+ pm_root: fallbackPmRoot,
2308
+ });
2309
+ await enforceItemFormatWriteGateAndPreflightMigration(commandPath, commandOptions, fallbackPmRoot, defaultPreflightDecision());
743
2310
  return;
744
2311
  }
745
- const globalOptions = getGlobalOptions(actionCommand);
746
- const commandPath = getCommandPath(actionCommand);
747
- const commandArgs = actionCommand.args.map(String);
2312
+ setActiveExtensionHooks(runtimeExtensions.hooks);
2313
+ setActiveExtensionCommands(runtimeExtensions.commands);
2314
+ setActiveExtensionParsers(runtimeExtensions.parsers);
2315
+ setActiveExtensionPreflight(runtimeExtensions.preflight);
2316
+ setActiveExtensionServices(runtimeExtensions.services);
2317
+ setActiveExtensionRenderers(runtimeExtensions.renderers);
2318
+ setActiveExtensionRegistrations(runtimeExtensions.registrations);
2319
+ const extensionFlagDefinitions = collectExtensionFlagDefinitionsForCommand(runtimeExtensions.registrations, commandPath);
2320
+ commandOptions = extractCommandScopedOptions(actionCommand, commandArgs, extensionFlagDefinitions);
2321
+ const parserOverride = await runActiveParserOverride({
2322
+ command: commandPath,
2323
+ args: commandArgs,
2324
+ options: commandOptions,
2325
+ global: globalOptions,
2326
+ pm_root: runtimeExtensions.pmRoot,
2327
+ });
2328
+ if (globalOptions.profile && parserOverride.warnings.length > 0) {
2329
+ printError(`profile:extensions parser_warnings=${formatHookWarnings(parserOverride.warnings)}`);
2330
+ }
2331
+ commandArgs = parserOverride.context.args;
2332
+ commandOptions = parserOverride.context.options;
2333
+ globalOptions = parserOverride.context.global;
2334
+ const preflightOverride = await runActivePreflightOverride({
2335
+ command: commandPath,
2336
+ args: commandArgs,
2337
+ options: commandOptions,
2338
+ global: globalOptions,
2339
+ pm_root: runtimeExtensions.pmRoot,
2340
+ decision: defaultPreflightDecision(),
2341
+ });
2342
+ if (globalOptions.profile && preflightOverride.warnings.length > 0) {
2343
+ printError(`profile:extensions preflight_warnings=${formatHookWarnings(preflightOverride.warnings)}`);
2344
+ }
2345
+ commandArgs = preflightOverride.context.args;
2346
+ commandOptions = preflightOverride.context.options;
2347
+ globalOptions = preflightOverride.context.global;
2348
+ const preflightDecision = preflightOverride.decision;
2349
+ await enforceItemFormatWriteGateAndPreflightMigration(commandPath, commandOptions, runtimeExtensions.pmRoot, preflightDecision);
2350
+ const migrationWarnings = preflightDecision.run_extension_migrations
2351
+ ? await executeRegisteredRuntimeMigrations(runtimeExtensions.registrations.migrations, runtimeExtensions.pmRoot)
2352
+ : [];
2353
+ if (globalOptions.profile && migrationWarnings.length > 0) {
2354
+ printError(`profile:extensions migration_warnings=${formatHookWarnings(migrationWarnings)}`);
2355
+ }
2356
+ const migrationBlockers = collectMandatoryMigrationBlockers(runtimeExtensions.registrations.migrations);
748
2357
  activeExtensionHookContext = {
749
2358
  hooks: runtimeExtensions.hooks,
750
2359
  commandName: commandPath,
751
2360
  commandArgs,
2361
+ commandOptions: { ...commandOptions },
2362
+ globalOptions: { ...globalOptions },
752
2363
  pmRoot: runtimeExtensions.pmRoot,
753
2364
  profileEnabled: Boolean(globalOptions.profile),
754
- migrationBlockers: runtimeExtensions.migrationBlockers,
2365
+ migrationBlockers,
755
2366
  };
756
- setActiveExtensionHooks(runtimeExtensions.hooks);
757
- setActiveExtensionCommands(runtimeExtensions.commands);
758
- setActiveExtensionRenderers(runtimeExtensions.renderers);
759
- const commandOptions = actionCommand.optsWithGlobals();
2367
+ setActiveCommandResult(undefined);
760
2368
  setActiveCommandContext({
761
2369
  command: commandPath,
762
2370
  args: commandArgs,
@@ -764,15 +2372,27 @@ program.hook("preAction", async (_thisCommand, actionCommand) => {
764
2372
  global: { ...globalOptions },
765
2373
  pm_root: runtimeExtensions.pmRoot,
766
2374
  });
2375
+ activeTelemetryCommandContext = await startTelemetryCommand({
2376
+ command: commandPath,
2377
+ pm_version: CLI_VERSION,
2378
+ args: commandArgs,
2379
+ options: commandOptions,
2380
+ global: globalOptions,
2381
+ pm_root: runtimeExtensions.pmRoot,
2382
+ });
767
2383
  const hookWarnings = await runBeforeCommandHooks(runtimeExtensions.hooks, {
768
2384
  command: commandPath,
769
2385
  args: commandArgs,
2386
+ options: { ...commandOptions },
2387
+ global: { ...globalOptions },
770
2388
  pm_root: runtimeExtensions.pmRoot,
771
2389
  });
772
2390
  if (globalOptions.profile && hookWarnings.length > 0) {
773
2391
  printError(`profile:extensions hook_warnings=${formatHookWarnings(hookWarnings)}`);
774
2392
  }
775
- enforceMandatoryMigrationWriteGate(commandPath, actionCommand.optsWithGlobals(), runtimeExtensions.migrationBlockers);
2393
+ if (preflightDecision.enforce_mandatory_migration_gate) {
2394
+ enforceMandatoryMigrationWriteGate(commandPath, commandOptions, migrationBlockers);
2395
+ }
776
2396
  });
777
2397
  program.hook("postAction", async () => {
778
2398
  await runAndClearAfterCommandHooks({ ok: true });
@@ -780,11 +2400,14 @@ program.hook("postAction", async () => {
780
2400
  program
781
2401
  .command("init")
782
2402
  .argument("[prefix]", "Optional id prefix")
2403
+ .option("--preset <value>", "Governance preset for new setups: minimal|default|strict")
783
2404
  .description("Initialize pm storage and defaults for the current workspace.")
784
- .action(async (prefix, _options, command) => {
2405
+ .action(async (prefix, options, command) => {
785
2406
  const globalOptions = getGlobalOptions(command);
786
2407
  const startedAt = Date.now();
787
- const result = await runInit(prefix, globalOptions);
2408
+ const result = await runInit(prefix, globalOptions, {
2409
+ preset: typeof options.preset === "string" ? options.preset : undefined,
2410
+ });
788
2411
  printResult(result, globalOptions);
789
2412
  if (globalOptions.profile) {
790
2413
  printError(`profile:command=init took_ms=${Date.now() - startedAt}`);
@@ -793,9 +2416,12 @@ program
793
2416
  program
794
2417
  .command("config")
795
2418
  .argument("<scope>", "Config scope: project|global")
796
- .argument("<action>", "Config action: get|set")
797
- .argument("<key>", "Config key: definition-of-done")
798
- .option("--criterion <text>", "Definition-of-Done criterion (repeatable for set)", collect)
2419
+ .argument("<action>", "Config action: get|set|list|export")
2420
+ .argument("[key]", "Config key for get|set: definition-of-done|item-format|history-missing-stream-policy|sprint-release-format-policy|parent-reference-policy|metadata-validation-profile|metadata-required-fields|lifecycle-stale-blocker-reason-patterns|lifecycle-closure-like-blocked-reason-patterns|lifecycle-closure-like-resolution-patterns|lifecycle-closure-like-actual-result-patterns|governance-preset|governance-ownership-enforcement|governance-create-mode-default|governance-close-validation-default|governance-parent-reference-policy|governance-metadata-validation-profile|governance-force-required-for-stale-lock|test-result-tracking|telemetry-tracking")
2421
+ .option("--criterion <text>", "Criteria value for definition-of-done, metadata-required-fields, or lifecycle pattern keys (repeatable for set)", collect)
2422
+ .option("--clear-criteria", "Clear criteria-list keys for config set operations")
2423
+ .option("--format <value>", "Item format for item-format key: toon|json_markdown")
2424
+ .option("--policy <value>", "Policy key values: history-missing-stream-policy=auto_create|strict_error; sprint-release-format-policy=warn|strict_error; parent-reference-policy=warn|strict_error; governance-preset=minimal|default|strict|custom; governance-ownership-enforcement=none|warn|strict; governance-create-mode-default=progressive|strict; governance-close-validation-default=off|warn|strict; governance-parent-reference-policy=warn|strict_error; governance-metadata-validation-profile=core|strict|custom; governance-force-required-for-stale-lock=enabled|disabled; test-result-tracking=enabled|disabled; telemetry-tracking=enabled|disabled")
799
2425
  .description("Read or update pm settings for the current workspace or global profile.")
800
2426
  .action(async (scope, action, key, options, command) => {
801
2427
  const globalOptions = getGlobalOptions(command);
@@ -803,28 +2429,271 @@ program
803
2429
  const criteria = Array.isArray(options.criterion) ? options.criterion : [];
804
2430
  const result = await runConfig(scope, action, key, {
805
2431
  criterion: criteria,
2432
+ format: typeof options.format === "string" ? options.format : undefined,
2433
+ policy: typeof options.policy === "string" ? options.policy : undefined,
2434
+ clearCriteria: options.clearCriteria === true,
806
2435
  }, globalOptions);
807
2436
  printResult(result, globalOptions);
808
2437
  if (globalOptions.profile) {
809
2438
  printError(`profile:command=config took_ms=${Date.now() - startedAt}`);
810
2439
  }
811
2440
  });
812
- program
2441
+ function normalizeExtensionOptions(options, forcedAction) {
2442
+ const isForcedAction = (action) => forcedAction === action;
2443
+ const readBoolean = (...keys) => keys.some((key) => options[key] === true);
2444
+ const readString = (...keys) => {
2445
+ for (const key of keys) {
2446
+ if (typeof options[key] === "string") {
2447
+ return options[key];
2448
+ }
2449
+ }
2450
+ return undefined;
2451
+ };
2452
+ return {
2453
+ init: isForcedAction("init") || readBoolean("init"),
2454
+ scaffold: readBoolean("scaffold"),
2455
+ install: isForcedAction("install") || readBoolean("install"),
2456
+ uninstall: isForcedAction("uninstall") || readBoolean("uninstall"),
2457
+ explore: isForcedAction("explore") || readBoolean("explore"),
2458
+ manage: isForcedAction("manage") || readBoolean("manage"),
2459
+ doctor: isForcedAction("doctor") || readBoolean("doctor"),
2460
+ adopt: isForcedAction("adopt") || readBoolean("adopt"),
2461
+ adoptAll: isForcedAction("adopt-all") || readBoolean("adoptAll", "adopt_all", "adopt-all"),
2462
+ activate: isForcedAction("activate") || readBoolean("activate"),
2463
+ deactivate: isForcedAction("deactivate") || readBoolean("deactivate"),
2464
+ project: readBoolean("project"),
2465
+ local: readBoolean("local"),
2466
+ global: readBoolean("global"),
2467
+ gh: readString("gh"),
2468
+ github: readString("github"),
2469
+ ref: readString("ref"),
2470
+ detail: readString("detail"),
2471
+ trace: readBoolean("trace"),
2472
+ runtimeProbe: readBoolean("runtimeProbe", "runtime_probe", "runtime-probe"),
2473
+ fixManagedState: readBoolean("fixManagedState", "fix_managed_state", "fix-managed-state"),
2474
+ strictExit: readBoolean("strictExit", "strict_exit", "strict-exit"),
2475
+ failOnWarn: readBoolean("failOnWarn", "fail_on_warn", "fail-on-warn"),
2476
+ };
2477
+ }
2478
+ async function executeExtensionCommand(target, options, command, forcedAction) {
2479
+ const globalOptions = getGlobalOptions(command);
2480
+ const startedAt = Date.now();
2481
+ const normalizedOptions = normalizeExtensionOptions(options, forcedAction);
2482
+ const result = await runExtension(target, normalizedOptions, globalOptions);
2483
+ printResult(result, globalOptions);
2484
+ const strictExit = Boolean(normalizedOptions.strictExit) || Boolean(normalizedOptions.failOnWarn);
2485
+ if (result.action === "doctor" && strictExit) {
2486
+ const detailsRecord = result.details;
2487
+ const summary = (detailsRecord.summary ?? null);
2488
+ const summaryStatus = summary && typeof summary.status === "string" ? summary.status : undefined;
2489
+ const shouldFail = summaryStatus ? summaryStatus !== "ok" : result.warnings.length > 0;
2490
+ if (shouldFail) {
2491
+ process.exitCode = EXIT_CODE.GENERIC_FAILURE;
2492
+ }
2493
+ }
2494
+ if (globalOptions.profile) {
2495
+ printError(`profile:command=extension took_ms=${Date.now() - startedAt}`);
2496
+ }
2497
+ }
2498
+ function addExtensionScopeOptions(command) {
2499
+ return command
2500
+ .option("--project", "Use project extension scope (default)")
2501
+ .option("--local", "Alias for --project")
2502
+ .option("--global", "Use global extension scope");
2503
+ }
2504
+ const extensionCommand = program
2505
+ .command("extension")
2506
+ .argument("[target]", "Extension source/name or scaffold target path (for --init/--scaffold)")
2507
+ .option("--init", "Generate a starter extension scaffold at target path")
2508
+ .option("--scaffold", "Alias for --init")
2509
+ .option("--install", "Install extension from local path or GitHub source")
2510
+ .option("--uninstall", "Uninstall an installed extension")
2511
+ .option("--explore", "List discovered extensions in selected scope")
2512
+ .option("--manage", "List managed extensions with update-check metadata")
2513
+ .option("--doctor", "Run consolidated extension diagnostics (summary/deep modes)")
2514
+ .option("--adopt", "Adopt an existing unmanaged extension into managed metadata")
2515
+ .option("--adopt-all", "Adopt all unmanaged extensions into managed metadata")
2516
+ .option("--activate", "Activate an extension in selected scope settings")
2517
+ .option("--deactivate", "Deactivate an extension in selected scope settings")
2518
+ .option("--project", "Use project extension scope (default)")
2519
+ .option("--local", "Alias for --project")
2520
+ .option("--global", "Use global extension scope")
2521
+ .option("--gh <owner/repo[/path]>", "Install from GitHub shorthand source")
2522
+ .option("--github <owner/repo[/path]>", "Alias for --gh")
2523
+ .option("--ref <ref>", "Git ref/branch/tag for GitHub install sources")
2524
+ .option("--detail <mode>", "Detail mode for extension diagnostics (summary|deep)")
2525
+ .option("--trace", "Include actionable registration traces in doctor deep diagnostics")
2526
+ .option("--runtime-probe", "Opt-in runtime activation probe for manage output parity")
2527
+ .option("--fix-managed-state", "Adopt unmanaged extensions before diagnostics/update checks")
2528
+ .option("--strict-exit", "Return non-zero exit when doctor warnings are present (ok=false)")
2529
+ .option("--fail-on-warn", "Alias for --strict-exit (doctor)")
2530
+ .description("Manage extension lifecycle operations for project or global scope.")
2531
+ .action(async (target, _options, command) => {
2532
+ await executeExtensionCommand(target, command.opts(), command);
2533
+ });
2534
+ addExtensionScopeOptions(extensionCommand
2535
+ .command("init")
2536
+ .alias("scaffold")
2537
+ .argument("<target>", "Scaffold target directory path")
2538
+ .description("Generate a starter extension scaffold with manifest and entrypoint.")).action(async (target, _options, command) => {
2539
+ await executeExtensionCommand(target, command.opts(), command, "init");
2540
+ });
2541
+ addExtensionScopeOptions(extensionCommand
813
2542
  .command("install")
814
- .argument("<target>", "Install target: pi")
815
- .option("--project", "Install Pi extension into resolved project root .pi/extensions (derived from --path, default)")
816
- .option("--global", "Install Pi extension into global PI_CODING_AGENT_DIR or ~/.pi/agent")
817
- .description("Install supported integrations and extensions.")
818
- .action(async (target, options, command) => {
2543
+ .argument("[target]", "Extension source (local path or GitHub source)")
2544
+ .option("--gh <owner/repo[/path]>", "Install from GitHub shorthand source")
2545
+ .option("--github <owner/repo[/path]>", "Alias for --gh")
2546
+ .option("--ref <ref>", "Git ref/branch/tag for GitHub install sources")
2547
+ .description("Install extension from local path or GitHub source.")).action(async (target, _options, command) => {
2548
+ await executeExtensionCommand(target, command.opts(), command, "install");
2549
+ });
2550
+ addExtensionScopeOptions(extensionCommand.command("uninstall").argument("<target>", "Extension name").description("Uninstall an installed extension.")).action(async (target, _options, command) => {
2551
+ await executeExtensionCommand(target, command.opts(), command, "uninstall");
2552
+ });
2553
+ addExtensionScopeOptions(extensionCommand.command("explore").description("List discovered extensions in selected scope.")).action(async (_options, command) => {
2554
+ await executeExtensionCommand(undefined, command.opts(), command, "explore");
2555
+ });
2556
+ addExtensionScopeOptions(extensionCommand
2557
+ .command("manage")
2558
+ .option("--runtime-probe", "Opt-in runtime activation probe for manage output parity")
2559
+ .option("--fix-managed-state", "Adopt unmanaged extensions before diagnostics/update checks")
2560
+ .description("List managed extensions with update-check metadata.")).action(async (_options, command) => {
2561
+ await executeExtensionCommand(undefined, command.opts(), command, "manage");
2562
+ });
2563
+ addExtensionScopeOptions(extensionCommand
2564
+ .command("doctor")
2565
+ .option("--detail <mode>", "Detail mode for extension diagnostics (summary|deep)")
2566
+ .option("--trace", "Include actionable registration traces in doctor deep diagnostics")
2567
+ .option("--fix-managed-state", "Adopt unmanaged extensions before diagnostics/update checks")
2568
+ .option("--strict-exit", "Return non-zero exit when doctor warnings are present (ok=false)")
2569
+ .option("--fail-on-warn", "Alias for --strict-exit (doctor)")
2570
+ .description("Run consolidated extension diagnostics (summary/deep modes).")).action(async (_options, command) => {
2571
+ await executeExtensionCommand(undefined, command.opts(), command, "doctor");
2572
+ });
2573
+ addExtensionScopeOptions(extensionCommand
2574
+ .command("adopt")
2575
+ .argument("<target>", "Extension name")
2576
+ .option("--gh <owner/repo[/path]>", "GitHub provenance shorthand for adopted extension")
2577
+ .option("--github <owner/repo[/path]>", "Alias for --gh")
2578
+ .option("--ref <ref>", "Git ref/branch/tag for GitHub shorthand source")
2579
+ .description("Adopt an existing unmanaged extension into managed metadata.")).action(async (target, _options, command) => {
2580
+ await executeExtensionCommand(target, command.opts(), command, "adopt");
2581
+ });
2582
+ addExtensionScopeOptions(extensionCommand.command("adopt-all").description("Adopt all unmanaged extensions into managed metadata.")).action(async (_options, command) => {
2583
+ await executeExtensionCommand(undefined, command.opts(), command, "adopt-all");
2584
+ });
2585
+ addExtensionScopeOptions(extensionCommand.command("activate").argument("<target>", "Extension name").description("Activate an extension in selected scope settings.")).action(async (target, _options, command) => {
2586
+ await executeExtensionCommand(target, command.opts(), command, "activate");
2587
+ });
2588
+ addExtensionScopeOptions(extensionCommand.command("deactivate").argument("<target>", "Extension name").description("Deactivate an extension in selected scope settings.")).action(async (target, _options, command) => {
2589
+ await executeExtensionCommand(target, command.opts(), command, "deactivate");
2590
+ });
2591
+ const templatesCommand = program.command("templates").description("Manage reusable create templates.");
2592
+ templatesCommand
2593
+ .command("save")
2594
+ .argument("<name>", "Template name")
2595
+ .option("--title, -t <value>", "Template default item title")
2596
+ .option("--description, -d <value>", "Template default item description")
2597
+ .option("--type <value>", "Template default item type")
2598
+ .option("--status, -s <value>", "Template default item status")
2599
+ .option("--priority, -p <value>", "Template default priority 0..4")
2600
+ .option("--tags <value>", "Template default comma-separated tags")
2601
+ .option("--body, -b <value>", "Template default item markdown body")
2602
+ .option("--deadline <value>", "Template default deadline")
2603
+ .option("--estimate, --estimated-minutes <value>", "Template default estimated minutes")
2604
+ .option("--estimated_minutes <value>", "Alias for --estimated-minutes")
2605
+ .option("--acceptance-criteria <value>", "Template default acceptance criteria")
2606
+ .option("--acceptance_criteria <value>", "Alias for --acceptance-criteria")
2607
+ .option("--ac <value>", "Alias for --acceptance-criteria")
2608
+ .option("--definition-of-ready <value>", "Template default definition of ready")
2609
+ .option("--definition_of_ready <value>", "Alias for --definition-of-ready")
2610
+ .option("--order <value>", "Template default planning order/rank integer")
2611
+ .option("--rank <value>", "Alias for --order")
2612
+ .option("--goal <value>", "Template default goal identifier")
2613
+ .option("--objective <value>", "Template default objective identifier")
2614
+ .option("--value <value>", "Template default business value summary")
2615
+ .option("--impact <value>", "Template default business impact summary")
2616
+ .option("--outcome <value>", "Template default expected outcome summary")
2617
+ .option("--why-now <value>", "Template default why-now rationale")
2618
+ .option("--why_now <value>", "Alias for --why-now")
2619
+ .option("--author <value>", "Template default mutation author")
2620
+ .option("--message <value>", "Template default history message")
2621
+ .option("--assignee <value>", "Template default assignee")
2622
+ .option("--parent <value>", "Template default parent item ID")
2623
+ .option("--reviewer <value>", "Template default reviewer")
2624
+ .option("--risk <value>", "Template default risk level")
2625
+ .option("--confidence <value>", "Template default confidence")
2626
+ .option("--sprint <value>", "Template default sprint identifier")
2627
+ .option("--release <value>", "Template default release identifier")
2628
+ .option("--blocked-by <value>", "Template default blocked-by item ID or reason")
2629
+ .option("--blocked_by <value>", "Alias for --blocked-by")
2630
+ .option("--blocked-reason <value>", "Template default blocked reason")
2631
+ .option("--blocked_reason <value>", "Alias for --blocked-reason")
2632
+ .option("--unblock-note <value>", "Template default unblock rationale note")
2633
+ .option("--unblock_note <value>", "Alias for --unblock-note")
2634
+ .option("--reporter <value>", "Template default issue reporter")
2635
+ .option("--severity <value>", "Template default issue severity")
2636
+ .option("--environment <value>", "Template default issue environment context")
2637
+ .option("--repro-steps <value>", "Template default issue reproduction steps")
2638
+ .option("--repro_steps <value>", "Alias for --repro-steps")
2639
+ .option("--resolution <value>", "Template default issue resolution summary")
2640
+ .option("--expected-result <value>", "Template default issue expected behavior")
2641
+ .option("--expected_result <value>", "Alias for --expected-result")
2642
+ .option("--actual-result <value>", "Template default issue observed behavior")
2643
+ .option("--actual_result <value>", "Alias for --actual-result")
2644
+ .option("--affected-version <value>", "Template default affected version identifier")
2645
+ .option("--affected_version <value>", "Alias for --affected-version")
2646
+ .option("--fixed-version <value>", "Template default fixed version identifier")
2647
+ .option("--fixed_version <value>", "Alias for --fixed-version")
2648
+ .option("--component <value>", "Template default issue component ownership")
2649
+ .option("--regression <value>", "Template default regression marker")
2650
+ .option("--customer-impact <value>", "Template default customer impact summary")
2651
+ .option("--customer_impact <value>", "Alias for --customer-impact")
2652
+ .option("--dep <value>", "Template default dependency entry (repeatable; CSV/markdown pairs or - for stdin)", collect)
2653
+ .option("--type-option <value>", "Template default type option entry (repeatable; key=value or markdown pairs; use - for stdin)", collect)
2654
+ .option("--type_option <value>", "Alias for --type-option", collect)
2655
+ .option("--reminder <value>", "Template default reminder entry (repeatable; at=<iso|relative>,text=<text>)", collect)
2656
+ .option("--event <value>", "Template default event entry (repeatable; start/end/title/recur_* fields)", collect)
2657
+ .option("--comment <value>", "Template default comment seed entry (repeatable; text=<value> CSV/markdown pairs or - for stdin)", collect)
2658
+ .option("--note <value>", "Template default note seed entry (repeatable; text=<value> CSV/markdown pairs or - for stdin)", collect)
2659
+ .option("--learning <value>", "Template default learning seed entry (repeatable; text=<value> CSV/markdown pairs or - for stdin)", collect)
2660
+ .option("--file <value>", "Template default linked file entry (repeatable; CSV/markdown pairs or - for stdin)", collect)
2661
+ .option("--test <value>", "Template default linked test entry (repeatable; CSV/markdown pairs or - for stdin)", collect)
2662
+ .option("--doc <value>", "Template default linked doc entry (repeatable; CSV/markdown pairs or - for stdin)", collect)
2663
+ .description("Save or update a named create template.")
2664
+ .action(async (name, options, command) => {
819
2665
  const globalOptions = getGlobalOptions(command);
820
2666
  const startedAt = Date.now();
821
- const result = await runInstall(target, {
822
- project: options.project === true,
823
- global: options.global === true,
824
- }, globalOptions);
2667
+ const normalized = normalizeCreateOptions(options, { requireType: false });
2668
+ const result = await runTemplatesSave(name, normalized, globalOptions);
2669
+ printResult(result, globalOptions);
2670
+ if (globalOptions.profile) {
2671
+ printError(`profile:command=templates save took_ms=${Date.now() - startedAt}`);
2672
+ }
2673
+ });
2674
+ templatesCommand
2675
+ .command("list")
2676
+ .description("List saved create templates.")
2677
+ .action(async (_options, command) => {
2678
+ const globalOptions = getGlobalOptions(command);
2679
+ const startedAt = Date.now();
2680
+ const result = await runTemplatesList(globalOptions);
2681
+ printResult(result, globalOptions);
2682
+ if (globalOptions.profile) {
2683
+ printError(`profile:command=templates list took_ms=${Date.now() - startedAt}`);
2684
+ }
2685
+ });
2686
+ templatesCommand
2687
+ .command("show")
2688
+ .argument("<name>", "Template name")
2689
+ .description("Show saved create template details.")
2690
+ .action(async (name, _options, command) => {
2691
+ const globalOptions = getGlobalOptions(command);
2692
+ const startedAt = Date.now();
2693
+ const result = await runTemplatesShow(name, globalOptions);
825
2694
  printResult(result, globalOptions);
826
2695
  if (globalOptions.profile) {
827
- printError(`profile:command=install took_ms=${Date.now() - startedAt}`);
2696
+ printError(`profile:command=templates show took_ms=${Date.now() - startedAt}`);
828
2697
  }
829
2698
  });
830
2699
  program
@@ -832,72 +2701,92 @@ program
832
2701
  .description("Create a new project management item.")
833
2702
  .requiredOption("--title, -t <value>", "Item title")
834
2703
  .requiredOption("--description, -d <value>", "Item description (allow empty string)")
835
- .requiredOption("--type <value>", "Item type: Epic|Feature|Task|Chore|Issue")
836
- .requiredOption("--status, -s <value>", "Item status")
837
- .requiredOption("--priority, -p <value>", "Priority 0..4")
838
- .requiredOption("--tags <value>", "Comma-separated tags, or 'none'")
839
- .requiredOption("--body, -b <value>", "Item markdown body (allow empty string)")
840
- .requiredOption("--deadline <value>", "ISO deadline, relative +6h/+1d/+2w, or none")
841
- .option("--estimate, --estimated-minutes <value>", "Estimated minutes, or none")
2704
+ .option("--type <value>", "Item type (built-ins plus any configured custom types)")
2705
+ .option("--template <value>", "Apply named create template defaults before explicit flags")
2706
+ .option("--create-mode <value>", "Create required-option policy mode: strict|progressive")
2707
+ .option("--create_mode <value>", "Alias for --create-mode")
2708
+ .option("--schedule-preset <value>", "Scheduling preset for Reminder|Meeting|Event: lightweight")
2709
+ .option("--schedule_preset <value>", "Alias for --schedule-preset")
2710
+ .option("--status, -s <value>", "Item status")
2711
+ .option("--priority, -p <value>", "Priority 0..4")
2712
+ .option("--tags <value>", "Comma-separated tags")
2713
+ .option("--body, -b <value>", "Item markdown body (allow empty string)")
2714
+ .option("--deadline <value>", "Deadline (ISO/date string or relative +6h/+1d/+2w/+6m)")
2715
+ .option("--estimate, --estimated-minutes <value>", "Estimated minutes")
842
2716
  .option("--estimated_minutes <value>", "Alias for --estimated-minutes")
843
2717
  .option("--acceptance-criteria <value>", "Acceptance criteria (allow empty string)")
844
2718
  .option("--acceptance_criteria <value>", "Alias for --acceptance-criteria")
845
2719
  .option("--ac <value>", "Alias for --acceptance-criteria")
846
- .option("--definition-of-ready <value>", "Definition of ready (allow empty string, or none)")
2720
+ .option("--definition-of-ready <value>", "Definition of ready (allow empty string)")
847
2721
  .option("--definition_of_ready <value>", "Alias for --definition-of-ready")
848
- .option("--order <value>", "Planning order/rank integer, or none")
2722
+ .option("--order <value>", "Planning order/rank integer")
849
2723
  .option("--rank <value>", "Alias for --order")
850
- .option("--goal <value>", "Goal identifier, or none")
851
- .option("--objective <value>", "Objective identifier, or none")
852
- .option("--value <value>", "Business value summary, or none")
853
- .option("--impact <value>", "Business impact summary, or none")
854
- .option("--outcome <value>", "Expected outcome summary, or none")
855
- .option("--why-now <value>", "Why-now rationale, or none")
2724
+ .option("--goal <value>", "Goal identifier")
2725
+ .option("--objective <value>", "Objective identifier")
2726
+ .option("--value <value>", "Business value summary")
2727
+ .option("--impact <value>", "Business impact summary")
2728
+ .option("--outcome <value>", "Expected outcome summary")
2729
+ .option("--why-now <value>", "Why-now rationale")
856
2730
  .option("--why_now <value>", "Alias for --why-now")
857
- .requiredOption("--author <value>", "Mutation author, or none")
858
- .requiredOption("--message <value>", "History message (allow empty string)")
859
- .requiredOption("--assignee <value>", "Item assignee, or none")
860
- .option("--parent <value>", "Parent item ID, or none")
861
- .option("--reviewer <value>", "Reviewer, or none")
862
- .option("--risk <value>", "Risk level: low|med|medium|high|critical, or none (med persists as medium)")
863
- .option("--confidence <value>", "Confidence level: 0..100|low|med|medium|high, or none (med persists as medium)")
864
- .option("--sprint <value>", "Sprint identifier, or none")
865
- .option("--release <value>", "Release identifier, or none")
866
- .option("--blocked-by <value>", "Blocked-by item ID or reason, or none")
2731
+ .option("--author <value>", "Mutation author")
2732
+ .option("--message <value>", "History message (allow empty string)")
2733
+ .option("--assignee <value>", "Item assignee")
2734
+ .option("--parent <value>", "Parent item ID")
2735
+ .option("--reviewer <value>", "Reviewer")
2736
+ .option("--risk <value>", "Risk level: low|med|medium|high|critical (med persists as medium)")
2737
+ .option("--confidence <value>", "Confidence level: 0..100|low|med|medium|high (med persists as medium)")
2738
+ .option("--sprint <value>", "Sprint identifier")
2739
+ .option("--release <value>", "Release identifier")
2740
+ .option("--blocked-by <value>", "Blocked-by item ID or reason")
867
2741
  .option("--blocked_by <value>", "Alias for --blocked-by")
868
- .option("--blocked-reason <value>", "Blocked reason, or none")
2742
+ .option("--blocked-reason <value>", "Blocked reason")
869
2743
  .option("--blocked_reason <value>", "Alias for --blocked-reason")
870
- .option("--unblock-note <value>", "Unblock rationale note, or none")
2744
+ .option("--unblock-note <value>", "Unblock rationale note")
871
2745
  .option("--unblock_note <value>", "Alias for --unblock-note")
872
- .option("--reporter <value>", "Issue reporter, or none")
873
- .option("--severity <value>", "Issue severity: low|med|medium|high|critical, or none (med persists as medium)")
874
- .option("--environment <value>", "Issue environment context, or none")
875
- .option("--repro-steps <value>", "Issue reproduction steps, or none")
2746
+ .option("--reporter <value>", "Issue reporter")
2747
+ .option("--severity <value>", "Issue severity: low|med|medium|high|critical (med persists as medium)")
2748
+ .option("--environment <value>", "Issue environment context")
2749
+ .option("--repro-steps <value>", "Issue reproduction steps")
876
2750
  .option("--repro_steps <value>", "Alias for --repro-steps")
877
- .option("--resolution <value>", "Issue resolution summary, or none")
878
- .option("--expected-result <value>", "Issue expected behavior, or none")
2751
+ .option("--resolution <value>", "Issue resolution summary")
2752
+ .option("--expected-result <value>", "Issue expected behavior")
879
2753
  .option("--expected_result <value>", "Alias for --expected-result")
880
- .option("--actual-result <value>", "Issue observed behavior, or none")
2754
+ .option("--actual-result <value>", "Issue observed behavior")
881
2755
  .option("--actual_result <value>", "Alias for --actual-result")
882
- .option("--affected-version <value>", "Affected version identifier, or none")
2756
+ .option("--affected-version <value>", "Affected version identifier")
883
2757
  .option("--affected_version <value>", "Alias for --affected-version")
884
- .option("--fixed-version <value>", "Fixed version identifier, or none")
2758
+ .option("--fixed-version <value>", "Fixed version identifier")
885
2759
  .option("--fixed_version <value>", "Alias for --fixed-version")
886
- .option("--component <value>", "Issue component ownership, or none")
887
- .option("--regression <value>", "Regression marker: true|false|1|0, or none")
888
- .option("--customer-impact <value>", "Customer impact summary, or none")
2760
+ .option("--component <value>", "Issue component ownership")
2761
+ .option("--regression <value>", "Regression marker: true|false|1|0")
2762
+ .option("--customer-impact <value>", "Customer impact summary")
889
2763
  .option("--customer_impact <value>", "Alias for --customer-impact")
890
- .option("--dep <value>", "Seed dependency entry (required; use none for empty)", collect)
891
- .option("--comment <value>", "Seed comment entry (required; use none for empty)", collect)
892
- .option("--note <value>", "Seed note entry (required; use none for empty)", collect)
893
- .option("--learning <value>", "Seed learning entry (required; use none for empty)", collect)
894
- .option("--file <value>", "Seed linked file entry (required; use none for empty)", collect)
895
- .option("--test <value>", "Seed linked test entry (required; use none for empty)", collect)
896
- .option("--doc <value>", "Seed linked doc entry (required; use none for empty)", collect)
2764
+ .option("--dep <value>", "Seed dependency entry (key=value CSV, markdown key:value lines, or - for stdin; repeatable)", collect)
2765
+ .option("--type-option <value>", "Type option key=value or key=<name>,value=<value> (also accepts key:value and markdown pairs; use - for stdin; repeatable)", collect)
2766
+ .option("--type_option <value>", "Alias for --type-option", collect)
2767
+ .option("--unset <field>", "Clear scalar metadata field by name (repeatable)", collect)
2768
+ .option("--clear-deps", "Clear dependency entries")
2769
+ .option("--clear-comments", "Clear comments")
2770
+ .option("--clear-notes", "Clear notes")
2771
+ .option("--clear-learnings", "Clear learnings")
2772
+ .option("--clear-files", "Clear linked files")
2773
+ .option("--clear-tests", "Clear linked tests")
2774
+ .option("--clear-docs", "Clear linked docs")
2775
+ .option("--clear-reminders", "Clear reminders")
2776
+ .option("--clear-events", "Clear events")
2777
+ .option("--clear-type-options", "Clear type options")
2778
+ .option("--reminder <value>", "Seed reminder entry at=<iso|relative>,text=<text> (also accepts markdown pairs and - for stdin; repeatable)", collect)
2779
+ .option("--event <value>", "Seed event entry start=<iso|relative>,end=<iso|relative>,title=<text>,all_day=<true|false>,recur_* fields (also accepts markdown pairs and - for stdin; repeatable)", collect)
2780
+ .option("--comment <value>", "Seed comment entry (text=<value> CSV/markdown pairs or - for stdin; repeatable)", collect)
2781
+ .option("--note <value>", "Seed note entry (text=<value> CSV/markdown pairs or - for stdin; repeatable)", collect)
2782
+ .option("--learning <value>", "Seed learning entry (text=<value> CSV/markdown pairs or - for stdin; repeatable)", collect)
2783
+ .option("--file <value>", "Seed linked file entry (CSV/markdown pairs or - for stdin; repeatable)", collect)
2784
+ .option("--test <value>", "Seed linked test entry (CSV/markdown pairs or - for stdin; repeatable)", collect)
2785
+ .option("--doc <value>", "Seed linked doc entry (CSV/markdown pairs or - for stdin; repeatable)", collect)
897
2786
  .action(async (options, command) => {
898
2787
  const globalOptions = getGlobalOptions(command);
899
2788
  const startedAt = Date.now();
900
- const normalized = normalizeCreateOptions(options);
2789
+ const normalized = normalizeCreateOptions(options, { requireType: false });
901
2790
  const result = await runCreate(normalized, globalOptions);
902
2791
  await invalidateSearchCachesForMutation(globalOptions, result);
903
2792
  printResult(result, globalOptions);
@@ -905,34 +2794,55 @@ program
905
2794
  printError(`profile:command=create took_ms=${Date.now() - startedAt}`);
906
2795
  }
907
2796
  });
908
- function registerListCommand(name, description, status, excludeTerminal) {
909
- program
910
- .command(name)
911
- .description(description)
2797
+ function registerListCommand(name, description, status, excludeTerminal, allowStatusFilter) {
2798
+ const command = program.command(name).description(description);
2799
+ if (allowStatusFilter) {
2800
+ command.option("--status <value>", "Filter by status");
2801
+ }
2802
+ command
912
2803
  .option("--type <value>", "Filter by item type")
913
2804
  .option("--tag <value>", "Filter by tag")
914
2805
  .option("--priority <value>", "Filter by priority")
915
- .option("--deadline-before <value>", "Filter by deadline upper bound")
916
- .option("--deadline-after <value>", "Filter by deadline lower bound")
917
- .option("--assignee <value>", "Filter by assignee (use 'none' for unassigned)")
2806
+ .option("--deadline-before <value>", "Filter by deadline upper bound (ISO/date string or relative)")
2807
+ .option("--deadline-after <value>", "Filter by deadline lower bound (ISO/date string or relative)")
2808
+ .option("--assignee <value>", "Filter by assignee")
2809
+ .option("--assignee-filter <value>", "Filter assignee presence: assigned|unassigned")
2810
+ .option("--assignee_filter <value>", "Alias for --assignee-filter")
2811
+ .option("--parent <value>", "Filter by parent item ID")
918
2812
  .option("--sprint <value>", "Filter by sprint")
919
2813
  .option("--release <value>", "Filter by release")
920
2814
  .option("--limit <n>", "Limit returned item count")
921
- .action(async (options, command) => {
922
- const globalOptions = getGlobalOptions(command);
2815
+ .option("--offset <n>", "Skip the first n matching rows before limit is applied")
2816
+ .option("--include-body", "Include item body in each returned list row")
2817
+ .option("--compact", "Render compact list projection fields (mutually exclusive with --fields)")
2818
+ .option("--fields <value>", "Render custom comma-separated list fields (mutually exclusive with --compact; valid: --fields id,title; invalid: --compact --fields id,title)")
2819
+ .option("--sort <value>", "Sort field: priority|deadline|updated_at|created_at|title|parent")
2820
+ .option("--order <value>", "Sort order: asc|desc (requires --sort)")
2821
+ .option("--stream", "Emit line-delimited JSON rows (requires --json)")
2822
+ .action(async (options, actionCommand) => {
2823
+ const globalOptions = getGlobalOptions(actionCommand);
923
2824
  const startedAt = Date.now();
924
2825
  const listOptions = normalizeListOptions(options);
925
2826
  if (excludeTerminal)
926
2827
  listOptions.excludeTerminal = true;
927
2828
  const result = await runList(status, listOptions, globalOptions);
928
- printResult(result, globalOptions);
2829
+ const streamMode = options.stream === true;
2830
+ if (streamMode && !globalOptions.json) {
2831
+ throw new PmCliError("--stream requires --json output mode.", EXIT_CODE.USAGE);
2832
+ }
2833
+ if (streamMode) {
2834
+ printListJsonStream(name, result, globalOptions);
2835
+ }
2836
+ else {
2837
+ printResult(result, globalOptions);
2838
+ }
929
2839
  if (globalOptions.profile) {
930
2840
  printError(`profile:command=${name} took_ms=${Date.now() - startedAt}`);
931
2841
  }
932
2842
  });
933
2843
  }
934
- registerListCommand("list", "List active items with optional filters.", undefined, true);
935
- registerListCommand("list-all", "List all items with optional filters.");
2844
+ registerListCommand("list", "List active items with optional filters.", undefined, true, true);
2845
+ registerListCommand("list-all", "List all items with optional filters.", undefined, false, true);
936
2846
  registerListCommand("list-draft", "List draft items with optional filters.", "draft");
937
2847
  registerListCommand("list-open", "List open items with optional filters.", "open");
938
2848
  registerListCommand("list-in-progress", "List in-progress items with optional filters.", "in_progress");
@@ -940,70 +2850,176 @@ registerListCommand("list-blocked", "List blocked items with optional filters.",
940
2850
  registerListCommand("list-closed", "List closed items with optional filters.", "closed");
941
2851
  registerListCommand("list-canceled", "List canceled items with optional filters.", "canceled");
942
2852
  program
943
- .command("beads")
944
- .description("Built-in Beads extension commands.")
945
- .command("import")
946
- .description("Import Beads JSONL records as PM items.")
947
- .option("--file <path>", "Path to Beads JSONL file, or - for stdin")
948
- .option("--author <value>", "Mutation author")
949
- .option("--message <value>", "History message for import entries")
950
- .option("--preserve-source-ids", "Preserve explicit Beads ids instead of rewriting them to the tracker prefix")
951
- .action(async (options, command) => {
952
- const globalOptions = getGlobalOptions(command);
953
- const startedAt = Date.now();
954
- const result = await runRequiredExtensionCommand(command, options, globalOptions);
955
- await invalidateSearchCachesForMutation(globalOptions, result);
956
- printResult(result, globalOptions);
957
- if (globalOptions.profile) {
958
- printError(`profile:command=beads import took_ms=${Date.now() - startedAt}`);
959
- }
960
- });
961
- const todosCommand = program.command("todos").description("Built-in todos extension commands.");
962
- todosCommand
963
- .command("import")
964
- .description("Import todos markdown files as PM items.")
965
- .option("--folder <path>", "Path to todos markdown folder", ".pi/todos")
966
- .option("--author <value>", "Mutation author")
967
- .option("--message <value>", "History message for import entries")
2853
+ .command("aggregate")
2854
+ .description("Aggregate grouped item counts for governance queries.")
2855
+ .option("--group-by <value>", "Comma-separated group-by fields (supported: parent,type,priority,status,assignee,tags,sprint,release)")
2856
+ .option("--count", "Return grouped counts (default behavior)")
2857
+ .option("--include-unparented", "Include unparented rows when grouping by parent")
2858
+ .option("--include_unparented", "Alias for --include-unparented")
2859
+ .option("--status <value>", "Filter by item status")
2860
+ .option("--type <value>", "Filter by item type")
2861
+ .option("--tag <value>", "Filter by tag")
2862
+ .option("--priority <value>", "Filter by priority")
2863
+ .option("--deadline-before <value>", "Filter by deadline upper bound (ISO/date string or relative)")
2864
+ .option("--deadline-after <value>", "Filter by deadline lower bound (ISO/date string or relative)")
2865
+ .option("--assignee <value>", "Filter by assignee")
2866
+ .option("--assignee-filter <value>", "Filter assignee presence: assigned|unassigned")
2867
+ .option("--assignee_filter <value>", "Alias for --assignee-filter")
2868
+ .option("--parent <value>", "Filter by parent item ID")
2869
+ .option("--sprint <value>", "Filter by sprint")
2870
+ .option("--release <value>", "Filter by release")
968
2871
  .action(async (options, command) => {
969
2872
  const globalOptions = getGlobalOptions(command);
970
2873
  const startedAt = Date.now();
971
- const result = await runRequiredExtensionCommand(command, options, globalOptions);
972
- await invalidateSearchCachesForMutation(globalOptions, result);
2874
+ const result = await runAggregate(normalizeAggregateOptions(options), globalOptions);
973
2875
  printResult(result, globalOptions);
974
2876
  if (globalOptions.profile) {
975
- printError(`profile:command=todos import took_ms=${Date.now() - startedAt}`);
2877
+ printError(`profile:command=aggregate took_ms=${Date.now() - startedAt}`);
976
2878
  }
977
2879
  });
978
- todosCommand
979
- .command("export")
980
- .description("Export PM items to todos markdown files.")
981
- .option("--folder <path>", "Path to todos markdown folder", ".pi/todos")
2880
+ program
2881
+ .command("dedupe-audit")
2882
+ .description("Audit potential duplicate items with exact, fuzzy, or parent-scoped matching.")
2883
+ .option("--mode <value>", "Dedupe mode: title_exact|title_fuzzy|parent_scope")
2884
+ .option("--limit <n>", "Limit returned duplicate clusters")
2885
+ .option("--threshold <value>", "Fuzzy mode token similarity threshold between 0 and 1")
2886
+ .option("--status <value>", "Filter by item status")
2887
+ .option("--type <value>", "Filter by item type")
2888
+ .option("--tag <value>", "Filter by tag")
2889
+ .option("--priority <value>", "Filter by priority")
2890
+ .option("--deadline-before <value>", "Filter by deadline upper bound (ISO/date string or relative)")
2891
+ .option("--deadline-after <value>", "Filter by deadline lower bound (ISO/date string or relative)")
2892
+ .option("--assignee <value>", "Filter by assignee")
2893
+ .option("--assignee-filter <value>", "Filter assignee presence: assigned|unassigned")
2894
+ .option("--assignee_filter <value>", "Alias for --assignee-filter")
2895
+ .option("--parent <value>", "Filter by parent item ID")
2896
+ .option("--sprint <value>", "Filter by sprint")
2897
+ .option("--release <value>", "Filter by release")
982
2898
  .action(async (options, command) => {
983
2899
  const globalOptions = getGlobalOptions(command);
984
2900
  const startedAt = Date.now();
985
- const result = await runRequiredExtensionCommand(command, options, globalOptions);
2901
+ const result = await runDedupeAudit(normalizeDedupeAuditOptions(options), globalOptions);
986
2902
  printResult(result, globalOptions);
987
2903
  if (globalOptions.profile) {
988
- printError(`profile:command=todos export took_ms=${Date.now() - startedAt}`);
2904
+ printError(`profile:command=dedupe-audit took_ms=${Date.now() - startedAt}`);
989
2905
  }
990
2906
  });
2907
+ function registerCalendarCommand() {
2908
+ program
2909
+ .command("calendar")
2910
+ .alias("cal")
2911
+ .description("Show deadline/reminder calendar views (agenda/day/week/month).")
2912
+ .option("--view <value>", "Calendar view: agenda|day|week|month (default: agenda)")
2913
+ .option("--date <value>", "Anchor date/time for view calculations (ISO/date string or relative)")
2914
+ .option("--from <value>", "Agenda lower bound (ISO/date string or relative)")
2915
+ .option("--to <value>", "Agenda upper bound (ISO/date string or relative)")
2916
+ .option("--past", "Include past entries in the selected view")
2917
+ .option("--full-period", "For day/week/month views, include the full anchored period without now-clipping")
2918
+ .option("--full_period", "Alias for --full-period")
2919
+ .option("--type <value>", "Filter by item type")
2920
+ .option("--tag <value>", "Filter by tag")
2921
+ .option("--priority <value>", "Filter by priority")
2922
+ .option("--status <value>", "Filter by status")
2923
+ .option("--assignee <value>", "Filter by assignee")
2924
+ .option("--assignee-filter <value>", "Filter assignee presence: assigned|unassigned")
2925
+ .option("--assignee_filter <value>", "Alias for --assignee-filter")
2926
+ .option("--sprint <value>", "Filter by sprint")
2927
+ .option("--release <value>", "Filter by release")
2928
+ .option("--include <value>", "Include sources: deadlines|reminders|events|all (comma or | separated)")
2929
+ .option("--recurrence-lookahead-days <n>", "Bound open-ended recurrence generation lookahead days")
2930
+ .option("--recurrence_lookahead_days <n>", "Alias for --recurrence-lookahead-days")
2931
+ .option("--recurrence-lookback-days <n>", "Bound open-ended recurrence generation lookback days")
2932
+ .option("--recurrence_lookback_days <n>", "Alias for --recurrence-lookback-days")
2933
+ .option("--occurrence-limit <n>", "Cap generated occurrences per recurring event")
2934
+ .option("--occurrence_limit <n>", "Alias for --occurrence-limit")
2935
+ .option("--limit <n>", "Limit returned event count")
2936
+ .option("--format <value>", "Calendar output format override: markdown|toon|json")
2937
+ .action(async (options, actionCommand) => {
2938
+ const globalOptions = getGlobalOptions(actionCommand);
2939
+ const startedAt = Date.now();
2940
+ const normalized = normalizeCalendarOptions(options);
2941
+ const result = await runCalendar(normalized, globalOptions);
2942
+ const outputFormat = resolveCalendarOutputFormat(normalized, globalOptions);
2943
+ if (outputFormat === "markdown") {
2944
+ if (!globalOptions.quiet) {
2945
+ writeStdout(`${renderCalendarMarkdown(result)}\n`);
2946
+ }
2947
+ }
2948
+ else {
2949
+ printResult(result, {
2950
+ ...globalOptions,
2951
+ json: outputFormat === "json",
2952
+ });
2953
+ }
2954
+ if (globalOptions.profile) {
2955
+ printError(`profile:command=calendar took_ms=${Date.now() - startedAt}`);
2956
+ }
2957
+ });
2958
+ }
2959
+ registerCalendarCommand();
2960
+ function registerContextCommand() {
2961
+ program
2962
+ .command("context")
2963
+ .alias("ctx")
2964
+ .description("Show a token-efficient project context snapshot for next-work decisions.")
2965
+ .option("--date <value>", "Anchor date/time for agenda window calculations (ISO/date string or relative)")
2966
+ .option("--from <value>", "Agenda lower bound (ISO/date string or relative)")
2967
+ .option("--to <value>", "Agenda upper bound (ISO/date string or relative)")
2968
+ .option("--past", "Include past agenda entries in bounded windows")
2969
+ .option("--type <value>", "Filter by item type")
2970
+ .option("--tag <value>", "Filter by tag")
2971
+ .option("--priority <value>", "Filter by priority")
2972
+ .option("--assignee <value>", "Filter by assignee")
2973
+ .option("--assignee-filter <value>", "Filter assignee presence: assigned|unassigned")
2974
+ .option("--assignee_filter <value>", "Alias for --assignee-filter")
2975
+ .option("--sprint <value>", "Filter by sprint")
2976
+ .option("--release <value>", "Filter by release")
2977
+ .option("--limit <n>", "Limit focus and agenda rows per section")
2978
+ .option("--format <value>", "Context output format override: markdown|toon|json")
2979
+ .action(async (options, actionCommand) => {
2980
+ const globalOptions = getGlobalOptions(actionCommand);
2981
+ const startedAt = Date.now();
2982
+ const normalized = normalizeContextOptions(options);
2983
+ const result = await runContext(normalized, globalOptions);
2984
+ const outputFormat = resolveContextOutputFormat(normalized, globalOptions);
2985
+ if (outputFormat === "markdown") {
2986
+ if (!globalOptions.quiet) {
2987
+ writeStdout(`${renderContextMarkdown(result)}\n`);
2988
+ }
2989
+ }
2990
+ else {
2991
+ printResult(result, {
2992
+ ...globalOptions,
2993
+ json: outputFormat === "json",
2994
+ });
2995
+ }
2996
+ if (globalOptions.profile) {
2997
+ printError(`profile:command=context took_ms=${Date.now() - startedAt}`);
2998
+ }
2999
+ });
3000
+ }
3001
+ registerContextCommand();
991
3002
  program
992
3003
  .command("search")
993
- .argument("<keywords>", "Keyword query string")
3004
+ .argument("<keywords...>", "Keyword query tokens")
994
3005
  .description("Search items with keyword, semantic, or hybrid modes.")
995
- .option("--mode <value>", "Search mode: keyword|semantic|hybrid (default: hybrid when semantic config is available, else keyword)")
3006
+ .option("--mode <value>", "Search mode: keyword|semantic|hybrid (default: hybrid when semantic config or local Ollama auto-defaults are available, else keyword)")
996
3007
  .option("--include-linked", "Include readable linked docs/files/tests content in keyword and hybrid lexical scoring")
3008
+ .option("--title-exact", "Require exact normalized title match against the full query")
3009
+ .option("--phrase-exact", "Require exact normalized query phrase match in item text fields")
997
3010
  .option("--type <value>", "Filter by item type")
998
3011
  .option("--tag <value>", "Filter by tag")
999
3012
  .option("--priority <value>", "Filter by priority")
1000
- .option("--deadline-before <value>", "Filter by deadline upper bound")
1001
- .option("--deadline-after <value>", "Filter by deadline lower bound")
3013
+ .option("--deadline-before <value>", "Filter by deadline upper bound (ISO/date string or relative)")
3014
+ .option("--deadline-after <value>", "Filter by deadline lower bound (ISO/date string or relative)")
3015
+ .option("--compact", "Render compact search hits (default; mutually exclusive with --full/--fields)")
3016
+ .option("--full", "Render full search hits with nested item payloads (mutually exclusive with --compact/--fields)")
3017
+ .option("--fields <value>", "Render custom comma-separated search hit fields (mutually exclusive with --compact/--full; valid: --fields id,title,score; invalid: --full --fields id,title)")
1002
3018
  .option("--limit <n>", "Limit returned item count")
1003
3019
  .action(async (keywords, options, command) => {
1004
3020
  const globalOptions = getGlobalOptions(command);
1005
3021
  const startedAt = Date.now();
1006
- const result = await runSearch(keywords, normalizeSearchOptions(options), globalOptions);
3022
+ const result = await runSearch(normalizeSearchKeywordsInput(keywords), normalizeSearchOptions(options), globalOptions);
1007
3023
  printResult(result, globalOptions);
1008
3024
  if (globalOptions.profile) {
1009
3025
  printError(`profile:command=search took_ms=${Date.now() - startedAt}`);
@@ -1013,11 +3029,13 @@ program
1013
3029
  .command("reindex")
1014
3030
  .description("Rebuild search artifacts for keyword, semantic, and hybrid modes.")
1015
3031
  .option("--mode <value>", "Reindex mode: keyword|semantic|hybrid", "keyword")
3032
+ .option("--progress", "Emit progress updates to stderr (always shown in TTY, opt-in for non-TTY)")
1016
3033
  .action(async (options, command) => {
1017
3034
  const globalOptions = getGlobalOptions(command);
1018
3035
  const startedAt = Date.now();
1019
3036
  const result = await runReindex({
1020
3037
  mode: typeof options.mode === "string" ? options.mode : undefined,
3038
+ progress: Boolean(options.progress),
1021
3039
  }, globalOptions);
1022
3040
  printResult(result, globalOptions);
1023
3041
  if (globalOptions.profile) {
@@ -1041,12 +3059,16 @@ program
1041
3059
  .command("history")
1042
3060
  .argument("<id>", "Item id")
1043
3061
  .option("--limit <n>", "Return only the latest n history entries")
3062
+ .option("--diff", "Include per-entry changed field summaries from history patches")
3063
+ .option("--verify", "Verify hash chain and replay integrity for the full history stream")
1044
3064
  .description("Show item history entries.")
1045
3065
  .action(async (id, options, command) => {
1046
3066
  const globalOptions = getGlobalOptions(command);
1047
3067
  const startedAt = Date.now();
1048
3068
  const result = await runHistory(id, {
1049
3069
  limit: typeof options.limit === "string" ? options.limit : undefined,
3070
+ diff: Boolean(options.diff),
3071
+ verify: Boolean(options.verify),
1050
3072
  }, globalOptions);
1051
3073
  printResult(result, globalOptions);
1052
3074
  if (globalOptions.profile) {
@@ -1055,15 +3077,29 @@ program
1055
3077
  });
1056
3078
  program
1057
3079
  .command("activity")
3080
+ .option("--id <value>", "Filter by item ID")
3081
+ .option("--op <value>", "Filter by history operation")
3082
+ .option("--author <value>", "Filter by history author")
3083
+ .option("--from <value>", "Lower timestamp bound (ISO/date string or relative)")
3084
+ .option("--to <value>", "Upper timestamp bound (ISO/date string or relative)")
1058
3085
  .option("--limit <n>", "Return only the latest n activity entries")
3086
+ .option("--stream [mode]", "Emit line-delimited JSON rows (requires --json). Optional mode: rows|ndjson|jsonl")
1059
3087
  .description("Show recent activity across items.")
1060
3088
  .action(async (options, command) => {
1061
3089
  const globalOptions = getGlobalOptions(command);
1062
3090
  const startedAt = Date.now();
1063
- const result = await runActivity({
1064
- limit: typeof options.limit === "string" ? options.limit : undefined,
1065
- }, globalOptions);
1066
- printResult(result, globalOptions);
3091
+ const normalized = normalizeActivityOptions(options);
3092
+ const result = await runActivity(normalized, globalOptions);
3093
+ const streamMode = resolveActivityStreamMode(options.stream);
3094
+ if (streamMode && !globalOptions.json) {
3095
+ throw new PmCliError("--stream requires --json output mode.", EXIT_CODE.USAGE);
3096
+ }
3097
+ if (streamMode) {
3098
+ printActivityJsonStream(result, normalized, globalOptions);
3099
+ }
3100
+ else {
3101
+ printResult(result, globalOptions);
3102
+ }
1067
3103
  if (globalOptions.profile) {
1068
3104
  printError(`profile:command=activity took_ms=${Date.now() - startedAt}`);
1069
3105
  }
@@ -1096,60 +3132,93 @@ program
1096
3132
  .description("Update item fields and metadata.")
1097
3133
  .option("--title, -t <value>", "Set title")
1098
3134
  .option("--description, -d <value>", "Set description")
3135
+ .option("--body, -b <value>", "Set body (allow empty string)")
1099
3136
  .option("--status, -s <value>", "Set status (use close command for closed)")
3137
+ .option("--close-reason <value>", "Set close reason")
3138
+ .option("--close_reason <value>", "Alias for --close-reason")
1100
3139
  .option("--priority, -p <value>", "Set priority")
1101
3140
  .option("--type <value>", "Set type")
1102
3141
  .option("--tags <value>", "Set comma-separated tags")
1103
- .option("--deadline <value>", "Set deadline (or none)")
1104
- .option("--estimate, --estimated-minutes <value>", "Set estimated minutes (or none)")
3142
+ .option("--deadline <value>", "Set deadline (ISO/date string or relative)")
3143
+ .option("--estimate, --estimated-minutes <value>", "Set estimated minutes")
1105
3144
  .option("--estimated_minutes <value>", "Alias for --estimated-minutes")
1106
- .option("--acceptance-criteria <value>", "Set acceptance criteria (or none)")
3145
+ .option("--acceptance-criteria <value>", "Set acceptance criteria")
1107
3146
  .option("--acceptance_criteria <value>", "Alias for --acceptance-criteria")
1108
3147
  .option("--ac <value>", "Alias for --acceptance-criteria")
1109
- .option("--definition-of-ready <value>", "Set definition of ready (or none)")
3148
+ .option("--definition-of-ready <value>", "Set definition of ready")
1110
3149
  .option("--definition_of_ready <value>", "Alias for --definition-of-ready")
1111
- .option("--order <value>", "Set planning order/rank integer (or none)")
3150
+ .option("--order <value>", "Set planning order/rank integer")
1112
3151
  .option("--rank <value>", "Alias for --order")
1113
- .option("--goal <value>", "Set goal identifier (or none)")
1114
- .option("--objective <value>", "Set objective identifier (or none)")
1115
- .option("--value <value>", "Set business value summary (or none)")
1116
- .option("--impact <value>", "Set business impact summary (or none)")
1117
- .option("--outcome <value>", "Set expected outcome summary (or none)")
1118
- .option("--why-now <value>", "Set why-now rationale (or none)")
3152
+ .option("--goal <value>", "Set goal identifier")
3153
+ .option("--objective <value>", "Set objective identifier")
3154
+ .option("--value <value>", "Set business value summary")
3155
+ .option("--impact <value>", "Set business impact summary")
3156
+ .option("--outcome <value>", "Set expected outcome summary")
3157
+ .option("--why-now <value>", "Set why-now rationale")
1119
3158
  .option("--why_now <value>", "Alias for --why-now")
1120
3159
  .option("--author <value>", "Mutation author")
1121
3160
  .option("--message <value>", "Mutation message")
1122
- .option("--assignee <value>", "Set assignee (or none)")
1123
- .option("--parent <value>", "Set parent item ID (or none)")
1124
- .option("--reviewer <value>", "Set reviewer (or none)")
1125
- .option("--risk <value>", "Set risk level: low|med|medium|high|critical (or none; med persists as medium)")
1126
- .option("--confidence <value>", "Set confidence level: 0..100|low|med|medium|high (or none; med persists as medium)")
1127
- .option("--sprint <value>", "Set sprint identifier (or none)")
1128
- .option("--release <value>", "Set release identifier (or none)")
1129
- .option("--blocked-by <value>", "Set blocked-by item ID or reason (or none)")
3161
+ .option("--assignee <value>", "Set assignee")
3162
+ .option("--parent <value>", "Set parent item ID")
3163
+ .option("--reviewer <value>", "Set reviewer")
3164
+ .option("--risk <value>", "Set risk level: low|med|medium|high|critical (med persists as medium)")
3165
+ .option("--confidence <value>", "Set confidence level: 0..100|low|med|medium|high (med persists as medium)")
3166
+ .option("--sprint <value>", "Set sprint identifier")
3167
+ .option("--release <value>", "Set release identifier")
3168
+ .option("--blocked-by <value>", "Set blocked-by item ID or reason")
1130
3169
  .option("--blocked_by <value>", "Alias for --blocked-by")
1131
- .option("--blocked-reason <value>", "Set blocked reason (or none)")
3170
+ .option("--blocked-reason <value>", "Set blocked reason")
1132
3171
  .option("--blocked_reason <value>", "Alias for --blocked-reason")
1133
- .option("--unblock-note <value>", "Set unblock rationale note (or none)")
3172
+ .option("--unblock-note <value>", "Set unblock rationale note")
1134
3173
  .option("--unblock_note <value>", "Alias for --unblock-note")
1135
- .option("--reporter <value>", "Set issue reporter (or none)")
1136
- .option("--severity <value>", "Set issue severity: low|med|medium|high|critical (or none; med persists as medium)")
1137
- .option("--environment <value>", "Set issue environment context (or none)")
1138
- .option("--repro-steps <value>", "Set issue reproduction steps (or none)")
3174
+ .option("--reporter <value>", "Set issue reporter")
3175
+ .option("--severity <value>", "Set issue severity: low|med|medium|high|critical (med persists as medium)")
3176
+ .option("--environment <value>", "Set issue environment context")
3177
+ .option("--repro-steps <value>", "Set issue reproduction steps")
1139
3178
  .option("--repro_steps <value>", "Alias for --repro-steps")
1140
- .option("--resolution <value>", "Set issue resolution summary (or none)")
1141
- .option("--expected-result <value>", "Set issue expected behavior (or none)")
3179
+ .option("--resolution <value>", "Set issue resolution summary")
3180
+ .option("--expected-result <value>", "Set issue expected behavior")
1142
3181
  .option("--expected_result <value>", "Alias for --expected-result")
1143
- .option("--actual-result <value>", "Set issue observed behavior (or none)")
3182
+ .option("--actual-result <value>", "Set issue observed behavior")
1144
3183
  .option("--actual_result <value>", "Alias for --actual-result")
1145
- .option("--affected-version <value>", "Set affected version identifier (or none)")
3184
+ .option("--affected-version <value>", "Set affected version identifier")
1146
3185
  .option("--affected_version <value>", "Alias for --affected-version")
1147
- .option("--fixed-version <value>", "Set fixed version identifier (or none)")
3186
+ .option("--fixed-version <value>", "Set fixed version identifier")
1148
3187
  .option("--fixed_version <value>", "Alias for --fixed-version")
1149
- .option("--component <value>", "Set issue component ownership (or none)")
1150
- .option("--regression <value>", "Set regression marker: true|false|1|0 (or none)")
1151
- .option("--customer-impact <value>", "Set customer impact summary (or none)")
3188
+ .option("--component <value>", "Set issue component ownership")
3189
+ .option("--regression <value>", "Set regression marker: true|false|1|0")
3190
+ .option("--customer-impact <value>", "Set customer impact summary")
1152
3191
  .option("--customer_impact <value>", "Alias for --customer-impact")
3192
+ .option("--dep <value>", "Add dependency entries id=<id>,kind=<value>,author=<value>,created_at=<iso|now>,source_kind=<value> (repeatable)", collect)
3193
+ .option("--dep-remove <value>", "Remove dependencies by id or id=<id>,kind=<value>,source_kind=<value> selectors (repeatable)", collect)
3194
+ .option("--dep_remove <value>", "Alias for --dep-remove", collect)
3195
+ .option("--replace-deps", "Atomically replace dependency entries with the provided --dep values")
3196
+ .option("--replace-tests", "Atomically replace linked test entries with the provided --test values")
3197
+ .option("--comment <value>", "Append comment seed author=<value>,created_at=<iso|now>,text=<value> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3198
+ .option("--note <value>", "Append note seed author=<value>,created_at=<iso|now>,text=<value> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3199
+ .option("--learning <value>", "Append learning seed author=<value>,created_at=<iso|now>,text=<value> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3200
+ .option("--file <value>", "Append linked file path=<value>,scope=<project|global>,note=<text> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3201
+ .option("--test <value>", "Append linked test command=<value>,path=<value>,scope=<project|global>,timeout_seconds=<n>,pm_context_mode=<schema|tracker|auto> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3202
+ .option("--doc <value>", "Append linked doc path=<value>,scope=<project|global>,note=<text> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3203
+ .option("--reminder <value>", "Set reminders at=<iso|relative>,text=<text> (also accepts markdown pairs and - for stdin; repeatable)", collect)
3204
+ .option("--event <value>", "Set events start=<iso|relative>,end=<iso|relative>,title=<text>,all_day=<true|false>,recur_* fields (also accepts markdown pairs and - for stdin; repeatable)", collect)
3205
+ .option("--type-option <value>", "Set type options key=value or key=<name>,value=<value> (also accepts key:value and markdown pairs; use - for stdin; repeatable)", collect)
3206
+ .option("--type_option <value>", "Alias for --type-option", collect)
3207
+ .option("--unset <field>", "Clear scalar metadata field by name (repeatable)", collect)
3208
+ .option("--clear-deps", "Clear dependency entries")
3209
+ .option("--clear-comments", "Clear comments")
3210
+ .option("--clear-notes", "Clear notes")
3211
+ .option("--clear-learnings", "Clear learnings")
3212
+ .option("--clear-files", "Clear linked files")
3213
+ .option("--clear-tests", "Clear linked tests")
3214
+ .option("--clear-docs", "Clear linked docs")
3215
+ .option("--clear-reminders", "Clear reminders")
3216
+ .option("--clear-events", "Clear events")
3217
+ .option("--clear-type-options", "Clear type options")
3218
+ .option("--allow-audit-update", "Allow non-owner metadata-only audit updates without requiring --force")
3219
+ .option("--allow_audit_update", "Alias for --allow-audit-update")
3220
+ .option("--allow-audit-dep-update", "Allow non-owner append-only dependency updates without requiring --force")
3221
+ .option("--allow_audit_dep_update", "Alias for --allow-audit-dep-update")
1153
3222
  .option("--force", "Force ownership override")
1154
3223
  .action(async (id, options, command) => {
1155
3224
  const globalOptions = getGlobalOptions(command);
@@ -1161,12 +3230,221 @@ program
1161
3230
  printError(`profile:command=update took_ms=${Date.now() - startedAt}`);
1162
3231
  }
1163
3232
  });
3233
+ program
3234
+ .command("update-many")
3235
+ .description("Bulk-update matched items with dry-run plans and rollback checkpoints.")
3236
+ .option("--filter-status <value>", "Filter by status before applying updates")
3237
+ .option("--filter-type <value>", "Filter by item type before applying updates")
3238
+ .option("--filter-tag <value>", "Filter by tag before applying updates")
3239
+ .option("--filter-priority <value>", "Filter by priority before applying updates")
3240
+ .option("--filter-deadline-before <value>", "Filter by deadline upper bound before applying updates")
3241
+ .option("--filter-deadline-after <value>", "Filter by deadline lower bound before applying updates")
3242
+ .option("--filter-assignee <value>", "Filter by assignee before applying updates")
3243
+ .option("--filter-assignee-filter <value>", "Filter assignee presence: assigned|unassigned before applying updates")
3244
+ .option("--filter-assignee_filter <value>", "Alias for --filter-assignee-filter")
3245
+ .option("--filter-parent <value>", "Filter by parent item ID before applying updates")
3246
+ .option("--filter-sprint <value>", "Filter by sprint before applying updates")
3247
+ .option("--filter-release <value>", "Filter by release before applying updates")
3248
+ .option("--limit <n>", "Limit matched item count before apply/preview")
3249
+ .option("--offset <n>", "Skip first n matched rows before apply/preview")
3250
+ .option("--dry-run", "Preview per-item diffs and checkpoint intent without mutating")
3251
+ .option("--rollback <value>", "Rollback a prior update-many checkpoint ID")
3252
+ .option("--no-checkpoint", "Disable checkpoint creation during apply mode")
3253
+ .option("--title, -t <value>", "Set title")
3254
+ .option("--description, -d <value>", "Set description")
3255
+ .option("--body, -b <value>", "Set body (allow empty string)")
3256
+ .option("--status, -s <value>", "Set status (use close command for closed)")
3257
+ .option("--priority, -p <value>", "Set priority")
3258
+ .option("--type <value>", "Set type")
3259
+ .option("--tags <value>", "Set comma-separated tags")
3260
+ .option("--deadline <value>", "Set deadline (ISO/date string or relative)")
3261
+ .option("--estimate, --estimated-minutes <value>", "Set estimated minutes")
3262
+ .option("--estimated_minutes <value>", "Alias for --estimated-minutes")
3263
+ .option("--acceptance-criteria <value>", "Set acceptance criteria")
3264
+ .option("--acceptance_criteria <value>", "Alias for --acceptance-criteria")
3265
+ .option("--ac <value>", "Alias for --acceptance-criteria")
3266
+ .option("--definition-of-ready <value>", "Set definition of ready")
3267
+ .option("--definition_of_ready <value>", "Alias for --definition-of-ready")
3268
+ .option("--order <value>", "Set planning order/rank integer")
3269
+ .option("--rank <value>", "Alias for --order")
3270
+ .option("--goal <value>", "Set goal identifier")
3271
+ .option("--objective <value>", "Set objective identifier")
3272
+ .option("--value <value>", "Set business value summary")
3273
+ .option("--impact <value>", "Set business impact summary")
3274
+ .option("--outcome <value>", "Set expected outcome summary")
3275
+ .option("--why-now <value>", "Set why-now rationale")
3276
+ .option("--why_now <value>", "Alias for --why-now")
3277
+ .option("--assignee <value>", "Set assignee")
3278
+ .option("--parent <value>", "Set parent item ID")
3279
+ .option("--reviewer <value>", "Set reviewer")
3280
+ .option("--risk <value>", "Set risk level")
3281
+ .option("--confidence <value>", "Set confidence level")
3282
+ .option("--sprint <value>", "Set sprint identifier")
3283
+ .option("--release <value>", "Set release identifier")
3284
+ .option("--blocked-by <value>", "Set blocked-by item ID or reason")
3285
+ .option("--blocked_by <value>", "Alias for --blocked-by")
3286
+ .option("--blocked-reason <value>", "Set blocked reason")
3287
+ .option("--blocked_reason <value>", "Alias for --blocked-reason")
3288
+ .option("--unblock-note <value>", "Set unblock rationale note")
3289
+ .option("--unblock_note <value>", "Alias for --unblock-note")
3290
+ .option("--reporter <value>", "Set issue reporter")
3291
+ .option("--severity <value>", "Set issue severity")
3292
+ .option("--environment <value>", "Set issue environment context")
3293
+ .option("--repro-steps <value>", "Set issue reproduction steps")
3294
+ .option("--repro_steps <value>", "Alias for --repro-steps")
3295
+ .option("--resolution <value>", "Set issue resolution summary")
3296
+ .option("--expected-result <value>", "Set issue expected behavior")
3297
+ .option("--expected_result <value>", "Alias for --expected-result")
3298
+ .option("--actual-result <value>", "Set issue observed behavior")
3299
+ .option("--actual_result <value>", "Alias for --actual-result")
3300
+ .option("--affected-version <value>", "Set affected version identifier")
3301
+ .option("--affected_version <value>", "Alias for --affected-version")
3302
+ .option("--fixed-version <value>", "Set fixed version identifier")
3303
+ .option("--fixed_version <value>", "Alias for --fixed-version")
3304
+ .option("--component <value>", "Set issue component ownership")
3305
+ .option("--regression <value>", "Set regression marker: true|false|1|0")
3306
+ .option("--customer-impact <value>", "Set customer impact summary")
3307
+ .option("--customer_impact <value>", "Alias for --customer-impact")
3308
+ .option("--dep <value>", "Add dependency entry id=<id>,kind=<kind>,author=<author>,created_at=<timestamp>", collect)
3309
+ .option("--dep-remove <value>", "Remove dependency entries by id/kind/author/timestamp signature", collect)
3310
+ .option("--dep_remove <value>", "Alias for --dep-remove", collect)
3311
+ .option("--replace-deps", "Atomically replace dependency entries with provided --dep values")
3312
+ .option("--replace-tests", "Atomically replace linked tests with provided --test values")
3313
+ .option("--comment <value>", "Add comment seed author=<value>,created_at=<iso|now>,text=<value>", collect)
3314
+ .option("--note <value>", "Add note seed author=<value>,created_at=<iso|now>,text=<value>", collect)
3315
+ .option("--learning <value>", "Add learning seed author=<value>,created_at=<iso|now>,text=<value>", collect)
3316
+ .option("--file <value>", "Add linked file path=<value>,scope=<project|global>,note=<text>", collect)
3317
+ .option("--test <value>", "Add linked test command=<value>,path=<value>,scope=<project|global>", collect)
3318
+ .option("--doc <value>", "Add linked doc path=<value>,scope=<project|global>,note=<text>", collect)
3319
+ .option("--reminder <value>", "Add reminder entry at=<iso|relative>,text=<text>", collect)
3320
+ .option("--event <value>", "Add event entry start=<iso|relative>,end=<iso|relative>,recur_*", collect)
3321
+ .option("--type-option <value>", "Set type options key=value (repeatable)", collect)
3322
+ .option("--type_option <value>", "Alias for --type-option", collect)
3323
+ .option("--unset <field>", "Clear scalar metadata field by name (repeatable)", collect)
3324
+ .option("--clear-deps", "Clear dependency entries")
3325
+ .option("--clear-comments", "Clear comments")
3326
+ .option("--clear-notes", "Clear notes")
3327
+ .option("--clear-learnings", "Clear learnings")
3328
+ .option("--clear-files", "Clear linked files")
3329
+ .option("--clear-tests", "Clear linked tests")
3330
+ .option("--clear-docs", "Clear linked docs")
3331
+ .option("--clear-reminders", "Clear reminders")
3332
+ .option("--clear-events", "Clear events")
3333
+ .option("--clear-type-options", "Clear type options")
3334
+ .option("--allow-audit-update", "Allow non-owner metadata-only audit updates without requiring --force")
3335
+ .option("--allow_audit_update", "Alias for --allow-audit-update")
3336
+ .option("--allow-audit-dep-update", "Allow non-owner append-only dependency updates without requiring --force")
3337
+ .option("--allow_audit_dep_update", "Alias for --allow-audit-dep-update")
3338
+ .option("--author <value>", "Mutation author")
3339
+ .option("--message <value>", "Mutation message")
3340
+ .option("--force", "Force ownership override")
3341
+ .action(async (options, command) => {
3342
+ const globalOptions = getGlobalOptions(command);
3343
+ const startedAt = Date.now();
3344
+ const result = await runUpdateMany({
3345
+ status: typeof options.filterStatus === "string" ? options.filterStatus : undefined,
3346
+ list: {
3347
+ type: typeof options.filterType === "string" ? options.filterType : undefined,
3348
+ tag: typeof options.filterTag === "string" ? options.filterTag : undefined,
3349
+ priority: typeof options.filterPriority === "string" ? options.filterPriority : undefined,
3350
+ deadlineBefore: typeof options.filterDeadlineBefore === "string" ? options.filterDeadlineBefore : undefined,
3351
+ deadlineAfter: typeof options.filterDeadlineAfter === "string" ? options.filterDeadlineAfter : undefined,
3352
+ assignee: typeof options.filterAssignee === "string" ? options.filterAssignee : undefined,
3353
+ assigneeFilter: typeof options.filterAssigneeFilter === "string"
3354
+ ? options.filterAssigneeFilter
3355
+ : typeof options.filterAssignee_filter === "string"
3356
+ ? options.filterAssignee_filter
3357
+ : undefined,
3358
+ parent: typeof options.filterParent === "string" ? options.filterParent : undefined,
3359
+ sprint: typeof options.filterSprint === "string" ? options.filterSprint : undefined,
3360
+ release: typeof options.filterRelease === "string" ? options.filterRelease : undefined,
3361
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3362
+ offset: typeof options.offset === "string" ? options.offset : undefined,
3363
+ includeBody: true,
3364
+ },
3365
+ update: normalizeUpdateOptions(extractUpdateManyMutationOptionSource(options)),
3366
+ dryRun: options.dryRun === true ? true : undefined,
3367
+ rollback: typeof options.rollback === "string" ? options.rollback : undefined,
3368
+ checkpoint: options.checkpoint === false ? false : undefined,
3369
+ }, globalOptions);
3370
+ await invalidateSearchCachesForMutation(globalOptions, result);
3371
+ printResult(result, globalOptions);
3372
+ if (globalOptions.profile) {
3373
+ printError(`profile:command=update-many took_ms=${Date.now() - startedAt}`);
3374
+ }
3375
+ });
3376
+ program
3377
+ .command("normalize")
3378
+ .description("Normalize lifecycle metadata with deterministic dry-run plans and optional apply mode.")
3379
+ .option("--filter-status <value>", "Filter by status before planning or apply")
3380
+ .option("--filter-type <value>", "Filter by item type before planning or apply")
3381
+ .option("--filter-tag <value>", "Filter by tag before planning or apply")
3382
+ .option("--filter-priority <value>", "Filter by priority before planning or apply")
3383
+ .option("--filter-deadline-before <value>", "Filter by deadline upper bound before planning or apply")
3384
+ .option("--filter-deadline-after <value>", "Filter by deadline lower bound before planning or apply")
3385
+ .option("--filter-assignee <value>", "Filter by assignee before planning or apply")
3386
+ .option("--filter-assignee-filter <value>", "Filter assignee presence: assigned|unassigned before planning or apply")
3387
+ .option("--filter-assignee_filter <value>", "Alias for --filter-assignee-filter")
3388
+ .option("--filter-parent <value>", "Filter by parent item ID before planning or apply")
3389
+ .option("--filter-sprint <value>", "Filter by sprint before planning or apply")
3390
+ .option("--filter-release <value>", "Filter by release before planning or apply")
3391
+ .option("--limit <n>", "Limit matched item count before planning/apply")
3392
+ .option("--offset <n>", "Skip first n matched rows before planning/apply")
3393
+ .option("--dry-run", "Preview normalize findings without mutating (default)")
3394
+ .option("--apply", "Apply normalize changes using update semantics")
3395
+ .option("--author <value>", "Mutation author for apply mode")
3396
+ .option("--message <value>", "Mutation message for apply mode")
3397
+ .option("--allow-audit-update", "Allow non-owner metadata-only audit updates without requiring --force")
3398
+ .option("--allow_audit_update", "Alias for --allow-audit-update")
3399
+ .option("--force", "Force ownership override for apply mode")
3400
+ .action(async (options, command) => {
3401
+ const globalOptions = getGlobalOptions(command);
3402
+ const startedAt = Date.now();
3403
+ const result = await runNormalize({
3404
+ status: typeof options.filterStatus === "string" ? options.filterStatus : undefined,
3405
+ list: {
3406
+ type: typeof options.filterType === "string" ? options.filterType : undefined,
3407
+ tag: typeof options.filterTag === "string" ? options.filterTag : undefined,
3408
+ priority: typeof options.filterPriority === "string" ? options.filterPriority : undefined,
3409
+ deadlineBefore: typeof options.filterDeadlineBefore === "string" ? options.filterDeadlineBefore : undefined,
3410
+ deadlineAfter: typeof options.filterDeadlineAfter === "string" ? options.filterDeadlineAfter : undefined,
3411
+ assignee: typeof options.filterAssignee === "string" ? options.filterAssignee : undefined,
3412
+ assigneeFilter: typeof options.filterAssigneeFilter === "string"
3413
+ ? options.filterAssigneeFilter
3414
+ : typeof options.filterAssignee_filter === "string"
3415
+ ? options.filterAssignee_filter
3416
+ : undefined,
3417
+ parent: typeof options.filterParent === "string" ? options.filterParent : undefined,
3418
+ sprint: typeof options.filterSprint === "string" ? options.filterSprint : undefined,
3419
+ release: typeof options.filterRelease === "string" ? options.filterRelease : undefined,
3420
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3421
+ offset: typeof options.offset === "string" ? options.offset : undefined,
3422
+ includeBody: true,
3423
+ },
3424
+ dryRun: options.dryRun === true ? true : undefined,
3425
+ apply: options.apply === true ? true : undefined,
3426
+ author: typeof options.author === "string" ? options.author : undefined,
3427
+ message: typeof options.message === "string" ? options.message : undefined,
3428
+ allowAuditUpdate: options.allowAuditUpdate === true || options.allow_audit_update === true || options.allowAudit_update === true
3429
+ ? true
3430
+ : undefined,
3431
+ force: options.force === true ? true : undefined,
3432
+ }, globalOptions);
3433
+ if (result.mode === "apply") {
3434
+ await invalidateSearchCachesForMutation(globalOptions, result);
3435
+ }
3436
+ printResult(result, globalOptions);
3437
+ if (globalOptions.profile) {
3438
+ printError(`profile:command=normalize took_ms=${Date.now() - startedAt}`);
3439
+ }
3440
+ });
1164
3441
  program
1165
3442
  .command("close")
1166
3443
  .argument("<id>", "Item id")
1167
3444
  .argument("<text>", "Close reason text")
1168
3445
  .option("--author <value>", "Mutation author")
1169
3446
  .option("--message <value>", "History message")
3447
+ .option("--validate-close [mode]", 'Validate closure metadata before close: "off", "warn", or "strict" (default: settings governance preset)')
1170
3448
  .option("--force", "Force ownership override")
1171
3449
  .description("Close an item with a required reason.")
1172
3450
  .action(async (id, text, options, command) => {
@@ -1175,6 +3453,11 @@ program
1175
3453
  const result = await runClose(id, text, {
1176
3454
  author: typeof options.author === "string" ? options.author : undefined,
1177
3455
  message: typeof options.message === "string" ? options.message : undefined,
3456
+ validateClose: options.validateClose === true
3457
+ ? "warn"
3458
+ : typeof options.validateClose === "string"
3459
+ ? options.validateClose
3460
+ : undefined,
1178
3461
  force: Boolean(options.force),
1179
3462
  }, globalOptions);
1180
3463
  await invalidateSearchCachesForMutation(globalOptions, result);
@@ -1207,7 +3490,7 @@ program
1207
3490
  program
1208
3491
  .command("append")
1209
3492
  .argument("<id>", "Item id")
1210
- .requiredOption("--body <value>", "Text to append to body")
3493
+ .requiredOption("--body <value>", "Text to append to body (or - for stdin)")
1211
3494
  .option("--author <value>", "Mutation author")
1212
3495
  .option("--message <value>", "Mutation message")
1213
3496
  .option("--force", "Force ownership override")
@@ -1224,58 +3507,196 @@ program
1224
3507
  await invalidateSearchCachesForMutation(globalOptions, result);
1225
3508
  printResult(result, globalOptions);
1226
3509
  if (globalOptions.profile) {
1227
- printError(`profile:command=append took_ms=${Date.now() - startedAt}`);
3510
+ printError(`profile:command=append took_ms=${Date.now() - startedAt}`);
3511
+ }
3512
+ });
3513
+ program
3514
+ .command("comments")
3515
+ .argument("<id>", "Item id")
3516
+ .argument("[text]", "Optional comment text shorthand (equivalent to --add; use - for stdin)")
3517
+ .option("--add <text>", "Add one comment entry (plain text fallback, text=<value>, markdown pairs, or - for stdin; CSV-like key fragments are preserved as plain text unless text is explicit)")
3518
+ .option("--limit <n>", "Return only latest n comments")
3519
+ .option("--author [value]", "Comment author (optional; falls back to PM_AUTHOR/settings)")
3520
+ .option("--message <value>", "History message")
3521
+ .option("--allow-audit-comment", "Allow non-owner append-only comment audits without requiring --force")
3522
+ .option("--force", "Force ownership override")
3523
+ .description("List or add comments for an item.")
3524
+ .action(async (id, text, options, command) => {
3525
+ const globalOptions = getGlobalOptions(command);
3526
+ const startedAt = Date.now();
3527
+ const addFromOption = typeof options.add === "string" ? options.add : undefined;
3528
+ const addFromPositional = typeof text === "string" ? text : undefined;
3529
+ if (addFromOption !== undefined && addFromPositional !== undefined) {
3530
+ throw new PmCliError("Specify comment text either as positional [text] or with --add, not both", EXIT_CODE.USAGE);
3531
+ }
3532
+ const add = addFromOption ?? addFromPositional;
3533
+ const result = await runComments(id, {
3534
+ add,
3535
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3536
+ author: typeof options.author === "string" ? options.author : undefined,
3537
+ message: typeof options.message === "string" ? options.message : undefined,
3538
+ allowAuditComment: Boolean(options.allowAuditComment),
3539
+ force: Boolean(options.force),
3540
+ }, globalOptions);
3541
+ if (typeof add === "string") {
3542
+ await invalidateSearchCachesForMutation(globalOptions, result);
3543
+ }
3544
+ printResult(result, globalOptions);
3545
+ if (globalOptions.profile) {
3546
+ printError(`profile:command=comments took_ms=${Date.now() - startedAt}`);
3547
+ }
3548
+ });
3549
+ program
3550
+ .command("comments-audit")
3551
+ .option("--status <value>", "Filter by item status")
3552
+ .option("--type <value>", "Filter by item type")
3553
+ .option("--tag <value>", "Filter by tag")
3554
+ .option("--priority <value>", "Filter by priority")
3555
+ .option("--parent <value>", "Filter by parent item ID")
3556
+ .option("--sprint <value>", "Filter by sprint")
3557
+ .option("--release <value>", "Filter by release")
3558
+ .option("--assignee <value>", "Filter by assignee")
3559
+ .option("--assignee-filter <value>", "Filter assignee presence: assigned|unassigned")
3560
+ .option("--assignee_filter <value>", "Alias for --assignee-filter")
3561
+ .option("--limit-items <n>", "Limit returned item count")
3562
+ .option("--limit <n>", "Alias for --limit-items")
3563
+ .option("--full-history", "Export full comment history rows (cannot be combined with --latest)")
3564
+ .option("--latest <n>", "Return latest n comments per item (default: 1, use 0 for summary-only rows)")
3565
+ .description("Audit latest comments or full comment history across filtered items.")
3566
+ .action(async (options, command) => {
3567
+ const globalOptions = getGlobalOptions(command);
3568
+ const startedAt = Date.now();
3569
+ const result = await runCommentsAudit({
3570
+ status: typeof options.status === "string" ? options.status : undefined,
3571
+ type: typeof options.type === "string" ? options.type : undefined,
3572
+ tag: typeof options.tag === "string" ? options.tag : undefined,
3573
+ priority: typeof options.priority === "string" ? options.priority : undefined,
3574
+ parent: typeof options.parent === "string" ? options.parent : undefined,
3575
+ sprint: typeof options.sprint === "string" ? options.sprint : undefined,
3576
+ release: typeof options.release === "string" ? options.release : undefined,
3577
+ assignee: typeof options.assignee === "string" ? options.assignee : undefined,
3578
+ assigneeFilter: typeof options.assigneeFilter === "string" ? options.assigneeFilter : undefined,
3579
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3580
+ limitItems: typeof options.limitItems === "string" ? options.limitItems : undefined,
3581
+ fullHistory: options.fullHistory === true,
3582
+ latest: typeof options.latest === "string" ? options.latest : undefined,
3583
+ }, globalOptions);
3584
+ printResult(result, globalOptions);
3585
+ if (globalOptions.profile) {
3586
+ printError(`profile:command=comments-audit took_ms=${Date.now() - startedAt}`);
3587
+ }
3588
+ });
3589
+ program
3590
+ .command("notes")
3591
+ .argument("<id>", "Item id")
3592
+ .argument("[text]", "Optional note text shorthand (equivalent to --add; use - for stdin)")
3593
+ .option("--add <text>", "Add one note entry (plain text fallback, text=<value>, markdown pairs, or - for stdin; CSV-like key fragments are preserved as plain text unless text is explicit)")
3594
+ .option("--limit <n>", "Return only latest n notes")
3595
+ .option("--author [value]", "Note author (optional; falls back to PM_AUTHOR/settings)")
3596
+ .option("--message <value>", "History message")
3597
+ .option("--allow-audit-note", "Allow non-owner append-only note audits without requiring --force")
3598
+ .option("--allow-audit-comment", "Backward-compatible alias for --allow-audit-note")
3599
+ .option("--force", "Force ownership override")
3600
+ .description("List or add notes for an item.")
3601
+ .action(async (id, text, options, command) => {
3602
+ const globalOptions = getGlobalOptions(command);
3603
+ const startedAt = Date.now();
3604
+ const addFromOption = typeof options.add === "string" ? options.add : undefined;
3605
+ const addFromPositional = typeof text === "string" ? text : undefined;
3606
+ if (addFromOption !== undefined && addFromPositional !== undefined) {
3607
+ throw new PmCliError("Specify note text either as positional [text] or with --add, not both", EXIT_CODE.USAGE);
3608
+ }
3609
+ const add = addFromOption ?? addFromPositional;
3610
+ const result = await runNotes(id, {
3611
+ add,
3612
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3613
+ author: typeof options.author === "string" ? options.author : undefined,
3614
+ message: typeof options.message === "string" ? options.message : undefined,
3615
+ allowAuditComment: Boolean(options.allowAuditNote || options.allowAuditComment),
3616
+ force: Boolean(options.force),
3617
+ }, globalOptions);
3618
+ if (typeof add === "string") {
3619
+ await invalidateSearchCachesForMutation(globalOptions, result);
3620
+ }
3621
+ printResult(result, globalOptions);
3622
+ if (globalOptions.profile) {
3623
+ printError(`profile:command=notes took_ms=${Date.now() - startedAt}`);
1228
3624
  }
1229
3625
  });
1230
3626
  program
1231
- .command("comments")
3627
+ .command("learnings")
1232
3628
  .argument("<id>", "Item id")
1233
- .option("--add <text>", "Add one comment entry")
1234
- .option("--limit <n>", "Return only latest n comments")
1235
- .option("--author <value>", "Comment author")
3629
+ .argument("[text]", "Optional learning text shorthand (equivalent to --add; use - for stdin)")
3630
+ .option("--add <text>", "Add one learning entry (plain text fallback, text=<value>, markdown pairs, or - for stdin; CSV-like key fragments are preserved as plain text unless text is explicit)")
3631
+ .option("--limit <n>", "Return only latest n learnings")
3632
+ .option("--author [value]", "Learning author (optional; falls back to PM_AUTHOR/settings)")
1236
3633
  .option("--message <value>", "History message")
3634
+ .option("--allow-audit-learning", "Allow non-owner append-only learning audits without requiring --force")
3635
+ .option("--allow-audit-comment", "Backward-compatible alias for --allow-audit-learning")
1237
3636
  .option("--force", "Force ownership override")
1238
- .description("List or add comments for an item.")
1239
- .action(async (id, options, command) => {
3637
+ .description("List or add learnings for an item.")
3638
+ .action(async (id, text, options, command) => {
1240
3639
  const globalOptions = getGlobalOptions(command);
1241
3640
  const startedAt = Date.now();
1242
- const result = await runComments(id, {
1243
- add: typeof options.add === "string" ? options.add : undefined,
3641
+ const addFromOption = typeof options.add === "string" ? options.add : undefined;
3642
+ const addFromPositional = typeof text === "string" ? text : undefined;
3643
+ if (addFromOption !== undefined && addFromPositional !== undefined) {
3644
+ throw new PmCliError("Specify learning text either as positional [text] or with --add, not both", EXIT_CODE.USAGE);
3645
+ }
3646
+ const add = addFromOption ?? addFromPositional;
3647
+ const result = await runLearnings(id, {
3648
+ add,
1244
3649
  limit: typeof options.limit === "string" ? options.limit : undefined,
1245
3650
  author: typeof options.author === "string" ? options.author : undefined,
1246
3651
  message: typeof options.message === "string" ? options.message : undefined,
3652
+ allowAuditComment: Boolean(options.allowAuditLearning || options.allowAuditComment),
1247
3653
  force: Boolean(options.force),
1248
3654
  }, globalOptions);
1249
- if (typeof options.add === "string") {
3655
+ if (typeof add === "string") {
1250
3656
  await invalidateSearchCachesForMutation(globalOptions, result);
1251
3657
  }
1252
3658
  printResult(result, globalOptions);
1253
3659
  if (globalOptions.profile) {
1254
- printError(`profile:command=comments took_ms=${Date.now() - startedAt}`);
3660
+ printError(`profile:command=learnings took_ms=${Date.now() - startedAt}`);
1255
3661
  }
1256
3662
  });
1257
- program
3663
+ const filesCommand = program
1258
3664
  .command("files")
3665
+ .description("Manage files linked to an item.");
3666
+ filesCommand
1259
3667
  .argument("<id>", "Item id")
1260
- .option("--add <value>", "Add linked file entry", collect)
1261
- .option("--remove <value>", "Remove linked file by path", collect)
3668
+ .option("--add <value>", "Add linked file entry (CSV/markdown pairs or - for stdin)", collect)
3669
+ .option("--add-glob <value>", "Add linked file entries from a glob (plain glob or pattern=<glob>,scope=<scope>,note=<text>; repeatable)", collect)
3670
+ .option("--remove <value>", "Remove linked file by path (path=<value>, path:<value>, plain path, or - for stdin)", collect)
3671
+ .option("--migrate <value>", "Migrate linked file paths in-place (from=<prefix>,to=<prefix>; repeatable)", collect)
3672
+ .option("--list", "List linked files without mutating")
3673
+ .option("--append-stable", "Preserve existing linked-file order and append new links without full-array resorting")
3674
+ .option("--validate-paths", "Validate linked file paths for existence and file shape")
3675
+ .option("--audit", "Audit linked file usage across all items for this item's linked paths")
1262
3676
  .option("--author <value>", "Mutation author")
1263
3677
  .option("--message <value>", "History message")
1264
3678
  .option("--force", "Force ownership override")
1265
- .description("Manage files linked to an item.")
1266
3679
  .action(async (id, options, command) => {
1267
3680
  const globalOptions = getGlobalOptions(command);
1268
3681
  const startedAt = Date.now();
1269
3682
  const addValues = Array.isArray(options.add) ? options.add : [];
3683
+ const addGlobValues = Array.isArray(options.addGlob) ? options.addGlob : [];
1270
3684
  const removeValues = Array.isArray(options.remove) ? options.remove : [];
3685
+ const migrateValues = Array.isArray(options.migrate) ? options.migrate : [];
1271
3686
  const result = await runFiles(id, {
1272
3687
  add: addValues,
3688
+ addGlob: addGlobValues,
1273
3689
  remove: removeValues,
3690
+ migrate: migrateValues,
3691
+ list: Boolean(options.list),
3692
+ appendStable: Boolean(options.appendStable),
3693
+ validatePaths: Boolean(options.validatePaths),
3694
+ audit: Boolean(options.audit),
1274
3695
  author: typeof options.author === "string" ? options.author : undefined,
1275
3696
  message: typeof options.message === "string" ? options.message : undefined,
1276
3697
  force: Boolean(options.force),
1277
3698
  }, globalOptions);
1278
- if (addValues.length > 0 || removeValues.length > 0) {
3699
+ if (addValues.length > 0 || addGlobValues.length > 0 || removeValues.length > 0 || migrateValues.length > 0) {
1279
3700
  await invalidateSearchCachesForMutation(globalOptions, result);
1280
3701
  }
1281
3702
  printResult(result, globalOptions);
@@ -1283,11 +3704,44 @@ program
1283
3704
  printError(`profile:command=files took_ms=${Date.now() - startedAt}`);
1284
3705
  }
1285
3706
  });
3707
+ filesCommand
3708
+ .command("discover")
3709
+ .argument("<id>", "Item id")
3710
+ .option("--apply", "Add discovered missing files to the item")
3711
+ .option("--note <value>", "Note to attach to discovered file links")
3712
+ .option("--append-stable", "Preserve existing linked-file order and append discovered links without full-array resorting")
3713
+ .option("--author <value>", "Mutation author")
3714
+ .option("--message <value>", "History message")
3715
+ .option("--force", "Force ownership override")
3716
+ .description("Discover existing file paths referenced in item text and optionally link missing files.")
3717
+ .action(async (id, options, command) => {
3718
+ const globalOptions = getGlobalOptions(command);
3719
+ const startedAt = Date.now();
3720
+ const result = await runFilesDiscover(id, {
3721
+ apply: Boolean(options.apply),
3722
+ note: typeof options.note === "string" ? options.note : undefined,
3723
+ appendStable: Boolean(options.appendStable),
3724
+ author: typeof options.author === "string" ? options.author : undefined,
3725
+ message: typeof options.message === "string" ? options.message : undefined,
3726
+ force: Boolean(options.force),
3727
+ }, globalOptions);
3728
+ if (result.changed) {
3729
+ await invalidateSearchCachesForMutation(globalOptions, result);
3730
+ }
3731
+ printResult(result, globalOptions);
3732
+ if (globalOptions.profile) {
3733
+ printError(`profile:command=files.discover took_ms=${Date.now() - startedAt}`);
3734
+ }
3735
+ });
1286
3736
  program
1287
3737
  .command("docs")
1288
3738
  .argument("<id>", "Item id")
1289
- .option("--add <value>", "Add linked doc entry", collect)
1290
- .option("--remove <value>", "Remove linked doc by path", collect)
3739
+ .option("--add <value>", "Add linked doc entry (CSV/markdown pairs or - for stdin)", collect)
3740
+ .option("--add-glob <value>", "Add linked doc entries from a glob (plain glob or pattern=<glob>,scope=<scope>,note=<text>; repeatable)", collect)
3741
+ .option("--remove <value>", "Remove linked doc by path (path=<value>, path:<value>, plain path, or - for stdin)", collect)
3742
+ .option("--migrate <value>", "Migrate linked doc paths in-place (from=<prefix>,to=<prefix>; repeatable)", collect)
3743
+ .option("--validate-paths", "Validate linked doc paths for existence and file shape")
3744
+ .option("--audit", "Audit linked doc usage across all items for this item's linked paths")
1291
3745
  .option("--author <value>", "Mutation author")
1292
3746
  .option("--message <value>", "History message")
1293
3747
  .option("--force", "Force ownership override")
@@ -1296,15 +3750,21 @@ program
1296
3750
  const globalOptions = getGlobalOptions(command);
1297
3751
  const startedAt = Date.now();
1298
3752
  const addValues = Array.isArray(options.add) ? options.add : [];
3753
+ const addGlobValues = Array.isArray(options.addGlob) ? options.addGlob : [];
1299
3754
  const removeValues = Array.isArray(options.remove) ? options.remove : [];
3755
+ const migrateValues = Array.isArray(options.migrate) ? options.migrate : [];
1300
3756
  const result = await runDocs(id, {
1301
3757
  add: addValues,
3758
+ addGlob: addGlobValues,
1302
3759
  remove: removeValues,
3760
+ migrate: migrateValues,
3761
+ validatePaths: Boolean(options.validatePaths),
3762
+ audit: Boolean(options.audit),
1303
3763
  author: typeof options.author === "string" ? options.author : undefined,
1304
3764
  message: typeof options.message === "string" ? options.message : undefined,
1305
3765
  force: Boolean(options.force),
1306
3766
  }, globalOptions);
1307
- if (addValues.length > 0 || removeValues.length > 0) {
3767
+ if (addValues.length > 0 || addGlobValues.length > 0 || removeValues.length > 0 || migrateValues.length > 0) {
1308
3768
  await invalidateSearchCachesForMutation(globalOptions, result);
1309
3769
  }
1310
3770
  printResult(result, globalOptions);
@@ -1312,13 +3772,48 @@ program
1312
3772
  printError(`profile:command=docs took_ms=${Date.now() - startedAt}`);
1313
3773
  }
1314
3774
  });
3775
+ program
3776
+ .command("deps")
3777
+ .argument("<id>", "Item id")
3778
+ .option("--format <value>", "Output format (tree or graph)", "tree")
3779
+ .option("--max-depth <value>", "Maximum dependency traversal depth (0 keeps only the root)")
3780
+ .option("--collapse <value>", "Collapse mode (none or repeated)", "none")
3781
+ .option("--summary", "Return counts only without full tree/graph payload")
3782
+ .description("Show dependency relationships for an item.")
3783
+ .action(async (id, options, command) => {
3784
+ const globalOptions = getGlobalOptions(command);
3785
+ const startedAt = Date.now();
3786
+ const result = await runDeps(id, {
3787
+ format: typeof options.format === "string" ? options.format : undefined,
3788
+ maxDepth: typeof options.maxDepth === "string" ? options.maxDepth : undefined,
3789
+ collapse: typeof options.collapse === "string" ? options.collapse : undefined,
3790
+ summary: options.summary === true,
3791
+ }, globalOptions);
3792
+ printResult(result, globalOptions);
3793
+ if (globalOptions.profile) {
3794
+ printError(`profile:command=deps took_ms=${Date.now() - startedAt}`);
3795
+ }
3796
+ });
1315
3797
  program
1316
3798
  .command("test")
1317
3799
  .argument("<id>", "Item id")
1318
- .option("--add <value>", "Add linked test entry", collect)
1319
- .option("--remove <value>", "Remove linked test entry by command/path", collect)
3800
+ .option("--add <value>", "Add linked test entry (CSV/markdown pairs or - for stdin)", collect)
3801
+ .option("--remove <value>", "Remove linked test entry by command/path (command=<value>, path=<value>, markdown pairs, plain value, or - for stdin)", collect)
1320
3802
  .option("--run", "Run linked test commands")
3803
+ .option("--background", "Run linked tests in managed background mode")
1321
3804
  .option("--timeout <seconds>", "Default run timeout in seconds")
3805
+ .option("--progress", "Emit linked-test progress to stderr (always shown in TTY, opt-in for non-TTY)")
3806
+ .option("--env-set <value>", "Set environment variable(s) for linked-test runs (KEY=VALUE, repeatable)", collect)
3807
+ .option("--env-clear <value>", "Clear environment variable(s) for linked-test runs (NAME, repeatable)", collect)
3808
+ .option("--shared-host-safe", "Apply additive shared-host-safe runtime defaults for linked-test runs")
3809
+ .option("--pm-context <mode>", "PM linked-test context mode: schema|tracker|auto (default: schema)")
3810
+ .option("--override-linked-pm-context", "Force run-level --pm-context to override per-linked-test pm_context_mode metadata")
3811
+ .option("--fail-on-context-mismatch", "Fail linked PM commands when context item counts differ")
3812
+ .option("--fail-on-skipped", "Treat skipped linked tests as dependency failures")
3813
+ .option("--fail-on-empty-test-run", "Treat successful linked-test commands that report zero executed tests as failures")
3814
+ .option("--require-assertions-for-pm", "Require assertion metadata for linked PM command tests")
3815
+ .option("--check-context", "Preflight linked PM command context diagnostics before executing commands")
3816
+ .option("--auto-pm-context", "Auto-remediate PM tracker-read context mismatches by routing those linked commands through tracker context")
1322
3817
  .option("--author <value>", "Mutation author")
1323
3818
  .option("--message <value>", "History message")
1324
3819
  .option("--force", "Force ownership override")
@@ -1328,19 +3823,59 @@ program
1328
3823
  const startedAt = Date.now();
1329
3824
  const addValues = Array.isArray(options.add) ? options.add : [];
1330
3825
  const removeValues = Array.isArray(options.remove) ? options.remove : [];
3826
+ const runInBackground = options.background === true;
3827
+ if (runInBackground && options.run !== true) {
3828
+ throw new PmCliError("--background requires --run", EXIT_CODE.USAGE);
3829
+ }
3830
+ if (runInBackground && (addValues.length > 0 || removeValues.length > 0)) {
3831
+ throw new PmCliError("--background does not support --add/--remove; update linked tests first, then run in background", EXIT_CODE.USAGE);
3832
+ }
3833
+ if (runInBackground) {
3834
+ const result = await runStartBackgroundRun({
3835
+ kind: "test",
3836
+ commandArgs: buildBackgroundTestCommandArgs(id, {
3837
+ ...options,
3838
+ add: addValues,
3839
+ remove: removeValues,
3840
+ }),
3841
+ targetId: id,
3842
+ author: typeof options.author === "string" ? options.author : undefined,
3843
+ noExtensions: globalOptions.noExtensions === true,
3844
+ }, globalOptions);
3845
+ printResult(result, globalOptions);
3846
+ if (globalOptions.profile) {
3847
+ printError(`profile:command=test took_ms=${Date.now() - startedAt}`);
3848
+ }
3849
+ return;
3850
+ }
1331
3851
  const result = await runTest(id, {
1332
3852
  add: addValues,
1333
3853
  remove: removeValues,
1334
3854
  run: Boolean(options.run),
1335
3855
  timeout: typeof options.timeout === "string" ? options.timeout : undefined,
3856
+ progress: Boolean(options.progress),
3857
+ envSet: Array.isArray(options.envSet) ? options.envSet : [],
3858
+ envClear: Array.isArray(options.envClear) ? options.envClear : [],
3859
+ sharedHostSafe: Boolean(options.sharedHostSafe),
3860
+ pmContext: typeof options.pmContext === "string" ? options.pmContext : undefined,
3861
+ overrideLinkedPmContext: Boolean(options.overrideLinkedPmContext),
3862
+ failOnContextMismatch: Boolean(options.failOnContextMismatch),
3863
+ failOnSkipped: Boolean(options.failOnSkipped),
3864
+ failOnEmptyTestRun: Boolean(options.failOnEmptyTestRun),
3865
+ requireAssertionsForPm: Boolean(options.requireAssertionsForPm),
3866
+ checkContext: Boolean(options.checkContext),
3867
+ autoPmContext: Boolean(options.autoPmContext),
1336
3868
  author: typeof options.author === "string" ? options.author : undefined,
1337
3869
  message: typeof options.message === "string" ? options.message : undefined,
1338
3870
  force: Boolean(options.force),
1339
3871
  }, globalOptions);
1340
- if (addValues.length > 0 || removeValues.length > 0) {
3872
+ if (addValues.length > 0 || removeValues.length > 0 || options.run === true) {
1341
3873
  await invalidateSearchCachesForMutation(globalOptions, result);
1342
3874
  }
1343
3875
  printResult(result, globalOptions);
3876
+ if (result.run_results.some((entry) => entry.status === "failed") || result.fail_on_skipped_triggered === true) {
3877
+ process.exitCode = EXIT_CODE.DEPENDENCY_FAILED;
3878
+ }
1344
3879
  if (globalOptions.profile) {
1345
3880
  printError(`profile:command=test took_ms=${Date.now() - startedAt}`);
1346
3881
  }
@@ -1349,22 +3884,169 @@ program
1349
3884
  .command("test-all")
1350
3885
  .description("Run linked tests across matching items.")
1351
3886
  .option("--status <value>", "Filter items by status before running tests")
3887
+ .option("--limit <n>", "Limit matching items before running linked tests")
3888
+ .option("--offset <n>", "Skip matching items before running linked tests")
3889
+ .option("--background", "Run linked tests in managed background mode")
1352
3890
  .option("--timeout <seconds>", "Default run timeout in seconds")
3891
+ .option("--progress", "Emit linked-test progress to stderr (always shown in TTY, opt-in for non-TTY)")
3892
+ .option("--env-set <value>", "Set environment variable(s) for linked-test runs (KEY=VALUE, repeatable)", collect)
3893
+ .option("--env-clear <value>", "Clear environment variable(s) for linked-test runs (NAME, repeatable)", collect)
3894
+ .option("--shared-host-safe", "Apply additive shared-host-safe runtime defaults for linked-test runs")
3895
+ .option("--pm-context <mode>", "PM linked-test context mode: schema|tracker|auto (default: schema)")
3896
+ .option("--override-linked-pm-context", "Force run-level --pm-context to override per-linked-test pm_context_mode metadata")
3897
+ .option("--fail-on-context-mismatch", "Fail linked PM commands when context item counts differ")
3898
+ .option("--fail-on-skipped", "Treat skipped linked tests as dependency failures")
3899
+ .option("--fail-on-empty-test-run", "Treat successful linked-test commands that report zero executed tests as failures")
3900
+ .option("--require-assertions-for-pm", "Require assertion metadata for linked PM command tests")
3901
+ .option("--check-context", "Preflight linked PM command context diagnostics before executing commands")
3902
+ .option("--auto-pm-context", "Auto-remediate PM tracker-read context mismatches by routing those linked commands through tracker context")
1353
3903
  .action(async (options, command) => {
1354
3904
  const globalOptions = getGlobalOptions(command);
1355
3905
  const startedAt = Date.now();
3906
+ const runInBackground = options.background === true;
3907
+ if (runInBackground) {
3908
+ const result = await runStartBackgroundRun({
3909
+ kind: "test-all",
3910
+ commandArgs: buildBackgroundTestAllCommandArgs(options),
3911
+ statusFilter: typeof options.status === "string" ? options.status : undefined,
3912
+ noExtensions: globalOptions.noExtensions === true,
3913
+ }, globalOptions);
3914
+ printResult(result, globalOptions);
3915
+ if (globalOptions.profile) {
3916
+ printError(`profile:command=test-all took_ms=${Date.now() - startedAt}`);
3917
+ }
3918
+ return;
3919
+ }
1356
3920
  const result = await runTestAll({
1357
3921
  status: typeof options.status === "string" ? options.status : undefined,
3922
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3923
+ offset: typeof options.offset === "string" ? options.offset : undefined,
1358
3924
  timeout: typeof options.timeout === "string" ? options.timeout : undefined,
3925
+ progress: Boolean(options.progress),
3926
+ envSet: Array.isArray(options.envSet) ? options.envSet : [],
3927
+ envClear: Array.isArray(options.envClear) ? options.envClear : [],
3928
+ sharedHostSafe: Boolean(options.sharedHostSafe),
3929
+ pmContext: typeof options.pmContext === "string" ? options.pmContext : undefined,
3930
+ overrideLinkedPmContext: Boolean(options.overrideLinkedPmContext),
3931
+ failOnContextMismatch: Boolean(options.failOnContextMismatch),
3932
+ failOnSkipped: Boolean(options.failOnSkipped),
3933
+ failOnEmptyTestRun: Boolean(options.failOnEmptyTestRun),
3934
+ requireAssertionsForPm: Boolean(options.requireAssertionsForPm),
3935
+ checkContext: Boolean(options.checkContext),
3936
+ autoPmContext: Boolean(options.autoPmContext),
1359
3937
  }, globalOptions);
3938
+ await invalidateSearchCachesForMutation(globalOptions, {
3939
+ ids: result.results.map((entry) => entry.id),
3940
+ });
1360
3941
  printResult(result, globalOptions);
1361
- if (result.failed > 0) {
3942
+ if (result.failed > 0 || result.fail_on_skipped_triggered === true) {
1362
3943
  process.exitCode = EXIT_CODE.DEPENDENCY_FAILED;
1363
3944
  }
1364
3945
  if (globalOptions.profile) {
1365
3946
  printError(`profile:command=test-all took_ms=${Date.now() - startedAt}`);
1366
3947
  }
1367
3948
  });
3949
+ const testRunsCommand = program
3950
+ .command("test-runs")
3951
+ .description("Manage background linked-test runs.")
3952
+ .action(async (_options, command) => {
3953
+ const globalOptions = getGlobalOptions(command);
3954
+ const startedAt = Date.now();
3955
+ const result = await runTestRunsList({}, globalOptions);
3956
+ printResult(result, globalOptions);
3957
+ if (globalOptions.profile) {
3958
+ printError(`profile:command=test-runs took_ms=${Date.now() - startedAt}`);
3959
+ }
3960
+ });
3961
+ testRunsCommand
3962
+ .command("list")
3963
+ .option("--status <value>", "Filter by background run status")
3964
+ .option("--limit <value>", "Limit number of runs returned")
3965
+ .description("List background test runs.")
3966
+ .action(async (options, command) => {
3967
+ const globalOptions = getGlobalOptions(command);
3968
+ const startedAt = Date.now();
3969
+ const result = await runTestRunsList({
3970
+ status: typeof options.status === "string" ? options.status : undefined,
3971
+ limit: typeof options.limit === "string" ? options.limit : undefined,
3972
+ }, globalOptions);
3973
+ printResult(result, globalOptions);
3974
+ if (globalOptions.profile) {
3975
+ printError(`profile:command=test-runs list took_ms=${Date.now() - startedAt}`);
3976
+ }
3977
+ });
3978
+ testRunsCommand
3979
+ .command("status")
3980
+ .argument("<runId>", "Background run id")
3981
+ .description("Show status, health, and resource snapshot for a background run.")
3982
+ .action(async (runId, _options, command) => {
3983
+ const globalOptions = getGlobalOptions(command);
3984
+ const startedAt = Date.now();
3985
+ const result = await runTestRunsStatus(runId, globalOptions);
3986
+ printResult(result, globalOptions);
3987
+ if (globalOptions.profile) {
3988
+ printError(`profile:command=test-runs status took_ms=${Date.now() - startedAt}`);
3989
+ }
3990
+ });
3991
+ testRunsCommand
3992
+ .command("logs")
3993
+ .argument("<runId>", "Background run id")
3994
+ .option("--stream <value>", "Log stream selector: stdout|stderr|both")
3995
+ .option("--tail <value>", "Tail number of lines per selected stream")
3996
+ .description("Show tailed logs for a background run.")
3997
+ .action(async (runId, options, command) => {
3998
+ const globalOptions = getGlobalOptions(command);
3999
+ const startedAt = Date.now();
4000
+ const result = await runTestRunsLogs(runId, {
4001
+ stream: typeof options.stream === "string" ? options.stream : undefined,
4002
+ tail: typeof options.tail === "string" ? options.tail : undefined,
4003
+ }, globalOptions);
4004
+ printResult(result, globalOptions);
4005
+ if (globalOptions.profile) {
4006
+ printError(`profile:command=test-runs logs took_ms=${Date.now() - startedAt}`);
4007
+ }
4008
+ });
4009
+ testRunsCommand
4010
+ .command("stop")
4011
+ .argument("<runId>", "Background run id")
4012
+ .option("--force", "Force-stop via SIGKILL")
4013
+ .description("Stop a running background test run.")
4014
+ .action(async (runId, options, command) => {
4015
+ const globalOptions = getGlobalOptions(command);
4016
+ const startedAt = Date.now();
4017
+ const result = await runTestRunsStop(runId, {
4018
+ force: options.force === true,
4019
+ }, globalOptions);
4020
+ printResult(result, globalOptions);
4021
+ if (globalOptions.profile) {
4022
+ printError(`profile:command=test-runs stop took_ms=${Date.now() - startedAt}`);
4023
+ }
4024
+ });
4025
+ testRunsCommand
4026
+ .command("resume")
4027
+ .argument("<runId>", "Background run id")
4028
+ .option("--author <value>", "Resume author (falls back to PM_AUTHOR/settings)")
4029
+ .description("Resume a previously terminal background test run by starting a new attempt.")
4030
+ .action(async (runId, options, command) => {
4031
+ const globalOptions = getGlobalOptions(command);
4032
+ const startedAt = Date.now();
4033
+ const result = await runTestRunsResume(runId, {
4034
+ author: typeof options.author === "string" ? options.author : undefined,
4035
+ noExtensions: globalOptions.noExtensions === true,
4036
+ }, globalOptions);
4037
+ printResult(result, globalOptions);
4038
+ if (globalOptions.profile) {
4039
+ printError(`profile:command=test-runs resume took_ms=${Date.now() - startedAt}`);
4040
+ }
4041
+ });
4042
+ program
4043
+ .command("test-runs-worker", { hidden: true })
4044
+ .argument("<runId>", "Background run id")
4045
+ .description("Internal background worker command.")
4046
+ .action(async (runId, _options, command) => {
4047
+ const globalOptions = getGlobalOptions(command);
4048
+ await runTestRunsWorker(runId, globalOptions);
4049
+ });
1368
4050
  program
1369
4051
  .command("stats")
1370
4052
  .description("Show project tracker statistics.")
@@ -1380,27 +4062,120 @@ program
1380
4062
  program
1381
4063
  .command("health")
1382
4064
  .description("Show project tracker health checks.")
1383
- .action(async (_options, command) => {
4065
+ .option("--strict-directories", "Treat optional item-type directories as required failures")
4066
+ .option("--check-only", "Run read-only health diagnostics without refreshing vectors")
4067
+ .option("--check-telemetry", "Probe telemetry endpoint health and include network diagnostics")
4068
+ .option("--no-refresh", "Disable automatic vector refresh attempts during health checks")
4069
+ .option("--refresh-vectors", "Explicitly enable vector refresh attempts during health checks")
4070
+ .option("--verbose-stale-items", "Include full stale vectorization ID lists in health output")
4071
+ .option("--strict-exit", "Return non-zero exit when health warnings are present (ok=false)")
4072
+ .option("--fail-on-warn", "Alias for --strict-exit")
4073
+ .action(async (options, command) => {
1384
4074
  const globalOptions = getGlobalOptions(command);
1385
4075
  const startedAt = Date.now();
1386
- const result = await runHealth(globalOptions);
4076
+ const result = await runHealth(globalOptions, {
4077
+ strictDirectories: Boolean(options.strictDirectories),
4078
+ checkOnly: Boolean(options.checkOnly),
4079
+ checkTelemetry: Boolean(options.checkTelemetry),
4080
+ noRefresh: Boolean(options.noRefresh),
4081
+ refreshVectors: Boolean(options.refreshVectors),
4082
+ verboseStaleItems: Boolean(options.verboseStaleItems),
4083
+ });
1387
4084
  printResult(result, globalOptions);
4085
+ const strictExit = Boolean(options.strictExit) || Boolean(options.failOnWarn);
4086
+ if (strictExit && !result.ok) {
4087
+ process.exitCode = EXIT_CODE.GENERIC_FAILURE;
4088
+ }
1388
4089
  if (globalOptions.profile) {
1389
4090
  printError(`profile:command=health took_ms=${Date.now() - startedAt}`);
1390
4091
  }
1391
4092
  });
4093
+ program
4094
+ .command("validate")
4095
+ .description("Run standalone metadata, resolution, lifecycle, files, linked-command reference, and history drift validation checks.")
4096
+ .option("--check-metadata", "Run metadata completeness checks")
4097
+ .option("--metadata-profile <value>", "Select metadata validation profile for --check-metadata (core|strict|custom)")
4098
+ .option("--check-resolution", "Run closed-item resolution metadata checks")
4099
+ .option("--check-lifecycle", "Run active-item lifecycle governance drift checks")
4100
+ .option("--check-stale-blockers", "Include stale blocker-pattern diagnostics in lifecycle checks")
4101
+ .option("--dependency-cycle-severity <value>", "Set dependency-cycle warning policy for lifecycle checks (off|warn|error)")
4102
+ .option("--check-files", "Run linked-file and orphaned-file checks")
4103
+ .option("--check-command-references", "Run linked-command PM-ID reference checks")
4104
+ .option("--scan-mode <value>", "Select file candidate scan mode for --check-files (default|tracked-all|tracked-all-strict)")
4105
+ .option("--include-pm-internals", "Include PM storage internals in tracked-all candidate scans")
4106
+ .option("--verbose-file-lists", "Include full file-path lists for validate --check-files details")
4107
+ .option("--strict-exit", "Return non-zero exit when validation warnings are present (ok=false)")
4108
+ .option("--fail-on-warn", "Alias for --strict-exit")
4109
+ .option("--check-history-drift", "Run item/history hash drift checks")
4110
+ .action(async (options, command) => {
4111
+ const globalOptions = getGlobalOptions(command);
4112
+ const startedAt = Date.now();
4113
+ const result = await runValidate({
4114
+ checkMetadata: Boolean(options.checkMetadata),
4115
+ metadataProfile: typeof options.metadataProfile === "string" ? options.metadataProfile : undefined,
4116
+ checkResolution: Boolean(options.checkResolution),
4117
+ checkLifecycle: Boolean(options.checkLifecycle),
4118
+ checkStaleBlockers: Boolean(options.checkStaleBlockers),
4119
+ dependencyCycleSeverity: typeof options.dependencyCycleSeverity === "string" ? options.dependencyCycleSeverity : undefined,
4120
+ checkFiles: Boolean(options.checkFiles),
4121
+ checkCommandReferences: Boolean(options.checkCommandReferences),
4122
+ scanMode: typeof options.scanMode === "string" ? options.scanMode : undefined,
4123
+ includePmInternals: Boolean(options.includePmInternals),
4124
+ verboseFileLists: Boolean(options.verboseFileLists),
4125
+ checkHistoryDrift: Boolean(options.checkHistoryDrift),
4126
+ }, globalOptions);
4127
+ printResult(result, globalOptions);
4128
+ const strictExit = Boolean(options.strictExit) || Boolean(options.failOnWarn);
4129
+ if (strictExit && !result.ok) {
4130
+ process.exitCode = EXIT_CODE.GENERIC_FAILURE;
4131
+ }
4132
+ if (globalOptions.profile) {
4133
+ printError(`profile:command=validate took_ms=${Date.now() - startedAt}`);
4134
+ }
4135
+ });
1392
4136
  program
1393
4137
  .command("gc")
4138
+ .option("--dry-run", "Preview cleanup targets without deleting files")
4139
+ .option("--scope <value>", "Limit cleanup to one or more scopes (comma-separated or repeatable): index, embeddings, runtime", collect)
1394
4140
  .description("Clean optional cache artifacts and show a summary.")
1395
- .action(async (_options, command) => {
4141
+ .action(async (options, command) => {
1396
4142
  const globalOptions = getGlobalOptions(command);
1397
4143
  const startedAt = Date.now();
1398
- const result = await runGc(globalOptions);
4144
+ const result = await runGc(globalOptions, {
4145
+ dryRun: options.dryRun === true,
4146
+ scope: Array.isArray(options.scope) ? options.scope : [],
4147
+ });
1399
4148
  printResult(result, globalOptions);
1400
4149
  if (globalOptions.profile) {
1401
4150
  printError(`profile:command=gc took_ms=${Date.now() - startedAt}`);
1402
4151
  }
1403
4152
  });
4153
+ program
4154
+ .command("contracts")
4155
+ .description("Show machine-readable command and schema contracts for agents.")
4156
+ .option("--action <value>", "Filter tool schema branches to a specific action")
4157
+ .option("--command <value>", "Scope contracts output to one CLI command (narrow-by-default)")
4158
+ .option("--schema-only", "Return schema-focused output only")
4159
+ .option("--flags-only", "Return command flag contracts only")
4160
+ .option("--availability-only", "Return action availability surface only")
4161
+ .option("--runtime-only", "Include only actions invocable in the current runtime")
4162
+ .option("--active-only", "Alias for --runtime-only")
4163
+ .action(async (options, command) => {
4164
+ const globalOptions = getGlobalOptions(command);
4165
+ const startedAt = Date.now();
4166
+ const result = await runContracts({
4167
+ action: typeof options.action === "string" ? options.action : undefined,
4168
+ command: typeof options.command === "string" ? options.command : undefined,
4169
+ schemaOnly: Boolean(options.schemaOnly),
4170
+ flagsOnly: Boolean(options.flagsOnly),
4171
+ availabilityOnly: Boolean(options.availabilityOnly),
4172
+ runtimeOnly: Boolean(options.runtimeOnly) || Boolean(options.activeOnly),
4173
+ }, globalOptions);
4174
+ printResult(result, globalOptions);
4175
+ if (globalOptions.profile) {
4176
+ printError(`profile:command=contracts took_ms=${Date.now() - startedAt}`);
4177
+ }
4178
+ });
1404
4179
  program
1405
4180
  .command("claim")
1406
4181
  .argument("<id>", "Item id")
@@ -1426,6 +4201,7 @@ program
1426
4201
  .argument("<id>", "Item id")
1427
4202
  .option("--author <value>", "Mutation author")
1428
4203
  .option("--message <value>", "History message")
4204
+ .option("--allow-audit-release", "Allow non-owner release handoffs without requiring --force")
1429
4205
  .option("--force", "Force release override")
1430
4206
  .description("Release an item's active claim.")
1431
4207
  .action(async (id, options, command) => {
@@ -1434,6 +4210,7 @@ program
1434
4210
  const result = await runRelease(id, Boolean(options.force), globalOptions, {
1435
4211
  author: typeof options.author === "string" ? options.author : undefined,
1436
4212
  message: typeof options.message === "string" ? options.message : undefined,
4213
+ allowAuditRelease: options.allowAuditRelease === true,
1437
4214
  });
1438
4215
  await invalidateSearchCachesForMutation(globalOptions, result);
1439
4216
  printResult(result, globalOptions);
@@ -1441,54 +4218,457 @@ program
1441
4218
  printError(`profile:command=release took_ms=${Date.now() - startedAt}`);
1442
4219
  }
1443
4220
  });
4221
+ program
4222
+ .command("start-task")
4223
+ .argument("<id>", "Item id")
4224
+ .option("--author <value>", "Mutation author")
4225
+ .option("--message <value>", "History message")
4226
+ .option("--force", "Force ownership or terminal override when required")
4227
+ .description("Lifecycle alias: claim an item and move it to in_progress.")
4228
+ .action(async (id, options, command) => {
4229
+ const globalOptions = getGlobalOptions(command);
4230
+ const startedAt = Date.now();
4231
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
4232
+ const settings = await readSettings(pmRoot);
4233
+ const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
4234
+ const inProgressStatus = normalizeStatusInput("in_progress", statusRegistry) ?? statusRegistry.open_status;
4235
+ const force = Boolean(options.force);
4236
+ const mutationOptions = {
4237
+ author: typeof options.author === "string" ? options.author : undefined,
4238
+ message: typeof options.message === "string" ? options.message : undefined,
4239
+ };
4240
+ const claimResult = await runClaim(id, force, globalOptions, mutationOptions);
4241
+ await invalidateSearchCachesForMutation(globalOptions, claimResult);
4242
+ const updateResult = await runUpdate(id, {
4243
+ ...mutationOptions,
4244
+ status: inProgressStatus,
4245
+ force,
4246
+ }, globalOptions);
4247
+ await invalidateSearchCachesForMutation(globalOptions, updateResult);
4248
+ printResult({
4249
+ id,
4250
+ action: "start_task",
4251
+ claim: claimResult,
4252
+ update: updateResult,
4253
+ }, globalOptions);
4254
+ if (globalOptions.profile) {
4255
+ printError(`profile:command=start-task took_ms=${Date.now() - startedAt}`);
4256
+ }
4257
+ });
4258
+ program
4259
+ .command("pause-task")
4260
+ .argument("<id>", "Item id")
4261
+ .option("--author <value>", "Mutation author")
4262
+ .option("--message <value>", "History message")
4263
+ .option("--force", "Force ownership override when required")
4264
+ .description("Lifecycle alias: move an item to open and release its claim.")
4265
+ .action(async (id, options, command) => {
4266
+ const globalOptions = getGlobalOptions(command);
4267
+ const startedAt = Date.now();
4268
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
4269
+ const settings = await readSettings(pmRoot);
4270
+ const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
4271
+ const openStatus = statusRegistry.open_status;
4272
+ const force = Boolean(options.force);
4273
+ const mutationOptions = {
4274
+ author: typeof options.author === "string" ? options.author : undefined,
4275
+ message: typeof options.message === "string" ? options.message : undefined,
4276
+ };
4277
+ const updateResult = await runUpdate(id, {
4278
+ ...mutationOptions,
4279
+ status: openStatus,
4280
+ force,
4281
+ }, globalOptions);
4282
+ await invalidateSearchCachesForMutation(globalOptions, updateResult);
4283
+ const releaseResult = await runRelease(id, force, globalOptions, mutationOptions);
4284
+ await invalidateSearchCachesForMutation(globalOptions, releaseResult);
4285
+ printResult({
4286
+ id,
4287
+ action: "pause_task",
4288
+ update: updateResult,
4289
+ release: releaseResult,
4290
+ }, globalOptions);
4291
+ if (globalOptions.profile) {
4292
+ printError(`profile:command=pause-task took_ms=${Date.now() - startedAt}`);
4293
+ }
4294
+ });
4295
+ program
4296
+ .command("close-task")
4297
+ .argument("<id>", "Item id")
4298
+ .argument("<reason>", "Close reason text")
4299
+ .option("--author <value>", "Mutation author")
4300
+ .option("--message <value>", "History message")
4301
+ .option("--validate-close <value>", "Close-time validation mode: off|warn|strict")
4302
+ .option("--force", "Force ownership or terminal override when required")
4303
+ .description("Lifecycle alias: close an item with reason and release assignment metadata.")
4304
+ .action(async (id, reason, options, command) => {
4305
+ const globalOptions = getGlobalOptions(command);
4306
+ const startedAt = Date.now();
4307
+ const force = Boolean(options.force);
4308
+ const mutationOptions = {
4309
+ author: typeof options.author === "string" ? options.author : undefined,
4310
+ message: typeof options.message === "string" ? options.message : undefined,
4311
+ };
4312
+ const closeResult = await runClose(id, reason, {
4313
+ ...mutationOptions,
4314
+ validateClose: typeof options.validateClose === "string" ? options.validateClose : undefined,
4315
+ force,
4316
+ }, globalOptions);
4317
+ await invalidateSearchCachesForMutation(globalOptions, closeResult);
4318
+ const releaseResult = await runRelease(id, force, globalOptions, mutationOptions);
4319
+ await invalidateSearchCachesForMutation(globalOptions, releaseResult);
4320
+ printResult({
4321
+ id,
4322
+ action: "close_task",
4323
+ close: closeResult,
4324
+ release: releaseResult,
4325
+ }, globalOptions);
4326
+ if (globalOptions.profile) {
4327
+ printError(`profile:command=close-task took_ms=${Date.now() - startedAt}`);
4328
+ }
4329
+ });
1444
4330
  program
1445
4331
  .command("completion")
1446
4332
  .argument("<shell>", "Shell type: bash, zsh, or fish")
4333
+ .option("--eager-tags", "Embed current tracker tags directly in generated scripts (legacy eager mode)")
1447
4334
  .description("Generate shell completion for pm.")
1448
- .action((shell, _options, command) => {
4335
+ .action(async (shell, options, command) => {
1449
4336
  const globalOptions = getGlobalOptions(command);
1450
- const result = runCompletion(shell);
4337
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
4338
+ let completionTypes;
4339
+ let completionTags;
4340
+ let completionStatuses;
4341
+ const completionCommandFlags = {};
4342
+ const eagerTags = Boolean(options.eagerTags);
4343
+ if (await pathExists(getSettingsPath(pmRoot))) {
4344
+ const settings = await readSettings(pmRoot);
4345
+ const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
4346
+ const runtimeFieldRegistry = resolveRuntimeFieldRegistry(settings.schema);
4347
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
4348
+ completionTypes = typeRegistry.types;
4349
+ completionStatuses = statusRegistry.definitions.map((definition) => definition.id);
4350
+ for (const [commandKey, definitions] of runtimeFieldRegistry.command_to_fields.entries()) {
4351
+ if (commandKey !== "list" &&
4352
+ commandKey !== "create" &&
4353
+ commandKey !== "update" &&
4354
+ commandKey !== "search" &&
4355
+ commandKey !== "calendar" &&
4356
+ commandKey !== "context") {
4357
+ continue;
4358
+ }
4359
+ const runtimeFlags = new Set();
4360
+ for (const definition of definitions) {
4361
+ runtimeFlags.add(`--${definition.cli_flag}`);
4362
+ for (const alias of definition.cli_aliases) {
4363
+ if (alias.startsWith("--") || (alias.startsWith("-") && !alias.startsWith("--"))) {
4364
+ runtimeFlags.add(alias);
4365
+ }
4366
+ else {
4367
+ runtimeFlags.add(`--${alias}`);
4368
+ }
4369
+ }
4370
+ }
4371
+ completionCommandFlags[commandKey] = [...runtimeFlags].sort((left, right) => left.localeCompare(right));
4372
+ }
4373
+ if (completionCommandFlags.update) {
4374
+ completionCommandFlags["update-many"] = [...completionCommandFlags.update];
4375
+ }
4376
+ if (eagerTags) {
4377
+ const items = await listAllFrontMatter(pmRoot, settings.item_format, typeRegistry.type_to_folder, undefined, settings.schema);
4378
+ completionTags = [...new Set(items.flatMap((item) => item.tags ?? []).map((tag) => tag.trim()).filter((tag) => tag.length > 0))]
4379
+ .sort((left, right) => left.localeCompare(right));
4380
+ }
4381
+ }
4382
+ const result = runCompletion(shell, completionTypes, completionTags ?? [], eagerTags, {
4383
+ statuses: completionStatuses,
4384
+ command_flags: completionCommandFlags,
4385
+ });
1451
4386
  if (globalOptions.json) {
1452
4387
  printResult(result, globalOptions);
1453
4388
  }
1454
4389
  else if (!globalOptions.quiet) {
1455
- process.stdout.write(`${result.script}\n`);
4390
+ writeStdout(`${result.script}\n`);
1456
4391
  }
1457
4392
  if (globalOptions.profile) {
1458
4393
  printError(`profile:command=completion took_ms=0`);
1459
4394
  }
1460
4395
  });
4396
+ program
4397
+ .command("completion-tags", { hidden: true })
4398
+ .description("Internal dynamic completion tag source.")
4399
+ .action(async (_options, command) => {
4400
+ const globalOptions = getGlobalOptions(command);
4401
+ const startedAt = Date.now();
4402
+ const pmRoot = resolvePmRoot(process.cwd(), globalOptions.path);
4403
+ let tags = [];
4404
+ if (await pathExists(getSettingsPath(pmRoot))) {
4405
+ const settings = await readSettings(pmRoot);
4406
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
4407
+ const items = await listAllFrontMatter(pmRoot, settings.item_format, typeRegistry.type_to_folder, undefined, settings.schema);
4408
+ tags = [...new Set(items.flatMap((item) => item.tags ?? []).map((tag) => tag.trim()).filter((tag) => tag.length > 0))].sort((left, right) => left.localeCompare(right));
4409
+ }
4410
+ if (globalOptions.json) {
4411
+ printResult({
4412
+ tags,
4413
+ count: tags.length,
4414
+ }, globalOptions);
4415
+ }
4416
+ else if (!globalOptions.quiet) {
4417
+ writeStdout(tags.join("\n"));
4418
+ if (tags.length > 0) {
4419
+ writeStdout("\n");
4420
+ }
4421
+ }
4422
+ if (globalOptions.profile) {
4423
+ printError(`profile:command=completion-tags took_ms=${Date.now() - startedAt}`);
4424
+ }
4425
+ });
4426
+ attachRichHelpText(program, normalizeLegacyExtensionActionSyntax(process.argv.slice(2)));
4427
+ function collectRuntimeCommandPaths(root) {
4428
+ const commandPaths = new Set();
4429
+ const queue = [...root.commands];
4430
+ while (queue.length > 0) {
4431
+ const current = queue.shift();
4432
+ if (!current) {
4433
+ continue;
4434
+ }
4435
+ const normalizedPath = normalizeHelpCommandPath(getCommandPath(current));
4436
+ const hasInternalSegment = normalizedPath.split(" ").some((segment) => segment.startsWith("_"));
4437
+ if (normalizedPath.length > 0 && !hasInternalSegment) {
4438
+ commandPaths.add(normalizedPath);
4439
+ }
4440
+ queue.push(...current.commands);
4441
+ }
4442
+ for (const descriptorPath of activeRuntimeExtensionCommandDescriptors.keys()) {
4443
+ const normalizedPath = normalizeHelpCommandPath(descriptorPath);
4444
+ const hasInternalSegment = normalizedPath.split(" ").some((segment) => segment.startsWith("_"));
4445
+ if (normalizedPath.length > 0 && !hasInternalSegment) {
4446
+ commandPaths.add(normalizedPath);
4447
+ }
4448
+ }
4449
+ return [...commandPaths].sort((left, right) => left.localeCompare(right));
4450
+ }
4451
+ function scoreCommandPathMatch(commandPath, queryToken) {
4452
+ const normalizedPath = commandPath.trim().toLowerCase();
4453
+ const normalizedToken = queryToken.trim().toLowerCase();
4454
+ if (normalizedToken.length === 0) {
4455
+ return Number.POSITIVE_INFINITY;
4456
+ }
4457
+ const pathSegments = normalizedPath.split(" ");
4458
+ if (normalizedPath === normalizedToken) {
4459
+ return 0;
4460
+ }
4461
+ if (pathSegments.includes(normalizedToken)) {
4462
+ return 1;
4463
+ }
4464
+ if (pathSegments.some((segment) => segment.startsWith(normalizedToken))) {
4465
+ return 2;
4466
+ }
4467
+ if (normalizedPath.includes(normalizedToken)) {
4468
+ return 3;
4469
+ }
4470
+ return Number.POSITIVE_INFINITY;
4471
+ }
4472
+ function buildUnknownCommandGuidanceFromRuntime(rawMessage, root) {
4473
+ const unknownCommandMatch = rawMessage.match(/unknown command '([^']+)'/i);
4474
+ if (!unknownCommandMatch || typeof unknownCommandMatch[1] !== "string") {
4475
+ return undefined;
4476
+ }
4477
+ const normalizedUnknown = normalizeHelpCommandPath(unknownCommandMatch[1]);
4478
+ if (normalizedUnknown.length === 0) {
4479
+ return undefined;
4480
+ }
4481
+ const commandPaths = collectRuntimeCommandPaths(root);
4482
+ if (commandPaths.length === 0) {
4483
+ return undefined;
4484
+ }
4485
+ const primaryToken = normalizedUnknown.split(" ")[0] ?? normalizedUnknown;
4486
+ const rankedCandidates = commandPaths
4487
+ .map((commandPath) => {
4488
+ const directScore = scoreCommandPathMatch(commandPath, normalizedUnknown);
4489
+ const fallbackScore = primaryToken !== normalizedUnknown ? scoreCommandPathMatch(commandPath, primaryToken) : Number.POSITIVE_INFINITY;
4490
+ const score = Math.min(directScore, fallbackScore);
4491
+ return { commandPath, score };
4492
+ })
4493
+ .filter((entry) => Number.isFinite(entry.score))
4494
+ .sort((left, right) => {
4495
+ if (left.score !== right.score) {
4496
+ return left.score - right.score;
4497
+ }
4498
+ return left.commandPath.localeCompare(right.commandPath);
4499
+ })
4500
+ .map((entry) => entry.commandPath);
4501
+ const fallbackTopLevel = [...new Set(commandPaths.map((commandPath) => commandPath.split(" ")[0]).filter((segment) => segment.length > 0))];
4502
+ fallbackTopLevel.sort((left, right) => left.localeCompare(right));
4503
+ const suggestedPaths = (rankedCandidates.length > 0 ? rankedCandidates : fallbackTopLevel).slice(0, 3);
4504
+ const examples = [...new Set(["pm --help", ...suggestedPaths.map((path) => `pm ${path} --help`)])];
4505
+ return {
4506
+ unknownCommandExamples: examples,
4507
+ unknownCommandNextSteps: [
4508
+ 'Run "pm --help" to list commands available in this runtime, including active extensions.',
4509
+ "Use one of the suggested command paths above with --help to inspect valid flags and usage.",
4510
+ ],
4511
+ };
4512
+ }
4513
+ function resolveChildCommandByToken(parent, token) {
4514
+ const normalizedToken = token.trim().toLowerCase();
4515
+ return parent.commands.find((candidate) => {
4516
+ if (candidate.name().trim().toLowerCase() === normalizedToken) {
4517
+ return true;
4518
+ }
4519
+ const aliases = typeof candidate.aliases === "function" ? candidate.aliases() : [];
4520
+ return aliases.some((alias) => alias.trim().toLowerCase() === normalizedToken);
4521
+ });
4522
+ }
4523
+ function isKnownHelpCommandPath(root, commandPathTokens) {
4524
+ if (commandPathTokens.length === 0) {
4525
+ return true;
4526
+ }
4527
+ let current = root;
4528
+ let matchedAny = false;
4529
+ for (const token of commandPathTokens) {
4530
+ const next = resolveChildCommandByToken(current, token);
4531
+ if (!next) {
4532
+ return matchedAny;
4533
+ }
4534
+ matchedAny = true;
4535
+ current = next;
4536
+ }
4537
+ return matchedAny;
4538
+ }
4539
+ async function resolveCommanderUsageContext(error) {
4540
+ const rawMessage = typeof error === "object" && error !== null ? error.message : undefined;
4541
+ const message = rawMessage ?? "Invalid command usage";
4542
+ const invocationArgv = normalizeLegacyExtensionActionSyntax(process.argv.slice(2));
4543
+ const bootstrapGlobal = parseBootstrapGlobalOptions(invocationArgv);
4544
+ const commandName = parseBootstrapCommandName(invocationArgv);
4545
+ let allowedTypes = BUILTIN_TYPE_HELP_VALUES;
4546
+ try {
4547
+ const pmRoot = resolvePmRoot(process.cwd(), bootstrapGlobal.path);
4548
+ if (await pathExists(getSettingsPath(pmRoot))) {
4549
+ const settings = await readSettings(pmRoot);
4550
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
4551
+ if (typeRegistry.types.length > 0) {
4552
+ allowedTypes = typeRegistry.types.join("|");
4553
+ }
4554
+ }
4555
+ }
4556
+ catch {
4557
+ // Fall back to built-in type guidance when settings cannot be read.
4558
+ }
4559
+ const unknownCommandGuidance = buildUnknownCommandGuidanceFromRuntime(message, program);
4560
+ return {
4561
+ message,
4562
+ commandName,
4563
+ allowedTypes,
4564
+ ...(unknownCommandGuidance ?? {}),
4565
+ };
4566
+ }
4567
+ async function formatCommanderUsageMessage(error) {
4568
+ const usageContext = await resolveCommanderUsageContext(error);
4569
+ const { message, commandName, allowedTypes, unknownCommandExamples, unknownCommandNextSteps } = usageContext;
4570
+ const formatted = formatCommanderErrorForDisplay(message, commandName, allowedTypes, {
4571
+ unknownCommandExamples,
4572
+ unknownCommandNextSteps,
4573
+ });
4574
+ const serviceOverride = await runActiveServiceOverride("help_format", {
4575
+ message: formatted,
4576
+ command: commandName,
4577
+ allowed_types: allowedTypes,
4578
+ });
4579
+ if (serviceOverride.handled && typeof serviceOverride.result === "string") {
4580
+ return serviceOverride.result;
4581
+ }
4582
+ return formatted;
4583
+ }
4584
+ async function formatCommanderUsageJson(error) {
4585
+ const usageContext = await resolveCommanderUsageContext(error);
4586
+ const envelope = formatCommanderErrorForJson(usageContext.message, usageContext.commandName, usageContext.allowedTypes, EXIT_CODE.USAGE, {
4587
+ unknownCommandExamples: usageContext.unknownCommandExamples,
4588
+ unknownCommandNextSteps: usageContext.unknownCommandNextSteps,
4589
+ });
4590
+ return JSON.stringify(envelope, null, 2);
4591
+ }
1461
4592
  async function main() {
4593
+ const invocationArgv = normalizeLegacyExtensionActionSyntax(process.argv.slice(2));
4594
+ const invocationProcessArgv = [process.argv[0], process.argv[1], ...invocationArgv];
1462
4595
  try {
4596
+ applyBootstrapPagerPolicy(invocationArgv);
1463
4597
  await registerDynamicExtensionCommandPaths(program);
1464
- await program.parseAsync(process.argv);
4598
+ await registerRuntimeSchemaFieldFlags(program);
4599
+ wrapProgramActionsForExtensionHandlers(program);
4600
+ const renderedBootstrapJsonHelp = await maybeRenderBootstrapJsonHelp(program, invocationArgv);
4601
+ if (renderedBootstrapJsonHelp) {
4602
+ return;
4603
+ }
4604
+ await program.parseAsync(invocationProcessArgv);
1465
4605
  }
1466
4606
  catch (error) {
1467
4607
  await runAndClearAfterCommandHooks({
1468
4608
  ok: false,
1469
4609
  error: describeUnknownError(error),
1470
4610
  });
4611
+ const bootstrapGlobal = parseBootstrapGlobalOptions(invocationArgv);
4612
+ const jsonErrors = bootstrapGlobal.json;
4613
+ if (!bootstrapGlobal.noExtensions) {
4614
+ const bootstrapPmRoot = resolvePmRoot(process.cwd(), bootstrapGlobal.path);
4615
+ const bootstrapSnapshot = await loadRuntimeExtensionSnapshot(bootstrapPmRoot);
4616
+ setActiveExtensionServices(bootstrapSnapshot?.services ?? { overrides: [] });
4617
+ }
1471
4618
  if (error instanceof PmCliError) {
1472
- printError(error.message);
1473
- process.exit(error.exitCode);
4619
+ if (jsonErrors) {
4620
+ printError(JSON.stringify(formatPmCliErrorForJson(error.message, error.exitCode, error.context), null, 2));
4621
+ }
4622
+ else {
4623
+ printError(formatPmCliErrorForDisplay(error.message, error.context));
4624
+ }
4625
+ process.exitCode = error.exitCode;
4626
+ return;
1474
4627
  }
1475
4628
  if (typeof error === "object" && error !== null && "code" in error) {
1476
4629
  const code = error.code;
1477
- if (code === "commander.helpDisplayed") {
1478
- process.exit(EXIT_CODE.SUCCESS);
4630
+ const rawMessage = typeof error.message === "string" ? (error.message ?? "") : "";
4631
+ const isHelpDisplayCode = code === "commander.helpDisplayed" || code === "commander.help" || code === "commander.helpCommand";
4632
+ if (isHelpDisplayCode || rawMessage.includes("(outputHelp)")) {
4633
+ const helpRequest = parseBootstrapHelpRequest(invocationArgv);
4634
+ if (helpRequest.requested && !isKnownHelpCommandPath(program, helpRequest.commandPathTokens)) {
4635
+ const unknownToken = helpRequest.commandPathTokens[0] ?? parseBootstrapCommandName(invocationArgv) ?? "<command>";
4636
+ const unknownMessage = `unknown command '${unknownToken}'`;
4637
+ if (jsonErrors) {
4638
+ printError(await formatCommanderUsageJson({ message: unknownMessage }));
4639
+ }
4640
+ else {
4641
+ printError(await formatCommanderUsageMessage({ message: unknownMessage }));
4642
+ }
4643
+ process.exitCode = EXIT_CODE.USAGE;
4644
+ return;
4645
+ }
4646
+ process.exitCode = EXIT_CODE.SUCCESS;
4647
+ return;
1479
4648
  }
1480
4649
  if (code === "commander.version") {
1481
- process.exit(EXIT_CODE.SUCCESS);
4650
+ process.exitCode = EXIT_CODE.SUCCESS;
4651
+ return;
1482
4652
  }
1483
4653
  if (code?.startsWith("commander.")) {
1484
- const message = error.message ?? "Invalid command usage";
1485
- printError(message.replace(/\(outputHelp\)/g, "").trim());
1486
- process.exit(EXIT_CODE.USAGE);
4654
+ if (jsonErrors) {
4655
+ printError(await formatCommanderUsageJson(error));
4656
+ }
4657
+ else {
4658
+ printError(await formatCommanderUsageMessage(error));
4659
+ }
4660
+ process.exitCode = EXIT_CODE.USAGE;
4661
+ return;
1487
4662
  }
1488
4663
  }
1489
4664
  const message = describeUnknownError(error);
1490
- printError(message);
1491
- process.exit(EXIT_CODE.GENERIC_FAILURE);
4665
+ if (jsonErrors) {
4666
+ printError(JSON.stringify(formatUnknownErrorForJson(message, EXIT_CODE.GENERIC_FAILURE), null, 2));
4667
+ }
4668
+ else {
4669
+ printError(message);
4670
+ }
4671
+ process.exitCode = EXIT_CODE.GENERIC_FAILURE;
1492
4672
  }
1493
4673
  }
1494
4674
  void main();