@unbrained/pm-cli 2026.9.2 → 2026.9.4

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +44 -0
  3. package/dist/cli/after-command-context-usage.js +16 -3
  4. package/dist/cli-bundle/bundle-manifest.json +151 -151
  5. package/dist/cli-bundle/chunks/chunk-6XSKPNCF.js +230 -0
  6. package/dist/cli-bundle/chunks/chunk-A24WAVE6.js +8 -0
  7. package/dist/cli-bundle/chunks/{chunk-TAG4VC22.js → chunk-HI6BHPCU.js} +27 -27
  8. package/dist/cli-bundle/chunks/{chunk-Y7VOMSZT.js → chunk-I2AS367G.js} +2 -2
  9. package/dist/cli-bundle/chunks/{chunk-X4RCGLTX.js → chunk-OGBVU67G.js} +5 -5
  10. package/dist/cli-bundle/chunks/{chunk-DTGWEL23.js → chunk-RI4XNOYS.js} +2 -2
  11. package/dist/cli-bundle/chunks/chunk-TXW6VL33.js +38 -0
  12. package/dist/cli-bundle/chunks/{chunk-FZ4Z2J4F.js → chunk-V72UV7QS.js} +76 -79
  13. package/dist/cli-bundle/chunks/chunk-VGLHECAP.js +9 -0
  14. package/dist/cli-bundle/chunks/{chunk-T6H5BZCP.js → chunk-X6CMGV6L.js} +2 -2
  15. package/dist/cli-bundle/chunks/{chunk-3WICXLVT.js → chunk-YW4QK2G7.js} +2 -2
  16. package/dist/cli-bundle/chunks/{register-list-query-XYNX4ASQ.js → register-list-query-Z5YYSJJG.js} +2 -2
  17. package/dist/cli-bundle/chunks/{register-mutation-WFQ7MJTX.js → register-mutation-ELSP6I4I.js} +2 -2
  18. package/dist/cli-bundle/chunks/{register-operations-JVTI7JPP.js → register-operations-OH7R3ZT4.js} +2 -2
  19. package/dist/cli-bundle/chunks/{register-setup-KVNC5KPL.js → register-setup-E2AE72QG.js} +2 -2
  20. package/dist/cli-bundle/focused-chunks/{chunk-MEASX544.js → chunk-25UJBH4M.js} +2 -2
  21. package/dist/cli-bundle/focused-chunks/{chunk-SVXJYD6G.js → chunk-3646BZ4W.js} +2 -2
  22. package/dist/cli-bundle/focused-chunks/chunk-4YRAVI43.js +12 -0
  23. package/dist/cli-bundle/focused-chunks/chunk-53IHPICX.js +3 -0
  24. package/dist/cli-bundle/focused-chunks/chunk-5P4HVOWG.js +176 -0
  25. package/dist/cli-bundle/focused-chunks/chunk-636VVQ7F.js +54 -0
  26. package/dist/cli-bundle/focused-chunks/{chunk-4O4CJAR6.js → chunk-6PWPQKDN.js} +2 -2
  27. package/dist/cli-bundle/focused-chunks/chunk-7N2TV7WZ.js +8 -0
  28. package/dist/cli-bundle/focused-chunks/chunk-AJR3OP2E.js +3 -0
  29. package/dist/cli-bundle/focused-chunks/chunk-B3A6MJYE.js +2 -0
  30. package/dist/cli-bundle/focused-chunks/chunk-BFDDJ2T2.js +2 -0
  31. package/dist/cli-bundle/focused-chunks/chunk-BHUIRZWQ.js +9 -0
  32. package/dist/cli-bundle/focused-chunks/chunk-JADHHPY6.js +6 -0
  33. package/dist/cli-bundle/focused-chunks/{chunk-KB5OFSFD.js → chunk-MHKUIPJW.js} +2 -2
  34. package/dist/cli-bundle/focused-chunks/chunk-NBMJD6MK.js +2 -0
  35. package/dist/cli-bundle/focused-chunks/chunk-NSKBLHVI.js +20 -0
  36. package/dist/cli-bundle/focused-chunks/{chunk-BEVHCEWG.js → chunk-P2GOAOQN.js} +2 -2
  37. package/dist/cli-bundle/focused-chunks/chunk-YARWJM2B.js +2 -0
  38. package/dist/cli-bundle/main.js +15 -15
  39. package/dist/cli-bundle/sdk-authoring.js +1 -1
  40. package/dist/cli-bundle/sdk-contracts.js +1 -1
  41. package/dist/cli-bundle/sdk-core.js +28 -31
  42. package/dist/cli-bundle/sdk-governance.js +1 -1
  43. package/dist/cli-bundle/sdk-graph.js +1 -1
  44. package/dist/cli-bundle/sdk-merge.js +43 -36
  45. package/dist/cli-bundle/sdk-query.js +1 -1
  46. package/dist/cli-bundle/sdk-runtime.js +1 -1
  47. package/dist/cli-bundle/sdk-testing.js +1 -1
  48. package/dist/cli-bundle/sdk.js +10 -10
  49. package/dist/core/diagnostics/remediation.js +4 -4
  50. package/dist/core/history/event-classification.d.ts +21 -0
  51. package/dist/core/history/event-classification.js +109 -0
  52. package/dist/core/history/event-index.d.ts +14 -3
  53. package/dist/core/history/event-index.js +543 -88
  54. package/dist/core/history/history.d.ts +37 -1
  55. package/dist/core/history/history.js +320 -71
  56. package/dist/core/history/replay.js +68 -34
  57. package/dist/core/history/workspace-history.js +7 -6
  58. package/dist/core/lock/lock.d.ts +32 -2
  59. package/dist/core/lock/lock.js +161 -20
  60. package/dist/core/output/output.js +31 -7
  61. package/dist/core/shared/author.d.ts +3 -1
  62. package/dist/core/shared/author.js +111 -23
  63. package/dist/core/shared/time.d.ts +2 -0
  64. package/dist/core/shared/time.js +8 -2
  65. package/dist/core/store/item-metadata-query-index.js +5 -5
  66. package/dist/sdk/context/recency.d.ts +9 -0
  67. package/dist/sdk/context/recency.js +34 -0
  68. package/dist/sdk/context/usage-egress.d.ts +9 -0
  69. package/dist/sdk/context/usage-egress.js +30 -0
  70. package/dist/sdk/context-intent-contracts.js +7 -3
  71. package/dist/sdk/context-relevance.d.ts +23 -0
  72. package/dist/sdk/context-relevance.js +94 -23
  73. package/dist/sdk/context-signal-store.d.ts +7 -3
  74. package/dist/sdk/context-signal-store.js +155 -42
  75. package/dist/sdk/context-usage.d.ts +54 -0
  76. package/dist/sdk/context-usage.js +245 -16
  77. package/dist/sdk/environment/agent-environment.d.ts +296 -0
  78. package/dist/sdk/environment/agent-environment.js +467 -0
  79. package/dist/sdk/environment/project-runtime-compatibility.d.ts +19 -1
  80. package/dist/sdk/environment/project-runtime-compatibility.js +118 -21
  81. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +14 -2
  82. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +14 -2
  83. package/dist/sdk/governance/health.js +13 -44
  84. package/dist/sdk/governance/merge-receipt-history.d.ts +62 -0
  85. package/dist/sdk/governance/merge-receipt-history.js +207 -0
  86. package/dist/sdk/governance/provenance-health.js +9 -3
  87. package/dist/sdk/history-compact.js +14 -5
  88. package/dist/sdk/history-redact.js +58 -23
  89. package/dist/sdk/history-repair.js +16 -6
  90. package/dist/sdk/index.d.ts +4 -0
  91. package/dist/sdk/index.js +6 -2
  92. package/dist/sdk/init.d.ts +2 -0
  93. package/dist/sdk/init.js +18 -9
  94. package/dist/sdk/merge/driver.d.ts +3 -1
  95. package/dist/sdk/merge/driver.js +9 -9
  96. package/dist/sdk/merge/index.d.ts +1 -1
  97. package/dist/sdk/merge/index.js +3 -3
  98. package/dist/sdk/merge/receipts.d.ts +17 -6
  99. package/dist/sdk/merge/receipts.js +246 -75
  100. package/dist/sdk/merge/reconcile.d.ts +9 -7
  101. package/dist/sdk/merge/reconcile.js +175 -12
  102. package/dist/sdk/merge/three-way.d.ts +22 -2
  103. package/dist/sdk/merge/three-way.js +92 -25
  104. package/dist/sdk/query/context.d.ts +3 -1
  105. package/dist/sdk/query/context.js +21 -9
  106. package/dist/sdk/query/next.js +18 -9
  107. package/dist/sdk/runtime-input.d.ts +2 -0
  108. package/dist/sdk/runtime-input.js +25 -2
  109. package/dist/sdk/runtime-primitives.d.ts +2 -2
  110. package/dist/sdk/runtime-primitives.js +4 -4
  111. package/dist/sdk/runtime.d.ts +1 -0
  112. package/dist/sdk/runtime.js +17 -26
  113. package/dist/types.d.ts +27 -0
  114. package/dist/types.js +2 -2
  115. package/docs/AGENT_ENVIRONMENTS.md +47 -0
  116. package/docs/AGENT_PROVENANCE_ADR.md +17 -2
  117. package/docs/CONTEXT_INTEGRITY_CONTRACTS.md +38 -1
  118. package/docs/CONTEXT_RELEVANCE.md +44 -1
  119. package/docs/MERGE_SAFETY.md +119 -23
  120. package/docs/PERFORMANCE.md +6 -3
  121. package/docs/README.md +1 -0
  122. package/docs/READ_OUTPUT_CONTRACTS.md +9 -1
  123. package/docs/SDK.md +11 -8
  124. package/docs/SDK_CONTEXT.md +7 -0
  125. package/docs/SDK_CONTEXT_EVIDENCE_CONTRACTS.md +24 -2
  126. package/docs/SDK_RUNTIME_BOUNDARIES.md +17 -1
  127. package/docs/examples/sdk-agent-environment/README.md +14 -0
  128. package/docs/examples/sdk-agent-environment/package.json +8 -0
  129. package/docs/examples/sdk-agent-environment/run-agent-environment.mjs +154 -0
  130. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +5 -3
  131. package/marketplace.json +2 -2
  132. package/package.json +5 -5
  133. package/packages/pm-beads/package.json +1 -1
  134. package/packages/pm-calendar/package.json +1 -1
  135. package/packages/pm-command-kit/package.json +1 -1
  136. package/packages/pm-digital-twin/package.json +1 -1
  137. package/packages/pm-governance-audit/package.json +1 -1
  138. package/packages/pm-guide-shell/package.json +1 -1
  139. package/packages/pm-kanban/package.json +1 -1
  140. package/packages/pm-lifecycle-hooks/package.json +1 -1
  141. package/packages/pm-linked-test-adapters/package.json +1 -1
  142. package/packages/pm-search-advanced/package.json +1 -1
  143. package/packages/pm-templates/package.json +1 -1
  144. package/packages/pm-todos/package.json +1 -1
  145. package/packages/pm-vcs/package.json +1 -1
  146. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  147. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  148. package/sdk/public-surface.json +773 -46
  149. package/dist/cli-bundle/chunks/chunk-GOACULJW.js +0 -13
  150. package/dist/cli-bundle/chunks/chunk-NOPL37X4.js +0 -205
  151. package/dist/cli-bundle/chunks/chunk-UDW64J2W.js +0 -8
  152. package/dist/cli-bundle/chunks/chunk-ZLJIFSOE.js +0 -35
  153. package/dist/cli-bundle/focused-chunks/chunk-242AXYB2.js +0 -2
  154. package/dist/cli-bundle/focused-chunks/chunk-2FEZ5SKM.js +0 -3
  155. package/dist/cli-bundle/focused-chunks/chunk-6GCRSLPG.js +0 -2
  156. package/dist/cli-bundle/focused-chunks/chunk-7VATS5QY.js +0 -16
  157. package/dist/cli-bundle/focused-chunks/chunk-JEFKLWMM.js +0 -2
  158. package/dist/cli-bundle/focused-chunks/chunk-MKHG3T76.js +0 -20
  159. package/dist/cli-bundle/focused-chunks/chunk-O67VDLOT.js +0 -51
  160. package/dist/cli-bundle/focused-chunks/chunk-PF3E6Q36.js +0 -8
  161. package/dist/cli-bundle/focused-chunks/chunk-QDFJIRKN.js +0 -4
  162. package/dist/cli-bundle/focused-chunks/chunk-RMRCQPWU.js +0 -2
  163. package/dist/cli-bundle/focused-chunks/chunk-S4U5CN5Q.js +0 -2
  164. package/dist/cli-bundle/focused-chunks/chunk-T2LXXSOD.js +0 -158
  165. package/dist/cli-bundle/focused-chunks/chunk-Z2USIBR2.js +0 -5
@@ -4,13 +4,13 @@
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]="c9488ed4-8228-5e7c-9717-69dd7a3275c4")}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]="ce8dfbdf-8471-5843-8179-5608140330e1")}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";
11
11
  import { toItemRecord } from "../item/item-record.js";
12
12
  import { orderObject, sha256Hex, stableStringify, } from "../shared/serialization.js";
13
- import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentVerificationCandidates, } from "./history.js";
13
+ import { CURRENT_HISTORY_ITEM_HASH_VERSION, SUPPORTED_HISTORY_ITEM_HASH_VERSIONS, hashDocumentVerificationCandidates, resealHistoryRewrite, verifyHistoryRecordHash, verifyHistoryRewriteEvidence, } from "./history.js";
14
14
  /** Public contract for empty replay document, shared by SDK and presentation-layer consumers. */
15
15
  export const EMPTY_REPLAY_DOCUMENT = {
16
16
  metadata: {},
@@ -165,6 +165,31 @@ function historyEntryHashCandidates(explicitVersion, authoritativeVersion) {
165
165
  ? [...SUPPORTED_HISTORY_ITEM_HASH_VERSIONS]
166
166
  : [authoritativeVersion];
167
167
  }
168
+ /** Return the first record or retained-rewrite integrity failure for one entry. */
169
+ function historyEntryIntegrityError(entry) {
170
+ const recordVerification = verifyHistoryRecordHash(entry);
171
+ if (!recordVerification.ok)
172
+ return recordVerification.error;
173
+ const rewriteVerification = verifyHistoryRewriteEvidence(entry);
174
+ return rewriteVerification.ok ? undefined : rewriteVerification.error;
175
+ }
176
+ /** Validate immutable metadata and apply one patch for chain verification. */
177
+ function verifyHistoryEntryInput(entry, replay) {
178
+ const integrityError = historyEntryIntegrityError(entry);
179
+ if (integrityError)
180
+ return { ok: false, error: integrityError };
181
+ const applied = tryApplyReplayPatch(replay, entry.patch);
182
+ return applied.ok
183
+ ? { ok: true, document: applied.document }
184
+ : { ok: false, error: "patch_apply_failed" };
185
+ }
186
+ /** Refuse any entry whose current or retained immutable envelope is invalid. */
187
+ function assertHistoryEntryIntegrity(entry, index) {
188
+ const integrityError = historyEntryIntegrityError(entry);
189
+ if (integrityError) {
190
+ throw new TypeError(`${integrityError}:entry_${index + 1}`);
191
+ }
192
+ }
168
193
  /** 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. */
169
194
  /** Verify a chain and report the explicit or auto-detected item hash epoch. */
170
195
  export function verifyHistoryChainWithVersion(entries) {
@@ -179,18 +204,15 @@ export function verifyHistoryChainWithVersion(entries) {
179
204
  if (unsupportedVersion) {
180
205
  errors.push(`verify_failed:unsupported_item_hash_version:${String(explicitVersion)}:entry_${index + 1}`);
181
206
  }
182
- const applied = tryApplyReplayPatch(replay, entry.patch);
183
- if (!applied.ok) {
207
+ const input = verifyHistoryEntryInput(entry, replay);
208
+ if (!input.ok) {
184
209
  return {
185
210
  ok: false,
186
- errors: [
187
- ...errors,
188
- `verify_failed:patch_apply_failed:entry_${index + 1}`,
189
- ],
211
+ errors: [...errors, `verify_failed:${input.error}:entry_${index + 1}`],
190
212
  };
191
213
  }
192
214
  if (unsupportedVersion) {
193
- replay = applied.document;
215
+ replay = input.document;
194
216
  detectedVersion = undefined;
195
217
  authoritativeExplicitVersion = undefined;
196
218
  continue;
@@ -201,7 +223,7 @@ export function verifyHistoryChainWithVersion(entries) {
201
223
  const candidates = historyEntryHashCandidates(explicitVersion, authoritativeExplicitVersion);
202
224
  const candidateMatches = candidates.map((version) => ({
203
225
  version,
204
- match: matchReplayHashCandidates(replay, applied.document, version, entry),
226
+ match: matchReplayHashCandidates(replay, input.document, version, entry),
205
227
  }));
206
228
  const matchingVersion = candidateMatches.find(({ match }) => match.pairIndex >= 0);
207
229
  if (matchingVersion === undefined) {
@@ -215,7 +237,7 @@ export function verifyHistoryChainWithVersion(entries) {
215
237
  };
216
238
  }
217
239
  const version = matchingVersion.version;
218
- replay = applied.document;
240
+ replay = input.document;
219
241
  detectedVersion = version;
220
242
  authoritativeExplicitVersion =
221
243
  explicitVersion ??
@@ -304,6 +326,27 @@ export function resolveHistoryRepairItemHashVersion(entries) {
304
326
  }
305
327
  return CURRENT_HISTORY_ITEM_HASH_VERSION;
306
328
  }
329
+ /** Apply one patch strictly or return its deterministic lenient repair projection. */
330
+ function applyReanchorPatch(replay, entry) {
331
+ const strict = tryApplyReplayPatch(replay, entry.patch);
332
+ if (strict.ok) {
333
+ return {
334
+ next: strict.document,
335
+ patch: entry.patch,
336
+ repaired: false,
337
+ converted: 0,
338
+ skipped: 0,
339
+ };
340
+ }
341
+ const lenient = lenientApplyReplayPatch(replay, entry.patch);
342
+ return {
343
+ next: lenient.document,
344
+ patch: jsonPatch.compare(replay, lenient.document),
345
+ repaired: true,
346
+ converted: lenient.convertedReplaceToAdd,
347
+ skipped: lenient.skippedOps,
348
+ };
349
+ }
307
350
  /** 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. */
308
351
  export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistoryRepairItemHashVersion(entries), options = {}) {
309
352
  const unsupportedIndex = entries.findIndex((entry) => isUnsupportedExplicitHistoryItemHashVersion(entry.item_hash_version));
@@ -322,27 +365,12 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
322
365
  entries.some((entry) => entry.item_hash_version !== undefined);
323
366
  for (let index = 0; index < entries.length; index += 1) {
324
367
  const entry = entries[index];
325
- const strict = tryApplyReplayPatch(replay, entry.patch);
326
- let next;
327
- let outPatch;
328
- let patchRepaired = false;
329
- let entryConverted = 0;
330
- let entrySkipped = 0;
331
- if (strict.ok) {
332
- next = strict.document;
333
- outPatch = entry.patch;
334
- }
335
- else {
336
- const lenient = lenientApplyReplayPatch(replay, entry.patch);
337
- next = lenient.document;
338
- outPatch = jsonPatch.compare(replay, next);
339
- patchRepaired = true;
340
- entryConverted = lenient.convertedReplaceToAdd;
341
- entrySkipped = lenient.skippedOps;
342
- convertedReplaceToAdd += entryConverted;
343
- skippedOps += entrySkipped;
344
- entriesPatchRepaired += 1;
345
- }
368
+ assertHistoryEntryIntegrity(entry, index);
369
+ const patchApplication = applyReanchorPatch(replay, entry);
370
+ const { next, patch: outPatch, repaired: patchRepaired, converted: entryConverted, skipped: entrySkipped, } = patchApplication;
371
+ convertedReplaceToAdd += entryConverted;
372
+ skippedOps += entrySkipped;
373
+ entriesPatchRepaired += Number(patchRepaired);
346
374
  const hashMatch = matchReplayHashCandidates(replay, next, itemHashVersion, entry);
347
375
  if (semanticIndex === undefined || options.continuousHashSurface !== true) {
348
376
  semanticIndex = [
@@ -361,7 +389,7 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
361
389
  if (rehashed) {
362
390
  entriesRehashed += 1;
363
391
  }
364
- const rewrittenEntry = {
392
+ let rewrittenEntry = {
365
393
  ...entry,
366
394
  patch: outPatch,
367
395
  before_hash: beforeHash,
@@ -373,6 +401,12 @@ export function reanchorHistoryEntries(entries, itemHashVersion = resolveHistory
373
401
  else {
374
402
  delete rewrittenEntry.item_hash_version;
375
403
  }
404
+ const versionChanged = rewrittenEntry.item_hash_version !== entry.item_hash_version;
405
+ if (rehashed || patchRepaired || versionChanged) {
406
+ rewrittenEntry = resealHistoryRewrite(entry, rewrittenEntry, {
407
+ retainOriginalPatch: patchRepaired,
408
+ });
409
+ }
376
410
  rewritten.push(rewrittenEntry);
377
411
  details.push({
378
412
  index: index + 1,
@@ -403,4 +437,4 @@ export function historyEntriesToRaw(entries) {
403
437
  return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
404
438
  }
405
439
  //# sourceMappingURL=replay.js.map
406
- //# debugId=c9488ed4-8228-5e7c-9717-69dd7a3275c4
440
+ //# debugId=ce8dfbdf-8471-5843-8179-5608140330e1
@@ -5,7 +5,7 @@
5
5
  * HistoryEntry format used by item mutations.
6
6
  */
7
7
 
8
- !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]="9027d972-ce29-5511-ba81-27e17d97446e")}catch(e){}}();
8
+ !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]="4df21a17-43c0-5b63-adf3-657699e6a058")}catch(e){}}();
9
9
  import fs from "node:fs/promises";
10
10
  import path from "node:path";
11
11
  import { readFileIfExists, writeFileAtomic } from "../fs/fs-utils.js";
@@ -13,6 +13,7 @@ import { acquireLock } from "../lock/lock.js";
13
13
  import { EMPTY_CANONICAL_DOCUMENT, EXIT_CODE } from "../shared/constants.js";
14
14
  import { PmCliError } from "../shared/errors.js";
15
15
  import { stableStringify } from "../shared/serialization.js";
16
+ import { nowIso } from "../shared/time.js";
16
17
  import { appendHistoryEntry, createHistoryEntry } from "./history.js";
17
18
  import { readHistoryEntries } from "./read.js";
18
19
  import { cloneEmptyReplayDocument, replayToItemDocument, tryApplyReplayPatch, verifyHistoryChain, } from "./replay.js";
@@ -153,7 +154,7 @@ async function appendWorkspaceHistoryChangeLocked(change) {
153
154
  if (idempotentEntry) {
154
155
  return { entry: idempotentEntry, historyPath };
155
156
  }
156
- const timestamp = new Date().toISOString();
157
+ const timestamp = nowIso();
157
158
  const beforeDocument = entries.length === 0
158
159
  ? EMPTY_CANONICAL_DOCUMENT
159
160
  : replayWorkspaceEntries(entries);
@@ -222,7 +223,7 @@ export async function appendWorkspaceAuditEvent(options) {
222
223
  ? EMPTY_CANONICAL_DOCUMENT
223
224
  : replayWorkspaceEntries(entries);
224
225
  const entry = createHistoryEntry({
225
- nowIso: new Date().toISOString(),
226
+ nowIso: nowIso(),
226
227
  author: options.author,
227
228
  op: options.op,
228
229
  before: beforeDocument,
@@ -340,7 +341,7 @@ export async function reconcileWorkspaceJsonHistory(options) {
340
341
  if (stableStringify(priorDocuments[documentPath]) === stableStringify(current)) {
341
342
  return { changed: false, historyPath };
342
343
  }
343
- const timestamp = new Date().toISOString();
344
+ const timestamp = nowIso();
344
345
  const entry = createHistoryEntry({
345
346
  nowIso: timestamp,
346
347
  author: options.author,
@@ -393,7 +394,7 @@ export async function restoreWorkspaceJsonFromHistory(options) {
393
394
  changed: false,
394
395
  };
395
396
  const beforeDocument = replayWorkspaceEntries(entries);
396
- const timestamp = new Date().toISOString();
397
+ const timestamp = nowIso();
397
398
  const entry = createHistoryEntry({
398
399
  nowIso: timestamp,
399
400
  author: options.author,
@@ -430,4 +431,4 @@ export async function restoreWorkspaceJsonFromHistory(options) {
430
431
  }
431
432
  }
432
433
  //# sourceMappingURL=workspace-history.js.map
433
- //# debugId=9027d972-ce29-5511-ba81-27e17d97446e
434
+ //# debugId=4df21a17-43c0-5b63-adf3-657699e6a058
@@ -2,6 +2,7 @@ interface LockInfo {
2
2
  id: string;
3
3
  pid: number;
4
4
  owner: string;
5
+ token?: string;
5
6
  created_at: string;
6
7
  ttl_seconds: number;
7
8
  }
@@ -14,14 +15,40 @@ interface StaleLockRemovalResult {
14
15
  shouldRetryCreate: boolean;
15
16
  staleRemovalCounted: boolean;
16
17
  }
18
+ /** Stable operating-system identity for a process when the host exposes one. */
19
+ export interface ProcessIdentity {
20
+ /** Operating-system process identifier captured with the owner record. */
21
+ pid: number;
22
+ /** Host process-start coordinate that distinguishes reuse of the same PID. */
23
+ process_start_identity?: string;
24
+ }
17
25
  declare function parseLockInfo(raw: string): LockReadResult;
18
- declare function readLockInfo(lockPath: string): Promise<LockReadResult>;
26
+ declare function readLockInfo(lockPath: string, emitReadHook?: boolean): Promise<LockReadResult>;
19
27
  declare function isErrno(error: unknown, code: string): boolean;
20
- declare function buildLockPayload(id: string, owner: string, ttlSeconds: number): LockInfo;
28
+ /**
29
+ * Classify exclusive-create failures that prove another lock lifecycle is in
30
+ * flight. Windows can report `EPERM`, `EACCES`, or `EBUSY` while a just-unlinked
31
+ * lock is still delete-pending; treating those as fatal makes bounded lock
32
+ * waiting fail nondeterministically. Existing paths are contention on every
33
+ * host. A missing or temporarily unstatable Windows path is contention only
34
+ * when its parent remains writable, preserving genuine permission failures.
35
+ */
36
+ declare function isLockCreateContention(error: unknown, lockPath: string, platform?: NodeJS.Platform): Promise<boolean>;
37
+ declare function buildLockPayload(id: string, owner: string, ttlSeconds: number, token?: string): LockInfo;
21
38
  declare function isStaleLock(info: LockInfo | null, ttlSeconds: number): boolean;
22
39
  declare function lockOwnerSuffix(info: LockInfo | null): string;
23
40
  /** Resolves the effective bounded wait budget for a contended lock: the PM_LOCK_WAIT_MS environment override wins when it parses as a non-negative integer, then the caller-provided budget (settings `locks.wait_ms`), then 0 (fail-fast, the pre-wait behavior). */
24
41
  declare function resolveLockWaitMs(waitMs: number | undefined): number;
42
+ declare function parseLinuxProcessStartIdentity(raw: string): string | null;
43
+ /** Read the host process-start coordinate used to distinguish PID reuse. */
44
+ export declare function readProcessStartIdentity(pid: number): Promise<string | null>;
45
+ /** Capture the current PID plus a host process-start coordinate when available. */
46
+ export declare function captureProcessIdentity(pid?: number): Promise<ProcessIdentity>;
47
+ /**
48
+ * Verify that a captured process owner still exists, with a bounded grace
49
+ * period when the host cannot expose a reusable-PID-safe start coordinate.
50
+ */
51
+ export declare function isProcessIdentityAlive(identity: ProcessIdentity, unverifiedAgeMs: number, maxUnverifiedAgeMs: number): Promise<boolean>;
25
52
  declare function acquireStaleCleanupGate(lockPath: string, _id: string): Promise<(() => Promise<void>) | null>;
26
53
  declare function removeConfirmedStaleLock(params: {
27
54
  lockPath: string;
@@ -39,10 +66,13 @@ export declare const _testOnly: {
39
66
  parseLockInfo: typeof parseLockInfo;
40
67
  readLockInfo: typeof readLockInfo;
41
68
  isErrno: typeof isErrno;
69
+ isLockCreateContention: typeof isLockCreateContention;
42
70
  buildLockPayload: typeof buildLockPayload;
43
71
  isStaleLock: typeof isStaleLock;
44
72
  lockOwnerSuffix: typeof lockOwnerSuffix;
45
73
  resolveLockWaitMs: typeof resolveLockWaitMs;
74
+ parseLinuxProcessStartIdentity: typeof parseLinuxProcessStartIdentity;
75
+ readProcessStartIdentity: typeof readProcessStartIdentity;
46
76
  acquireStaleCleanupGate: typeof acquireStaleCleanupGate;
47
77
  removeConfirmedStaleLock: typeof removeConfirmedStaleLock;
48
78
  };
@@ -4,8 +4,9 @@
4
4
  * Coordinates tracker lock ownership and cleanup for Lock.
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]="a9ad8a9f-de6e-5bce-8685-844f1129d5fb")}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]="b21a63b8-9da9-5c7c-b288-3a2dae342bef")}catch(e){}}();
8
8
  import { isFileMissingError } from "../fs/fs-utils.js";
9
+ import { constants as fsConstants } from "node:fs";
9
10
  import fs from "node:fs/promises";
10
11
  import path from "node:path";
11
12
  import { randomUUID } from "node:crypto";
@@ -36,12 +37,15 @@ function parseLockInfo(raw) {
36
37
  const id = candidate.id;
37
38
  const pid = candidate.pid;
38
39
  const owner = candidate.owner;
40
+ const token = candidate.token;
39
41
  const createdAt = candidate.created_at;
40
42
  const ttlSeconds = candidate.ttl_seconds;
41
43
  if (typeof id !== "string" ||
42
44
  typeof pid !== "number" ||
43
45
  !Number.isFinite(pid) ||
44
46
  typeof owner !== "string" ||
47
+ (token !== undefined &&
48
+ (typeof token !== "string" || token.trim().length === 0)) ||
45
49
  typeof createdAt !== "string" ||
46
50
  typeof ttlSeconds !== "number" ||
47
51
  !Number.isFinite(ttlSeconds)) {
@@ -55,19 +59,22 @@ function parseLockInfo(raw) {
55
59
  id,
56
60
  pid,
57
61
  owner,
62
+ ...(typeof token === "string" ? { token } : {}),
58
63
  created_at: createdAt,
59
64
  ttl_seconds: ttlSeconds,
60
65
  },
61
66
  warnings: [],
62
67
  };
63
68
  }
64
- async function readLockInfo(lockPath) {
69
+ async function readLockInfo(lockPath, emitReadHook = true) {
65
70
  try {
66
71
  const raw = await fs.readFile(lockPath, "utf8");
67
- await runActiveOnReadHooks({
68
- path: lockPath,
69
- scope: "project",
70
- });
72
+ if (emitReadHook) {
73
+ await runActiveOnReadHooks({
74
+ path: lockPath,
75
+ scope: "project",
76
+ });
77
+ }
71
78
  return parseLockInfo(raw);
72
79
  }
73
80
  catch (error) {
@@ -89,6 +96,43 @@ function isErrno(error, code) {
89
96
  "code" in error &&
90
97
  error.code === code);
91
98
  }
99
+ /**
100
+ * Classify exclusive-create failures that prove another lock lifecycle is in
101
+ * flight. Windows can report `EPERM`, `EACCES`, or `EBUSY` while a just-unlinked
102
+ * lock is still delete-pending; treating those as fatal makes bounded lock
103
+ * waiting fail nondeterministically. Existing paths are contention on every
104
+ * host. A missing or temporarily unstatable Windows path is contention only
105
+ * when its parent remains writable, preserving genuine permission failures.
106
+ */
107
+ async function isLockCreateContention(error, lockPath, platform = process.platform) {
108
+ if (isErrno(error, "EEXIST"))
109
+ return true;
110
+ if (!isErrno(error, "EPERM") &&
111
+ !isErrno(error, "EACCES") &&
112
+ !isErrno(error, "EBUSY")) {
113
+ return false;
114
+ }
115
+ try {
116
+ await fs.lstat(lockPath);
117
+ return true;
118
+ }
119
+ catch (probeError) {
120
+ if (platform !== "win32" ||
121
+ (!isFileMissingError(probeError) &&
122
+ !isErrno(probeError, "EPERM") &&
123
+ !isErrno(probeError, "EACCES") &&
124
+ !isErrno(probeError, "EBUSY"))) {
125
+ return false;
126
+ }
127
+ }
128
+ try {
129
+ await fs.access(path.dirname(lockPath), fsConstants.W_OK);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
92
136
  async function emitLockWriteHook(lockPath, op) {
93
137
  await runActiveOnWriteHooks({
94
138
  path: lockPath,
@@ -96,20 +140,21 @@ async function emitLockWriteHook(lockPath, op) {
96
140
  op,
97
141
  });
98
142
  }
99
- function buildLockPayload(id, owner, ttlSeconds) {
143
+ function buildLockPayload(id, owner, ttlSeconds, token = randomUUID()) {
100
144
  return {
101
145
  id,
102
146
  pid: process.pid,
103
147
  owner,
148
+ token,
104
149
  created_at: nowIso(),
105
150
  ttl_seconds: ttlSeconds,
106
151
  };
107
152
  }
108
- async function createLockFile(lockPath, id, owner, ttlSeconds) {
153
+ async function createLockFile(lockPath, id, owner, ttlSeconds, token) {
109
154
  await fs.mkdir(path.dirname(lockPath), { recursive: true });
110
155
  const handle = await fs.open(lockPath, "wx");
111
156
  try {
112
- await handle.writeFile(`${JSON.stringify(buildLockPayload(id, owner, ttlSeconds), null, 2)}\n`, "utf8");
157
+ await handle.writeFile(`${JSON.stringify(buildLockPayload(id, owner, ttlSeconds, token), null, 2)}\n`, "utf8");
113
158
  }
114
159
  finally {
115
160
  await handle.close();
@@ -127,6 +172,41 @@ async function unlinkLockWithHook(lockPath, op) {
127
172
  return false;
128
173
  }
129
174
  }
175
+ async function releaseOwnedLock(lockPath, id, owner, token) {
176
+ const startedAtMs = Date.now();
177
+ let backoffMs = LOCK_WAIT_INITIAL_DELAY_MS;
178
+ for (;;) {
179
+ const releaseCleanupGate = await acquireStaleCleanupGate(lockPath, id);
180
+ if (releaseCleanupGate !== null) {
181
+ try {
182
+ const current = await readLockInfo(lockPath, false);
183
+ if (isOwnedLock(current.info, id, owner, token)) {
184
+ await unlinkLockWithHook(lockPath, "lock:release");
185
+ }
186
+ }
187
+ finally {
188
+ await releaseCleanupGate();
189
+ }
190
+ return;
191
+ }
192
+ const current = await readLockInfo(lockPath, false);
193
+ if (!isOwnedLock(current.info, id, owner, token)) {
194
+ return;
195
+ }
196
+ const elapsedMs = Date.now() - startedAtMs;
197
+ if (elapsedMs >= LOCK_RELEASE_GATE_WAIT_MS) {
198
+ return;
199
+ }
200
+ await sleepWithJitter(Math.min(backoffMs, LOCK_RELEASE_GATE_WAIT_MS - elapsedMs));
201
+ backoffMs = Math.min(backoffMs * 2, LOCK_WAIT_MAX_DELAY_MS);
202
+ }
203
+ }
204
+ function isOwnedLock(info, id, owner, token) {
205
+ return (info?.id === id &&
206
+ info.pid === process.pid &&
207
+ info.owner === owner &&
208
+ info.token === token);
209
+ }
130
210
  function isStaleLock(info, ttlSeconds) {
131
211
  const createdAtMs = info?.created_at
132
212
  ? Date.parse(info.created_at)
@@ -146,8 +226,8 @@ const MAX_STALE_LOCK_REMOVALS = 3;
146
226
  const STALE_CLEANUP_GATE_SUFFIX = ".stale-cleanup";
147
227
  const STALE_CLEANUP_GATE_OWNER_FILE = "owner.json";
148
228
  const STALE_CLEANUP_GATE_STALE_MS = 10_000;
149
- // Bounds PID-reuse false positives: a live recycled PID can delay cleanup, but not indefinitely.
150
- const STALE_CLEANUP_GATE_MAX_ACTIVE_MS = 5 * 60_000;
229
+ const LOCK_RELEASE_GATE_WAIT_MS = STALE_CLEANUP_GATE_STALE_MS + LOCK_WAIT_MAX_DELAY_MS;
230
+ const STALE_CLEANUP_GATE_MAX_UNVERIFIED_MS = 60 * 60_000;
151
231
  function parseNonNegativeIntegerWaitMs(value) {
152
232
  if (typeof value === "string") {
153
233
  const trimmed = value.trim();
@@ -220,14 +300,24 @@ function parseStaleCleanupGateOwner(raw) {
220
300
  const candidate = parsed;
221
301
  const pid = candidate.pid;
222
302
  const token = candidate.token;
303
+ const processStartIdentity = candidate.process_start_identity;
223
304
  if (typeof pid !== "number" ||
224
305
  !Number.isInteger(pid) ||
225
306
  pid <= 0 ||
226
307
  typeof token !== "string" ||
227
- token.length === 0) {
308
+ token.length === 0 ||
309
+ (processStartIdentity !== undefined &&
310
+ (typeof processStartIdentity !== "string" ||
311
+ !/^\d+$/u.test(processStartIdentity)))) {
228
312
  return null;
229
313
  }
230
- return { pid, token };
314
+ return {
315
+ pid,
316
+ token,
317
+ ...(typeof processStartIdentity === "string"
318
+ ? { process_start_identity: processStartIdentity }
319
+ : {}),
320
+ };
231
321
  }
232
322
  async function readStaleCleanupGateOwner(gatePath) {
233
323
  try {
@@ -238,6 +328,8 @@ async function readStaleCleanupGateOwner(gatePath) {
238
328
  }
239
329
  }
240
330
  function isProcessAlive(pid) {
331
+ if (!Number.isSafeInteger(pid) || pid <= 0)
332
+ return false;
241
333
  try {
242
334
  process.kill(pid, 0);
243
335
  return true;
@@ -249,6 +341,49 @@ function isProcessAlive(pid) {
249
341
  return true;
250
342
  }
251
343
  }
344
+ function parseLinuxProcessStartIdentity(raw) {
345
+ const closeParenIndex = raw.lastIndexOf(")");
346
+ if (closeParenIndex < 0)
347
+ return null;
348
+ const startTime = raw
349
+ .slice(closeParenIndex + 2)
350
+ .trim()
351
+ .split(/\s+/u)[19];
352
+ return startTime !== undefined && /^\d+$/u.test(startTime) ? startTime : null;
353
+ }
354
+ /** Read the host process-start coordinate used to distinguish PID reuse. */
355
+ export async function readProcessStartIdentity(pid) {
356
+ try {
357
+ return parseLinuxProcessStartIdentity(await fs.readFile(`/proc/${pid}/stat`, "utf8"));
358
+ }
359
+ catch {
360
+ return null;
361
+ }
362
+ }
363
+ /** Capture the current PID plus a host process-start coordinate when available. */
364
+ export async function captureProcessIdentity(pid = process.pid) {
365
+ const processStartIdentity = await readProcessStartIdentity(pid);
366
+ return {
367
+ pid,
368
+ ...(processStartIdentity === null
369
+ ? {}
370
+ : { process_start_identity: processStartIdentity }),
371
+ };
372
+ }
373
+ /**
374
+ * Verify that a captured process owner still exists, with a bounded grace
375
+ * period when the host cannot expose a reusable-PID-safe start coordinate.
376
+ */
377
+ export async function isProcessIdentityAlive(identity, unverifiedAgeMs, maxUnverifiedAgeMs) {
378
+ if (!isProcessAlive(identity.pid))
379
+ return false;
380
+ const currentStartIdentity = await readProcessStartIdentity(identity.pid);
381
+ if (identity.process_start_identity !== undefined &&
382
+ currentStartIdentity !== null) {
383
+ return currentStartIdentity === identity.process_start_identity;
384
+ }
385
+ return unverifiedAgeMs <= maxUnverifiedAgeMs;
386
+ }
252
387
  async function removeStaleCleanupGateDirectory(gatePath) {
253
388
  try {
254
389
  await fs.rm(gatePath, { recursive: true, force: true });
@@ -275,7 +410,10 @@ async function tryCreateStaleCleanupGate(gatePath) {
275
410
  }
276
411
  return null;
277
412
  }
278
- const owner = { pid: process.pid, token: randomUUID() };
413
+ const owner = {
414
+ ...(await captureProcessIdentity()),
415
+ token: randomUUID(),
416
+ };
279
417
  try {
280
418
  await fs.writeFile(path.join(gatePath, STALE_CLEANUP_GATE_OWNER_FILE), `${JSON.stringify(owner)}\n`, "utf8");
281
419
  }
@@ -299,8 +437,7 @@ async function removeExpiredStaleCleanupGate(gatePath) {
299
437
  }
300
438
  const owner = await readStaleCleanupGateOwner(gatePath);
301
439
  if (owner !== null &&
302
- isProcessAlive(owner.pid) &&
303
- gateAgeMs <= STALE_CLEANUP_GATE_MAX_ACTIVE_MS) {
440
+ (await isProcessIdentityAlive(owner, gateAgeMs, STALE_CLEANUP_GATE_MAX_UNVERIFIED_MS))) {
304
441
  return false;
305
442
  }
306
443
  return removeStaleCleanupGateDirectory(gatePath);
@@ -365,10 +502,13 @@ export const _testOnly = {
365
502
  parseLockInfo,
366
503
  readLockInfo,
367
504
  isErrno,
505
+ isLockCreateContention,
368
506
  buildLockPayload,
369
507
  isStaleLock,
370
508
  lockOwnerSuffix,
371
509
  resolveLockWaitMs,
510
+ parseLinuxProcessStartIdentity,
511
+ readProcessStartIdentity,
372
512
  acquireStaleCleanupGate,
373
513
  removeConfirmedStaleLock,
374
514
  };
@@ -400,11 +540,12 @@ export async function acquireLock(pmRoot, id, ttlSeconds, owner, force = false,
400
540
  const startedAtMs = Date.now();
401
541
  let staleRemovals = 0;
402
542
  let backoffMs = LOCK_WAIT_INITIAL_DELAY_MS;
543
+ const token = randomUUID();
403
544
  for (;;) {
404
545
  try {
405
- await createLockFile(lockPath, id, owner, ttlSeconds);
546
+ await createLockFile(lockPath, id, owner, ttlSeconds, token);
406
547
  return async () => {
407
- await unlinkLockWithHook(lockPath, "lock:release");
548
+ await releaseOwnedLock(lockPath, id, owner, token);
408
549
  await runActiveServiceOverride("lock_release", {
409
550
  pm_root: pmRoot,
410
551
  id,
@@ -413,7 +554,7 @@ export async function acquireLock(pmRoot, id, ttlSeconds, owner, force = false,
413
554
  };
414
555
  }
415
556
  catch (error) {
416
- if (!isErrno(error, "EEXIST")) {
557
+ if (!(await isLockCreateContention(error, lockPath))) {
417
558
  throw new PmCliError(`Failed to acquire lock for ${id}: ${toErrorMessage(error)}`, EXIT_CODE.GENERIC_FAILURE);
418
559
  }
419
560
  let lockInfo = await readLockInfo(lockPath);
@@ -447,4 +588,4 @@ export async function acquireLock(pmRoot, id, ttlSeconds, owner, force = false,
447
588
  }
448
589
  }
449
590
  //# sourceMappingURL=lock.js.map
450
- //# debugId=a9ad8a9f-de6e-5bce-8685-844f1129d5fb
591
+ //# debugId=b21a63b8-9da9-5c7c-b288-3a2dae342bef