@unbrained/pm-cli 2026.8.28 → 2026.8.30

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 (98) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +30 -0
  3. package/dist/cli-bundle/bundle-manifest.json +125 -125
  4. package/dist/cli-bundle/chunks/{chunk-E73FDIWT.js → chunk-2MKLQL2E.js} +2 -2
  5. package/dist/cli-bundle/chunks/{chunk-NTXZHRKA.js → chunk-2UETTHY6.js} +46 -46
  6. package/dist/cli-bundle/chunks/{chunk-FEVBFFCQ.js → chunk-3TEF3HM5.js} +2 -2
  7. package/dist/cli-bundle/chunks/{chunk-BY2FQ2NI.js → chunk-7SKMLSMX.js} +2 -2
  8. package/dist/cli-bundle/chunks/chunk-7X6OZEIV.js +34 -0
  9. package/dist/cli-bundle/chunks/{chunk-M7OXRQE3.js → chunk-CMIEDWAY.js} +50 -50
  10. package/dist/cli-bundle/chunks/{chunk-XWEQGHHG.js → chunk-GNWPTEBW.js} +64 -61
  11. package/dist/cli-bundle/chunks/{chunk-X2RROGZE.js → chunk-MTTKAUBX.js} +2 -2
  12. package/dist/cli-bundle/chunks/chunk-N6OR6FAR.js +13 -0
  13. package/dist/cli-bundle/chunks/{chunk-NBCBFVZI.js → chunk-TGNAA66W.js} +2 -2
  14. package/dist/cli-bundle/chunks/{chunk-QE6WQXFO.js → chunk-WS2GJQVG.js} +2 -2
  15. package/dist/cli-bundle/chunks/{register-list-query-J35ZPQQ5.js → register-list-query-OMKJ5ONN.js} +2 -2
  16. package/dist/cli-bundle/chunks/{register-mutation-J6XJJOGU.js → register-mutation-QBSNCOY3.js} +2 -2
  17. package/dist/cli-bundle/chunks/{register-operations-AE3JEMFT.js → register-operations-UC77FQZC.js} +2 -2
  18. package/dist/cli-bundle/chunks/{register-setup-OQERLLWE.js → register-setup-GLQ5SYV3.js} +2 -2
  19. package/dist/cli-bundle/focused-chunks/{chunk-HC7ODMH3.js → chunk-3AVAWVZA.js} +2 -2
  20. package/dist/cli-bundle/focused-chunks/chunk-3UMBF26I.js +158 -0
  21. package/dist/cli-bundle/focused-chunks/{chunk-YVVZ3LQ6.js → chunk-73EDXGYE.js} +16 -15
  22. package/dist/cli-bundle/focused-chunks/chunk-AXZ5AJAF.js +50 -0
  23. package/dist/cli-bundle/focused-chunks/{chunk-AHAM2HAU.js → chunk-G6PATS5E.js} +3 -3
  24. package/dist/cli-bundle/focused-chunks/chunk-RKOKG43G.js +2 -0
  25. package/dist/cli-bundle/focused-chunks/chunk-TDK3OCX3.js +2 -0
  26. package/dist/cli-bundle/focused-chunks/{chunk-FC2AXLB5.js → chunk-XXOWRJZ5.js} +2 -2
  27. package/dist/cli-bundle/main.js +2 -2
  28. package/dist/cli-bundle/sdk-contracts.js +1 -1
  29. package/dist/cli-bundle/sdk-core.js +31 -32
  30. package/dist/cli-bundle/sdk-governance.js +1 -1
  31. package/dist/cli-bundle/sdk-graph.js +1 -1
  32. package/dist/cli-bundle/sdk-merge.js +34 -32
  33. package/dist/cli-bundle/sdk-query.js +1 -1
  34. package/dist/cli-bundle/sdk-runtime.js +1 -1
  35. package/dist/cli-bundle/sdk-testing.js +1 -1
  36. package/dist/cli-bundle/sdk.js +8 -8
  37. package/dist/core/diagnostics/remediation.js +16 -6
  38. package/dist/core/history/drift-scan.d.ts +2 -0
  39. package/dist/core/history/drift-scan.js +72 -36
  40. package/dist/core/history/history.d.ts +3 -3
  41. package/dist/core/history/history.js +5 -5
  42. package/dist/core/history/replay.d.ts +1 -1
  43. package/dist/core/history/replay.js +50 -19
  44. package/dist/core/item/actionability.d.ts +7 -1
  45. package/dist/core/item/actionability.js +44 -11
  46. package/dist/core/item/dependency-reference.d.ts +10 -0
  47. package/dist/core/item/dependency-reference.js +21 -0
  48. package/dist/core/item/parse.d.ts +4 -0
  49. package/dist/core/item/parse.js +38 -16
  50. package/dist/core/shared/author.js +69 -17
  51. package/dist/sdk/dependency-provenance.d.ts +51 -2
  52. package/dist/sdk/dependency-provenance.js +91 -7
  53. package/dist/sdk/extension/install-sources.d.ts +1 -7
  54. package/dist/sdk/extension/install-sources.js +94 -33
  55. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +50 -14
  56. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +62 -26
  57. package/dist/sdk/governance/health.js +86 -22
  58. package/dist/sdk/governance/validate-history-drift.js +5 -2
  59. package/dist/sdk/graph/assembly.js +51 -22
  60. package/dist/sdk/history-repair.d.ts +1 -1
  61. package/dist/sdk/history-repair.js +20 -10
  62. package/dist/sdk/lifecycle/create.js +43 -19
  63. package/dist/sdk/linked-artifacts.js +21 -31
  64. package/dist/sdk/merge/reconcile.js +3 -16
  65. package/dist/sdk/query/next.d.ts +6 -0
  66. package/dist/sdk/query/next.js +7 -2
  67. package/dist/sdk/test/entry.d.ts +1 -1
  68. package/dist/sdk/test/entry.js +5 -9
  69. package/dist/sdk/test/execution.d.ts +3 -0
  70. package/dist/sdk/test/execution.js +54 -13
  71. package/docs/CONTEXT_RECOVERY_PRIMITIVES.md +67 -0
  72. package/docs/MERGE_SAFETY.md +23 -10
  73. package/docs/README.md +1 -0
  74. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +10 -4
  75. package/marketplace.json +2 -2
  76. package/package.json +8 -8
  77. package/packages/pm-beads/package.json +1 -1
  78. package/packages/pm-calendar/package.json +1 -1
  79. package/packages/pm-command-kit/package.json +1 -1
  80. package/packages/pm-digital-twin/package.json +1 -1
  81. package/packages/pm-governance-audit/package.json +1 -1
  82. package/packages/pm-guide-shell/package.json +1 -1
  83. package/packages/pm-kanban/package.json +1 -1
  84. package/packages/pm-lifecycle-hooks/package.json +1 -1
  85. package/packages/pm-linked-test-adapters/package.json +1 -1
  86. package/packages/pm-search-advanced/package.json +1 -1
  87. package/packages/pm-templates/package.json +1 -1
  88. package/packages/pm-todos/package.json +1 -1
  89. package/packages/pm-vcs/package.json +1 -1
  90. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  91. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  92. package/sdk/public-surface.json +100 -11
  93. package/dist/cli-bundle/chunks/chunk-TIQ6AMH2.js +0 -13
  94. package/dist/cli-bundle/chunks/chunk-XRVVYRRO.js +0 -35
  95. package/dist/cli-bundle/focused-chunks/chunk-4ZDRZYYJ.js +0 -50
  96. package/dist/cli-bundle/focused-chunks/chunk-XDPYBQCF.js +0 -156
  97. package/dist/cli-bundle/focused-chunks/chunk-Y5A7SJJ7.js +0 -2
  98. package/dist/cli-bundle/focused-chunks/chunk-YJLDHJOD.js +0 -2
@@ -19,7 +19,7 @@
19
19
  */
20
20
  /** Registry of every non-extension `pm health` / `pm validate` warning code and its remediation. Extension findings keep their richer, contextual `details.triage.remediation` produced by the extension health triage and are intentionally excluded here (see pm-0hnu: "all non-extension checks"). */
21
21
 
22
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c088607b-1be3-54c8-a38b-79c188924102")}catch(e){}}();
22
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bb094b44-42b2-5a1c-9fc2-7a0eeaf9fbfd")}catch(e){}}();
23
23
  export const REMEDIATION_REGISTRY = Object.freeze([
24
24
  // --- pm health: directories ---
25
25
  {
@@ -195,8 +195,8 @@ export const REMEDIATION_REGISTRY = Object.freeze([
195
195
  },
196
196
  {
197
197
  code: "merge_receipts_pending",
198
- command: "pm merge reconcile --dry-run",
199
- summary: "A lossless merge receipt is still pending; preview and run pm merge reconcile so a green health gate certifies receipt settlement, not only intact data.",
198
+ command: "pm merge reconcile",
199
+ summary: "A lossless merge receipt is still pending; run pm merge reconcile to settle it (optionally preview first with --dry-run) so a green health gate certifies receipt settlement, not only intact data.",
200
200
  },
201
201
  {
202
202
  code: "merge_receipt_evidence_invalid",
@@ -231,10 +231,15 @@ export const REMEDIATION_REGISTRY = Object.freeze([
231
231
  command: "pm history-repair <id>",
232
232
  summary: "The item's history chain is broken; re-anchor the chain (pm history-repair --all repairs every drifted stream in one pass).",
233
233
  },
234
+ {
235
+ code: "history_drift_version_skew",
236
+ command: "npm install -g @unbrained/pm-cli@latest",
237
+ summary: "A history stream declares an item hash version this runtime does not support; upgrade pm to verify the chain instead of re-anchoring it.",
238
+ },
234
239
  {
235
240
  code: "history_drift_merge_receipt",
236
- command: "pm merge reconcile --dry-run",
237
- summary: "Canonical path, changed-field, and merged-value hash evidence attributes this drift to a pending receipt; preview reconciliation instead of repairing history directly.",
241
+ command: "pm merge reconcile",
242
+ summary: "Canonical path, changed-field, and merged-value hash evidence attributes this drift to a pending receipt; run reconciliation (optionally preview first with --dry-run) instead of repairing history directly.",
238
243
  },
239
244
  // --- pm health: storage (history compaction policy) ---
240
245
  {
@@ -426,6 +431,11 @@ export const REMEDIATION_REGISTRY = Object.freeze([
426
431
  command: "pm history-repair <id>",
427
432
  summary: "Re-anchor the history chains of the items with a broken history chain.",
428
433
  },
434
+ {
435
+ code: "validate_history_drift_version_skews",
436
+ command: "npm install -g @unbrained/pm-cli@latest",
437
+ summary: "One or more history streams declare an item hash version this runtime does not support; upgrade pm to verify the chains instead of re-anchoring them.",
438
+ },
429
439
  // --- pm validate: command_references ---
430
440
  {
431
441
  code: "validate_command_references_stale_pm_ids",
@@ -489,4 +499,4 @@ export function buildRemediationCommands(warnings) {
489
499
  return Object.values(buildRemediationMap(warnings));
490
500
  }
491
501
  //# sourceMappingURL=remediation.js.map
492
- //# debugId=c088607b-1be3-54c8-a38b-79c188924102
502
+ //# debugId=bb094b44-42b2-5a1c-9fc2-7a0eeaf9fbfd
@@ -9,6 +9,8 @@ export interface DriftScanResult {
9
9
  hashMismatches: string[];
10
10
  /** Value that configures or reports chain mismatches for this contract. */
11
11
  chainMismatches: string[];
12
+ /** Streams written with a hash capability newer than this runtime understands. */
13
+ versionSkews: string[];
12
14
  /** Value that configures or reports drifted items for this contract. */
13
15
  driftedItems: string[];
14
16
  /** Governed singleton files whose JSON differs from workspace-history replay. */
@@ -1,14 +1,14 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="32538934-2cbb-50fe-b721-ad9197cef7d8")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="65c0fed6-315a-5930-a23f-83996d3f5081")}catch(e){}}();
3
3
  import fs from "node:fs/promises";
4
4
  import { createHash } from "node:crypto";
5
5
  import path from "node:path";
6
6
  import { getHistoryPath } from "../store/paths.js";
7
7
  import { isFileMissingError, writeFileAtomic } from "../fs/fs-utils.js";
8
- import { CURRENT_HISTORY_ITEM_HASH_VERSION, hashDocumentForVersion, } from "./history.js";
8
+ import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentForVersion, } from "./history.js";
9
9
  import { verifyHistoryChainWithVersion } from "./replay.js";
10
10
  import { getWorkspaceHistoryPath, inspectWorkspaceHistoryState, WORKSPACE_HISTORY_ID, } from "./workspace-history.js";
11
- const DRIFT_CACHE_VERSION = 4;
11
+ const DRIFT_CACHE_VERSION = 7;
12
12
  const DRIFT_CACHE_FILENAME = "history-drift-cache.json";
13
13
  function getDriftCachePath(pmRoot) {
14
14
  return path.join(pmRoot, "runtime", DRIFT_CACHE_FILENAME);
@@ -18,8 +18,17 @@ async function loadDriftCache(pmRoot) {
18
18
  const raw = await fs.readFile(getDriftCachePath(pmRoot), "utf8");
19
19
  const parsed = JSON.parse(raw);
20
20
  if (parsed.version !== DRIFT_CACHE_VERSION ||
21
+ parsed.history_item_hash_version !== CURRENT_HISTORY_ITEM_HASH_VERSION ||
21
22
  typeof parsed.entries !== "object" ||
22
- parsed.entries === null) {
23
+ parsed.entries === null ||
24
+ Object.values(parsed.entries).some((entry) => typeof entry.chain_ok !== "boolean" ||
25
+ typeof entry.latest_hash_comparable !== "boolean" ||
26
+ (!SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(entry.item_hash_version) &&
27
+ entry.version_skew !== true) ||
28
+ (entry.chain_ok &&
29
+ entry.version_skew !== true &&
30
+ SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(entry.item_hash_version) &&
31
+ !entry.latest_hash_comparable))) {
23
32
  return null;
24
33
  }
25
34
  return parsed;
@@ -53,6 +62,9 @@ async function scanWorkspaceHistory(pmRoot, previousEntries, nextEntries, verify
53
62
  accumulator.missingStreams.splice(missingCount);
54
63
  return resolved.cacheDirty;
55
64
  }
65
+ if (resolved.verification.versionSkew) {
66
+ accumulator.versionSkews.push(WORKSPACE_HISTORY_ID);
67
+ }
56
68
  if (!resolved.verification.chainOk) {
57
69
  accumulator.chainMismatches.push(WORKSPACE_HISTORY_ID);
58
70
  }
@@ -63,6 +75,8 @@ async function scanWorkspaceHistory(pmRoot, previousEntries, nextEntries, verify
63
75
  content_hash: resolved.verification.contentHash,
64
76
  latest_after_hash: resolved.verification.latestAfterHash,
65
77
  chain_ok: resolved.verification.chainOk,
78
+ version_skew: resolved.verification.versionSkew,
79
+ latest_hash_comparable: resolved.verification.latestHashComparable,
66
80
  item_hash_version: resolved.verification.itemHashVersion,
67
81
  };
68
82
  return resolved.cacheDirty;
@@ -70,10 +84,6 @@ async function scanWorkspaceHistory(pmRoot, previousEntries, nextEntries, verify
70
84
  function hashContent(raw) {
71
85
  return createHash("sha256").update(raw).digest("hex");
72
86
  }
73
- async function readHistoryContentHash(historyPath) {
74
- const raw = await fs.readFile(historyPath, "utf8");
75
- return hashContent(raw);
76
- }
77
87
  /** Read and fully verify one history stream's hash chain. Returns null for an empty/missing stream (caller records it as a missing stream). */
78
88
  async function verifyHistoryStream(historyPath) {
79
89
  const raw = await fs.readFile(historyPath, "utf8");
@@ -102,11 +112,25 @@ async function verifyHistoryStream(historyPath) {
102
112
  }
103
113
  /* c8 ignore stop */
104
114
  const verification = verifyHistoryChainWithVersion(entries);
115
+ const versionSkew = entries.some((entry) => entry.item_hash_version !== undefined &&
116
+ !SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(entry.item_hash_version));
117
+ const latestEntry = entries[entries.length - 1];
118
+ const latestExplicitVersion = latestEntry.item_hash_version;
119
+ const latestExplicitVersionSupported = latestExplicitVersion !== undefined &&
120
+ SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(latestExplicitVersion);
121
+ const latestHashVersion = latestExplicitVersionSupported
122
+ ? latestExplicitVersion
123
+ : verification.item_hash_version;
105
124
  return {
106
125
  latestAfterHash,
107
126
  chainOk: verification.ok,
127
+ versionSkew,
128
+ latestHashComparable: latestHashVersion !== undefined &&
129
+ (!versionSkew ||
130
+ latestExplicitVersionSupported ||
131
+ latestExplicitVersion === undefined),
108
132
  contentHash,
109
- itemHashVersion: verification.item_hash_version ?? CURRENT_HISTORY_ITEM_HASH_VERSION,
133
+ itemHashVersion: latestHashVersion ?? CURRENT_HISTORY_ITEM_HASH_VERSION,
110
134
  };
111
135
  }
112
136
  function cacheMetadataMatches(cached, stat) {
@@ -143,34 +167,31 @@ async function resolveStreamVerification(params) {
143
167
  cacheDirty: true,
144
168
  };
145
169
  }
146
- let currentContentHash;
147
170
  if (params.verifyCacheHitByContent) {
148
- try {
149
- currentContentHash = await readHistoryContentHash(params.historyPath);
150
- }
151
- catch {
152
- params.accumulator.unreadableStreams.push(params.itemId);
153
- return { verification: null, cacheDirty: false };
154
- }
155
- }
156
- else {
157
- currentContentHash = cachedContentHash;
158
- }
159
- if (!params.verifyCacheHitByContent ||
160
- currentContentHash === cachedContentHash) {
171
+ const verification = await loadFreshStreamVerification(params.itemId, params.historyPath, params.accumulator);
161
172
  return {
162
- verification: {
163
- latestAfterHash: params.cached.latest_after_hash,
164
- chainOk: params.cached.chain_ok,
165
- contentHash: currentContentHash,
166
- itemHashVersion: params.cached.item_hash_version ?? CURRENT_HISTORY_ITEM_HASH_VERSION,
167
- },
168
- cacheDirty: false,
173
+ verification,
174
+ cacheDirty: verification === null ||
175
+ verification.contentHash !== cachedContentHash ||
176
+ verification.latestAfterHash !== params.cached.latest_after_hash ||
177
+ verification.chainOk !== params.cached.chain_ok ||
178
+ verification.versionSkew !== (params.cached.version_skew === true) ||
179
+ verification.latestHashComparable !==
180
+ params.cached.latest_hash_comparable ||
181
+ verification.itemHashVersion !== params.cached.item_hash_version,
169
182
  };
170
183
  }
171
184
  return {
172
- verification: await loadFreshStreamVerification(params.itemId, params.historyPath, params.accumulator),
173
- cacheDirty: true,
185
+ verification: {
186
+ latestAfterHash: params.cached.latest_after_hash,
187
+ chainOk: params.cached.chain_ok,
188
+ versionSkew: params.cached.version_skew === true,
189
+ latestHashComparable: params.cached.latest_hash_comparable &&
190
+ SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(params.cached.item_hash_version),
191
+ contentHash: cachedContentHash,
192
+ itemHashVersion: params.cached.item_hash_version,
193
+ },
194
+ cacheDirty: false,
174
195
  };
175
196
  }
176
197
  /** Scan one item stream and return whether its cache entry needs persistence. */
@@ -196,8 +217,12 @@ async function scanItemHistory(pmRoot, item, cache, accumulator) {
196
217
  });
197
218
  if (!resolved.verification)
198
219
  return resolved.cacheDirty;
199
- if (!resolved.verification.chainOk)
220
+ if (resolved.verification.versionSkew) {
221
+ accumulator.versionSkews.push(item.id);
222
+ }
223
+ if (!resolved.verification.chainOk) {
200
224
  accumulator.chainMismatches.push(item.id);
225
+ }
201
226
  cache.nextEntries[item.id] = {
202
227
  mtime_ms: stat.mtimeMs,
203
228
  ctime_ms: stat.ctimeMs,
@@ -205,9 +230,13 @@ async function scanItemHistory(pmRoot, item, cache, accumulator) {
205
230
  content_hash: resolved.verification.contentHash,
206
231
  latest_after_hash: resolved.verification.latestAfterHash,
207
232
  chain_ok: resolved.verification.chainOk,
233
+ version_skew: resolved.verification.versionSkew,
234
+ latest_hash_comparable: resolved.verification.latestHashComparable,
208
235
  item_hash_version: resolved.verification.itemHashVersion,
209
236
  };
210
237
  const { body, ...itemMetadata } = item;
238
+ if (!resolved.verification.latestHashComparable)
239
+ return resolved.cacheDirty;
211
240
  const currentHash = hashDocumentForVersion({ metadata: itemMetadata, body }, resolved.verification.itemHashVersion);
212
241
  if (currentHash !== resolved.verification.latestAfterHash)
213
242
  accumulator.hashMismatches.push(item.id);
@@ -216,7 +245,8 @@ async function scanItemHistory(pmRoot, item, cache, accumulator) {
216
245
  /** Compare the verified workspace stream with every governed singleton file. */
217
246
  async function scanWorkspaceStateAgreement(pmRoot, accumulator) {
218
247
  if (accumulator.unreadableStreams.includes(WORKSPACE_HISTORY_ID) ||
219
- accumulator.chainMismatches.includes(WORKSPACE_HISTORY_ID))
248
+ accumulator.chainMismatches.includes(WORKSPACE_HISTORY_ID) ||
249
+ accumulator.versionSkews.includes(WORKSPACE_HISTORY_ID))
220
250
  return;
221
251
  try {
222
252
  const agreement = await inspectWorkspaceHistoryState(pmRoot);
@@ -244,6 +274,7 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
244
274
  unreadableStreams: [],
245
275
  hashMismatches: [],
246
276
  chainMismatches: [],
277
+ versionSkews: [],
247
278
  workspaceStateMismatches: [],
248
279
  workspaceStateMissing: [],
249
280
  workspaceStateUnreadable: [],
@@ -272,7 +303,11 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
272
303
  const cachePath = getDriftCachePath(pmRoot);
273
304
  try {
274
305
  await fs.mkdir(path.dirname(cachePath), { recursive: true });
275
- await writeFileAtomic(cachePath, JSON.stringify({ version: DRIFT_CACHE_VERSION, entries: nextEntries }));
306
+ await writeFileAtomic(cachePath, JSON.stringify({
307
+ version: DRIFT_CACHE_VERSION,
308
+ history_item_hash_version: CURRENT_HISTORY_ITEM_HASH_VERSION,
309
+ entries: nextEntries,
310
+ }));
276
311
  }
277
312
  catch {
278
313
  // Best-effort cache write: a failed persist must never fail a health scan.
@@ -284,6 +319,7 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
284
319
  ...accumulator.unreadableStreams,
285
320
  ...accumulator.hashMismatches,
286
321
  ...accumulator.chainMismatches,
322
+ ...accumulator.versionSkews,
287
323
  ...(accumulator.workspaceStateMismatches.length > 0 ||
288
324
  accumulator.workspaceStateMissing.length > 0 ||
289
325
  accumulator.workspaceStateUnreadable.length > 0
@@ -294,4 +330,4 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
294
330
  return { ...accumulator, driftedItems };
295
331
  }
296
332
  //# sourceMappingURL=drift-scan.js.map
297
- //# debugId=32538934-2cbb-50fe-b721-ad9197cef7d8
333
+ //# debugId=65c0fed6-315a-5930-a23f-83996d3f5081
@@ -1,9 +1,9 @@
1
1
  import { type AuthorSource } from "../shared/author.js";
2
2
  import type { HistoryEntry, ItemDocument } from "../../types/index.js";
3
- /** Hash epoch that preserves linked-test insertion order in immutable history. */
4
- export declare const CURRENT_HISTORY_ITEM_HASH_VERSION: 2;
3
+ /** Hash epoch for the current canonical item surface written into immutable history. */
4
+ export declare const CURRENT_HISTORY_ITEM_HASH_VERSION: 3;
5
5
  /** Hash epochs understood by this runtime. */
6
- export declare const SUPPORTED_HISTORY_ITEM_HASH_VERSIONS: readonly [1, 2];
6
+ export declare const SUPPORTED_HISTORY_ITEM_HASH_VERSIONS: readonly [1, 2, 3];
7
7
  /** Restricts item hash versions accepted by history replay. */
8
8
  export type HistoryItemHashVersion = (typeof SUPPORTED_HISTORY_ITEM_HASH_VERSIONS)[number];
9
9
  declare function decodeJsonPointer(path: string): string[];
@@ -4,7 +4,7 @@
4
4
  * Implements append-only history and replay behavior for History.
5
5
  */
6
6
 
7
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f7cf18cb-0068-58ad-bd7f-759a5d91eb9f")}catch(e){}}();
7
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8581413f-e74e-5db3-86bf-5e39290de741")}catch(e){}}();
8
8
  import jsonPatch from "fast-json-patch";
9
9
  import { ITEM_METADATA_KEY_ORDER } from "../shared/constants.js";
10
10
  import { runActiveServiceOverride } from "../extensions/index.js";
@@ -20,10 +20,10 @@ const EMPTY_LEGACY_HASH_DOCUMENT = {
20
20
  front_matter: {},
21
21
  body: "",
22
22
  };
23
- /** Hash epoch that preserves linked-test insertion order in immutable history. */
24
- export const CURRENT_HISTORY_ITEM_HASH_VERSION = 2;
23
+ /** Hash epoch for the current canonical item surface written into immutable history. */
24
+ export const CURRENT_HISTORY_ITEM_HASH_VERSION = 3;
25
25
  /** Hash epochs understood by this runtime. */
26
- export const SUPPORTED_HISTORY_ITEM_HASH_VERSIONS = [1, 2];
26
+ export const SUPPORTED_HISTORY_ITEM_HASH_VERSIONS = [1, 2, 3];
27
27
  function compareOptionalStrings(left, right) {
28
28
  return (left ?? "").localeCompare(right ?? "");
29
29
  }
@@ -292,4 +292,4 @@ export const _testOnly = {
292
292
  isDefinedPointerPath,
293
293
  };
294
294
  //# sourceMappingURL=history.js.map
295
- //# debugId=f7cf18cb-0068-58ad-bd7f-759a5d91eb9f
295
+ //# debugId=8581413f-e74e-5db3-86bf-5e39290de741
@@ -108,6 +108,6 @@ export interface ReanchorResult {
108
108
  */
109
109
  export declare function resolveHistoryRepairItemHashVersion(entries: HistoryEntry[]): HistoryItemHashVersion;
110
110
  /** Re-anchor a drifted history chain: replay every entry from empty, recompute the before/after hashes, and only rewrite a patch when the original op set no longer strictly applies (legacy drift). Clean entries keep their patch verbatim so the on-disk diff stays minimal. The returned chain verifies via verifyHistoryChain. */
111
- export declare function reanchorHistoryEntries(entries: HistoryEntry[], itemHashVersion?: 1 | 2): ReanchorResult;
111
+ export declare function reanchorHistoryEntries(entries: HistoryEntry[], itemHashVersion?: 1 | 2 | 3): ReanchorResult;
112
112
  /** Implements history entries to raw for the public runtime surface of this module. */
113
113
  export declare function historyEntriesToRaw(entries: HistoryEntry[]): string;
@@ -4,7 +4,7 @@
4
4
  * Implements append-only history and replay behavior for Replay.
5
5
  */
6
6
 
7
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bf8797ec-53d3-5d23-8a6e-8296a027ddf2")}catch(e){}}();
7
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c823af51-0cfd-536b-8f85-1f3e72db1c86")}catch(e){}}();
8
8
  import jsonPatch from "fast-json-patch";
9
9
  import { ITEM_METADATA_KEY_ORDER } from "../shared/constants.js";
10
10
  import { canonicalDocument } from "../item/item-format.js";
@@ -132,13 +132,18 @@ function supportedExplicitHistoryItemHashVersions(entries) {
132
132
  SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(version))),
133
133
  ];
134
134
  }
135
+ /** Return whether an explicit hash epoch cannot be replayed by this build. */
136
+ function isUnsupportedExplicitHistoryItemHashVersion(version) {
137
+ return (version !== undefined &&
138
+ !SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(version));
139
+ }
135
140
  /** Select the hash epochs allowed for one entry under an authoritative stream marker. */
136
141
  function historyEntryHashCandidates(explicitVersion, authoritativeVersion) {
137
142
  if (explicitVersion !== undefined) {
138
143
  return [explicitVersion];
139
144
  }
140
145
  return authoritativeVersion === undefined
141
- ? [1, CURRENT_HISTORY_ITEM_HASH_VERSION]
146
+ ? [...SUPPORTED_HISTORY_ITEM_HASH_VERSIONS]
142
147
  : [authoritativeVersion];
143
148
  }
144
149
  /** Deterministically verify a history chain: each entry's before_hash must equal the prior replayed after_hash, the patch must strictly apply, and the recorded after_hash must equal the replayed result. */
@@ -147,23 +152,35 @@ export function verifyHistoryChainWithVersion(entries) {
147
152
  let replay = cloneEmptyReplayDocument();
148
153
  let detectedVersion;
149
154
  let authoritativeExplicitVersion;
155
+ const errors = [];
150
156
  for (let index = 0; index < entries.length; index += 1) {
151
157
  const entry = entries[index];
152
158
  const explicitVersion = entry.item_hash_version;
153
- if (explicitVersion !== undefined &&
154
- !SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(explicitVersion)) {
155
- return {
156
- ok: false,
157
- errors: [
158
- `verify_failed:unsupported_item_hash_version:${String(explicitVersion)}:entry_${index + 1}`,
159
- ],
160
- };
159
+ if (isUnsupportedExplicitHistoryItemHashVersion(explicitVersion)) {
160
+ errors.push(`verify_failed:unsupported_item_hash_version:${String(explicitVersion)}:entry_${index + 1}`);
161
+ const applied = tryApplyReplayPatch(replay, entry.patch);
162
+ if (!applied.ok) {
163
+ return {
164
+ ok: false,
165
+ errors: [
166
+ ...errors,
167
+ `verify_failed:patch_apply_failed:entry_${index + 1}`,
168
+ ],
169
+ };
170
+ }
171
+ replay = applied.document;
172
+ detectedVersion = undefined;
173
+ authoritativeExplicitVersion = undefined;
174
+ continue;
161
175
  }
162
176
  const applied = tryApplyReplayPatch(replay, entry.patch);
163
177
  if (!applied.ok) {
164
178
  return {
165
179
  ok: false,
166
- errors: [`verify_failed:patch_apply_failed:entry_${index + 1}`],
180
+ errors: [
181
+ ...errors,
182
+ `verify_failed:patch_apply_failed:entry_${index + 1}`,
183
+ ],
167
184
  };
168
185
  }
169
186
  // Prefer legacy epoch 1 when an unversioned entry is valid under both
@@ -174,21 +191,36 @@ export function verifyHistoryChainWithVersion(entries) {
174
191
  if (beforeMatches.length === 0) {
175
192
  return {
176
193
  ok: false,
177
- errors: [`verify_failed:before_hash_mismatch:entry_${index + 1}`],
194
+ errors: [
195
+ ...errors,
196
+ `verify_failed:before_hash_mismatch:entry_${index + 1}`,
197
+ ],
178
198
  };
179
199
  }
180
200
  const version = beforeMatches.find((candidate) => replayHash(applied.document, candidate) === entry.after_hash);
181
201
  if (version === undefined) {
182
202
  return {
183
203
  ok: false,
184
- errors: [`verify_failed:after_hash_mismatch:entry_${index + 1}`],
204
+ errors: [
205
+ ...errors,
206
+ `verify_failed:after_hash_mismatch:entry_${index + 1}`,
207
+ ],
185
208
  };
186
209
  }
187
210
  replay = applied.document;
188
211
  detectedVersion = version;
189
- if (explicitVersion !== undefined) {
190
- authoritativeExplicitVersion = version;
191
- }
212
+ authoritativeExplicitVersion =
213
+ explicitVersion ??
214
+ authoritativeExplicitVersion;
215
+ }
216
+ if (errors.length > 0) {
217
+ return {
218
+ ok: false,
219
+ errors,
220
+ ...(detectedVersion === undefined
221
+ ? {}
222
+ : { item_hash_version: detectedVersion }),
223
+ };
192
224
  }
193
225
  return {
194
226
  ok: true,
@@ -266,8 +298,7 @@ export function resolveHistoryRepairItemHashVersion(entries) {
266
298
  }
267
299
  /** Re-anchor a drifted history chain: replay every entry from empty, recompute the before/after hashes, and only rewrite a patch when the original op set no longer strictly applies (legacy drift). Clean entries keep their patch verbatim so the on-disk diff stays minimal. The returned chain verifies via verifyHistoryChain. */
268
300
  export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistoryRepairItemHashVersion(entries)) {
269
- const unsupportedIndex = entries.findIndex((entry) => entry.item_hash_version !== undefined &&
270
- !SUPPORTED_HISTORY_ITEM_HASH_VERSIONS.includes(entry.item_hash_version));
301
+ const unsupportedIndex = entries.findIndex((entry) => isUnsupportedExplicitHistoryItemHashVersion(entry.item_hash_version));
271
302
  if (unsupportedIndex >= 0) {
272
303
  throw new TypeError(`unsupported_item_hash_version:${String(entries[unsupportedIndex]?.item_hash_version)}:entry_${unsupportedIndex + 1}`);
273
304
  }
@@ -351,4 +382,4 @@ export function historyEntriesToRaw(entries) {
351
382
  return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
352
383
  }
353
384
  //# sourceMappingURL=replay.js.map
354
- //# debugId=bf8797ec-53d3-5d23-8a6e-8296a027ddf2
385
+ //# debugId=c823af51-0cfd-536b-8f85-1f3e72db1c86
@@ -18,9 +18,15 @@ export interface ResolvedBlocker {
18
18
  status: ItemStatus | null;
19
19
  /** True when the blocker no longer gates work because the referenced item is terminal. */
20
20
  resolved: boolean;
21
+ /** Whether this blocker is a cross-system locator requiring an external resolver. */
22
+ external?: boolean;
23
+ /** Last holder mutation timestamp, used as a conservative external-block staleness marker. */
24
+ blocked_since?: string;
25
+ /** Resolver identity when an integration resolved this external blocker; null means unverifiable. */
26
+ resolver?: string | null;
21
27
  }
22
28
  /** Resolves an item's blockers against a corpus index with reverse `blocks` support. */
23
- export declare function resolveItemBlockers(item: Pick<ItemMetadata, "blocked_by" | "dependencies">, itemsById: Map<string, ItemMetadata>, statusRegistry: RuntimeStatusRegistry): ResolvedBlocker[];
29
+ export declare function resolveItemBlockers(item: Pick<ItemMetadata, "blocked_by" | "dependencies"> & Partial<Pick<ItemMetadata, "id" | "updated_at">>, itemsById: Map<string, ItemMetadata>, statusRegistry: RuntimeStatusRegistry): ResolvedBlocker[];
24
30
  /**
25
31
  * Collects the normalized (trimmed, lowercased) ids of every corpus item that is
26
32
  * blocked under the single shared edge-aware definition: a non-terminal item
@@ -10,8 +10,9 @@
10
10
  * `blocked_by` resolution a single source of truth.
11
11
  */
12
12
 
13
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="de8a82e1-0b02-5d5a-9086-686529a1ccab")}catch(e){}}();
13
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0605f770-d868-5fe9-a186-45f00d2e93b4")}catch(e){}}();
14
14
  import { isTerminalStatus, normalizeStatusForRegistry } from "./status.js";
15
+ import { isExternalDependencyReference, isExternalDependencySourceKind, } from "./dependency-reference.js";
15
16
  /** Dependency kind that marks "this item is blocked by the referenced item". */
16
17
  const BLOCKED_BY_DEPENDENCY_KIND = "blocked_by";
17
18
  /** Reverse dependency kind that marks "this item blocks the referenced item". */
@@ -32,22 +33,32 @@ const NO_ACTIVE_BLOCKER_SENTINEL = "no-active-blocker";
32
33
  function normalizeItemId(id) {
33
34
  return id.trim().toLowerCase();
34
35
  }
36
+ /** Build a collision-safe blocker key without normalizing external identity. */
37
+ function blockerReferenceKey(id, external) {
38
+ return external ? `external:${id}` : `local:${normalizeItemId(id)}`;
39
+ }
35
40
  /** Collects the blocker item ids declared by an item: the legacy scalar `blocked_by` field plus every `blocked_by` dependency edge. Ids are trimmed, de-duplicated, and returned in stable lexicographic order. This is the single source of truth for "what must close before this item can proceed", shared by `pm next` readiness classification and the close-time auto-unblock sweep. */
36
41
  export function collectBlockedByIds(item) {
37
- const ids = new Set();
38
- const scalar = typeof item.blocked_by === "string" ? normalizeItemId(item.blocked_by) : "";
39
- if (scalar.length > 0 && scalar !== NO_ACTIVE_BLOCKER_SENTINEL) {
40
- ids.add(scalar);
42
+ const ids = new Map();
43
+ const scalar = typeof item.blocked_by === "string" ? item.blocked_by.trim() : "";
44
+ if (scalar.length > 0 &&
45
+ normalizeItemId(scalar) !== NO_ACTIVE_BLOCKER_SENTINEL) {
46
+ const external = isExternalDependencyReference(scalar);
47
+ ids.set(blockerReferenceKey(scalar, external), external ? scalar : normalizeItemId(scalar));
41
48
  }
42
49
  for (const dependency of item.dependencies ?? []) {
43
- const dependencyId = typeof dependency.id === "string" ? normalizeItemId(dependency.id) : "";
50
+ const dependencyId = typeof dependency.id === "string" ? dependency.id.trim() : "";
44
51
  if (dependency.kind === BLOCKED_BY_DEPENDENCY_KIND &&
45
52
  dependencyId.length > 0 &&
46
- dependencyId !== NO_ACTIVE_BLOCKER_SENTINEL) {
47
- ids.add(dependencyId);
53
+ normalizeItemId(dependencyId) !== NO_ACTIVE_BLOCKER_SENTINEL) {
54
+ const external = isExternalDependencyReference(dependencyId) ||
55
+ isExternalDependencySourceKind(dependency.source_kind);
56
+ ids.set(blockerReferenceKey(dependencyId, external), external
57
+ ? dependencyId
58
+ : normalizeItemId(dependencyId));
48
59
  }
49
60
  }
50
- return [...ids].sort((left, right) => left.localeCompare(right));
61
+ return [...ids.values()].sort((left, right) => left.localeCompare(right));
51
62
  }
52
63
  /** Build every forward and reverse blocker set in one corpus pass. */
53
64
  export function indexBlockedByIds(corpus) {
@@ -92,9 +103,31 @@ function resolveItemBlockersWithIndex(item, itemsById, statusRegistry, blockerId
92
103
  const itemWithId = item;
93
104
  const blockerIds = typeof itemWithId.id === "string"
94
105
  ? (blockerIdsByItem?.get(normalizeItemId(itemWithId.id)) ??
95
- collectBlockedByIdsFromCorpus(itemWithId, [...itemsById.values()]))
106
+ collectBlockedByIdsFromCorpus(itemWithId, [
107
+ ...itemsById.values(),
108
+ ]))
96
109
  : collectBlockedByIds(item);
110
+ const structuredExternalBlockerIds = new Set((item.dependencies ?? [])
111
+ .filter((dependency) => dependency.kind === BLOCKED_BY_DEPENDENCY_KIND &&
112
+ isExternalDependencySourceKind(dependency.source_kind) &&
113
+ typeof dependency.id === "string" &&
114
+ dependency.id.trim().length > 0)
115
+ .map((dependency) => blockerReferenceKey(dependency.id.trim(), true)));
97
116
  return blockerIds.map((id) => {
117
+ if (isExternalDependencyReference(id) ||
118
+ structuredExternalBlockerIds.has(blockerReferenceKey(id, true))) {
119
+ return {
120
+ id,
121
+ title: null,
122
+ status: null,
123
+ resolved: false,
124
+ external: true,
125
+ ...(typeof item.updated_at === "string"
126
+ ? { blocked_since: item.updated_at }
127
+ : {}),
128
+ resolver: null,
129
+ };
130
+ }
98
131
  const blocker = itemsById.get(normalizeItemId(id));
99
132
  if (!blocker) {
100
133
  return { id, title: null, status: null, resolved: false };
@@ -257,4 +290,4 @@ export function computeActionabilityReport(candidates, corpus, statusRegistry) {
257
290
  };
258
291
  }
259
292
  //# sourceMappingURL=actionability.js.map
260
- //# debugId=de8a82e1-0b02-5d5a-9086-686529a1ccab
293
+ //# debugId=0605f770-d868-5fe9-a186-45f00d2e93b4
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module core/item/dependency-reference
3
+ *
4
+ * Classifies dependency locators that intentionally resolve outside the local
5
+ * tracker so scheduling and graph governance do not confuse them with typos.
6
+ */
7
+ /** Return whether a dependency target is an explicit cross-system locator rather than a local pm item id. */
8
+ export declare function isExternalDependencyReference(target: string | undefined): boolean;
9
+ /** Return whether dependency provenance explicitly places a target outside the local workspace. */
10
+ export declare function isExternalDependencySourceKind(sourceKind: string | undefined): boolean;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module core/item/dependency-reference
3
+ *
4
+ * Classifies dependency locators that intentionally resolve outside the local
5
+ * tracker so scheduling and graph governance do not confuse them with typos.
6
+ */
7
+ /** Return whether a dependency target is an explicit cross-system locator rather than a local pm item id. */
8
+
9
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="971d929f-12be-5648-93c9-8a9878b40dde")}catch(e){}}();
10
+ export function isExternalDependencyReference(target) {
11
+ const normalized = target?.trim() ?? "";
12
+ return (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(normalized) ||
13
+ /^(?:github|gitlab|jira|linear):/iu.test(normalized));
14
+ }
15
+ /** Return whether dependency provenance explicitly places a target outside the local workspace. */
16
+ export function isExternalDependencySourceKind(sourceKind) {
17
+ const normalized = sourceKind?.trim().toLowerCase();
18
+ return normalized === "global" || normalized === "external";
19
+ }
20
+ //# sourceMappingURL=dependency-reference.js.map
21
+ //# debugId=971d929f-12be-5648-93c9-8a9878b40dde
@@ -1,3 +1,5 @@
1
+ /** Classify plaintext as structured key/value input using an explicit accepted-key vocabulary. */
2
+ export declare function looksLikeStructuredKeyValueEntry(raw: string, keys: readonly string[]): boolean;
1
3
  /** Implements parse tags for the public runtime surface of this module. */
2
4
  export declare function parseTags(raw: string): string[];
3
5
  /** Merge repeated `--add-tags` / `--remove-tags` values into a single normalized tag list. Each entry can itself be CSV or a JSON array, mirroring the format accepted by `--tags`. Returns a deterministically sorted, deduped list. */
@@ -29,6 +31,8 @@ export declare function parseCsvKv(raw: string, optionName: string): Record<stri
29
31
  * never misread as a `C=…` key/value entry.
30
32
  */
31
33
  export declare function looksLikeGenericKeyValueEntry(raw: string): boolean;
34
+ /** Refuse a comma-bearing bare value whose meaning depends on guessing whether the comma separates values or belongs to one value. */
35
+ export declare function assertNoAmbiguousBareCommaEntry(raw: string, optionName: string, noun: string, structured: boolean): void;
32
36
  /**
33
37
  * Reject any key in a parsed CSV/markdown key/value map that is not part of the
34
38
  * caller's allowed-key contract, mirroring the strict `test --add` behavior