pm-context 2026.10.26 → 2026.10.28

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.
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { buildSuggestedCommand } from "./suggested-command.js";
2
+ export { buildSuggestedCommand, buildRestoreCommand, quotePosixArgument } from "./suggested-command.js";
3
+ import { buildPhaseContext, encodePhaseContext, readPhaseContextJson, PhaseContextError } from "./task-phase.js";
4
+ export { buildPhaseContext, encodePhaseContext, parsePhaseContextRequest, readPhaseContextJson, PhaseContextError, TASK_PHASES } from "./task-phase.js";
1
5
  import { planNextEvidence, parseNextEvidencePacket, readNextEvidenceJson, validateNextEvidencePlan, encodeNextEvidencePlan, NextEvidenceError } from "./next-evidence.js";
2
6
  export { planNextEvidence, parseNextEvidencePacket, readNextEvidenceJson, validateNextEvidencePlan, encodeNextEvidencePlan, NextEvidenceError } from "./next-evidence.js";
3
7
  import { packCriterionEvidence, encodeEvidencePack, readEvidencePacket, EvidencePackingError } from "./evidence-packing.js";
@@ -9,7 +13,7 @@ export { createFreshnessReader, encodeFreshness, FRESHNESS_LIMITS, FreshnessErro
9
13
  import { buildSincePack, encodeSincePack } from "./since.js";
10
14
  import { writeFileSync } from "node:fs";
11
15
  import { isDeepStrictEqual } from "node:util";
12
- import { resolveFileFocus, matchFileFocus, rankFileFocus, renderFileFocus, readFileFocusItems } from "./file-focus.js";
16
+ import { resolveFileFocus, matchFileFocus, rankFileFocus, renderFileFocus, fileLinkCommand, readFileFocusItems } from "./file-focus.js";
13
17
  import { createRelationshipKindRegistry, readSettings, resolveRuntimeStatusRegistry, resolveWorkspaceRelationshipKindRegistry, } from "@unbrained/pm-cli/sdk";
14
18
  import { listAllComplete } from "@unbrained/pm-cli/sdk/core";
15
19
  import { buildItemContextRelevanceCandidates, certifyCompleteListResult, PmCompleteListValidationError, defaultScoreContextCandidates, packContextCandidates, recordContextUsageServing, } from "@unbrained/pm-cli/sdk/query";
@@ -146,11 +150,6 @@ function intOptionMin0(options, keys, fallback) {
146
150
  }
147
151
  return fallback;
148
152
  }
149
- function shellQuote(value) {
150
- if (/^[A-Za-z0-9._/:=,-]+$/.test(value))
151
- return value;
152
- return `'${value.replace(/'/g, "'\\''")}'`;
153
- }
154
153
  export function resolveSelectionOptions(options, defaults = {}) {
155
154
  const ids = Array.from(new Set([...asArray(options.id), ...asArray(options.ids)]));
156
155
  const status = stringOption(options, "status", "state");
@@ -167,21 +166,22 @@ function idSelectorArgs(ids) {
167
166
  if (normalizedIds.length === 0)
168
167
  return [];
169
168
  if (normalizedIds.length === 1)
170
- return ["--id", shellQuote(normalizedIds[0])];
171
- return ["--ids", shellQuote(normalizedIds.join(","))];
169
+ return ["--id", normalizedIds[0]];
170
+ return ["--ids", normalizedIds.join(",")];
172
171
  }
173
- export function buildSuggestedAgentCommand(input) {
172
+ /** Build a refresh argv before applying any display quoting. */
173
+ export function buildSuggestedAgentCommandArgv(input) {
174
174
  const args = ["pm", input.commandName];
175
175
  if (input.selection.ids.length > 0) {
176
176
  args.push(...idSelectorArgs(input.selection.ids));
177
177
  }
178
178
  else {
179
179
  if (input.selection.status)
180
- args.push("--status", shellQuote(input.selection.status));
180
+ args.push("--status", input.selection.status);
181
181
  if (input.selection.type)
182
- args.push("--type", shellQuote(input.selection.type));
182
+ args.push("--type", input.selection.type);
183
183
  if (input.selection.tag)
184
- args.push("--tag", shellQuote(input.selection.tag));
184
+ args.push("--tag", input.selection.tag);
185
185
  }
186
186
  if (input.includeFormatFlag)
187
187
  args.push("--format", "agent");
@@ -199,7 +199,7 @@ export function buildSuggestedAgentCommand(input) {
199
199
  args.push("--max-items", String(input.maxItems));
200
200
  if (input.sections && input.sections.length > 0) {
201
201
  for (const section of input.sections)
202
- args.push("--section", shellQuote(section));
202
+ args.push("--section", section);
203
203
  }
204
204
  if (!input.neighborhood) {
205
205
  args.push("--without-neighborhood");
@@ -207,7 +207,11 @@ export function buildSuggestedAgentCommand(input) {
207
207
  else if (input.neighborhoodDepth !== 1) {
208
208
  args.push("--neighborhood-depth", String(input.neighborhoodDepth));
209
209
  }
210
- return args.join(" ");
210
+ return buildSuggestedCommand(args).argv;
211
+ }
212
+ /** Render the refresh argv with the shared POSIX argument quoting policy. */
213
+ export function buildSuggestedAgentCommand(input) {
214
+ return buildSuggestedCommand(buildSuggestedAgentCommandArgv(input)).command;
211
215
  }
212
216
  function normalizeText(value) {
213
217
  return typeof value === "string" ? value.trim() : "";
@@ -451,7 +455,7 @@ export function buildContextPack(allItems, options = {}) {
451
455
  byStatus,
452
456
  byType,
453
457
  },
454
- ...(fileMatches ? { unlinked_changed_files: options.fileFocus.files.filter((file) => ![...fileMatches.values()].some((matches) => matches.includes(file))) } : {}),
458
+ ...(fileMatches ? { unlinked_changed_files: options.fileFocus.files.filter((file) => ![...fileMatches.values()].some((matches) => matches.includes(file))), link_command: fileLinkCommand() } : {}),
455
459
  items: packItems,
456
460
  neighbors: packNeighbors,
457
461
  links: buildLinks(selectedAndNeighbors),
@@ -597,10 +601,12 @@ export function buildAgentHandoff(pack, options = {}) {
597
601
  ? `priority ${item.priority}`
598
602
  : "selected focus item",
599
603
  }));
604
+ if (Object.hasOwn(options, "suggestedCommand"))
605
+ throw new CommandError("Supply suggestedArgv instead of a free-form suggestedCommand shell string.", EXIT_CODE.USAGE);
600
606
  const ids = pack.items.map((item) => item.id).join(",");
601
- const suggestedCommand = normalizeText(options.suggestedCommand) || (ids
602
- ? `pm context-pack --id ${ids} --format agent`
603
- : "pm context-pack --status in_progress --format agent");
607
+ const suggestion = buildSuggestedCommand(options.suggestedArgv ?? (ids
608
+ ? ["pm", "context-pack", "--id", ids, "--format", "agent"]
609
+ : ["pm", "context-pack", "--status", "in_progress", "--format", "agent"]));
604
610
  const recentLimit = options.recentLimit ?? 5;
605
611
  const recent = [...allVisible]
606
612
  .filter((item) => !isClosedStatus(itemStatus(item)))
@@ -636,8 +642,9 @@ export function buildAgentHandoff(pack, options = {}) {
636
642
  recent,
637
643
  links: pack.links.slice(0, 12),
638
644
  deps: pack.deps,
639
- ...(pack.unlinked_changed_files ? { unlinked_changed_files: pack.unlinked_changed_files } : {}),
640
- suggestedCommand,
645
+ ...(pack.unlinked_changed_files ? { unlinked_changed_files: pack.unlinked_changed_files, link_command: fileLinkCommand() } : {}),
646
+ suggestedCommand: suggestion.command,
647
+ suggestedArgv: suggestion.argv,
641
648
  };
642
649
  }
643
650
  export function renderAgentHandoff(pack, options = {}) {
@@ -731,7 +738,7 @@ export function renderAgentHandoff(pack, options = {}) {
731
738
  }
732
739
  }
733
740
  if (include("refresh")) {
734
- lines.push("", "## Refresh", "", `\`${handoff.suggestedCommand}\``, "");
741
+ lines.push("", "## Refresh", "", `\`${handoff.suggestedCommand}\``, `argv: ${JSON.stringify(handoff.suggestedArgv)}`, "");
735
742
  }
736
743
  let output = `${lines.join("\n")}\n`;
737
744
  if (compress) {
@@ -1072,7 +1079,73 @@ async function measureContextOutput(ctx, pack, output, format, command, compress
1072
1079
  return fallback;
1073
1080
  }
1074
1081
  }
1082
+ /** Return fresh structured examples for SDK/JSON/TOON callers and host help registration. */
1083
+ export function commandExamples() {
1084
+ return {
1085
+ "context-pack": [
1086
+ buildSuggestedCommand(["pm", "context-pack", "--id", "pm-1234", "--include-body", "--output", "context.md"]),
1087
+ buildSuggestedCommand(["pm", "context-pack", "--id", "pm-1234", "--format", "agent"]),
1088
+ buildSuggestedCommand(["pm", "context-pack", "--ids", "pm-1234,pm-5678", "--state", "blocked", "--format", "compact"]),
1089
+ buildSuggestedCommand(["pm", "context-pack", "--id", "pm-1234", "--format", "compact", "--recent", "8"]),
1090
+ buildSuggestedCommand(["pm", "context-pack", "--status", "in_progress", "--tag", "release", "--format", "json"]),
1091
+ buildSuggestedCommand(["pm", "context-pack", "--id", "pm-1234", "--compress", "--format", "json"]),
1092
+ buildSuggestedCommand(["pm", "context-pack", "--id", "pm-1234", "--format", "agent", "--include-deps", "--section", "focus", "--section", "blockers"]),
1093
+ buildSuggestedCommand(["pm", "context-pack", "--id", "pm-1234", "--max-items", "10"]),
1094
+ buildSuggestedCommand(["pm", "context-pack", "--status", "in_progress", "--explain"]),
1095
+ ],
1096
+ "context-handoff": [
1097
+ buildSuggestedCommand(["pm", "context-handoff", "--id", "pm-1234"]),
1098
+ buildSuggestedCommand(["pm", "context-handoff", "--ids", "pm-1234,pm-5678", "--state", "blocked"]),
1099
+ buildSuggestedCommand(["pm", "context-handoff", "--status", "blocked", "--recent", "8"]),
1100
+ buildSuggestedCommand(["pm", "context-handoff", "--tag", "release", "--without-neighborhood"]),
1101
+ buildSuggestedCommand(["pm", "context-handoff", "--id", "pm-1234", "--format", "json"]),
1102
+ buildSuggestedCommand(["pm", "context-handoff", "--id", "pm-1234", "--compress", "--include-deps"]),
1103
+ ],
1104
+ "context-usage": [
1105
+ buildSuggestedCommand(["pm", "context-usage"]),
1106
+ buildSuggestedCommand(["pm", "context-usage", "--format", "json"]),
1107
+ buildSuggestedCommand(["pm", "context-usage", "--surface", "next", "--since", "7d"]),
1108
+ buildSuggestedCommand(["pm", "context-usage", "--by", "agent-a", "--limit", "50"]),
1109
+ ],
1110
+ };
1111
+ }
1112
+ /** Register commands with structured help and argv-first refresh suggestions. */
1075
1113
  function setupCommands(api) {
1114
+ api.registerCommand({
1115
+ name: "context-phase",
1116
+ description: "Preserve mandatory task-phase fields or refuse; explain changes and reuse SDK session facts.",
1117
+ flags: [
1118
+ { long: "--packet", value_name: "file", description: "JSON request with four phase contracts and selected item ids", type: "string" },
1119
+ { long: "--phase", value_name: "phase", description: "Override phase: investigation, implementation, review or handoff", type: "string" },
1120
+ { long: "--previous-phase", value_name: "phase", description: "Compare requirements against this phase in the same contract", type: "string" },
1121
+ { long: "--include-fields", value_name: "fields", description: "Restore comma-separated optional top-level fields", type: "string" },
1122
+ { long: "--byte-budget", value_name: "n", description: "Complete transport byte ceiling, 512-262144 (default 16000)", type: "string" },
1123
+ { long: "--format", value_name: "format", description: "toon (default) or json", type: "string" },
1124
+ ],
1125
+ async run(ctx) {
1126
+ const path = stringOption(ctx.options, "packet");
1127
+ if (!path || ctx.args.length)
1128
+ throw new PhaseContextError("INVALID_REQUEST", "Supply --packet JSON without positional arguments.");
1129
+ const input = readPhaseContextJson(path);
1130
+ if (input === null || typeof input !== "object" || Array.isArray(input))
1131
+ throw new PhaseContextError("INVALID_REQUEST", "Supply a JSON object with phase, contracts and items.");
1132
+ const overrides = {};
1133
+ for (const [option, camel, key] of [["phase", "phase", "phase"], ["previous-phase", "previousPhase", "previous_phase"], ["format", "format", "format"]]) {
1134
+ const value = Object.hasOwn(ctx.options, option) ? ctx.options[option] : ctx.options[camel];
1135
+ if (value !== undefined)
1136
+ overrides[key] = value;
1137
+ }
1138
+ const fields = Object.hasOwn(ctx.options, "include-fields") ? ctx.options["include-fields"] : ctx.options.includeFields;
1139
+ if (fields !== undefined)
1140
+ overrides.include_fields = typeof fields === "string" ? fields.split(",") : fields;
1141
+ const budget = Object.hasOwn(ctx.options, "byte-budget") ? ctx.options["byte-budget"] : ctx.options.byteBudget;
1142
+ if (budget !== undefined)
1143
+ overrides.byte_budget = typeof budget === "string" && budget.trim() ? Number(budget) : budget;
1144
+ if (ctx.global.json && overrides.format === undefined)
1145
+ overrides.format = "json";
1146
+ return renderedCommandResult(encodePhaseContext(await buildPhaseContext({ ...input, ...overrides }, ctx.pm_root)));
1147
+ },
1148
+ });
1076
1149
  const contextPackDefaultLimit = 25;
1077
1150
  const contextHandoffDefaultLimit = 12;
1078
1151
  const defaultRecentLimit = 5;
@@ -1080,17 +1153,7 @@ function setupCommands(api) {
1080
1153
  name: "context-pack",
1081
1154
  description: "Generate a durable pm context pack for handoffs and reviews.",
1082
1155
  intent: "turn selected pm work into portable context",
1083
- examples: [
1084
- "pm context-pack --id pm-1234 --include-body --output context.md",
1085
- "pm context-pack --id pm-1234 --format agent",
1086
- "pm context-pack --ids pm-1234,pm-5678 --state blocked --format compact",
1087
- "pm context-pack --id pm-1234 --format compact --recent 8",
1088
- "pm context-pack --status in_progress --tag release --format json",
1089
- "pm context-pack --id pm-1234 --compress --format json",
1090
- "pm context-pack --id pm-1234 --format agent --include-deps --section focus --section blockers",
1091
- "pm context-pack --id pm-1234 --max-items 10",
1092
- "pm context-pack --status in_progress --explain",
1093
- ],
1156
+ examples: commandExamples()["context-pack"].map((example) => example.command),
1094
1157
  arguments: [{ name: "base", required: false, description: "Git base for --changed (defaults to origin/HEAD, then main)" }],
1095
1158
  flags: [
1096
1159
  { long: "--changed", description: "Focus linked files changed from an optional Git base, including working changes", type: "boolean" },
@@ -1176,6 +1239,7 @@ function setupCommands(api) {
1176
1239
  }));
1177
1240
  explainReport.available_signals = ["file_overlap", "status", "priority"];
1178
1241
  explainReport.unlinked_changed_files = pack.unlinked_changed_files;
1242
+ explainReport.link_command = fileLinkCommand();
1179
1243
  explainReport.model = "file_overlap_status_priority";
1180
1244
  }
1181
1245
  const output = format === "json"
@@ -1188,7 +1252,7 @@ function setupCommands(api) {
1188
1252
  }
1189
1253
  return renderedCommandResult(output);
1190
1254
  }
1191
- let suggestedCommand = buildSuggestedAgentCommand({
1255
+ const suggestedArgv = buildSuggestedAgentCommandArgv({
1192
1256
  commandName: "context-pack",
1193
1257
  selection,
1194
1258
  limit,
@@ -1205,17 +1269,18 @@ function setupCommands(api) {
1205
1269
  sections: sections.length > 0 ? sections : undefined,
1206
1270
  });
1207
1271
  if (fileFocus) {
1208
- suggestedCommand += fileFocus.refresh;
1272
+ suggestedArgv.push(...fileFocus.refresh_argv);
1209
1273
  if (selection.ids.length > 0) {
1210
1274
  if (selection.status)
1211
- suggestedCommand += ` --status ${shellQuote(selection.status)}`;
1275
+ suggestedArgv.push("--status", selection.status);
1212
1276
  if (selection.type)
1213
- suggestedCommand += ` --type ${shellQuote(selection.type)}`;
1277
+ suggestedArgv.push("--type", selection.type);
1214
1278
  if (selection.tag)
1215
- suggestedCommand += ` --tag ${shellQuote(selection.tag)}`;
1279
+ suggestedArgv.push("--tag", selection.tag);
1216
1280
  }
1217
1281
  }
1218
- const renderOpts = { recentLimit, suggestedCommand, compress, relationshipRegistry, sections: sections.length > 0 ? sections : undefined };
1282
+ pack.refresh_command = buildSuggestedCommand(suggestedArgv);
1283
+ const renderOpts = { recentLimit, suggestedArgv, compress, relationshipRegistry, sections: sections.length > 0 ? sections : undefined };
1219
1284
  let output = format === "json"
1220
1285
  ? `${JSON.stringify(pack, null, compress ? 0 : 2)}\n`
1221
1286
  : format === "agent"
@@ -1349,14 +1414,7 @@ function setupCommands(api) {
1349
1414
  name: "context-handoff",
1350
1415
  description: "Generate an agent-ready handoff with concise defaults.",
1351
1416
  intent: "produce compact, actionable handoff context for another agent",
1352
- examples: [
1353
- "pm context-handoff --id pm-1234",
1354
- "pm context-handoff --ids pm-1234,pm-5678 --state blocked",
1355
- "pm context-handoff --status blocked --recent 8",
1356
- "pm context-handoff --tag release --without-neighborhood",
1357
- "pm context-handoff --id pm-1234 --format json",
1358
- "pm context-handoff --id pm-1234 --compress --include-deps",
1359
- ],
1417
+ examples: commandExamples()["context-handoff"].map((example) => example.command),
1360
1418
  flags: [
1361
1419
  { long: "--session", value_name: "id", description: "Usage session id (defaults to PM_CONTEXT_SESSION or the receipt id)", type: "string" },
1362
1420
  { long: "--id", value_name: "id", description: "Focus item id (repeatable or comma-separated)", type: "string" },
@@ -1420,7 +1478,7 @@ function setupCommands(api) {
1420
1478
  packer,
1421
1479
  relationshipRegistry,
1422
1480
  });
1423
- const suggestedCommand = buildSuggestedAgentCommand({
1481
+ const suggestedArgv = buildSuggestedAgentCommandArgv({
1424
1482
  commandName: "context-handoff",
1425
1483
  selection,
1426
1484
  limit,
@@ -1435,7 +1493,8 @@ function setupCommands(api) {
1435
1493
  maxItems,
1436
1494
  sections: sections.length > 0 ? sections : undefined,
1437
1495
  });
1438
- const renderOpts = { recentLimit, suggestedCommand, compress, relationshipRegistry, sections: sections.length > 0 ? sections : undefined };
1496
+ pack.refresh_command = buildSuggestedCommand(suggestedArgv);
1497
+ const renderOpts = { recentLimit, suggestedArgv, compress, relationshipRegistry, sections: sections.length > 0 ? sections : undefined };
1439
1498
  const handoff = buildAgentHandoff(pack, renderOpts);
1440
1499
  let output = format === "json"
1441
1500
  ? `${JSON.stringify(handoff, null, compress ? 0 : 2)}\n`
@@ -1458,12 +1517,7 @@ function setupCommands(api) {
1458
1517
  name: "context-usage",
1459
1518
  description: "Report which items pm context/next served and which of those were actually touched.",
1460
1519
  intent: "measure whether served context is being used",
1461
- examples: [
1462
- "pm context-usage",
1463
- "pm context-usage --format json",
1464
- "pm context-usage --surface next --since 7d",
1465
- "pm context-usage --by agent-a --limit 50",
1466
- ],
1520
+ examples: commandExamples()["context-usage"].map((example) => example.command),
1467
1521
  flags: [
1468
1522
  { long: "--session", value_name: "id", description: "Filter bundle receipts to a session", type: "string" },
1469
1523
  { long: "--receipt", value_name: "id", description: "Filter or report usage against one bundle receipt", type: "string" },
@@ -1547,13 +1601,13 @@ function setupCommands(api) {
1547
1601
  /** Register context commands and renderers with checked host behavior and package metadata. */
1548
1602
  export default {
1549
1603
  name: "pm-context",
1550
- version: "2026.10.26",
1604
+ version: "2026.10.28",
1551
1605
  description: "Generate deterministic pm context packs for agent handoffs, reviews, and status briefs",
1552
1606
  activate(api) {
1553
1607
  setupCommands(api);
1554
1608
  if (typeof api.registerRenderer === "function") {
1555
1609
  const rendererOwnership = {
1556
- commands: ["context-pack", "context-pack since", "context-handoff", "context-usage", "context-freshness record", "context-freshness resume", "context-witnesses", "context-evidence", "context-next-evidence"],
1610
+ commands: ["context-pack", "context-pack since", "context-handoff", "context-usage", "context-freshness record", "context-freshness resume", "context-witnesses", "context-evidence", "context-next-evidence", "context-phase"],
1557
1611
  resultDiscriminator: isRenderedCommandResult,
1558
1612
  };
1559
1613
  api.registerRenderer("toon", renderCommandResult, rendererOwnership);