@unbrained/pm-cli 2026.10.21 → 2026.10.23

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 (186) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +134 -77
  3. package/dist/cli/error-guidance.d.ts +12 -0
  4. package/dist/cli/error-guidance.js +2 -2
  5. package/dist/cli/register-setup.js +1 -1
  6. package/dist/cli-bundle/bundle-manifest.json +1 -1
  7. package/dist/cli-bundle/chunks/{chunk-DIR3YEFD.js → chunk-2LTGJ5DL.js} +2 -2
  8. package/dist/cli-bundle/chunks/{chunk-EMGMLKRV.js → chunk-2UX2TAN2.js} +15 -15
  9. package/dist/cli-bundle/chunks/{chunk-2B5HAZFK.js → chunk-3S5FLCG7.js} +2 -2
  10. package/dist/cli-bundle/chunks/chunk-5DKCD4KV.js +126 -0
  11. package/dist/cli-bundle/chunks/{chunk-V3XONSVC.js → chunk-5V4HU6GK.js} +48 -48
  12. package/dist/cli-bundle/chunks/{chunk-QPV6UD55.js → chunk-AAX7CNL7.js} +2 -2
  13. package/dist/cli-bundle/chunks/{chunk-TMXWWOPJ.js → chunk-AXGFNNI3.js} +2 -2
  14. package/dist/cli-bundle/chunks/chunk-F76ZUP73.js +113 -0
  15. package/dist/cli-bundle/chunks/chunk-HDKY2YX3.js +3 -0
  16. package/dist/cli-bundle/chunks/{chunk-DR6DUBTU.js → chunk-KY34HTT2.js} +29 -29
  17. package/dist/cli-bundle/chunks/chunk-MMVX5L32.js +20 -0
  18. package/dist/cli-bundle/chunks/{chunk-C6SAXKBO.js → chunk-MTRYEVZR.js} +2 -2
  19. package/dist/cli-bundle/chunks/chunk-QSY55FTA.js +2 -0
  20. package/dist/cli-bundle/chunks/{chunk-GWIGZDRS.js → chunk-R7KWBFM2.js} +2 -2
  21. package/dist/cli-bundle/chunks/{chunk-5PZ7JNMI.js → chunk-REXCO5AG.js} +2 -2
  22. package/dist/cli-bundle/chunks/chunk-ZDVTXLA6.js +9 -0
  23. package/dist/cli-bundle/chunks/{chunk-DJQVNDXO.js → chunk-ZEI3DC5E.js} +6 -6
  24. package/dist/cli-bundle/chunks/{chunk-MOC5BJHW.js → chunk-ZMJF5HQW.js} +2 -2
  25. package/dist/cli-bundle/chunks/chunk-ZNDYAZZ6.js +6 -0
  26. package/dist/cli-bundle/chunks/{indexed-receipt-blobs-65XXSPPR.js → indexed-receipt-blobs-XANVELIV.js} +2 -2
  27. package/dist/cli-bundle/chunks/{mutation-admission-FNWOVJLN.js → mutation-admission-P6LOBNLO.js} +2 -2
  28. package/dist/cli-bundle/chunks/{register-list-query-6PT5Y2EZ.js → register-list-query-JOB65KDN.js} +2 -2
  29. package/dist/cli-bundle/chunks/{register-mutation-TAIQV46L.js → register-mutation-QMNIN3RJ.js} +2 -2
  30. package/dist/cli-bundle/chunks/{register-operations-ONIRO22G.js → register-operations-Z4VET3I7.js} +2 -2
  31. package/dist/cli-bundle/chunks/register-setup-GNKWL3VL.js +2 -0
  32. package/dist/cli-bundle/chunks/{settlement-hook-UVFYSBMU.js → settlement-hook-YUVCO65H.js} +2 -2
  33. package/dist/cli-bundle/chunks/{stage-FCPVFO3B.js → stage-FW5OXCCR.js} +2 -2
  34. package/dist/cli-bundle/focused-chunks/{chunk-VD64TPXI.js → chunk-4MNUQCGR.js} +2 -2
  35. package/dist/cli-bundle/focused-chunks/chunk-6GTZ24YV.js +3 -0
  36. package/dist/cli-bundle/focused-chunks/{chunk-XE7P7N5X.js → chunk-BM652VCB.js} +6 -6
  37. package/dist/cli-bundle/focused-chunks/{chunk-3LWH2RJC.js → chunk-BUZ2Z4P3.js} +2 -2
  38. package/dist/cli-bundle/focused-chunks/chunk-CGL36F6I.js +2 -0
  39. package/dist/cli-bundle/focused-chunks/{chunk-KVYUQS6Y.js → chunk-DWQMYLFM.js} +2 -2
  40. package/dist/cli-bundle/focused-chunks/{chunk-4HJVAUFX.js → chunk-EDXTOAXP.js} +2 -2
  41. package/dist/cli-bundle/focused-chunks/chunk-FO4U3FIP.js +91 -0
  42. package/dist/cli-bundle/focused-chunks/chunk-GXKP56W7.js +2 -0
  43. package/dist/cli-bundle/focused-chunks/chunk-I2USTQNQ.js +2 -0
  44. package/dist/cli-bundle/focused-chunks/{chunk-44353U27.js → chunk-JDN34LEZ.js} +2 -2
  45. package/dist/cli-bundle/focused-chunks/{chunk-CUWNNU4N.js → chunk-KI5YFKNN.js} +2 -2
  46. package/dist/cli-bundle/focused-chunks/{chunk-7UD7XTOE.js → chunk-LEHGCPZ6.js} +11 -11
  47. package/dist/cli-bundle/focused-chunks/{chunk-HNCMYF2W.js → chunk-MLET6I3I.js} +2 -2
  48. package/dist/cli-bundle/focused-chunks/{chunk-PDOWLRHC.js → chunk-NSMZY6IK.js} +2 -2
  49. package/dist/cli-bundle/focused-chunks/chunk-OMXMYTEX.js +26 -0
  50. package/dist/cli-bundle/focused-chunks/{chunk-JI3RXXRN.js → chunk-P7DPS5M4.js} +2 -2
  51. package/dist/cli-bundle/focused-chunks/chunk-R536272H.js +9 -0
  52. package/dist/cli-bundle/focused-chunks/{chunk-5IMM7GOF.js → chunk-UNTCCJPL.js} +3 -3
  53. package/dist/cli-bundle/focused-chunks/{chunk-32ZKLRWM.js → chunk-XIZYJDYW.js} +2 -2
  54. package/dist/cli-bundle/focused-chunks/chunk-Z5LDTMZ3.js +2 -0
  55. package/dist/cli-bundle/focused-chunks/chunk-ZHMF34DT.js +65 -0
  56. package/dist/cli-bundle/focused-chunks/{chunk-P5D7MHDS.js → chunk-ZR6ZKQUK.js} +15 -15
  57. package/dist/cli-bundle/focused-chunks/{indexed-receipt-blobs-CRP6HAZY.js → indexed-receipt-blobs-FVOA3XMI.js} +2 -2
  58. package/dist/cli-bundle/focused-chunks/{mutation-admission-KM57KDDF.js → mutation-admission-HTFAIHJL.js} +2 -2
  59. package/dist/cli-bundle/focused-chunks/{settlement-hook-MNCHFCAX.js → settlement-hook-2VXEB6PP.js} +2 -2
  60. package/dist/cli-bundle/focused-chunks/{stage-UJYGWNZ7.js → stage-X3BUGEJT.js} +2 -2
  61. package/dist/cli-bundle/main.js +21 -21
  62. package/dist/cli-bundle/sdk-authoring.js +1 -1
  63. package/dist/cli-bundle/sdk-contracts.js +5 -2
  64. package/dist/cli-bundle/sdk-core.js +33 -33
  65. package/dist/cli-bundle/sdk-governance.js +1 -1
  66. package/dist/cli-bundle/sdk-graph.js +1 -1
  67. package/dist/cli-bundle/sdk-merge.js +58 -58
  68. package/dist/cli-bundle/sdk-query.js +1 -1
  69. package/dist/cli-bundle/sdk-runtime.js +1 -1
  70. package/dist/cli-bundle/sdk-testing.js +1 -1
  71. package/dist/cli-bundle/sdk.js +12 -9
  72. package/dist/core/config/nested-settings.js +1 -1
  73. package/dist/core/extensions/loader.js +1 -1
  74. package/dist/core/extensions/version-diagnostics.d.ts +2 -0
  75. package/dist/core/extensions/version-diagnostics.js +2 -0
  76. package/dist/core/lock/lock.d.ts +40 -6
  77. package/dist/core/lock/lock.js +3 -3
  78. package/dist/core/shared/errors.d.ts +28 -0
  79. package/dist/core/shared/errors.js +1 -1
  80. package/dist/core/store/item-store.js +1 -1
  81. package/dist/mcp/http-server.js +2 -2
  82. package/dist/mcp/server.d.ts +2 -2
  83. package/dist/mcp/server.js +2 -2
  84. package/dist/sdk/agent/command-recovery.d.ts +2 -0
  85. package/dist/sdk/agent/command-recovery.js +1 -1
  86. package/dist/sdk/cli-contracts/agent-output-contracts.js +1 -1
  87. package/dist/sdk/cli-contracts/tool-option-contracts.d.ts +6 -0
  88. package/dist/sdk/cli-contracts/tool-option-contracts.js +1 -1
  89. package/dist/sdk/cli-contracts/tool-schema.js +1 -1
  90. package/dist/sdk/context-intent-contracts.d.ts +13 -0
  91. package/dist/sdk/context-intent-contracts.js +1 -1
  92. package/dist/sdk/contracts.d.ts +2 -1
  93. package/dist/sdk/contracts.js +1 -1
  94. package/dist/sdk/core.d.ts +2 -2
  95. package/dist/sdk/core.js +1 -1
  96. package/dist/sdk/define.d.ts +15 -16
  97. package/dist/sdk/define.js +1 -1
  98. package/dist/sdk/dependencies.js +1 -1
  99. package/dist/sdk/extension/doctor.js +1 -1
  100. package/dist/sdk/extension/install-receipts.js +1 -1
  101. package/dist/sdk/extension/install-snapshot.d.ts +11 -0
  102. package/dist/sdk/extension/install-snapshot.js +1 -1
  103. package/dist/sdk/extension.d.ts +12 -0
  104. package/dist/sdk/extension.js +1 -1
  105. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1 -1
  106. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +1 -1
  107. package/dist/sdk/governance/health.d.ts +1 -0
  108. package/dist/sdk/governance/health.js +2 -2
  109. package/dist/sdk/index.d.ts +3 -2
  110. package/dist/sdk/index.js +1 -1
  111. package/dist/sdk/lifecycle/hierarchy-mutation-lock.js +1 -1
  112. package/dist/sdk/lifecycle/restore.js +1 -1
  113. package/dist/sdk/mcp/executable.d.ts +13 -0
  114. package/dist/sdk/mcp/executable.js +5 -0
  115. package/dist/sdk/merge/install.js +2 -2
  116. package/dist/sdk/public-surface.json +1 -1
  117. package/dist/sdk/query/context.d.ts +5 -0
  118. package/dist/sdk/query/context.js +2 -2
  119. package/dist/sdk/read-output/session-result.d.ts +39 -0
  120. package/dist/sdk/read-output/session-result.js +2 -0
  121. package/dist/sdk/read-output-contracts.d.ts +18 -5
  122. package/dist/sdk/read-output-contracts.js +1 -1
  123. package/dist/sdk/read-output-session.d.ts +2 -0
  124. package/dist/sdk/read-output-session.js +1 -1
  125. package/dist/sdk/relationship-context.js +1 -1
  126. package/dist/sdk/runtime-input.js +1 -1
  127. package/dist/sdk/runtime-primitives.d.ts +7 -1
  128. package/dist/sdk/runtime-primitives.js +1 -1
  129. package/dist/sdk/runtime.d.ts +11 -9
  130. package/dist/sdk/runtime.js +1 -1
  131. package/dist/sdk/workspace-snapshot.d.ts +5 -1
  132. package/dist/sdk/workspace-snapshot.js +3 -3
  133. package/dist/sdk/workspace.d.ts +3 -1
  134. package/dist/sdk/workspace.js +8 -5
  135. package/docs/CONFIGURATION.md +28 -3
  136. package/docs/DEVELOPMENT_DEPENDENCY_SECURITY.md +30 -0
  137. package/docs/EXTENSION_AUTHOR_CONTRACTS.md +2 -1
  138. package/docs/EXTENSION_LIFECYCLE.md +47 -0
  139. package/docs/MCP_OPTION_CONFORMANCE.md +27 -1
  140. package/docs/MCP_REMOTE_TRANSPORT_SECURITY.md +16 -1
  141. package/docs/MERGE_SAFETY.md +8 -0
  142. package/docs/READ_OUTPUT_CONTRACTS.md +79 -0
  143. package/docs/RELATIONSHIP_GRAPH.md +15 -0
  144. package/docs/RELEASING.md +17 -0
  145. package/docs/SDK.md +76 -12
  146. package/docs/SDK_STORAGE_LIFECYCLE.md +2 -1
  147. package/docs/TESTING.md +16 -7
  148. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +6 -3
  149. package/marketplace.json +1 -1
  150. package/package.json +1 -1
  151. package/packages/pm-beads/package.json +1 -1
  152. package/packages/pm-calendar/package.json +1 -1
  153. package/packages/pm-command-kit/package.json +1 -1
  154. package/packages/pm-digital-twin/package.json +1 -1
  155. package/packages/pm-governance-audit/package.json +1 -1
  156. package/packages/pm-guide-shell/package.json +1 -1
  157. package/packages/pm-kanban/package.json +1 -1
  158. package/packages/pm-lifecycle-hooks/package.json +1 -1
  159. package/packages/pm-linked-test-adapters/package.json +1 -1
  160. package/packages/pm-search-advanced/package.json +1 -1
  161. package/packages/pm-templates/package.json +1 -1
  162. package/packages/pm-todos/package.json +1 -1
  163. package/packages/pm-vcs/package.json +1 -1
  164. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  165. package/plugins/pm-claude/package.json +1 -1
  166. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  167. package/plugins/pm-codex/package.json +1 -1
  168. package/plugins/pm-codex/plugin.json +1 -1
  169. package/runtime-dependencies.json +1 -1
  170. package/dist/cli-bundle/chunks/chunk-4OYU5OXQ.js +0 -6
  171. package/dist/cli-bundle/chunks/chunk-DIKQ2AL5.js +0 -2
  172. package/dist/cli-bundle/chunks/chunk-KBSVPUFD.js +0 -113
  173. package/dist/cli-bundle/chunks/chunk-MZZIAC6S.js +0 -20
  174. package/dist/cli-bundle/chunks/chunk-NNB65OOO.js +0 -3
  175. package/dist/cli-bundle/chunks/chunk-V5GJTXYW.js +0 -126
  176. package/dist/cli-bundle/chunks/chunk-ZNKFMIVA.js +0 -6
  177. package/dist/cli-bundle/chunks/register-setup-OQMUZBJV.js +0 -2
  178. package/dist/cli-bundle/focused-chunks/chunk-6QSZHRM7.js +0 -6
  179. package/dist/cli-bundle/focused-chunks/chunk-E2S42XCT.js +0 -2
  180. package/dist/cli-bundle/focused-chunks/chunk-GRSQGMRR.js +0 -2
  181. package/dist/cli-bundle/focused-chunks/chunk-IAGFXKMZ.js +0 -3
  182. package/dist/cli-bundle/focused-chunks/chunk-JMZPZQHR.js +0 -26
  183. package/dist/cli-bundle/focused-chunks/chunk-O3M6YWVL.js +0 -2
  184. package/dist/cli-bundle/focused-chunks/chunk-T27TLSEU.js +0 -65
  185. package/dist/cli-bundle/focused-chunks/chunk-VE6MTRH5.js +0 -2
  186. package/dist/cli-bundle/focused-chunks/chunk-YIWDX6KO.js +0 -91
@@ -1,6 +1,6 @@
1
- (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3b4a5521-a665-5f60-8d01-16033b078916")}catch{}})();import crypto from"node:crypto";import{cp,lstat,mkdir,readFile,readdir,rename,rm,writeFile}from"node:fs/promises";import path from"node:path";import{isFileMissingError,writeFileAtomic}from"../core/fs/fs-utils.js";import{appendWorkspaceAuditEvent}from"../core/history/workspace-history.js";import{acquireLock}from"../core/lock/lock.js";import{getLockPath}from"../core/store/paths.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{withHostEnvironmentBoundary}from"./environment/host-environment-errors.js";const SNAPSHOT_SCHEMA="https://schema.unbrained.dev/pm/workspace-snapshot/v1",WORKSPACE_SNAPSHOT_ACTIONS=["create","list","inspect","restore","delete"],SNAPSHOT_RUNTIME_PATH=path.join("runtime","workspace-snapshots"),EXCLUDED_ROOT_NAMES=new Set(["checkpoints","locks","runtime","search","transactions"]),SNAPSHOT_TARGET_PATTERN=/^[a-z0-9][a-z0-9._-]*$/,WORKSPACE_WRITER_LOCK_ID="sdk-workspace-transaction",SNAPSHOT_HOST_FAULT_CODES={capacity:{code:"workspace_snapshot_storage_exhausted"},permission:{code:"workspace_snapshot_permission_denied"},resource:{code:"workspace_snapshot_resource_exhausted"}},DEFAULT_ATOMIC_OPERATIONS={renameEntry:rename,removeEntry:async target=>{await rm(target,{recursive:!0,force:!0})}};class WorkspaceLockHeartbeat{lockPath;owner;intervalMs;timer;refresh;failure;constructor(lockPath,owner,ttlSeconds){this.lockPath=lockPath,this.owner=owner,this.intervalMs=Math.max(10,Math.floor(ttlSeconds*1e3/3))}start(){this.scheduleRenewal(),this.timer=setInterval(()=>{this.scheduleRenewal()},this.intervalMs),this.timer.unref()}async refreshNow(){this.scheduleRenewal(),await this.refresh,this.assertHealthy()}assertHealthy(){if(this.failure!==void 0)throw new Error("Workspace snapshot restore lost its writer lock",{cause:this.failure})}async stop(){this.timer!==void 0&&clearInterval(this.timer),await this.refresh,this.assertHealthy()}scheduleRenewal(){this.refresh!==void 0||this.failure!==void 0||(this.refresh=this.renew().catch(error=>{this.failure=error}).finally(()=>{this.refresh=void 0}))}async renew(){const parsed=JSON.parse(await readFile(this.lockPath,"utf8"));if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))throw new Error("Workspace writer lock has invalid structure");const lock=parsed;if(lock.id!==WORKSPACE_WRITER_LOCK_ID||lock.owner!==this.owner||lock.pid!==process.pid)throw new Error("Workspace writer lock ownership changed");await writeFileAtomic(this.lockPath,`${JSON.stringify({...lock,created_at:new Date().toISOString()},null,2)}
2
- `)}}const _testOnlyWorkspaceSnapshot={WorkspaceLockHeartbeat,withSnapshotFilesystemGuard};async function collectAuthoritativeFiles(root,relative=""){const entries=await readdir(path.join(root,relative),{withFileTypes:!0}),files=[];for(const entry of entries.sort((left,right)=>left.name.localeCompare(right.name))){if(relative.length===0&&EXCLUDED_ROOT_NAMES.has(entry.name))continue;const entryRelative=path.posix.join(relative.replaceAll(path.sep,"/"),entry.name);if(entry.isSymbolicLink())throw new Error(`Workspace snapshots reject symbolic links: ${entryRelative}`);if(entry.isDirectory())files.push(...await collectAuthoritativeFiles(root,entryRelative));else if(entry.isFile())files.push(entryRelative);else throw new Error(`Workspace snapshots reject unsupported filesystem entries: ${entryRelative}`)}return files}async function buildManifest(pmRoot){const files=await collectAuthoritativeFiles(pmRoot),hash=crypto.createHash("sha256"),contents=[];let bytes=0;for(const file of files){const content=await readFile(path.join(pmRoot,file));contents.push(content),bytes+=content.byteLength,hash.update(file),hash.update("\0"),hash.update(String(content.byteLength)),hash.update("\0"),hash.update(content)}const fingerprint=hash.digest("hex");return{manifest:{schema:SNAPSHOT_SCHEMA,fingerprint,files,bytes},contents}}function lineCount(content){return content===void 0||content.byteLength===0?0:content.toString("utf8").split(`
1
+ (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="66cb8c82-3760-5628-a1de-69e417cf0292")}catch{}})();import crypto from"node:crypto";import{cp,lstat,mkdir,readFile,readdir,rename,rm,writeFile}from"node:fs/promises";import path from"node:path";import{isFileMissingError,writeFileAtomic}from"../core/fs/fs-utils.js";import{appendWorkspaceAuditEvent}from"../core/history/workspace-history.js";import{acquireLockLease,waitForStaleCleanupGate}from"../core/lock/lock.js";import{getLockPath}from"../core/store/paths.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";import{withHostEnvironmentBoundary}from"./environment/host-environment-errors.js";const SNAPSHOT_SCHEMA="https://schema.unbrained.dev/pm/workspace-snapshot/v1",WORKSPACE_SNAPSHOT_ACTIONS=["create","list","inspect","restore","delete"],SNAPSHOT_RUNTIME_PATH=path.join("runtime","workspace-snapshots"),EXCLUDED_ROOT_NAMES=new Set(["checkpoints","locks","runtime","search","transactions"]),SNAPSHOT_TARGET_PATTERN=/^[a-z0-9][a-z0-9._-]*$/,WORKSPACE_WRITER_LOCK_ID="sdk-workspace-transaction",SNAPSHOT_HOST_FAULT_CODES={capacity:{code:"workspace_snapshot_storage_exhausted"},permission:{code:"workspace_snapshot_permission_denied"},resource:{code:"workspace_snapshot_resource_exhausted"}},DEFAULT_ATOMIC_OPERATIONS={renameEntry:rename,removeEntry:async target=>{await rm(target,{recursive:!0,force:!0})}};class WorkspaceLockHeartbeat{lockPath;owner;intervalMs;timer;refresh;failure;constructor(lockPath,owner,ttlSeconds){this.lockPath=lockPath,this.owner=owner,this.intervalMs=Math.max(10,Math.floor(ttlSeconds*1e3/3))}start(){this.scheduleRenewal(),this.timer=setInterval(()=>{this.scheduleRenewal()},this.intervalMs),this.timer.unref()}async refreshNow(){this.scheduleRenewal(),await this.refresh,this.assertHealthy()}assertHealthy(){if(this.failure!==void 0)throw new Error("Workspace snapshot restore lost its writer lock",{cause:this.failure})}async stop(){this.timer!==void 0&&clearInterval(this.timer),await this.refresh,this.assertHealthy()}scheduleRenewal(){this.refresh!==void 0||this.failure!==void 0||(this.refresh=this.renew().catch(error=>{this.failure=error}).finally(()=>{this.refresh=void 0}))}async renew(){const releaseGate=await waitForStaleCleanupGate(this.lockPath,WORKSPACE_WRITER_LOCK_ID);if(releaseGate===null)throw new Error("Workspace writer lock cleanup gate stayed held");try{const parsed=JSON.parse(await readFile(this.lockPath,"utf8"));if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))throw new Error("Workspace writer lock has invalid structure");const lock=parsed;if(lock.id!==WORKSPACE_WRITER_LOCK_ID||lock.owner!==this.owner||lock.pid!==process.pid)throw new Error("Workspace writer lock ownership changed");await writeFileAtomic(this.lockPath,`${JSON.stringify({...lock,created_at:new Date().toISOString()},null,2)}
2
+ `)}finally{await releaseGate()}}}const _testOnlyWorkspaceSnapshot={WorkspaceLockHeartbeat,withSnapshotFilesystemGuard};async function collectAuthoritativeFiles(root,relative=""){const entries=await readdir(path.join(root,relative),{withFileTypes:!0}),files=[];for(const entry of entries.sort((left,right)=>left.name.localeCompare(right.name))){if(relative.length===0&&EXCLUDED_ROOT_NAMES.has(entry.name))continue;const entryRelative=path.posix.join(relative.replaceAll(path.sep,"/"),entry.name);if(entry.isSymbolicLink())throw new Error(`Workspace snapshots reject symbolic links: ${entryRelative}`);if(entry.isDirectory())files.push(...await collectAuthoritativeFiles(root,entryRelative));else if(entry.isFile())files.push(entryRelative);else throw new Error(`Workspace snapshots reject unsupported filesystem entries: ${entryRelative}`)}return files}async function buildManifest(pmRoot){const files=await collectAuthoritativeFiles(pmRoot),hash=crypto.createHash("sha256"),contents=[];let bytes=0;for(const file of files){const content=await readFile(path.join(pmRoot,file));contents.push(content),bytes+=content.byteLength,hash.update(file),hash.update("\0"),hash.update(String(content.byteLength)),hash.update("\0"),hash.update(content)}const fingerprint=hash.digest("hex");return{manifest:{schema:SNAPSHOT_SCHEMA,fingerprint,files,bytes},contents}}function lineCount(content){return content===void 0||content.byteLength===0?0:content.toString("utf8").split(`
3
3
  `).filter(line=>line.trim().length>0).length}async function snapshotContents(root,files){const contents=new Map;for(let offset=0;offset<files.length;offset+=32){const batch=files.slice(offset,offset+32),entries=await Promise.all(batch.map(async file=>[file,await readFile(path.join(root,file))]));for(const[file,content]of entries)contents.set(file,content)}return contents}function validateSnapshotTarget(target){if(!SNAPSHOT_TARGET_PATTERN.test(target))throw new PmCliError("Snapshot names and fingerprints must use lowercase letters, digits, dots, underscores, or hyphens",EXIT_CODE.USAGE,{code:"invalid_workspace_snapshot_target",required:"Provide a non-empty lowercase snapshot name or 64-character hexadecimal fingerprint.",why:"Snapshot targets are used as portable reference filenames and content identities.",examples:["pm workspace snapshot list --json","pm workspace snapshot inspect baseline --json"],nextSteps:["List available snapshots, then retry with a returned name or fingerprint."],recovery:{suggested_retry:"pm workspace snapshot list --json"}})}async function withSnapshotFilesystemGuard(operation,run){return withHostEnvironmentBoundary(`workspace_snapshot_${operation}`,run,{codes:{capacity:SNAPSHOT_HOST_FAULT_CODES.capacity.code,permission:SNAPSHOT_HOST_FAULT_CODES.permission.code,resource:SNAPSHOT_HOST_FAULT_CODES.resource.code},why:"Snapshot create and restore copy authoritative tracker state before activation, so the stage needs capacity and write access up front.",nextSteps:["Reclaim rebuildable tracker cache space with pm gc, then retry."],suggestedRetry:"pm gc --json"})}function snapshotStore(pmRoot){return path.join(pmRoot,SNAPSHOT_RUNTIME_PATH)}function workspaceSnapshotNotFound(target){return new PmCliError(`Unknown workspace snapshot: ${target}`,EXIT_CODE.NOT_FOUND,{code:"workspace_snapshot_not_found",required:"Use a snapshot name or fingerprint returned by snapshot list.",why:"The requested reference or immutable snapshot object does not exist.",examples:["pm workspace snapshot list --json"],nextSteps:["List available snapshots and retry with an exact target."],recovery:{suggested_retry:"pm workspace snapshot list --json"}})}async function readSnapshotJson(file,target){try{return JSON.parse(await readFile(file,"utf8"))}catch(error){throw isFileMissingError(error)?workspaceSnapshotNotFound(target):error}}async function removeSnapshotEntry(entry,target,recursive){try{await rm(entry,{recursive})}catch(error){throw isFileMissingError(error)?workspaceSnapshotNotFound(target):error}}async function publishWorkspaceSnapshotObject(temporaryRoot,objectRoot,operations=DEFAULT_ATOMIC_OPERATIONS){try{return await operations.renameEntry(temporaryRoot,objectRoot),!1}catch(error){if(!(error instanceof Error&&"code"in error&&(error.code==="EEXIST"||error.code==="ENOTEMPTY")))throw error;return await operations.removeEntry(temporaryRoot),!0}}async function swapWorkspaceSnapshotRootWithActivation(staging,pmRoot,backup,operations,afterActivation){await operations.renameEntry(pmRoot,backup);try{await operations.renameEntry(staging,pmRoot),await afterActivation?.()}catch(error){throw await operations.renameEntry(backup,pmRoot),await operations.removeEntry(staging),error}await operations.removeEntry(backup)}async function swapWorkspaceSnapshotRoot(staging,pmRoot,backup,operations=DEFAULT_ATOMIC_OPERATIONS){await swapWorkspaceSnapshotRootWithActivation(staging,pmRoot,backup,operations)}async function resolveSnapshotFingerprint(pmRoot,target){return validateSnapshotTarget(target),/^[a-f0-9]{64}$/.test(target)?target:(await readSnapshotJson(path.join(snapshotStore(pmRoot),"refs",`${target}.json`),target)).fingerprint}async function createWorkspaceSnapshot(pmRoot,options={}){if(options.name!==void 0&&(validateSnapshotTarget(options.name),/^[a-f0-9]{64}$/.test(options.name)))throw new PmCliError("Snapshot names must not be 64-character lowercase hexadecimal fingerprints",EXIT_CODE.USAGE,{code:"workspace_snapshot_name_reserved_fingerprint",required:"Choose a human-readable reference name that cannot be mistaken for a content fingerprint.",why:"Exact 64-character hexadecimal values address immutable objects.",examples:["pm workspace snapshot create baseline --json"],nextSteps:["Retry with a shorter descriptive snapshot name."]});const{manifest,contents}=await buildManifest(pmRoot),store=snapshotStore(pmRoot),objectRoot=path.join(store,"objects",manifest.fingerprint);let deduplicated=!1;try{deduplicated=(await lstat(objectRoot)).isDirectory()}catch(error){if(!(error instanceof Error&&"code"in error&&isFileMissingError(error)))throw error}if(!deduplicated){const temporaryRoot=path.join(store,"objects",`.create-${process.pid}-${crypto.randomUUID()}`);await withSnapshotFilesystemGuard("create_object",async()=>{await mkdir(path.join(temporaryRoot,"files"),{recursive:!0});for(const[index,file]of manifest.files.entries()){const target=path.join(temporaryRoot,"files",file);await mkdir(path.dirname(target),{recursive:!0}),await writeFile(target,contents[index])}await writeFile(path.join(temporaryRoot,"manifest.json"),`${JSON.stringify(manifest,null,2)}
4
4
  `,"utf8"),await mkdir(path.dirname(objectRoot),{recursive:!0})}),deduplicated=await publishWorkspaceSnapshotObject(temporaryRoot,objectRoot)}if(options.name!==void 0){const reference={name:options.name,fingerprint:manifest.fingerprint},refsRoot=path.join(store,"refs");await mkdir(refsRoot,{recursive:!0});const temporaryRef=path.join(refsRoot,`.ref-${process.pid}-${crypto.randomUUID()}`);await writeFile(temporaryRef,`${JSON.stringify(reference,null,2)}
5
- `,"utf8"),await rename(temporaryRef,path.join(refsRoot,`${options.name}.json`))}return{manifest,...options.name===void 0?{}:{name:options.name},deduplicated}}async function inspectWorkspaceSnapshot(pmRoot,target){const fingerprint=await resolveSnapshotFingerprint(pmRoot,target),manifest=await readSnapshotJson(path.join(snapshotStore(pmRoot),"objects",fingerprint,"manifest.json"),target);if(manifest.schema!==SNAPSHOT_SCHEMA||manifest.fingerprint!==fingerprint)throw new PmCliError(`Snapshot manifest identity mismatch: ${target}`,EXIT_CODE.CONFLICT,{code:"workspace_snapshot_manifest_mismatch",required:"Use an intact snapshot whose manifest fingerprint matches its object path.",why:"Content identity must be verified before snapshot data is trusted.",examples:["pm workspace snapshot list --json"],nextSteps:["Inspect or recreate the snapshot before restoring it."]});return manifest}async function listWorkspaceSnapshots(pmRoot){const store=snapshotStore(pmRoot),objectNames=await readdir(path.join(store,"objects")).catch(error=>{if(error instanceof Error&&"code"in error&&isFileMissingError(error))return[];throw error}),referenceNames=await readdir(path.join(store,"refs")).catch(error=>{if(error instanceof Error&&"code"in error&&isFileMissingError(error))return[];throw error});return{objects:await Promise.all(objectNames.filter(name=>/^[a-f0-9]{64}$/.test(name)).sort().map(name=>inspectWorkspaceSnapshot(pmRoot,name))),references:await Promise.all(referenceNames.filter(name=>name.endsWith(".json")).sort().map(async name=>JSON.parse(await readFile(path.join(store,"refs",name),"utf8"))))}}async function planWorkspaceSnapshotRestore(pmRoot,target){const targetManifest=await inspectWorkspaceSnapshot(pmRoot,target),{manifest:currentManifest,contents:currentContentList}=await buildManifest(pmRoot),currentContents=new Map(currentManifest.files.map((file,index)=>[file,currentContentList[index]])),targetContents=await snapshotContents(path.join(snapshotStore(pmRoot),"objects",targetManifest.fingerprint,"files"),targetManifest.files),currentFiles=new Set(currentManifest.files),targetFiles=new Set(targetManifest.files),changedFiles=new Set([...currentFiles,...targetFiles]);for(const file of changedFiles){const currentContent=currentContents.get(file),targetContent=targetContents.get(file);currentContent!==void 0&&targetContent!==void 0&&currentContent.equals(targetContent)&&changedFiles.delete(file)}const affectedHistoryStreams=currentManifest.files.filter(file=>file.startsWith("history/")&&file.endsWith(".jsonl")).map(file=>{const currentEntries=lineCount(currentContents.get(file)),targetEntries=lineCount(targetContents.get(file));return{path:file,current_entries:currentEntries,target_entries:targetEntries,removed_entries:Math.max(0,currentEntries-targetEntries)}}).filter(entry=>entry.removed_entries>0).sort((left,right)=>left.path.localeCompare(right.path));return{target_fingerprint:targetManifest.fingerprint,current_fingerprint:currentManifest.fingerprint,changed_file_count:changedFiles.size,removed_file_count:[...currentFiles].filter(file=>!targetFiles.has(file)).length,added_file_count:[...targetFiles].filter(file=>!currentFiles.has(file)).length,removed_item_count:[...currentFiles].filter(file=>file.endsWith(".toon")&&!targetFiles.has(file)).length,affected_history_stream_count:affectedHistoryStreams.length,removed_history_entry_count:affectedHistoryStreams.reduce((total,entry)=>total+entry.removed_entries,0),affected_history_streams:affectedHistoryStreams,changes_authoritative_state:currentManifest.fingerprint!==targetManifest.fingerprint}}async function restoreWorkspaceSnapshotWithRecovery(pmRoot,target,options={}){if(options.force!==!0)throw new PmCliError("Workspace snapshot restore requires explicit force confirmation; inspect the impact with planWorkspaceSnapshotRestore or pm workspace snapshot restore <target> --dry-run, then retry with force",EXIT_CODE.USAGE,{code:"workspace_snapshot_force_required",required:"Preview the destructive impact, then explicitly confirm the restore.",why:"A restore replaces the complete authoritative tracker state.",examples:[`pm workspace snapshot restore ${target} --dry-run --json`,`pm workspace snapshot restore ${target} --force --json`],nextSteps:["Review the dry-run counts before retrying with --force."],recovery:{suggested_retry:`pm workspace snapshot restore ${target} --dry-run --json`}});const author=options.author?.trim()||"pm-sdk",lockTtlSeconds=options.lockTtlSeconds??60,lockWaitMs=options.lockWaitMs??5e3,releaseWorkspaceLock=await acquireLock(pmRoot,WORKSPACE_WRITER_LOCK_ID,lockTtlSeconds,author,!1,!1,lockWaitMs),heartbeat=new WorkspaceLockHeartbeat(getLockPath(pmRoot,WORKSPACE_WRITER_LOCK_ID),author,lockTtlSeconds);heartbeat.start(),await heartbeat.refreshNow();let staging,swapStarted=!1,heartbeatStopped=!1;try{const plan=await planWorkspaceSnapshotRestore(pmRoot,target),manifest=await inspectWorkspaceSnapshot(pmRoot,target),recovery=await createWorkspaceSnapshot(pmRoot),fingerprint=manifest.fingerprint,store=snapshotStore(pmRoot),source=path.join(store,"objects",fingerprint,"files"),parent=path.dirname(pmRoot),base=path.basename(pmRoot);staging=path.join(parent,`.${base}.restore-${crypto.randomUUID()}`);const backup=path.join(parent,`.${base}.backup-${crypto.randomUUID()}`);await withSnapshotFilesystemGuard("restore_stage",async()=>{await mkdir(staging,{recursive:!0}),await cp(source,staging,{recursive:!0,force:!1}),await mkdir(path.join(staging,"runtime"),{recursive:!0}),await cp(store,path.join(staging,SNAPSHOT_RUNTIME_PATH),{recursive:!0,force:!1})});const audit=await appendWorkspaceAuditEvent({pmRoot:staging,op:"workspace_snapshot_restore",author,message:options.message??`Restore workspace snapshot ${fingerprint}; recovery snapshot ${recovery.manifest.fingerprint}`,context:{target_fingerprint:fingerprint,pre_restore_fingerprint:recovery.manifest.fingerprint,changed_file_count:plan.changed_file_count,removed_file_count:plan.removed_file_count,removed_item_count:plan.removed_item_count,affected_history_stream_count:plan.affected_history_stream_count,removed_history_entry_count:plan.removed_history_entry_count},lockTtlSeconds,lockWaitMs}),auditHistoryPath=path.relative(staging,audit.historyPath).replaceAll("\\","/");await heartbeat.stop(),heartbeatStopped=!0;const stagedWorkspaceLock=getLockPath(staging,WORKSPACE_WRITER_LOCK_ID);return await mkdir(path.dirname(stagedWorkspaceLock),{recursive:!0}),await writeFile(stagedWorkspaceLock,await readFile(getLockPath(pmRoot,WORKSPACE_WRITER_LOCK_ID)),{flag:"wx"}),swapStarted=!0,await swapWorkspaceSnapshotRootWithActivation(staging,pmRoot,backup,DEFAULT_ATOMIC_OPERATIONS,async()=>{heartbeat.start(),heartbeatStopped=!1,await heartbeat.refreshNow()}),await heartbeat.stop(),heartbeatStopped=!0,{manifest,plan,recovery_fingerprint:recovery.manifest.fingerprint,audit_history_path:auditHistoryPath,audit_operation:"workspace_snapshot_restore"}}catch(error){throw staging!==void 0&&!swapStarted&&await Promise.allSettled([rm(staging,{recursive:!0,force:!0})]),error}finally{try{heartbeatStopped||await heartbeat.stop()}catch{}finally{await releaseWorkspaceLock()}}}async function restoreWorkspaceSnapshot(pmRoot,target){return(await restoreWorkspaceSnapshotWithRecovery(pmRoot,target,{force:!0,message:"Restore requested through the compatibility SDK signature"})).manifest}async function deleteWorkspaceSnapshot(pmRoot,target){validateSnapshotTarget(target);const store=snapshotStore(pmRoot);if(!/^[a-f0-9]{64}$/.test(target))return await removeSnapshotEntry(path.join(store,"refs",`${target}.json`),target,!1),{deleted:"reference",target};const{references}=await listWorkspaceSnapshots(pmRoot);if(references.some(reference=>reference.fingerprint===target))throw new Error(`Snapshot object ${target} is still referenced`);return await removeSnapshotEntry(path.join(store,"objects",target),target,!0),{deleted:"object",target}}export{SNAPSHOT_SCHEMA,WORKSPACE_SNAPSHOT_ACTIONS,_testOnlyWorkspaceSnapshot,createWorkspaceSnapshot,deleteWorkspaceSnapshot,inspectWorkspaceSnapshot,listWorkspaceSnapshots,planWorkspaceSnapshotRestore,publishWorkspaceSnapshotObject,restoreWorkspaceSnapshot,restoreWorkspaceSnapshotWithRecovery,swapWorkspaceSnapshotRoot};
5
+ `,"utf8"),await rename(temporaryRef,path.join(refsRoot,`${options.name}.json`))}return{manifest,...options.name===void 0?{}:{name:options.name},deduplicated}}async function inspectWorkspaceSnapshot(pmRoot,target){const fingerprint=await resolveSnapshotFingerprint(pmRoot,target),manifest=await readSnapshotJson(path.join(snapshotStore(pmRoot),"objects",fingerprint,"manifest.json"),target);if(manifest.schema!==SNAPSHOT_SCHEMA||manifest.fingerprint!==fingerprint)throw new PmCliError(`Snapshot manifest identity mismatch: ${target}`,EXIT_CODE.CONFLICT,{code:"workspace_snapshot_manifest_mismatch",required:"Use an intact snapshot whose manifest fingerprint matches its object path.",why:"Content identity must be verified before snapshot data is trusted.",examples:["pm workspace snapshot list --json"],nextSteps:["Inspect or recreate the snapshot before restoring it."]});return manifest}async function listWorkspaceSnapshots(pmRoot){const store=snapshotStore(pmRoot),objectNames=await readdir(path.join(store,"objects")).catch(error=>{if(error instanceof Error&&"code"in error&&isFileMissingError(error))return[];throw error}),referenceNames=await readdir(path.join(store,"refs")).catch(error=>{if(error instanceof Error&&"code"in error&&isFileMissingError(error))return[];throw error});return{objects:await Promise.all(objectNames.filter(name=>/^[a-f0-9]{64}$/.test(name)).sort().map(name=>inspectWorkspaceSnapshot(pmRoot,name))),references:await Promise.all(referenceNames.filter(name=>name.endsWith(".json")).sort().map(async name=>JSON.parse(await readFile(path.join(store,"refs",name),"utf8"))))}}async function planWorkspaceSnapshotRestore(pmRoot,target){const targetManifest=await inspectWorkspaceSnapshot(pmRoot,target),{manifest:currentManifest,contents:currentContentList}=await buildManifest(pmRoot),currentContents=new Map(currentManifest.files.map((file,index)=>[file,currentContentList[index]])),targetContents=await snapshotContents(path.join(snapshotStore(pmRoot),"objects",targetManifest.fingerprint,"files"),targetManifest.files),currentFiles=new Set(currentManifest.files),targetFiles=new Set(targetManifest.files),changedFiles=new Set([...currentFiles,...targetFiles]);for(const file of changedFiles){const currentContent=currentContents.get(file),targetContent=targetContents.get(file);currentContent!==void 0&&targetContent!==void 0&&currentContent.equals(targetContent)&&changedFiles.delete(file)}const affectedHistoryStreams=currentManifest.files.filter(file=>file.startsWith("history/")&&file.endsWith(".jsonl")).map(file=>{const currentEntries=lineCount(currentContents.get(file)),targetEntries=lineCount(targetContents.get(file));return{path:file,current_entries:currentEntries,target_entries:targetEntries,removed_entries:Math.max(0,currentEntries-targetEntries)}}).filter(entry=>entry.removed_entries>0).sort((left,right)=>left.path.localeCompare(right.path));return{target_fingerprint:targetManifest.fingerprint,current_fingerprint:currentManifest.fingerprint,changed_file_count:changedFiles.size,removed_file_count:[...currentFiles].filter(file=>!targetFiles.has(file)).length,added_file_count:[...targetFiles].filter(file=>!currentFiles.has(file)).length,removed_item_count:[...currentFiles].filter(file=>file.endsWith(".toon")&&!targetFiles.has(file)).length,affected_history_stream_count:affectedHistoryStreams.length,removed_history_entry_count:affectedHistoryStreams.reduce((total,entry)=>total+entry.removed_entries,0),affected_history_streams:affectedHistoryStreams,changes_authoritative_state:currentManifest.fingerprint!==targetManifest.fingerprint}}async function restoreWorkspaceSnapshotWithRecovery(pmRoot,target,options={}){if(options.force!==!0)throw new PmCliError("Workspace snapshot restore requires explicit force confirmation; inspect the impact with planWorkspaceSnapshotRestore or pm workspace snapshot restore <target> --dry-run, then retry with force",EXIT_CODE.USAGE,{code:"workspace_snapshot_force_required",required:"Preview the destructive impact, then explicitly confirm the restore.",why:"A restore replaces the complete authoritative tracker state.",examples:[`pm workspace snapshot restore ${target} --dry-run --json`,`pm workspace snapshot restore ${target} --force --json`],nextSteps:["Review the dry-run counts before retrying with --force."],recovery:{suggested_retry:`pm workspace snapshot restore ${target} --dry-run --json`}});const author=options.author?.trim()||"pm-sdk",lockTtlSeconds=options.lockTtlSeconds??60,lockWaitMs=options.lockWaitMs??5e3,workspaceLease=await acquireLockLease(pmRoot,WORKSPACE_WRITER_LOCK_ID,lockTtlSeconds,author,!1,!1,lockWaitMs),releaseWorkspaceLock=workspaceLease.release,heartbeat=new WorkspaceLockHeartbeat(getLockPath(pmRoot,WORKSPACE_WRITER_LOCK_ID),author,lockTtlSeconds);heartbeat.start(),await heartbeat.refreshNow();let staging,swapStarted=!1,heartbeatStopped=!1;try{const plan=await planWorkspaceSnapshotRestore(pmRoot,target),manifest=await inspectWorkspaceSnapshot(pmRoot,target),recovery=await createWorkspaceSnapshot(pmRoot),fingerprint=manifest.fingerprint,store=snapshotStore(pmRoot),source=path.join(store,"objects",fingerprint,"files"),parent=path.dirname(pmRoot),base=path.basename(pmRoot);staging=path.join(parent,`.${base}.restore-${crypto.randomUUID()}`);const backup=path.join(parent,`.${base}.backup-${crypto.randomUUID()}`);await withSnapshotFilesystemGuard("restore_stage",async()=>{await mkdir(staging,{recursive:!0}),await cp(source,staging,{recursive:!0,force:!1}),await mkdir(path.join(staging,"runtime"),{recursive:!0}),await cp(store,path.join(staging,SNAPSHOT_RUNTIME_PATH),{recursive:!0,force:!1})});const audit=await appendWorkspaceAuditEvent({pmRoot:staging,op:"workspace_snapshot_restore",author,message:options.message??`Restore workspace snapshot ${fingerprint}; recovery snapshot ${recovery.manifest.fingerprint}`,context:{target_fingerprint:fingerprint,pre_restore_fingerprint:recovery.manifest.fingerprint,changed_file_count:plan.changed_file_count,removed_file_count:plan.removed_file_count,removed_item_count:plan.removed_item_count,affected_history_stream_count:plan.affected_history_stream_count,removed_history_entry_count:plan.removed_history_entry_count},lockTtlSeconds,lockWaitMs}),auditHistoryPath=path.relative(staging,audit.historyPath).replaceAll("\\","/");await heartbeat.stop(),heartbeatStopped=!0;const stagedWorkspaceLock=getLockPath(staging,WORKSPACE_WRITER_LOCK_ID),stagedRoot=staging;return await workspaceLease.fence(async()=>{await mkdir(path.dirname(stagedWorkspaceLock),{recursive:!0}),await writeFile(stagedWorkspaceLock,await readFile(getLockPath(pmRoot,WORKSPACE_WRITER_LOCK_ID)),{flag:"wx"}),swapStarted=!0,await swapWorkspaceSnapshotRootWithActivation(stagedRoot,pmRoot,backup,DEFAULT_ATOMIC_OPERATIONS,async()=>{heartbeat.start(),heartbeatStopped=!1,await heartbeat.refreshNow()})}),await heartbeat.stop(),heartbeatStopped=!0,{manifest,plan,recovery_fingerprint:recovery.manifest.fingerprint,audit_history_path:auditHistoryPath,audit_operation:"workspace_snapshot_restore"}}catch(error){throw staging!==void 0&&!swapStarted&&await Promise.allSettled([rm(staging,{recursive:!0,force:!0})]),error}finally{try{heartbeatStopped||await heartbeat.stop()}catch{}finally{await releaseWorkspaceLock()}}}async function restoreWorkspaceSnapshot(pmRoot,target){return(await restoreWorkspaceSnapshotWithRecovery(pmRoot,target,{force:!0,message:"Restore requested through the compatibility SDK signature"})).manifest}async function deleteWorkspaceSnapshot(pmRoot,target){validateSnapshotTarget(target);const store=snapshotStore(pmRoot);if(!/^[a-f0-9]{64}$/.test(target))return await removeSnapshotEntry(path.join(store,"refs",`${target}.json`),target,!1),{deleted:"reference",target};const{references}=await listWorkspaceSnapshots(pmRoot);if(references.some(reference=>reference.fingerprint===target))throw new Error(`Snapshot object ${target} is still referenced`);return await removeSnapshotEntry(path.join(store,"objects",target),target,!0),{deleted:"object",target}}export{SNAPSHOT_SCHEMA,WORKSPACE_SNAPSHOT_ACTIONS,_testOnlyWorkspaceSnapshot,createWorkspaceSnapshot,deleteWorkspaceSnapshot,inspectWorkspaceSnapshot,listWorkspaceSnapshots,planWorkspaceSnapshotRestore,publishWorkspaceSnapshotObject,restoreWorkspaceSnapshot,restoreWorkspaceSnapshotWithRecovery,swapWorkspaceSnapshotRoot};
6
6
  //# sourceMappingURL=workspace-snapshot.js.map
@@ -28,10 +28,12 @@ export declare function isPathOutsideRoot(relativePath:string):boolean;
28
28
  /** Return the canonical pm ignore block rendered for the given workspace-relative tracker root (defaults to `.agents/pm`), primarily for documentation and tests. */
29
29
  export declare function getPmGitignoreBlock(trackerRelativeRoot?:string):string;
30
30
  /**
31
- * Idempotently create or replace the fenced pm cache block in a workspace
31
+ * Idempotently reconcile the fenced pm cache rules in a workspace
32
32
  * `.gitignore`. When `pmRoot` is provided the ignored runtime directories are
33
33
  * rendered under the resolved workspace-relative tracker root, so custom-root
34
34
  * workspaces (`--pm-path`/`PM_PATH`) stop committing runtime caches (GH-598).
35
+ * Sibling tracker rules are retained, and an explicitly root-layout tracker
36
+ * uses anchored repository-root rules rather than the default tracker path.
35
37
  * A tracker root outside the workspace needs no ignore rules here, so the
36
38
  * file is left untouched.
37
39
  */
@@ -1,6 +1,9 @@
1
- (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1de99e25-e132-5071-833e-37361b4cc7e6")}catch{}})();import{isFileMissingError}from"../core/fs/fs-utils.js";import{readFile,writeFile}from"node:fs/promises";import path from"node:path";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";const PM_GITIGNORE_START="# pm-cli:runtime-cache:start",PM_GITIGNORE_END="# pm-cli:runtime-cache:end",PM_GITIGNORE_DEFAULT_TRACKER_ROOT=".agents/pm",PM_GITIGNORE_RUNTIME_DIRECTORIES=["runtime/","search/","locks/","transactions/","checkpoints/"],PM_GITIGNORE_TRACKED_FILES=["search/eval-queries.json"];async function withGitignorePermissionRecovery(operation){try{return await operation()}catch(error){throw error instanceof Error&&"code"in error&&typeof error.code=="string"&&["EACCES","EPERM","EROFS"].includes(error.code)?new PmCliError("The workspace .gitignore is not writable.",EXIT_CODE.GENERIC_FAILURE,{code:"init_gitignore_unwritable",reason:error.code.toLowerCase(),required:"Grant the current user read and write access to the workspace .gitignore before initialization.",why:"pm init must publish its managed runtime-cache ignore fence without replacing unrelated entries.",nextSteps:["Grant read and write access to the workspace .gitignore and rerun pm init.","If the workspace is intentionally read-only, initialize pm in a writable workspace or clone."]}):error}}function normalizeTrackerRelativeRoot(trackerRelativeRoot){return trackerRelativeRoot.replaceAll("\\","/").replace(/^\.\/+/,"").replace(/\/+$/,"")}function isPathOutsideRoot(relativePath){const normalized=relativePath.replaceAll("\\","/");return path.isAbsolute(relativePath)||normalized===".."||normalized.startsWith("../")}function getPmGitignoreBlock(trackerRelativeRoot=PM_GITIGNORE_DEFAULT_TRACKER_ROOT){const root=normalizeTrackerRelativeRoot(trackerRelativeRoot);return[PM_GITIGNORE_START,...PM_GITIGNORE_RUNTIME_DIRECTORIES.map(directory=>directory==="search/"?`${root}/${directory}*`:`${root}/${directory}`),...PM_GITIGNORE_TRACKED_FILES.map(file=>`!${root}/${file}`),PM_GITIGNORE_END].join(`
2
- `)}async function ensurePmGitignore(workspaceRoot,options={}){const gitignorePath=path.join(workspaceRoot,".gitignore");let trackerRelativeRoot=PM_GITIGNORE_DEFAULT_TRACKER_ROOT;if(options.pmRoot!==void 0){const relative=normalizeTrackerRelativeRoot(path.relative(path.resolve(workspaceRoot),path.resolve(options.pmRoot)));if(isPathOutsideRoot(relative))return{path:gitignorePath,changed:!1};relative.length>0&&(trackerRelativeRoot=relative)}let current="";try{current=await withGitignorePermissionRecovery(()=>readFile(gitignorePath,"utf8"))}catch(error){if(!(error instanceof Error&&"code"in error&&isFileMissingError(error)))throw error}const start=current.indexOf(PM_GITIGNORE_START),end=current.indexOf(PM_GITIGNORE_END),prefix=(start>=0&&end>=start?`${current.slice(0,start)}${current.slice(end+PM_GITIGNORE_END.length)}`:current).trimEnd(),next=`${prefix.length>0?`${prefix}
3
-
4
- `:""}${getPmGitignoreBlock(trackerRelativeRoot)}
5
- `;return next===current?{path:gitignorePath,changed:!1}:(await withGitignorePermissionRecovery(()=>writeFile(gitignorePath,next,"utf8")),{path:gitignorePath,changed:!0})}export{PM_GITIGNORE_DEFAULT_TRACKER_ROOT,PM_GITIGNORE_END,PM_GITIGNORE_RUNTIME_DIRECTORIES,PM_GITIGNORE_START,PM_GITIGNORE_TRACKED_FILES,ensurePmGitignore,getPmGitignoreBlock,isPathOutsideRoot};
1
+ (function(){try{var e=typeof window<"u"?window:typeof global<"u"?global:typeof globalThis<"u"?globalThis:typeof self<"u"?self:{},n=new e.Error().stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="38df9536-d87a-52ac-958d-091a17c9da9c")}catch{}})();import{isFileMissingError}from"../core/fs/fs-utils.js";import{readFile,realpath,stat,writeFile}from"node:fs/promises";import path from"node:path";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";const PM_GITIGNORE_START="# pm-cli:runtime-cache:start",PM_GITIGNORE_END="# pm-cli:runtime-cache:end",PM_GITIGNORE_DEFAULT_TRACKER_ROOT=".agents/pm";async function physicalInitPath(input){let ancestor=path.resolve(input);const missing=[];for(;;)try{const physical=await realpath(ancestor);if(!(await stat(physical)).isDirectory())throw Object.assign(new Error("Initialization path ancestry must be a directory."),{code:"ENOTDIR"});return path.join(physical,...missing)}catch(error){if(!isFileMissingError(error)||path.dirname(ancestor)===ancestor)throw error;missing.unshift(path.basename(ancestor)),ancestor=path.dirname(ancestor)}}const PM_GITIGNORE_RUNTIME_DIRECTORIES=["runtime/","search/","locks/","transactions/","checkpoints/"],PM_GITIGNORE_TRACKED_FILES=["search/eval-queries.json"];async function withGitignorePermissionRecovery(operation){try{return await operation()}catch(error){throw error instanceof Error&&"code"in error&&typeof error.code=="string"&&["EACCES","EPERM","EROFS"].includes(error.code)?new PmCliError("The workspace .gitignore is not writable.",EXIT_CODE.GENERIC_FAILURE,{code:"init_gitignore_unwritable",reason:error.code.toLowerCase(),required:"Grant the current user read and write access to the workspace .gitignore before initialization.",why:"pm init must publish its managed runtime-cache ignore fence without replacing unrelated entries.",nextSteps:["Grant read and write access to the workspace .gitignore and rerun pm init.","If the workspace is intentionally read-only, initialize pm in a writable workspace or clone."]}):error}}function normalizeTrackerRelativeRoot(trackerRelativeRoot){const root=trackerRelativeRoot.split(path.sep).join("/").replace(/^\.\/+/,"");let end=root.length;for(;end>0&&root[end-1]==="/";)end-=1;return root.slice(0,end)}function isPathOutsideRoot(relativePath){const normalized=relativePath.replaceAll("\\","/");return path.isAbsolute(relativePath)||normalized===".."||normalized.startsWith("../")}function getPmGitignoreBlock(trackerRelativeRoot=PM_GITIGNORE_DEFAULT_TRACKER_ROOT){const root=normalizeTrackerRelativeRoot(trackerRelativeRoot);if(/[\r\n]/u.test(root))throw new PmCliError("A tracker path containing a line break cannot be represented safely in .gitignore.",EXIT_CODE.USAGE,{code:"invalid_argument_value",required:"a tracker path without line breaks",nextSteps:["Choose a tracker path without carriage returns or newlines and rerun pm init."]});const literalRoot=root.replace(/[\\*?[\]#! \t]/gu,"\\$&");return[PM_GITIGNORE_START,...PM_GITIGNORE_RUNTIME_DIRECTORIES.map(directory=>directory==="search/"?`${literalRoot}/${directory}*`:`${literalRoot}/${directory}`),...PM_GITIGNORE_TRACKED_FILES.map(file=>`!${literalRoot}/${file}`),PM_GITIGNORE_END].join(`
2
+ `)}function mergeTrackerIgnoreRules(lines,trackerRoot,rules){const literalRoot=trackerRoot.replace(/[\\*?[\]#! \t]/gu,"\\$&"),prefixes=[...new Set([trackerRoot,literalRoot])].flatMap(root=>PM_GITIGNORE_RUNTIME_DIRECTORIES.flatMap(directory=>[`${root}/${directory}`,`!${root}/${directory}`])),merged=[];let replaced=!1;for(const line of lines)prefixes.some(prefix=>line.startsWith(prefix))?(replaced||merged.push(...rules),replaced=!0):merged.push(line);return replaced||merged.push(...rules),merged}function managedGitignoreBlocks(current){const blocks=[];let start=-1,bodyStart=0,offset=0;for(const line of current.split(`
3
+ `)){const marker=line.endsWith("\r")?line.slice(0,-1):line;marker===PM_GITIGNORE_START?(start=offset,bodyStart=offset+line.length+1):marker===PM_GITIGNORE_END&&start>=0&&(blocks.push({index:start,end:offset+marker.length,lines:current.slice(bodyStart,offset).split(/\r?\n/u).filter(entry=>entry.length>0)}),start=-1),offset+=line.length+1}return blocks}function reconcileGitignoreContent(current,trackerRoot){const newline=current.includes(`\r
4
+ `)?`\r
5
+ `:`
6
+ `,rules=getPmGitignoreBlock(trackerRoot).split(`
7
+ `).slice(1,-1),blocks=managedGitignoreBlocks(current),existingRules=blocks.flatMap(entry=>entry.lines),block=[PM_GITIGNORE_START,...mergeTrackerIgnoreRules(existingRules,trackerRoot,rules),PM_GITIGNORE_END].join(newline);if(blocks.length===0){const separator=current.length===0?"":current.endsWith(`
8
+ `)?newline:`${newline}${newline}`;return`${current}${separator}${block}${newline}`}let next=current;for(const[index,match]of[...blocks.entries()].reverse())next=`${next.slice(0,match.index)}${index===0?block:""}${next.slice(match.end)}`;return next}async function ensurePmGitignore(workspaceRoot,options={}){const gitignorePath=path.join(workspaceRoot,".gitignore");let trackerRelativeRoot=PM_GITIGNORE_DEFAULT_TRACKER_ROOT;if(options.pmRoot!==void 0){const relative=normalizeTrackerRelativeRoot(path.relative(await physicalInitPath(workspaceRoot),await physicalInitPath(options.pmRoot)));if(isPathOutsideRoot(relative))return{path:gitignorePath,changed:!1};trackerRelativeRoot=relative}let current="";try{current=await withGitignorePermissionRecovery(()=>readFile(gitignorePath,"utf8"))}catch(error){if(!(error instanceof Error&&"code"in error&&isFileMissingError(error)))throw error}const next=reconcileGitignoreContent(current,trackerRelativeRoot);return next===current?{path:gitignorePath,changed:!1}:(await withGitignorePermissionRecovery(()=>writeFile(gitignorePath,next,"utf8")),{path:gitignorePath,changed:!0})}export{PM_GITIGNORE_DEFAULT_TRACKER_ROOT,PM_GITIGNORE_END,PM_GITIGNORE_RUNTIME_DIRECTORIES,PM_GITIGNORE_START,PM_GITIGNORE_TRACKED_FILES,ensurePmGitignore,getPmGitignoreBlock,isPathOutsideRoot};
6
9
  //# sourceMappingURL=workspace.js.map
@@ -82,7 +82,7 @@ If the existing project `settings.json` has invalid JSON, a configuration write
82
82
  | `item_format` | item storage format (`toon` writes; legacy markdown is read/migrate only) |
83
83
  | `output.default_format` | default renderer, usually `toon` |
84
84
  | `locks.ttl_seconds` | stale lock threshold |
85
- | `locks.wait_ms` | bounded jittered wait budget before a contended item mutation returns `lock_conflict` (default `3000`; set `0` to fail fast) |
85
+ | `locks.wait_ms` | wait budget before a contended lock returns `lock_conflict`; item-store mutations, restore and the hierarchy lock restart it on each owner handoff (120 s cap), other locks use it as a fixed total (default `3000`; `0` fails fast) |
86
86
  | `history.missing_stream` | `auto_create` or `strict_error` |
87
87
  | `history.compact_policy.enabled` | enable the compaction advisory: `pm health` warns on over-threshold streams (default `false`); when disabled, `max_entries`/`trigger` are inert |
88
88
  | `history.compact_policy.max_entries` | when the policy is enabled, the entry count above which a stream is flagged by `pm health` and the default `pm history-compact --all-over` threshold (default `500`) |
@@ -115,7 +115,18 @@ receipt before removal; it records the original owner and is an attempted
115
115
  recovery audit, not proof that a later mutation committed. Keep these local
116
116
  runtime receipts out of version control.
117
117
 
118
- For expected live contention, set `PM_LOCK_WAIT_MS` to a larger bounded
118
+ For item-store mutations (update, close, claim, comments, notes and the other
119
+ item-store writes), restore and the hierarchy-integrity lock (parented creates,
120
+ parent changes and integrity-sensitive dependency additions) the wait budget
121
+ measures stalls, not queue length: while other writers keep acquiring and releasing the
122
+ lock, a waiter keeps waiting (up to 120 s total), so many concurrent agents on
123
+ one tracker queue instead of failing. Other lock users, including package import
124
+ commits and internal short-lived locks, keep a fixed total budget. A lock held by
125
+ the same owner for the whole budget still refuses with `lock_conflict`, whose
126
+ recovery reports `waited_ms` and `owner_handoffs`. A writer whose lease expired
127
+ and was taken over refuses its commit with `lock_lost` instead of overwriting
128
+ the newer write; re-read and apply the change again. For an owner that is
129
+ expected to hold the lock longer, set `PM_LOCK_WAIT_MS` to a larger bounded
119
130
  millisecond budget, or configure `locks.wait_ms`, and retry the same operation.
120
131
  CLI recovery preserves its normalized arguments and quoted values, including
121
132
  whitespace and empty argument strings. Malformed argument vectors are omitted
@@ -167,7 +178,7 @@ underscore aliases remain accepted for compatibility.
167
178
  | `PM_AUTHOR` | optional explicit session-wide mutation author override |
168
179
  | `PM_PATH` | override project tracker root for tests or sandboxes |
169
180
  | `PM_GLOBAL_PATH` | override global profile root for tests or sandboxes |
170
- | `PM_LOCK_WAIT_MS` | per-process override for `locks.wait_ms`; non-negative integer milliseconds, with `0` restoring fail-fast lock conflicts |
181
+ | `PM_LOCK_WAIT_MS` | per-process override for the `locks.wait_ms` budget (a no-progress window only where that lock uses the progress policy); non-negative integer milliseconds, with `0` restoring fail-fast lock conflicts |
171
182
  | `PM_OLLAMA_MODEL` | choose default Ollama embedding model |
172
183
  | `PM_DISABLE_OLLAMA_AUTO_DEFAULTS` | disable implicit Ollama search defaults |
173
184
  | `PM_CONTEXT_USAGE_DISABLED` | set to `1` to disable the derived context/next usage-feedback ledger with zero filesystem reads or writes |
@@ -177,6 +188,20 @@ For command-level tracker targeting, prefer `--pm-path <tracker-root>`. The old
177
188
  itself, not the repository workspace. During `pm init`, explicit tracker paths
178
189
  that look like workspace roots are rejected unless `--force` is provided.
179
190
 
191
+ Initialization maintains the repository's runtime-cache `.gitignore` fence for
192
+ every tracker sharing that repository. Initializing or reinitializing a nested
193
+ tracker preserves the enclosing tracker's exclusions and unrelated user rules.
194
+ Equivalent physical and symlink-addressed paths share the same containment
195
+ check, including tracker directories that initialization has not created yet.
196
+ A symlink leading outside the repository remains an external tracker and does
197
+ not add rules here. This boundary is tracked by
198
+ [pm-p8s5k7e6lhda](../.agents/pm/issues/pm-p8s5k7e6lhda.toon).
199
+ Custom tracker paths are literal Git paths, including spaces and wildcard
200
+ characters. An intentional root-layout tracker uses repository-root exclusions.
201
+ Paths containing line breaks are refused because `.gitignore` cannot safely
202
+ represent them. Curated `search/eval-queries.json` files remain trackable in
203
+ each tracker. See [merge safety](MERGE_SAFETY.md#runtime-receipts-and-retention).
204
+
180
205
  Implicit discovery checks the current directory and its ancestors up to the
181
206
  nearest `.git` directory or file, including worktrees and submodules. An
182
207
  uninitialized repository selects its own `.agents/pm` and returns initialization
@@ -36,6 +36,11 @@ immediately before SDK assurance evaluation, which verifies advisory evidence
36
36
  before slower providers. The blocking registry command settles before OSV starts
37
37
  its network deadlines.
38
38
 
39
+ CI retains this single receipt after successful and failed static jobs, with a
40
+ separate artifact name for each run attempt and seven-day retention. Inspect both
41
+ feeds before diagnosing a blocked collector: a positive advisory is a refusal,
42
+ even when the other feed is clean.
43
+
39
44
  The command then verifies the installed CodSpeed bundle policy. Network/audit errors,
40
45
  missing packages, incomplete policy entries, changed package files, and unreviewed
41
46
  upgrades all fail. `quality:static` includes this command, so the existing CI,
@@ -50,6 +55,31 @@ defaults to the `latest` image. The workflow contract checks both values on
50
55
  every TruffleHog step. Recheck the action's executable selectors when renewing
51
56
  its commit pin.
52
57
 
58
+ ## Vitest mocker advisory pair
59
+
60
+ [pm-vitest4112](../.agents/pm/tasks/pm-vitest4112.toon) coordinates the
61
+ [manual-registration finding](../.agents/pm/issues/pm-vitestmanual.toon) and
62
+ [Windows redirect finding](../.agents/pm/issues/pm-vitestredirect.toon).
63
+ [GHSA-qxg6-89mg-rqv8](https://github.com/advisories/GHSA-qxg6-89mg-rqv8)
64
+ and [GHSA-xp26-3454-mjgp](https://github.com/advisories/GHSA-xp26-3454-mjgp)
65
+ affect the previously locked `@vitest/mocker` 4.1.11. The compatible Vitest and
66
+ coverage-provider pair now requires 4.1.12, whose shared mock-source generator
67
+ serializes module URLs and export names, including asynchronous rebinding, and
68
+ whose public redirect handler normalizes backslashes before the file allowlist
69
+ check. Both fixes apply to public mocker/interceptor WebSocket handlers. The
70
+ repository's Node test configurations do not register these handlers, so the
71
+ affected development dependency does not establish an exploitable CLI or SDK
72
+ route.
73
+
74
+ The retained hosted OSV receipt identified both advisories while the registry
75
+ audit returned zero. Keep full-lock admission mandatory and verify the patched
76
+ graph against both feeds. This patch keeps Vitest 4, the CodSpeed peer contract
77
+ and the Vite 7 override; the broader Vitest 5 migration remains separate. The
78
+ installed-handler regressions run in coverage and the required native Windows
79
+ cohort, whose aggregate refuses failed, canceled or skipped partitions. The
80
+ October 15 patch releases meet pnpm's one-day minimum age on October 21 without
81
+ an exclusion. Dependabot's seven-day scheduling cooldown is unchanged.
82
+
53
83
  ## Indexed source-map offsets
54
84
 
55
85
  [pm-dba47](../.agents/pm/issues/pm-dba47.toon) tracks Dependabot alert 47 and
@@ -76,8 +76,9 @@ Create path:
76
76
  `mutateItem` path:
77
77
 
78
78
  - Used by `pm update`, `pm close`, `pm claim`, `pm release`, `pm restore`, `pm delete`, and other mutations of existing items.
79
- - Locates the item, acquires the lock, reads the item through the `onRead` hook, enforces ownership and history policy, applies the mutation, consults `item_store_write`, writes the item atomically, appends history, fires `onWrite` for the item and history stream, records the after-command affected-item snapshot, then releases the lock.
79
+ - Locates the item, acquires the lock, reads the item through the `onRead` hook, enforces ownership and history policy, applies the mutation, enters the commit fence, consults `item_store_write`, writes the item atomically, appends history, fires `onWrite` for the item and history stream, records the after-command affected-item snapshot, then releases the lock.
80
80
  - `item_store_write` handlers can redirect the target path, replace serialized contents, or skip the physical item write.
81
+ - The commit fence verifies the lock lease and the stored item revision first. When the lease was taken over (`lock_lost`) or the item changed after the read (`stale_revision`), the mutation refuses before `item_store_write` or `item_store_delete` (non-dry-run) runs, so a handler that persists the payload itself never commits a refused write.
81
82
 
82
83
  Both paths guarantee:
83
84
 
@@ -4,6 +4,9 @@ Tracked by [pm-ig5cfe](../.agents/pm/issues/pm-ig5cfe.toon),
4
4
  [pm-495lkc](../.agents/pm/issues/pm-495lkc.toon), and
5
5
  [pm-miy5k6](../.agents/pm/issues/pm-miy5k6.toon).
6
6
  Static inventory is tracked by [pm-lhhnx9](../.agents/pm/issues/pm-lhhnx9.toon).
7
+ Verified replacements and disabled compatibility findings are tracked by
8
+ [pm-c9wz5ef4ulnv](../.agents/pm/issues/pm-c9wz5ef4ulnv.toon) and
9
+ [pm-5xn8we1wue0m](../.agents/pm/issues/pm-5xn8we1wue0m.toon).
7
10
 
8
11
  ## Read-only configured inventory
9
12
 
@@ -32,6 +35,45 @@ each. Use `pm install npm:<package>` to force npm identity or the reported bare
32
35
  alias command to force the bundled package. Install-all results carry the same
33
36
  receipt on every package row.
34
37
 
38
+ ## Verified replacements
39
+
40
+ `pm package install` and the SDK's `runExtension(source, { install: true }, global)`
41
+ retain the previous installation through fresh-module runtime verification and
42
+ contribution persistence. When a copied replacement fails to load or activate,
43
+ pm restores the previous package tree and exact managed/settings content. A
44
+ settings restoration appends a compensating workspace-history event.
45
+
46
+ The failed attempt still returns `ok: false` and its candidate diagnostics, with
47
+ `previous_state_preserved: true`, `overwritten: false`, and
48
+ `rollback.status: "restored"`. The previous command remains available in a new
49
+ process. Multi-target and install-all receipts retain these fields per package.
50
+ SDK `ExtensionCommandResult` / `PackageCommandResult` consumers can narrow
51
+ `details.rollback.status` to access typed recovery arguments on failed restoration.
52
+ These two diagnostic keys are now reserved typed fields. Code constructing
53
+ lifecycle result objects or test doubles must use the boolean and rollback
54
+ shapes, or rename custom metadata stored under those keys. Ordinary result
55
+ readers remain compatible; the runtime implementation is unchanged by this
56
+ declaration refinement.
57
+
58
+ If restoration fails, `previous_state_preserved` is false. The retained backup
59
+ and executable `rollback.recovery_args` / `rollback.recovery_command` identify a
60
+ source for reinstalling the previous package. Resolve the reported filesystem
61
+ failure first. Fresh installations and installations whose source is already
62
+ the destination retain failed code for diagnosis and report
63
+ `previous_state_preserved: false`; they have no earlier package version to
64
+ recover. Package code can have external side effects during activation; this
65
+ snapshot protects the installation and its managed/settings content.
66
+
67
+ ## Disabled package compatibility
68
+
69
+ An explicitly disabled package remains visible in inventory and discovery.
70
+ Its host-version warnings use `extension_disabled_pm_*` codes and are advisory
71
+ in health, including `--strict-exit`. Each finding identifies the package,
72
+ required bound and scoped activation command to use after satisfying that
73
+ bound. Enabled packages retain the ordinary `extension_pm_*` compatibility
74
+ checks and blocking policy. Deactivation does not permit incompatible code to
75
+ load or suppress malformed-manifest, missing-entry or other unrelated findings.
76
+
35
77
  ## Durable Extension Migrations
36
78
 
37
79
  Active packages register schema migrations through `api.registerMigration`.
@@ -62,3 +104,8 @@ compose without warnings, and runtime invokes only the matching owner. Empty or
62
104
  omitted command ownership retains the legacy global behavior and collides with
63
105
  every other override. Activation summaries and persisted contribution
64
106
  inventories expose `preflight_ownership` for static doctor and tooling output.
107
+
108
+ Rollback failures retain the initial error as their cause and expose an original
109
+ typed refusal as `context.original_code`, while keeping the rollback code and
110
+ exit policy. This diagnostic boundary is tracked by
111
+ [pm-4vymxc](../.agents/pm/issues/pm-4vymxc.toon).
@@ -1,12 +1,38 @@
1
1
  # MCP Option Conformance
2
2
 
3
- Tracked by [pm-v0s2rm](../.agents/pm/chores/pm-v0s2rm.toon).
3
+ Tracked by [pm-v0s2rm](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/chores/pm-v0s2rm.toon), [pm-gh1538](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1538.toon), and [pm-ap7ilf](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-ap7ilf.toon).
4
+ These repository-only tracking records are excluded from npm.
4
5
 
5
6
  An integration test can pass while an undeclared option is ignored. That leaves
6
7
  the test asserting a behavior it never exercised. The repository therefore
7
8
  treats emitted MCP unknown-option diagnostics as test failures, while the
8
9
  production server retains its advisory warning behavior for clients.
9
10
 
11
+ `pm_create` and `pm_update` accept the canonical mutation fields advertised by
12
+ their input schemas either at the top level or inside `options`. `pm_run` with
13
+ the corresponding action uses the same SDK normalization. Explicit nested
14
+ options take precedence, including CLI
15
+ aliases for the same field. Repeatable inputs, numeric values, and linked-resource
16
+ aliases use the shared mutation contracts before persistence. Native SDK `run`
17
+ callers continue to put update fields inside `options`.
18
+
19
+ Malformed option containers, values discarded from known scalar fields, and malformed repeatable
20
+ entries refuse before execution with `invalid_argument_value` and
21
+ `changed: false`. Declared boolean mutation fields, their aliases, and receipt
22
+ controls must be booleans, including `false`; this covers clear/replace flags,
23
+ parent/dependency exceptions, duplicate allowance, and `force`. These checks preserve project-defined
24
+ custom fields and template-based type resolution while preventing a bad field
25
+ from disappearing as a sibling field is written.
26
+ Array values that become CLI CSV inputs, such as `tags`, must contain only
27
+ strings and are checked before conversion. Validation follows nested precedence:
28
+ an explicit valid nested value replaces a flat value before either is normalized.
29
+
30
+ For example, `{ "id": "pm-one", "options": {}, "priority": 4 }` changes priority;
31
+ with `"options": { "priority": 2 }`, priority becomes `2`. Create also honors its
32
+ advertised explicit `id`, `deadline`, and other metadata. Regressions check the
33
+ discovered schema, persisted values and verified history, and compare item and
34
+ history bytes after invalid mutations to prove atomic refusal.
35
+
10
36
  Every run using [vitest.config.ts](../vitest.config.ts) installs
11
37
  [McpContractReporter](../scripts/mcp-contract-reporter.mts). The reporter records
12
38
  the emitting file, full test name, option, tool, and action, and rejects the run
@@ -3,7 +3,8 @@
3
3
  Tracker references: [pm-3g3f8z](../.agents/pm/features/pm-3g3f8z.toon),
4
4
  [pm-v7e337](../.agents/pm/features/pm-v7e337.toon),
5
5
  [pm-3zh9s4](../.agents/pm/features/pm-3zh9s4.toon), and
6
- [pm-vzcisw](../.agents/pm/chores/pm-vzcisw.toon).
6
+ [pm-vzcisw](../.agents/pm/chores/pm-vzcisw.toon), and executable probes
7
+ [pm-gh1540](../.agents/pm/issues/pm-gh1540.toon).
7
8
 
8
9
  pm exposes the same MCP 2026-07-28 dispatcher through two adapters:
9
10
 
@@ -20,6 +21,20 @@ thin bindings over those contracts.
20
21
 
21
22
  ## Run the HTTP adapter
22
23
 
24
+ Both executables accept `--help` / `-h` and `--version` / `-V`. These probes
25
+ return before reading transport settings, opening stdin, or binding a listener,
26
+ including when server configuration is invalid. Help includes configuration,
27
+ supported protocol revisions, and a version-pinned client or launch example.
28
+ No arguments starts the selected transport. Unsupported arguments return usage
29
+ exit code 64 with a help command. Server configuration uses environment variables.
30
+
31
+ Embedded launchers can import `resolvePmMcpExecutableInvocation` and its
32
+ `PmMcpExecutableTransport` / `PmMcpExecutableInvocation` types from
33
+ `@unbrained/pm-cli/sdk/contracts` or the aggregate SDK. This pure function takes
34
+ the argument vector, transport, and package version, and returns either a
35
+ `serve` decision or an `exit` decision with stdout, stderr, and exit code.
36
+ It does not read the environment, workspace, or process streams.
37
+
23
38
  The safe default binds only `127.0.0.1:3000`:
24
39
 
25
40
  ```bash
@@ -667,6 +667,14 @@ Never remove conflict markers manually while leaving the authoritative item and
667
667
 
668
668
  `transactions/` and `checkpoints/` are per-branch crash-recovery state, not shared project context. `pm init` places both below the resolved tracker root in the managed `.gitignore` block, including custom `--pm-path` roots.
669
669
 
670
+ Each tracker sharing a repository retains its own cache exclusions and merge
671
+ patterns in either initialization order. Tracker names containing Git wildcard
672
+ characters are escaped as literal paths, while the generated item/history globs
673
+ remain active. Unrelated user rules and curated evaluation files are preserved.
674
+ These contracts are tracked by
675
+ [pm-nl60tbxzkjwp](../.agents/pm/issues/pm-nl60tbxzkjwp.toon) and
676
+ [pm-47wuv8awpqvj](../.agents/pm/issues/pm-47wuv8awpqvj.toon).
677
+
670
678
  Ignore rules do not remove files that were already committed. `pm health`
671
679
  (`integrity`) and `pm validate` (`storage_integrity`) therefore scan Git's
672
680
  index for tracked files below `runtime/`, `search/`, `locks/`,
@@ -85,8 +85,87 @@ If brief still exceeds the ceiling, normal compaction and eventual refusal
85
85
  apply. `estimated_tokens` measures the emitted envelope;
86
86
  `omitted_result_estimated_tokens` measures the useful result that could not fit.
87
87
 
88
+ ## Typed producer pages
89
+
90
+ Tracked by [pm-oybmsd](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-oybmsd.toon),
91
+ [pm-jqgkis](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-jqgkis.toon), and
92
+ [pm-qal1uw](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-qal1uw.toon)
93
+ (repository-only tracker records excluded from npm).
94
+ Native `PmClient.list`, `search`, and `context` reads and their standalone
95
+ convenience functions return sparse metadata when `after` resumes a producer
96
+ page. Retain the first page's filters, projection, sorting, totals and completeness
97
+ certificate. Subsequent pages carry `continuation_contract` with
98
+ `metadata: "reference"` and `restore_with: "omit --after"`; repeated metadata
99
+ is optional in `PmReadOutputProducerContinuation<Result>`. Follow `next_cursor`
100
+ until it is empty, rather than relying on the optional `has_more` field.
101
+
102
+ Calls whose options contain no producer cursor retain their first-page types.
103
+ Omitting the options argument has the same precise type as passing `{}` for
104
+ client and standalone `list`, `context`, and `search` calls; it does not imply
105
+ that an explicit budget, session, or output cursor was supplied.
106
+ Options with an optional cursor can return either page shape. When a budget,
107
+ session, or output cursor is possible, first use `isReadOutputBudgetExceeded`
108
+ to narrow the omission branch before accessing rows. This remains required for
109
+ unions of option shapes: a budgeted variant cannot be erased by an unbudgeted
110
+ variant. The low-level query producers retain their original result contracts;
111
+ metadata suppression belongs to the shared native SDK transport.
112
+
113
+ ```ts
114
+ const first = await client.list({ brief: true, limit: "10" });
115
+ consume(first.items);
116
+ let cursor = first.next_cursor;
117
+ while (cursor) {
118
+ const page = await client.list({ brief: true, limit: "10", after: cursor });
119
+ consume(page.items);
120
+ cursor = page.next_cursor;
121
+ }
122
+ ```
123
+
88
124
  ## Cross-Call Context Sessions
89
125
 
126
+ Typed session results are tracked by
127
+ [pm-t1p1jv](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-t1p1jv.toon)
128
+ (repository-only tracker record excluded from npm). After narrowing budget
129
+ omission with `isReadOutputBudgetExceeded`, use `isReadOutputSessionReference`
130
+ from the SDK or `sdk/core` before accessing row prose or a search hit's nested
131
+ `item`. A reference contains only `id` and `context_ref`. Calls with no session,
132
+ including calls that specify only an output budget, retain their ordinary row
133
+ types. The direct `applyReadOutputDimensions` helper also infers session
134
+ capability from its options, including the accepted `output_session` spelling.
135
+ Unspecified or explicitly undefined session options retain ordinary rows;
136
+ optional, union, or dynamically typed session options admit references.
137
+ `PmReadOutputSessionResult<Result>` models literal `row_contract.row_keys`
138
+ paths (including nested arrays and keyed maps). Dynamic path declarations
139
+ conservatively admit references in nested collections, and envelopes without a
140
+ row declaration use the top-level array fallback. Keep package row paths literal
141
+ with `as const` to preserve precision in package result declarations.
142
+
143
+ ```ts
144
+ const page = await client.context({ outputSession: sessionState });
145
+ if (!isReadOutputBudgetExceeded(page)) {
146
+ for (const row of page.high_level) {
147
+ if (isReadOutputSessionReference(row)) consumeReference(row.context_ref);
148
+ else consumeTitle(row.title);
149
+ }
150
+ }
151
+ ```
152
+
153
+ Read-option recovery is tracked by
154
+ [pm-7gpx34](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-7gpx34.toon)
155
+ (repository-only tracker record excluded from npm). For explicit static encoding,
156
+ use `--output-format toon|json` across read commands. Completeness uses
157
+ `--output-include full` on list, search and get; context detail depth uses
158
+ `--depth full`. These controls retain the normal row and token ceilings.
159
+ When `context --full`, `list/search --depth full`, or `--toon` on list, search,
160
+ context or get is refused,
161
+ the error provides `recovery.suggested_retry_args` for the same command with the
162
+ supported spelling. The SDK's `resolveReadOptionRetryArgs` exposes the same
163
+ token-preserving correction. Conflicting controls or ambiguous operands do not
164
+ receive a guessed retry.
165
+ An explicit `--json` conflicts with `--toon`, so that invocation receives no
166
+ encoding-changing retry. Non-conflicting TOON refusals include the corrected
167
+ command in human output without requiring JSON diagnostics.
168
+
90
169
  `--output-session <json>` / `outputSession` composes the four per-call
91
170
  dimensions across a request group. The caller supplies versioned state and
92
171
  passes the returned `read_session.next_state` to the next read:
@@ -238,6 +238,21 @@ Every returned node is explainable: `role` names its semantic family (`prerequis
238
238
 
239
239
  `meta.completeness` reports result quality: the exact in-memory kernel emits `complete` or `truncated`, and the contract reserves `sampled`, `approximate`, `stale_index`, and `redacted` for index-backed or policy-filtered providers so consumers can branch on one field.
240
240
 
241
+ The packer orders direct prerequisite and ancestor endpoints before optional
242
+ descendants or related nodes, reserves their direct relationship costs, and
243
+ emits root-incident edges first. An oversized edge is skipped so later feasible
244
+ edges can still be returned. The same order governs cursor pages; the cursor
245
+ fingerprint identifies this packing policy and rejects earlier-policy cursors. An
246
+ edge limit or a budget too small for an endpoint and its edges can still omit
247
+ evidence: inspect the direct-family totals, omitted counts and completeness
248
+ before using the packet to make a decision. The native adapter also reports
249
+ `relationship_context_bounds` in `omission_receipt` whenever the producer is
250
+ truncated. Increase the relevant bounds or follow the node cursor for more
251
+ detail; a full projection alone does not remove traversal limits.
252
+
253
+ Tracked by [pm-gh1534](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1534.toon)
254
+ (repository tracker records are excluded from npm).
255
+
241
256
  Node, edge, depth, kind, direction, and token bounds are independent. The cursor fingerprint covers semantic filters and traversal shape, so it is rejected when reused for a different root or query. Output remains a plain object suitable for TOON, JSON, JSONL, MCP, or a custom UI; adapters own rendering and do not reimplement traversal.
242
257
 
243
258
  The native adapter is `pm deps <id> --format context`. It is also available through `PmClient.deps`, `runAction({ action: "deps" })`, and the MCP `pm_deps` tool. `--max-depth`, `--node-limit`, `--edge-limit`, `--token-budget`, `--cursor`, `--direction`, and repeatable or comma-separated `--kind` map directly to the public SDK context options; `--summary` keeps only counts. Unknown `--kind` values fail fast with the registered-kind list instead of silently matching nothing. Tree and graph formats share those first four bounds, use safe defaults when omitted, and report deterministic truncation metadata instead of recursively expanding a cyclic or multiply-linked graph without limit.
package/docs/RELEASING.md CHANGED
@@ -171,6 +171,15 @@ configuration belong in private operator documentation.
171
171
  result before candidate provenance, proves publication, and exits without
172
172
  evaluating the generated release commit as a new candidate
173
173
 
174
+ Dependency advisory diagnostics are tracked by
175
+ [pm-z20n3s](../.agents/pm/tasks/pm-z20n3s.toon). Both Auto Release and Release
176
+ retain the original `.cache/dependency-security/receipt.json` after the gate,
177
+ including failures, as `dependency-advisory-<run-id>-<attempt>` for seven days.
178
+ Only that public receipt is uploaded, with explicit hidden-file inclusion.
179
+ It records registry and OSV findings or unavailable evidence without raw
180
+ stderr, credentials, telemetry, or sibling caches. Missing receipts warn rather
181
+ than masking the original failure. Retention never changes the gate verdict.
182
+
174
183
  Pipeline entrypoint:
175
184
 
176
185
  ```bash
@@ -315,6 +324,14 @@ missing locked package. npm 12 ignores published shrinkwraps; shipping
315
324
  a shrinkwrap alone therefore cannot enforce this contract. SDK consumers
316
325
  supply the bounded optional Node types peer themselves.
317
326
 
327
+ Physical entrypoint verification is tracked by
328
+ [pm-tl7vlu](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-tl7vlu.toon)
329
+ (repository-only tracker record excluded from npm). The dependency advisory CLI
330
+ resolves both its invocation and module to physical paths, so directory aliases
331
+ and Node's symlink-preserving main-module mode still execute verification and
332
+ return a receipt or failure status. Module imports expose the collector without
333
+ starting verification.
334
+
318
335
  Push the final implementation commit through a reviewed pull request first,
319
336
  wait for DeepScan and CodeFactor to finish on that reviewed SHA, then run the
320
337
  canonical registry-owned preflight: