@unbrained/pm-cli 2026.5.2 → 2026.5.3-6

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 (115) hide show
  1. package/AGENTS.md +8 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +9 -1
  4. package/dist/cli/bootstrap-args.d.ts +18 -0
  5. package/dist/cli/bootstrap-args.js +242 -0
  6. package/dist/cli/bootstrap-args.js.map +1 -0
  7. package/dist/cli/commander-usage.d.ts +17 -0
  8. package/dist/cli/commander-usage.js +178 -0
  9. package/dist/cli/commander-usage.js.map +1 -0
  10. package/dist/cli/commands/activity.js +1 -9
  11. package/dist/cli/commands/activity.js.map +1 -1
  12. package/dist/cli/commands/calendar.js +3 -29
  13. package/dist/cli/commands/calendar.js.map +1 -1
  14. package/dist/cli/commands/comments.js +1 -9
  15. package/dist/cli/commands/comments.js.map +1 -1
  16. package/dist/cli/commands/config.d.ts +21 -3
  17. package/dist/cli/commands/config.js +118 -2
  18. package/dist/cli/commands/config.js.map +1 -1
  19. package/dist/cli/commands/context.d.ts +90 -1
  20. package/dist/cli/commands/context.js +485 -23
  21. package/dist/cli/commands/context.js.map +1 -1
  22. package/dist/cli/commands/dedupe-audit.js +2 -11
  23. package/dist/cli/commands/dedupe-audit.js.map +1 -1
  24. package/dist/cli/commands/history.js +1 -9
  25. package/dist/cli/commands/history.js.map +1 -1
  26. package/dist/cli/commands/learnings.js +1 -9
  27. package/dist/cli/commands/learnings.js.map +1 -1
  28. package/dist/cli/commands/list.js +3 -29
  29. package/dist/cli/commands/list.js.map +1 -1
  30. package/dist/cli/commands/normalize.js +9 -6
  31. package/dist/cli/commands/normalize.js.map +1 -1
  32. package/dist/cli/commands/notes.js +1 -9
  33. package/dist/cli/commands/notes.js.map +1 -1
  34. package/dist/cli/commands/reindex.js +2 -7
  35. package/dist/cli/commands/reindex.js.map +1 -1
  36. package/dist/cli/commands/search.js +4 -35
  37. package/dist/cli/commands/search.js.map +1 -1
  38. package/dist/cli/commands/test-runs.js +1 -11
  39. package/dist/cli/commands/test-runs.js.map +1 -1
  40. package/dist/cli/error-guidance.d.ts +13 -0
  41. package/dist/cli/error-guidance.js +43 -4
  42. package/dist/cli/error-guidance.js.map +1 -1
  43. package/dist/cli/extension-command-help.d.ts +48 -0
  44. package/dist/cli/extension-command-help.js +389 -0
  45. package/dist/cli/extension-command-help.js.map +1 -0
  46. package/dist/cli/help-content.js +9 -3
  47. package/dist/cli/help-content.js.map +1 -1
  48. package/dist/cli/help-json-payload.d.ts +25 -0
  49. package/dist/cli/help-json-payload.js +265 -0
  50. package/dist/cli/help-json-payload.js.map +1 -0
  51. package/dist/cli/main.js +996 -4468
  52. package/dist/cli/main.js.map +1 -1
  53. package/dist/cli/migration-gates.d.ts +22 -0
  54. package/dist/cli/migration-gates.js +146 -0
  55. package/dist/cli/migration-gates.js.map +1 -0
  56. package/dist/cli/register-list-query.d.ts +2 -0
  57. package/dist/cli/register-list-query.js +317 -0
  58. package/dist/cli/register-list-query.js.map +1 -0
  59. package/dist/cli/register-mutation.d.ts +2 -0
  60. package/dist/cli/register-mutation.js +795 -0
  61. package/dist/cli/register-mutation.js.map +1 -0
  62. package/dist/cli/register-operations.d.ts +2 -0
  63. package/dist/cli/register-operations.js +610 -0
  64. package/dist/cli/register-operations.js.map +1 -0
  65. package/dist/cli/register-setup.d.ts +2 -0
  66. package/dist/cli/register-setup.js +334 -0
  67. package/dist/cli/register-setup.js.map +1 -0
  68. package/dist/cli/registration-helpers.d.ts +53 -0
  69. package/dist/cli/registration-helpers.js +669 -0
  70. package/dist/cli/registration-helpers.js.map +1 -0
  71. package/dist/cli/shared-parsers.d.ts +6 -0
  72. package/dist/cli/shared-parsers.js +40 -0
  73. package/dist/cli/shared-parsers.js.map +1 -0
  74. package/dist/core/search/http-client.d.ts +29 -0
  75. package/dist/core/search/http-client.js +64 -0
  76. package/dist/core/search/http-client.js.map +1 -0
  77. package/dist/core/search/providers.d.ts +3 -13
  78. package/dist/core/search/providers.js +19 -69
  79. package/dist/core/search/providers.js.map +1 -1
  80. package/dist/core/search/semantic-defaults.js +2 -7
  81. package/dist/core/search/semantic-defaults.js.map +1 -1
  82. package/dist/core/search/vector-stores.d.ts +3 -13
  83. package/dist/core/search/vector-stores.js +17 -66
  84. package/dist/core/search/vector-stores.js.map +1 -1
  85. package/dist/core/sentry/helpers.d.ts +23 -2
  86. package/dist/core/sentry/helpers.js +101 -3
  87. package/dist/core/sentry/helpers.js.map +1 -1
  88. package/dist/core/sentry/instrument.d.ts +21 -0
  89. package/dist/core/sentry/instrument.js +34 -3
  90. package/dist/core/sentry/instrument.js.map +1 -1
  91. package/dist/core/shared/constants.d.ts +3 -0
  92. package/dist/core/shared/constants.js +58 -1
  93. package/dist/core/shared/constants.js.map +1 -1
  94. package/dist/core/store/front-matter-cache.d.ts +6 -0
  95. package/dist/core/store/front-matter-cache.js +150 -0
  96. package/dist/core/store/front-matter-cache.js.map +1 -0
  97. package/dist/core/store/item-store.js +2 -1
  98. package/dist/core/store/item-store.js.map +1 -1
  99. package/dist/core/store/settings.js +36 -0
  100. package/dist/core/store/settings.js.map +1 -1
  101. package/dist/core/telemetry/observability.d.ts +24 -0
  102. package/dist/core/telemetry/observability.js +185 -0
  103. package/dist/core/telemetry/observability.js.map +1 -0
  104. package/dist/core/telemetry/runtime.d.ts +27 -3
  105. package/dist/core/telemetry/runtime.js +298 -13
  106. package/dist/core/telemetry/runtime.js.map +1 -1
  107. package/dist/sdk/cli-contracts.js +28 -0
  108. package/dist/sdk/cli-contracts.js.map +1 -1
  109. package/dist/types.d.ts +21 -0
  110. package/dist/types.js +11 -0
  111. package/dist/types.js.map +1 -1
  112. package/docs/ARCHITECTURE.md +7 -1
  113. package/docs/COMMANDS.md +11 -1
  114. package/docs/RELEASING.md +56 -29
  115. package/package.json +8 -3
@@ -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,11 +545,80 @@ 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
+ }
214
618
  const isEmpty = result.summary.active_items === 0 &&
215
619
  result.summary.blocked === 0 &&
216
620
  result.agenda.summary.events === 0;
217
621
  if (isEmpty) {
218
- lines.push("");
219
622
  lines.push("## Suggestions");
220
623
  lines.push("No active work items or upcoming events. Consider:");
221
624
  lines.push("- `pm create --type Task --title \"...\"` to add a new work item");
@@ -225,14 +628,29 @@ export function renderContextMarkdown(result) {
225
628
  }
226
629
  return lines.join("\n");
227
630
  }
631
+ // ---------------------------------------------------------------------------
632
+ // Main runner
633
+ // ---------------------------------------------------------------------------
228
634
  export async function runContext(options, global) {
229
635
  const pmRoot = resolvePmRoot(process.cwd(), global.path);
230
636
  const settings = await readSettings(pmRoot);
637
+ const contextSettings = settings.context ?? SETTINGS_DEFAULTS.context;
231
638
  const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
232
- 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));
233
645
  const listOptions = { ...options, excludeTerminal: true };
234
646
  const listed = await runList(undefined, listOptions, global);
235
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
+ }
236
654
  const ranked = [...listedFrontMatter].sort((left, right) => compareCriticalItems(left, right, statusRegistry));
237
655
  const activeStatuses = statusRegistry.active_statuses.size > 0
238
656
  ? statusRegistry.active_statuses
@@ -266,9 +684,36 @@ export async function runContext(options, global) {
266
684
  ? activeItems.filter((item) => normalizeStatusForRegistry(item.status, statusRegistry) === inProgressStatus).length
267
685
  : 0;
268
686
  const openCount = activeItems.filter((item) => normalizeStatusForRegistry(item.status, statusRegistry) === openStatus).length;
269
- 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 = {
270
713
  output_default: "toon",
271
- now: agenda.now,
714
+ now,
715
+ depth,
716
+ sections_included: sectionsIncluded,
272
717
  window: {
273
718
  anchor: agenda.anchor,
274
719
  start: agenda.range.start,
@@ -297,6 +742,7 @@ export async function runContext(options, global) {
297
742
  high_level: highLevel.length,
298
743
  low_level: lowLevel.length,
299
744
  agenda_events: agendaSummary.events,
745
+ ...summaryExtras,
300
746
  },
301
747
  high_level: highLevel,
302
748
  low_level: lowLevel,
@@ -305,17 +751,33 @@ export async function runContext(options, global) {
305
751
  summary: agendaSummary,
306
752
  events: agendaEvents,
307
753
  },
308
- ...(warnings.length > 0 ? { warnings } : {}),
309
- ...(activeItems.length === 0 && blockedItems.length === 0 && agendaEvents.length === 0
310
- ? {
311
- suggestions: [
312
- 'pm create --type Task --title "..." to add a new work item',
313
- "pm list --status closed --limit 5 to review recent completions",
314
- "pm search <keywords> to find related past work",
315
- "pm aggregate for a full project status overview",
316
- ],
317
- }
318
- : {}),
319
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;
320
782
  }
321
783
  //# sourceMappingURL=context.js.map