@unbrained/pm-cli 2026.5.1 → 2026.5.3-5

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 (265) hide show
  1. package/AGENTS.md +8 -1
  2. package/CHANGELOG.md +73 -4
  3. package/CONTRIBUTING.md +11 -5
  4. package/PRD.md +17 -1
  5. package/README.md +55 -1099
  6. package/SECURITY.md +6 -11
  7. package/dist/cli/bootstrap-args.d.ts +18 -0
  8. package/dist/cli/bootstrap-args.js +242 -0
  9. package/dist/cli/bootstrap-args.js.map +1 -0
  10. package/dist/cli/commander-usage.d.ts +17 -0
  11. package/dist/cli/commander-usage.js +178 -0
  12. package/dist/cli/commander-usage.js.map +1 -0
  13. package/dist/cli/commands/activity.d.ts +10 -0
  14. package/dist/cli/commands/activity.js +14 -10
  15. package/dist/cli/commands/activity.js.map +1 -1
  16. package/dist/cli/commands/aggregate.js.map +1 -1
  17. package/dist/cli/commands/append.js.map +1 -1
  18. package/dist/cli/commands/calendar.js +19 -34
  19. package/dist/cli/commands/calendar.js.map +1 -1
  20. package/dist/cli/commands/claim.js.map +1 -1
  21. package/dist/cli/commands/close.js.map +1 -1
  22. package/dist/cli/commands/comments-audit.js.map +1 -1
  23. package/dist/cli/commands/comments.js +1 -9
  24. package/dist/cli/commands/comments.js.map +1 -1
  25. package/dist/cli/commands/completion.js.map +1 -1
  26. package/dist/cli/commands/config.d.ts +21 -3
  27. package/dist/cli/commands/config.js +118 -2
  28. package/dist/cli/commands/config.js.map +1 -1
  29. package/dist/cli/commands/context.d.ts +90 -1
  30. package/dist/cli/commands/context.js +496 -12
  31. package/dist/cli/commands/context.js.map +1 -1
  32. package/dist/cli/commands/contracts.js.map +1 -1
  33. package/dist/cli/commands/create.js +2 -2
  34. package/dist/cli/commands/create.js.map +1 -1
  35. package/dist/cli/commands/dedupe-audit.js +2 -11
  36. package/dist/cli/commands/dedupe-audit.js.map +1 -1
  37. package/dist/cli/commands/delete.js.map +1 -1
  38. package/dist/cli/commands/deps.js.map +1 -1
  39. package/dist/cli/commands/docs.js.map +1 -1
  40. package/dist/cli/commands/extension.js.map +1 -1
  41. package/dist/cli/commands/files.js +14 -2
  42. package/dist/cli/commands/files.js.map +1 -1
  43. package/dist/cli/commands/gc.js.map +1 -1
  44. package/dist/cli/commands/get.js.map +1 -1
  45. package/dist/cli/commands/health.js +16 -12
  46. package/dist/cli/commands/health.js.map +1 -1
  47. package/dist/cli/commands/history.js +1 -9
  48. package/dist/cli/commands/history.js.map +1 -1
  49. package/dist/cli/commands/index.js.map +1 -1
  50. package/dist/cli/commands/init.js.map +1 -1
  51. package/dist/cli/commands/learnings.js +1 -9
  52. package/dist/cli/commands/learnings.js.map +1 -1
  53. package/dist/cli/commands/list.d.ts +1 -0
  54. package/dist/cli/commands/list.js +13 -31
  55. package/dist/cli/commands/list.js.map +1 -1
  56. package/dist/cli/commands/normalize.js +14 -23
  57. package/dist/cli/commands/normalize.js.map +1 -1
  58. package/dist/cli/commands/notes.js +1 -9
  59. package/dist/cli/commands/notes.js.map +1 -1
  60. package/dist/cli/commands/reindex.js +2 -7
  61. package/dist/cli/commands/reindex.js.map +1 -1
  62. package/dist/cli/commands/restore.js.map +1 -1
  63. package/dist/cli/commands/search.js +4 -35
  64. package/dist/cli/commands/search.js.map +1 -1
  65. package/dist/cli/commands/stats.js.map +1 -1
  66. package/dist/cli/commands/templates.js.map +1 -1
  67. package/dist/cli/commands/test-all.js.map +1 -1
  68. package/dist/cli/commands/test-runs.js +1 -11
  69. package/dist/cli/commands/test-runs.js.map +1 -1
  70. package/dist/cli/commands/test.js.map +1 -1
  71. package/dist/cli/commands/update-many.js +1 -6
  72. package/dist/cli/commands/update-many.js.map +1 -1
  73. package/dist/cli/commands/update.js +2 -2
  74. package/dist/cli/commands/update.js.map +1 -1
  75. package/dist/cli/commands/validate.js +23 -18
  76. package/dist/cli/commands/validate.js.map +1 -1
  77. package/dist/cli/error-guidance.d.ts +13 -0
  78. package/dist/cli/error-guidance.js +56 -6
  79. package/dist/cli/error-guidance.js.map +1 -1
  80. package/dist/cli/extension-command-help.d.ts +48 -0
  81. package/dist/cli/extension-command-help.js +389 -0
  82. package/dist/cli/extension-command-help.js.map +1 -0
  83. package/dist/cli/extension-command-options.js.map +1 -1
  84. package/dist/cli/help-content.js +9 -3
  85. package/dist/cli/help-content.js.map +1 -1
  86. package/dist/cli/help-json-payload.d.ts +25 -0
  87. package/dist/cli/help-json-payload.js +265 -0
  88. package/dist/cli/help-json-payload.js.map +1 -0
  89. package/dist/cli/main.js +1000 -4456
  90. package/dist/cli/main.js.map +1 -1
  91. package/dist/cli/migration-gates.d.ts +22 -0
  92. package/dist/cli/migration-gates.js +146 -0
  93. package/dist/cli/migration-gates.js.map +1 -0
  94. package/dist/cli/register-list-query.d.ts +2 -0
  95. package/dist/cli/register-list-query.js +317 -0
  96. package/dist/cli/register-list-query.js.map +1 -0
  97. package/dist/cli/register-mutation.d.ts +2 -0
  98. package/dist/cli/register-mutation.js +795 -0
  99. package/dist/cli/register-mutation.js.map +1 -0
  100. package/dist/cli/register-operations.d.ts +2 -0
  101. package/dist/cli/register-operations.js +610 -0
  102. package/dist/cli/register-operations.js.map +1 -0
  103. package/dist/cli/register-setup.d.ts +2 -0
  104. package/dist/cli/register-setup.js +334 -0
  105. package/dist/cli/register-setup.js.map +1 -0
  106. package/dist/cli/registration-helpers.d.ts +53 -0
  107. package/dist/cli/registration-helpers.js +669 -0
  108. package/dist/cli/registration-helpers.js.map +1 -0
  109. package/dist/cli/shared-parsers.d.ts +6 -0
  110. package/dist/cli/shared-parsers.js +40 -0
  111. package/dist/cli/shared-parsers.js.map +1 -0
  112. package/dist/cli.d.ts +1 -1
  113. package/dist/cli.js +3 -1
  114. package/dist/cli.js.map +1 -1
  115. package/dist/core/extensions/extension-types.d.ts +605 -0
  116. package/dist/core/extensions/extension-types.js +22 -0
  117. package/dist/core/extensions/extension-types.js.map +1 -0
  118. package/dist/core/extensions/index.js.map +1 -1
  119. package/dist/core/extensions/item-fields.js.map +1 -1
  120. package/dist/core/extensions/loader.d.ts +2 -586
  121. package/dist/core/extensions/loader.js +3 -21
  122. package/dist/core/extensions/loader.js.map +1 -1
  123. package/dist/core/extensions/runtime-registrations.js.map +1 -1
  124. package/dist/core/fs/fs-utils.js.map +1 -1
  125. package/dist/core/fs/index.js.map +1 -1
  126. package/dist/core/history/history-stream-policy.js.map +1 -1
  127. package/dist/core/history/history.js.map +1 -1
  128. package/dist/core/history/index.js.map +1 -1
  129. package/dist/core/item/id.js.map +1 -1
  130. package/dist/core/item/index.js.map +1 -1
  131. package/dist/core/item/item-format.js.map +1 -1
  132. package/dist/core/item/parent-reference-policy.js.map +1 -1
  133. package/dist/core/item/parse.js +6 -0
  134. package/dist/core/item/parse.js.map +1 -1
  135. package/dist/core/item/sprint-release-format.js.map +1 -1
  136. package/dist/core/item/status.js.map +1 -1
  137. package/dist/core/item/type-registry.js.map +1 -1
  138. package/dist/core/lock/index.js.map +1 -1
  139. package/dist/core/lock/lock.js +1 -6
  140. package/dist/core/lock/lock.js.map +1 -1
  141. package/dist/core/output/command-aware.js.map +1 -1
  142. package/dist/core/output/output.js.map +1 -1
  143. package/dist/core/schema/runtime-field-filters.js.map +1 -1
  144. package/dist/core/schema/runtime-field-values.js.map +1 -1
  145. package/dist/core/schema/runtime-schema.js.map +1 -1
  146. package/dist/core/search/cache.js +1 -7
  147. package/dist/core/search/cache.js.map +1 -1
  148. package/dist/core/search/embedding-batches.js +4 -0
  149. package/dist/core/search/embedding-batches.js.map +1 -1
  150. package/dist/core/search/http-client.d.ts +29 -0
  151. package/dist/core/search/http-client.js +64 -0
  152. package/dist/core/search/http-client.js.map +1 -0
  153. package/dist/core/search/providers.d.ts +3 -13
  154. package/dist/core/search/providers.js +19 -88
  155. package/dist/core/search/providers.js.map +1 -1
  156. package/dist/core/search/semantic-defaults.js +2 -7
  157. package/dist/core/search/semantic-defaults.js.map +1 -1
  158. package/dist/core/search/vector-stores.d.ts +4 -13
  159. package/dist/core/search/vector-stores.js +40 -93
  160. package/dist/core/search/vector-stores.js.map +1 -1
  161. package/dist/core/sentry/helpers.d.ts +27 -0
  162. package/dist/core/sentry/helpers.js +171 -0
  163. package/dist/core/sentry/helpers.js.map +1 -0
  164. package/dist/core/sentry/instrument.d.ts +25 -0
  165. package/dist/core/sentry/instrument.js +204 -0
  166. package/dist/core/sentry/instrument.js.map +1 -0
  167. package/dist/core/shared/command-types.js.map +1 -1
  168. package/dist/core/shared/conflict-markers.js.map +1 -1
  169. package/dist/core/shared/constants.d.ts +3 -0
  170. package/dist/core/shared/constants.js +58 -1
  171. package/dist/core/shared/constants.js.map +1 -1
  172. package/dist/core/shared/errors.js.map +1 -1
  173. package/dist/core/shared/index.d.ts +1 -0
  174. package/dist/core/shared/index.js +1 -0
  175. package/dist/core/shared/index.js.map +1 -1
  176. package/dist/core/shared/primitives.d.ts +13 -0
  177. package/dist/core/shared/primitives.js +33 -0
  178. package/dist/core/shared/primitives.js.map +1 -0
  179. package/dist/core/shared/serialization.js.map +1 -1
  180. package/dist/core/shared/text-normalization.js.map +1 -1
  181. package/dist/core/shared/time.js.map +1 -1
  182. package/dist/core/store/front-matter-cache.d.ts +6 -0
  183. package/dist/core/store/front-matter-cache.js +150 -0
  184. package/dist/core/store/front-matter-cache.js.map +1 -0
  185. package/dist/core/store/index.js.map +1 -1
  186. package/dist/core/store/item-format-migration.js.map +1 -1
  187. package/dist/core/store/item-store.js +46 -36
  188. package/dist/core/store/item-store.js.map +1 -1
  189. package/dist/core/store/paths.js.map +1 -1
  190. package/dist/core/store/settings.js +36 -0
  191. package/dist/core/store/settings.js.map +1 -1
  192. package/dist/core/telemetry/consent.js.map +1 -1
  193. package/dist/core/telemetry/observability.d.ts +24 -0
  194. package/dist/core/telemetry/observability.js +185 -0
  195. package/dist/core/telemetry/observability.js.map +1 -0
  196. package/dist/core/telemetry/runtime.d.ts +29 -3
  197. package/dist/core/telemetry/runtime.js +337 -25
  198. package/dist/core/telemetry/runtime.js.map +1 -1
  199. package/dist/core/test/background-runs.js.map +1 -1
  200. package/dist/core/test/item-test-run-tracking.js.map +1 -1
  201. package/dist/sdk/cli-contracts.js +28 -0
  202. package/dist/sdk/cli-contracts.js.map +1 -1
  203. package/dist/sdk/index.d.ts +1 -1
  204. package/dist/sdk/index.js.map +1 -1
  205. package/dist/types/index.js.map +1 -1
  206. package/dist/types.d.ts +21 -0
  207. package/dist/types.js +11 -0
  208. package/dist/types.js.map +1 -1
  209. package/docs/AGENT_GUIDE.md +125 -0
  210. package/docs/ARCHITECTURE.md +201 -478
  211. package/docs/COMMANDS.md +209 -0
  212. package/docs/CONFIGURATION.md +146 -0
  213. package/docs/EXTENSIONS.md +146 -645
  214. package/docs/QUICKSTART.md +108 -0
  215. package/docs/README.md +70 -0
  216. package/docs/RELEASING.md +92 -50
  217. package/docs/SDK.md +127 -68
  218. package/docs/TESTING.md +125 -0
  219. package/docs/examples/starter-extension/README.md +39 -25
  220. package/package.json +24 -11
  221. package/dist/command-types.d.ts +0 -1
  222. package/dist/command-types.js +0 -2
  223. package/dist/command-types.js.map +0 -1
  224. package/dist/constants.d.ts +0 -1
  225. package/dist/constants.js +0 -2
  226. package/dist/constants.js.map +0 -1
  227. package/dist/errors.d.ts +0 -1
  228. package/dist/errors.js +0 -2
  229. package/dist/errors.js.map +0 -1
  230. package/dist/fs-utils.d.ts +0 -1
  231. package/dist/fs-utils.js +0 -2
  232. package/dist/fs-utils.js.map +0 -1
  233. package/dist/history.d.ts +0 -1
  234. package/dist/history.js +0 -2
  235. package/dist/history.js.map +0 -1
  236. package/dist/id.d.ts +0 -1
  237. package/dist/id.js +0 -2
  238. package/dist/id.js.map +0 -1
  239. package/dist/item-format.d.ts +0 -1
  240. package/dist/item-format.js +0 -2
  241. package/dist/item-format.js.map +0 -1
  242. package/dist/item-store.d.ts +0 -1
  243. package/dist/item-store.js +0 -2
  244. package/dist/item-store.js.map +0 -1
  245. package/dist/lock.d.ts +0 -1
  246. package/dist/lock.js +0 -2
  247. package/dist/lock.js.map +0 -1
  248. package/dist/output.d.ts +0 -1
  249. package/dist/output.js +0 -2
  250. package/dist/output.js.map +0 -1
  251. package/dist/parse.d.ts +0 -1
  252. package/dist/parse.js +0 -2
  253. package/dist/parse.js.map +0 -1
  254. package/dist/paths.d.ts +0 -1
  255. package/dist/paths.js +0 -2
  256. package/dist/paths.js.map +0 -1
  257. package/dist/serialization.d.ts +0 -1
  258. package/dist/serialization.js +0 -2
  259. package/dist/serialization.js.map +0 -1
  260. package/dist/settings.d.ts +0 -1
  261. package/dist/settings.js +0 -2
  262. package/dist/settings.js.map +0 -1
  263. package/dist/time.d.ts +0 -1
  264. package/dist/time.js +0 -2
  265. package/dist/time.js.map +0 -1
@@ -1,15 +1,35 @@
1
- import { EXIT_CODE } from "../../core/shared/constants.js";
1
+ import { SETTINGS_DEFAULTS, EXIT_CODE } from "../../core/shared/constants.js";
2
2
  import { PmCliError } from "../../core/shared/errors.js";
3
3
  import { compareTimestampStrings } from "../../core/shared/time.js";
4
4
  import { normalizeStatusInput } from "../../core/item/status.js";
5
5
  import { resolveRuntimeStatusRegistry } from "../../core/schema/runtime-schema.js";
6
6
  import { resolvePmRoot } from "../../core/store/paths.js";
7
7
  import { readSettings } from "../../core/store/settings.js";
8
+ import { CONTEXT_DEPTH_VALUES, CONTEXT_SECTION_VALUES } from "../../types/index.js";
9
+ import { parseIntegerLimit } from "../shared-parsers.js";
8
10
  import { runCalendar } from "./calendar.js";
9
11
  import { runList } from "./list.js";
12
+ import { runActivity } from "./activity.js";
13
+ // ---------------------------------------------------------------------------
14
+ // Output format
15
+ // ---------------------------------------------------------------------------
10
16
  export const CONTEXT_OUTPUT_VALUES = ["markdown", "toon", "json"];
17
+ // ---------------------------------------------------------------------------
18
+ // Constants
19
+ // ---------------------------------------------------------------------------
11
20
  const HIGH_LEVEL_TYPES = new Set(["Epic", "Feature"]);
12
21
  const DEFAULT_CONTEXT_LIMIT = 10;
22
+ const STANDARD_SECTIONS = ["hierarchy", "activity", "progress", "workload"];
23
+ const DEEP_SECTIONS = [
24
+ ...STANDARD_SECTIONS,
25
+ "blockers",
26
+ "files",
27
+ "staleness",
28
+ "tests",
29
+ ];
30
+ // ---------------------------------------------------------------------------
31
+ // Parsers
32
+ // ---------------------------------------------------------------------------
13
33
  function parseOutputFormat(raw) {
14
34
  if (!raw)
15
35
  return undefined;
@@ -29,16 +49,59 @@ export function resolveContextOutputFormat(options, global) {
29
49
  }
30
50
  return commandFormat ?? "toon";
31
51
  }
32
- function parseLimit(raw) {
33
- if (raw === undefined) {
34
- return DEFAULT_CONTEXT_LIMIT;
52
+ function parseContextLimit(raw) {
53
+ return parseIntegerLimit(raw, "--limit") ?? DEFAULT_CONTEXT_LIMIT;
54
+ }
55
+ export function parseContextDepth(raw, settings) {
56
+ if (!raw)
57
+ return settings.default_depth;
58
+ const normalized = raw.trim().toLowerCase();
59
+ if (!CONTEXT_DEPTH_VALUES.includes(normalized)) {
60
+ throw new PmCliError(`Context --depth must be one of ${CONTEXT_DEPTH_VALUES.join("|")}`, EXIT_CODE.USAGE);
61
+ }
62
+ return normalized;
63
+ }
64
+ export function parseContextSections(raw, depth, settings) {
65
+ if (raw && raw.length > 0) {
66
+ const sections = [];
67
+ for (const value of raw) {
68
+ const normalized = value.trim().toLowerCase();
69
+ if (!CONTEXT_SECTION_VALUES.includes(normalized)) {
70
+ throw new PmCliError(`Context --section must be one of ${CONTEXT_SECTION_VALUES.join("|")}`, EXIT_CODE.USAGE);
71
+ }
72
+ if (!sections.includes(normalized)) {
73
+ sections.push(normalized);
74
+ }
75
+ }
76
+ return sections;
77
+ }
78
+ if (depth === "brief")
79
+ return [];
80
+ const pool = depth === "deep" ? DEEP_SECTIONS : STANDARD_SECTIONS;
81
+ return pool.filter((section) => settings.sections[section]);
82
+ }
83
+ function parseActivityLimit(raw, settings) {
84
+ if (!raw)
85
+ return settings.activity_limit;
86
+ return parseIntegerLimit(raw, "--activity-limit") ?? settings.activity_limit;
87
+ }
88
+ function parseStaleThresholdDays(raw, settings) {
89
+ if (!raw)
90
+ return settings.stale_threshold_days;
91
+ const trimmed = raw.trim().toLowerCase();
92
+ const match = /^(\d+)d?$/.exec(trimmed);
93
+ if (!match) {
94
+ throw new PmCliError("--stale-threshold must be a number of days (e.g. 7 or 7d)", EXIT_CODE.USAGE);
35
95
  }
36
- const parsed = Number(raw);
37
- if (!Number.isInteger(parsed) || parsed < 0) {
38
- throw new PmCliError("Context limit must be a non-negative integer", EXIT_CODE.USAGE);
96
+ const days = parseInt(match[1], 10);
97
+ if (days <= 0) {
98
+ throw new PmCliError("--stale-threshold must be positive", EXIT_CODE.USAGE);
39
99
  }
40
- return parsed;
100
+ return days;
41
101
  }
102
+ // ---------------------------------------------------------------------------
103
+ // Status helpers
104
+ // ---------------------------------------------------------------------------
42
105
  function normalizeStatusForRegistry(status, statusRegistry) {
43
106
  return normalizeStatusInput(status, statusRegistry) ?? status;
44
107
  }
@@ -64,6 +127,27 @@ function statusRank(status, statusRegistry) {
64
127
  return 7;
65
128
  return 6;
66
129
  }
130
+ function isTerminal(status, statusRegistry) {
131
+ return statusRegistry.terminal_statuses.has(normalizeStatusForRegistry(status, statusRegistry));
132
+ }
133
+ function isClosedStatus(status, statusRegistry) {
134
+ const closeStatus = normalizeStatusInput("closed", statusRegistry);
135
+ return closeStatus ? normalizeStatusForRegistry(status, statusRegistry) === closeStatus : false;
136
+ }
137
+ function isInProgressStatus(status, statusRegistry) {
138
+ const inProgressStatus = normalizeStatusInput("in_progress", statusRegistry);
139
+ return inProgressStatus ? normalizeStatusForRegistry(status, statusRegistry) === inProgressStatus : false;
140
+ }
141
+ function isOpenStatus(status, statusRegistry) {
142
+ const openStatus = normalizeStatusInput("open", statusRegistry) ?? statusRegistry.open_status;
143
+ return normalizeStatusForRegistry(status, statusRegistry) === openStatus;
144
+ }
145
+ function isBlockedStatus(status, statusRegistry) {
146
+ return statusRegistry.blocked_statuses.has(normalizeStatusForRegistry(status, statusRegistry));
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // Sorting / mapping helpers (unchanged from original)
150
+ // ---------------------------------------------------------------------------
67
151
  function compareOptionalOrder(left, right) {
68
152
  const leftValue = left ?? null;
69
153
  const rightValue = right ?? null;
@@ -145,6 +229,249 @@ function summarizeAgenda(events) {
145
229
  function filterTerminalCalendarEvents(events, statusRegistry) {
146
230
  return events.filter((event) => !statusRegistry.terminal_statuses.has(normalizeStatusForRegistry(event.item_status, statusRegistry)));
147
231
  }
232
+ // ---------------------------------------------------------------------------
233
+ // Section builders
234
+ // ---------------------------------------------------------------------------
235
+ function buildHierarchy(allItems, activeItems, statusRegistry, limit) {
236
+ const itemMap = new Map();
237
+ for (const item of allItems) {
238
+ itemMap.set(item.id, item);
239
+ }
240
+ const childrenByParent = new Map();
241
+ for (const item of allItems) {
242
+ if (!item.parent)
243
+ continue;
244
+ const children = childrenByParent.get(item.parent) ?? [];
245
+ children.push(item);
246
+ childrenByParent.set(item.parent, children);
247
+ }
248
+ const activeHighLevelIds = new Set(activeItems.filter((item) => HIGH_LEVEL_TYPES.has(item.type)).map((item) => item.id));
249
+ const nodes = [];
250
+ for (const parentId of activeHighLevelIds) {
251
+ const parent = itemMap.get(parentId);
252
+ if (!parent)
253
+ continue;
254
+ const allDescendants = collectDescendants(parentId, childrenByParent);
255
+ const childItems = childrenByParent.get(parentId) ?? [];
256
+ let closedCount = 0;
257
+ let openCount = 0;
258
+ let inProgressCount = 0;
259
+ let blockedCount = 0;
260
+ for (const desc of allDescendants) {
261
+ if (isClosedStatus(desc.status, statusRegistry))
262
+ closedCount++;
263
+ else if (isInProgressStatus(desc.status, statusRegistry))
264
+ inProgressCount++;
265
+ else if (isBlockedStatus(desc.status, statusRegistry))
266
+ blockedCount++;
267
+ else if (isOpenStatus(desc.status, statusRegistry))
268
+ openCount++;
269
+ }
270
+ const children = childItems
271
+ .sort((a, b) => compareCriticalItems(a, b, statusRegistry))
272
+ .slice(0, limit)
273
+ .map((child) => {
274
+ const grandchildren = collectDescendants(child.id, childrenByParent);
275
+ const gcClosed = grandchildren.filter((gc) => isClosedStatus(gc.status, statusRegistry)).length;
276
+ return {
277
+ id: child.id,
278
+ title: child.title,
279
+ type: child.type,
280
+ status: child.status,
281
+ children_total: grandchildren.length,
282
+ children_closed: gcClosed,
283
+ };
284
+ });
285
+ nodes.push({
286
+ id: parent.id,
287
+ title: parent.title,
288
+ type: parent.type,
289
+ status: parent.status,
290
+ children_total: allDescendants.length,
291
+ children_closed: closedCount,
292
+ children_open: openCount,
293
+ children_in_progress: inProgressCount,
294
+ children_blocked: blockedCount,
295
+ children,
296
+ });
297
+ }
298
+ return nodes
299
+ .sort((a, b) => {
300
+ const aParent = itemMap.get(a.id);
301
+ const bParent = itemMap.get(b.id);
302
+ return compareCriticalItems(aParent, bParent, statusRegistry);
303
+ })
304
+ .slice(0, limit);
305
+ }
306
+ function collectDescendants(parentId, childrenByParent) {
307
+ const result = [];
308
+ const stack = [parentId];
309
+ const visited = new Set();
310
+ while (stack.length > 0) {
311
+ const current = stack.pop();
312
+ if (visited.has(current))
313
+ continue;
314
+ visited.add(current);
315
+ const children = childrenByParent.get(current) ?? [];
316
+ for (const child of children) {
317
+ result.push(child);
318
+ stack.push(child.id);
319
+ }
320
+ }
321
+ return result;
322
+ }
323
+ async function buildActivity(activityLimit, global) {
324
+ const result = await runActivity({ compact: true, limit: String(activityLimit) }, global);
325
+ return result.compact_activity ?? [];
326
+ }
327
+ function buildProgress(allItems, activeItems, statusRegistry, limit) {
328
+ const childrenByParent = new Map();
329
+ for (const item of allItems) {
330
+ if (!item.parent)
331
+ continue;
332
+ const children = childrenByParent.get(item.parent) ?? [];
333
+ children.push(item);
334
+ childrenByParent.set(item.parent, children);
335
+ }
336
+ const activeHighLevel = activeItems.filter((item) => HIGH_LEVEL_TYPES.has(item.type));
337
+ const entries = [];
338
+ for (const parent of activeHighLevel) {
339
+ const descendants = collectDescendants(parent.id, childrenByParent);
340
+ const total = descendants.length;
341
+ if (total === 0)
342
+ continue;
343
+ let closed = 0;
344
+ let open = 0;
345
+ let inProgress = 0;
346
+ let blocked = 0;
347
+ for (const desc of descendants) {
348
+ if (isClosedStatus(desc.status, statusRegistry))
349
+ closed++;
350
+ else if (isInProgressStatus(desc.status, statusRegistry))
351
+ inProgress++;
352
+ else if (isBlockedStatus(desc.status, statusRegistry))
353
+ blocked++;
354
+ else if (isOpenStatus(desc.status, statusRegistry))
355
+ open++;
356
+ }
357
+ entries.push({
358
+ id: parent.id,
359
+ title: parent.title,
360
+ type: parent.type,
361
+ total,
362
+ closed,
363
+ open,
364
+ in_progress: inProgress,
365
+ blocked,
366
+ completion_pct: total > 0 ? Math.round((closed / total) * 100) : 0,
367
+ });
368
+ }
369
+ return entries
370
+ .sort((a, b) => a.completion_pct - b.completion_pct)
371
+ .slice(0, limit);
372
+ }
373
+ function buildBlockers(blockedItems, itemMap, limit) {
374
+ return blockedItems.slice(0, limit).map((item) => {
375
+ const blockerItem = item.blocked_by ? itemMap.get(item.blocked_by) : undefined;
376
+ return {
377
+ id: item.id,
378
+ title: item.title,
379
+ blocked_by: item.blocked_by ?? null,
380
+ blocked_by_title: blockerItem?.title ?? null,
381
+ blocked_by_status: blockerItem?.status ?? null,
382
+ blocked_reason: item.blocked_reason ?? null,
383
+ unblock_note: item.unblock_note ?? null,
384
+ };
385
+ });
386
+ }
387
+ function buildHotFiles(activeItems, limit) {
388
+ const fileMap = new Map();
389
+ for (const item of activeItems) {
390
+ for (const file of item.files ?? []) {
391
+ const existing = fileMap.get(file.path) ?? new Set();
392
+ existing.add(item.id);
393
+ fileMap.set(file.path, existing);
394
+ }
395
+ }
396
+ return [...fileMap.entries()]
397
+ .map(([filePath, itemIds]) => ({
398
+ path: filePath,
399
+ references: itemIds.size,
400
+ items: [...itemIds].sort(),
401
+ }))
402
+ .sort((a, b) => b.references - a.references)
403
+ .slice(0, limit);
404
+ }
405
+ function buildWorkload(activeItems, statusRegistry, limit) {
406
+ const groups = new Map();
407
+ for (const item of activeItems) {
408
+ const key = item.assignee ?? null;
409
+ const existing = groups.get(key) ?? [];
410
+ existing.push(item);
411
+ groups.set(key, existing);
412
+ }
413
+ return [...groups.entries()]
414
+ .map(([assignee, items]) => ({
415
+ assignee,
416
+ active: items.length,
417
+ in_progress: items.filter((item) => isInProgressStatus(item.status, statusRegistry)).length,
418
+ items: items.map((item) => item.id).sort(),
419
+ }))
420
+ .sort((a, b) => b.active - a.active)
421
+ .slice(0, limit);
422
+ }
423
+ function buildStaleness(allNonTerminal, staleThresholdDays, now, limit) {
424
+ const cutoffMs = new Date(now).getTime() - staleThresholdDays * 24 * 60 * 60 * 1000;
425
+ return allNonTerminal
426
+ .filter((item) => new Date(item.updated_at).getTime() < cutoffMs)
427
+ .map((item) => ({
428
+ id: item.id,
429
+ title: item.title,
430
+ status: item.status,
431
+ updated_at: item.updated_at,
432
+ stale_days: Math.floor((new Date(now).getTime() - new Date(item.updated_at).getTime()) / (24 * 60 * 60 * 1000)),
433
+ }))
434
+ .sort((a, b) => b.stale_days - a.stale_days)
435
+ .slice(0, limit);
436
+ }
437
+ function buildTestHealth(activeItems) {
438
+ let itemsWithTests = 0;
439
+ let itemsWithRecentRuns = 0;
440
+ let passed = 0;
441
+ let failed = 0;
442
+ let skipped = 0;
443
+ const itemsFailing = [];
444
+ for (const item of activeItems) {
445
+ if ((item.tests ?? []).length > 0) {
446
+ itemsWithTests++;
447
+ }
448
+ const runs = item.test_runs ?? [];
449
+ if (runs.length > 0) {
450
+ itemsWithRecentRuns++;
451
+ let itemHasFailure = false;
452
+ for (const run of runs) {
453
+ passed += run.passed ?? 0;
454
+ failed += run.failed ?? 0;
455
+ skipped += run.skipped ?? 0;
456
+ if ((run.failed ?? 0) > 0) {
457
+ itemHasFailure = true;
458
+ }
459
+ }
460
+ if (itemHasFailure) {
461
+ itemsFailing.push(item.id);
462
+ }
463
+ }
464
+ }
465
+ return {
466
+ items_with_tests: itemsWithTests,
467
+ items_with_recent_runs: itemsWithRecentRuns,
468
+ recent_runs: { passed, failed, skipped },
469
+ items_failing: itemsFailing.sort(),
470
+ };
471
+ }
472
+ // ---------------------------------------------------------------------------
473
+ // Markdown formatting
474
+ // ---------------------------------------------------------------------------
148
475
  function formatClock(timestamp) {
149
476
  return `${new Date(timestamp).toISOString().slice(11, 16)}Z`;
150
477
  }
@@ -170,9 +497,16 @@ export function renderContextMarkdown(result) {
170
497
  lines.push("# pm context");
171
498
  lines.push("");
172
499
  lines.push(`- now: ${result.now}`);
500
+ lines.push(`- depth: ${result.depth}`);
173
501
  lines.push(`- active_items: ${result.summary.active_items} (in_progress: ${result.summary.in_progress}, open: ${result.summary.open})`);
502
+ if (result.summary.total_items !== undefined) {
503
+ lines.push(`- total_items: ${result.summary.total_items} (closed: ${result.summary.closed ?? 0}, canceled: ${result.summary.canceled ?? 0})`);
504
+ }
174
505
  lines.push(`- agenda_events: ${result.summary.agenda_events}`);
175
506
  lines.push(`- blocked_fallback_used: ${result.summary.blocked_fallback_used}`);
507
+ if (result.sections_included.length > 0) {
508
+ lines.push(`- sections: ${result.sections_included.join(", ")}`);
509
+ }
176
510
  lines.push("");
177
511
  lines.push("## High-level focus");
178
512
  if (result.high_level.length === 0) {
@@ -211,16 +545,112 @@ export function renderContextMarkdown(result) {
211
545
  lines.push(`- ${formatAgendaLine(event)}`);
212
546
  }
213
547
  }
548
+ lines.push("");
549
+ if (result.hierarchy && result.hierarchy.length > 0) {
550
+ lines.push("## Hierarchy");
551
+ for (const node of result.hierarchy) {
552
+ const pct = node.children_total > 0 ? Math.round((node.children_closed / node.children_total) * 100) : 0;
553
+ lines.push(`- ${node.id} ${node.type} ${node.status} "${node.title}" [${node.children_closed}/${node.children_total} done ${pct}%]`);
554
+ for (const child of node.children) {
555
+ const cpct = child.children_total > 0 ? Math.round((child.children_closed / child.children_total) * 100) : 0;
556
+ lines.push(` - ${child.id} ${child.type} ${child.status} "${child.title}" [${child.children_closed}/${child.children_total} done ${cpct}%]`);
557
+ }
558
+ }
559
+ lines.push("");
560
+ }
561
+ if (result.progress && result.progress.length > 0) {
562
+ lines.push("## Progress");
563
+ for (const entry of result.progress) {
564
+ lines.push(`- ${entry.id} "${entry.title}" ${entry.completion_pct}% (${entry.closed}/${entry.total} closed, ${entry.in_progress} wip, ${entry.open} open, ${entry.blocked} blocked)`);
565
+ }
566
+ lines.push("");
567
+ }
568
+ if (result.activity && result.activity.length > 0) {
569
+ lines.push("## Recent activity");
570
+ for (const entry of result.activity) {
571
+ const msg = entry.msg ? ` ${entry.msg}` : "";
572
+ lines.push(`- ${entry.ts.slice(0, 16)}Z ${entry.id} ${entry.op} by:${entry.author}${msg}`);
573
+ }
574
+ lines.push("");
575
+ }
576
+ if (result.blockers && result.blockers.length > 0) {
577
+ lines.push("## Blockers");
578
+ for (const entry of result.blockers) {
579
+ const by = entry.blocked_by ? `blocked_by:${entry.blocked_by}(${entry.blocked_by_status ?? "?"})` : "blocked_by:-";
580
+ const reason = entry.blocked_reason ? ` reason:"${entry.blocked_reason}"` : "";
581
+ const note = entry.unblock_note ? ` unblock:"${entry.unblock_note}"` : "";
582
+ lines.push(`- ${entry.id} "${entry.title}" ${by}${reason}${note}`);
583
+ }
584
+ lines.push("");
585
+ }
586
+ if (result.files && result.files.length > 0) {
587
+ lines.push("## Hot files");
588
+ for (const file of result.files) {
589
+ lines.push(`- ${file.path} refs:${file.references} items:[${file.items.join(",")}]`);
590
+ }
591
+ lines.push("");
592
+ }
593
+ if (result.workload && result.workload.length > 0) {
594
+ lines.push("## Workload");
595
+ for (const entry of result.workload) {
596
+ const who = entry.assignee ?? "(unassigned)";
597
+ lines.push(`- ${who} active:${entry.active} wip:${entry.in_progress} items:[${entry.items.join(",")}]`);
598
+ }
599
+ lines.push("");
600
+ }
601
+ if (result.staleness && result.staleness.length > 0) {
602
+ lines.push("## Stale items");
603
+ for (const entry of result.staleness) {
604
+ lines.push(`- ${entry.id} ${entry.status} stale:${entry.stale_days}d last:${entry.updated_at.slice(0, 10)} "${entry.title}"`);
605
+ }
606
+ lines.push("");
607
+ }
608
+ if (result.tests) {
609
+ lines.push("## Test health");
610
+ lines.push(`- items_with_tests: ${result.tests.items_with_tests}`);
611
+ lines.push(`- items_with_recent_runs: ${result.tests.items_with_recent_runs}`);
612
+ lines.push(`- passed: ${result.tests.recent_runs.passed}, failed: ${result.tests.recent_runs.failed}, skipped: ${result.tests.recent_runs.skipped}`);
613
+ if (result.tests.items_failing.length > 0) {
614
+ lines.push(`- items_failing: [${result.tests.items_failing.join(",")}]`);
615
+ }
616
+ lines.push("");
617
+ }
618
+ const isEmpty = result.summary.active_items === 0 &&
619
+ result.summary.blocked === 0 &&
620
+ result.agenda.summary.events === 0;
621
+ if (isEmpty) {
622
+ lines.push("## Suggestions");
623
+ lines.push("No active work items or upcoming events. Consider:");
624
+ lines.push("- `pm create --type Task --title \"...\"` to add a new work item");
625
+ lines.push("- `pm list --status closed --limit 5` to review recent completions");
626
+ lines.push("- `pm search <keywords>` to find related past work");
627
+ lines.push("- `pm aggregate` for a full project status overview");
628
+ }
214
629
  return lines.join("\n");
215
630
  }
631
+ // ---------------------------------------------------------------------------
632
+ // Main runner
633
+ // ---------------------------------------------------------------------------
216
634
  export async function runContext(options, global) {
217
635
  const pmRoot = resolvePmRoot(process.cwd(), global.path);
218
636
  const settings = await readSettings(pmRoot);
637
+ const contextSettings = settings.context ?? SETTINGS_DEFAULTS.context;
219
638
  const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
220
- const limit = parseLimit(options.limit);
639
+ const limit = parseContextLimit(options.limit);
640
+ const depth = parseContextDepth(options.depth, contextSettings);
641
+ const sectionsIncluded = parseContextSections(options.section, depth, contextSettings);
642
+ const activityLimit = parseActivityLimit(options.activityLimit, contextSettings);
643
+ const staleThresholdDays = parseStaleThresholdDays(options.staleThreshold, contextSettings);
644
+ const needsAllItems = sectionsIncluded.some((s) => ["hierarchy", "progress", "blockers", "staleness"].includes(s));
221
645
  const listOptions = { ...options, excludeTerminal: true };
222
646
  const listed = await runList(undefined, listOptions, global);
223
647
  const listedFrontMatter = listed.items;
648
+ let allItems = listedFrontMatter;
649
+ if (needsAllItems) {
650
+ const allListOptions = { ...options, excludeTerminal: false };
651
+ const allListed = await runList(undefined, allListOptions, global);
652
+ allItems = allListed.items;
653
+ }
224
654
  const ranked = [...listedFrontMatter].sort((left, right) => compareCriticalItems(left, right, statusRegistry));
225
655
  const activeStatuses = statusRegistry.active_statuses.size > 0
226
656
  ? statusRegistry.active_statuses
@@ -254,9 +684,36 @@ export async function runContext(options, global) {
254
684
  ? activeItems.filter((item) => normalizeStatusForRegistry(item.status, statusRegistry) === inProgressStatus).length
255
685
  : 0;
256
686
  const openCount = activeItems.filter((item) => normalizeStatusForRegistry(item.status, statusRegistry) === openStatus).length;
257
- return {
687
+ const now = agenda.now;
688
+ const itemMap = new Map();
689
+ for (const item of allItems) {
690
+ itemMap.set(item.id, item);
691
+ }
692
+ const allNonTerminal = allItems.filter((item) => !isTerminal(item.status, statusRegistry));
693
+ const has = (section) => sectionsIncluded.includes(section);
694
+ const hierarchy = has("hierarchy") ? buildHierarchy(allItems, activeItems, statusRegistry, limit) : undefined;
695
+ const activity = has("activity") ? await buildActivity(activityLimit, global) : undefined;
696
+ const progress = has("progress") ? buildProgress(allItems, activeItems, statusRegistry, limit) : undefined;
697
+ const blockersSection = has("blockers") ? buildBlockers(blockedItems, itemMap, limit) : undefined;
698
+ const filesSection = has("files") ? buildHotFiles(activeItems, limit) : undefined;
699
+ const workload = has("workload") ? buildWorkload(activeItems, statusRegistry, limit) : undefined;
700
+ const staleness = has("staleness") ? buildStaleness(allNonTerminal, staleThresholdDays, now, limit) : undefined;
701
+ const tests = has("tests") ? buildTestHealth(activeItems) : undefined;
702
+ const summaryExtras = needsAllItems
703
+ ? {
704
+ total_items: allItems.length,
705
+ closed: allItems.filter((i) => isClosedStatus(i.status, statusRegistry)).length,
706
+ canceled: allItems.filter((i) => {
707
+ const canceledStatus = normalizeStatusInput("canceled", statusRegistry);
708
+ return canceledStatus ? normalizeStatusForRegistry(i.status, statusRegistry) === canceledStatus : false;
709
+ }).length,
710
+ }
711
+ : {};
712
+ const result = {
258
713
  output_default: "toon",
259
- now: agenda.now,
714
+ now,
715
+ depth,
716
+ sections_included: sectionsIncluded,
260
717
  window: {
261
718
  anchor: agenda.anchor,
262
719
  start: agenda.range.start,
@@ -285,6 +742,7 @@ export async function runContext(options, global) {
285
742
  high_level: highLevel.length,
286
743
  low_level: lowLevel.length,
287
744
  agenda_events: agendaSummary.events,
745
+ ...summaryExtras,
288
746
  },
289
747
  high_level: highLevel,
290
748
  low_level: lowLevel,
@@ -293,7 +751,33 @@ export async function runContext(options, global) {
293
751
  summary: agendaSummary,
294
752
  events: agendaEvents,
295
753
  },
296
- ...(warnings.length > 0 ? { warnings } : {}),
297
754
  };
755
+ if (hierarchy)
756
+ result.hierarchy = hierarchy;
757
+ if (activity)
758
+ result.activity = activity;
759
+ if (progress)
760
+ result.progress = progress;
761
+ if (blockersSection)
762
+ result.blockers = blockersSection;
763
+ if (filesSection)
764
+ result.files = filesSection;
765
+ if (workload)
766
+ result.workload = workload;
767
+ if (staleness)
768
+ result.staleness = staleness;
769
+ if (tests)
770
+ result.tests = tests;
771
+ if (warnings.length > 0)
772
+ result.warnings = warnings;
773
+ if (activeItems.length === 0 && blockedItems.length === 0 && agendaEvents.length === 0) {
774
+ result.suggestions = [
775
+ 'pm create --type Task --title "..." to add a new work item',
776
+ "pm list --status closed --limit 5 to review recent completions",
777
+ "pm search <keywords> to find related past work",
778
+ "pm aggregate for a full project status overview",
779
+ ];
780
+ }
781
+ return result;
298
782
  }
299
783
  //# sourceMappingURL=context.js.map