@unbrained/pm-cli 2026.8.23 → 2026.8.25

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 (172) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +27 -1
  3. package/dist/cli/description-stdin.js +5 -4
  4. package/dist/cli/extension-command-help.d.ts +5 -0
  5. package/dist/cli/extension-command-help.js +6 -2
  6. package/dist/cli/help-content.d.ts +5 -0
  7. package/dist/cli/help-content.js +56 -3
  8. package/dist/cli/help-json-payload.d.ts +5 -2
  9. package/dist/cli/help-json-payload.js +57 -17
  10. package/dist/cli/main.d.ts +3 -0
  11. package/dist/cli/main.js +28 -3
  12. package/dist/cli/register-annotations.js +5 -5
  13. package/dist/cli/register-mutation.js +73 -27
  14. package/dist/cli-bundle/bundle-manifest.json +152 -152
  15. package/dist/cli-bundle/chunks/{chunk-HWZ6CELB.js → chunk-244MI4GS.js} +3 -3
  16. package/dist/cli-bundle/chunks/{chunk-QDVHNHAQ.js → chunk-2F3LUFMW.js} +2 -2
  17. package/dist/cli-bundle/chunks/{chunk-EG6CLDZT.js → chunk-5I5RWIJC.js} +2 -2
  18. package/dist/cli-bundle/chunks/{chunk-KPH73XXJ.js → chunk-65MHLHAA.js} +2 -2
  19. package/dist/cli-bundle/chunks/chunk-6C7GIMIL.js +13 -0
  20. package/dist/cli-bundle/chunks/chunk-OWHNAR2B.js +2 -0
  21. package/dist/cli-bundle/chunks/chunk-QKGMHGEI.js +202 -0
  22. package/dist/cli-bundle/chunks/chunk-QLUORNIB.js +2092 -0
  23. package/dist/cli-bundle/chunks/{chunk-JB4KZWBF.js → chunk-T2ENPRXF.js} +2 -2
  24. package/dist/cli-bundle/chunks/{chunk-F3AI2JFL.js → chunk-TPQIBSL2.js} +2 -2
  25. package/dist/cli-bundle/chunks/chunk-YQMYF3YD.js +35 -0
  26. package/dist/cli-bundle/chunks/{register-list-query-UJB5FMV2.js → register-list-query-XVN2ZLI7.js} +2 -2
  27. package/dist/cli-bundle/chunks/register-mutation-QCKAEGIJ.js +20 -0
  28. package/dist/cli-bundle/chunks/{register-operations-RXIJIYXC.js → register-operations-SDEAXE7E.js} +2 -2
  29. package/dist/cli-bundle/chunks/{register-setup-GFUZEKRE.js → register-setup-LXVBRCJ3.js} +2 -2
  30. package/dist/cli-bundle/focused-chunks/{chunk-QGFNI65N.js → chunk-2UIWOP3O.js} +2 -2
  31. package/dist/cli-bundle/focused-chunks/chunk-42S3GGZ7.js +50 -0
  32. package/dist/cli-bundle/focused-chunks/{chunk-OGLQBL46.js → chunk-7I23XGWO.js} +2 -2
  33. package/dist/cli-bundle/focused-chunks/chunk-7YCDTCBC.js +2 -0
  34. package/dist/cli-bundle/focused-chunks/{chunk-OFVUGTLJ.js → chunk-A644DUFQ.js} +2 -2
  35. package/dist/cli-bundle/focused-chunks/{chunk-AUFH5H35.js → chunk-GQR3WH3F.js} +2 -2
  36. package/dist/cli-bundle/focused-chunks/{chunk-3UPTYG44.js → chunk-H5JZEIQV.js} +2 -2
  37. package/dist/cli-bundle/focused-chunks/chunk-IBZZZGK3.js +2 -0
  38. package/dist/cli-bundle/focused-chunks/chunk-LMKG3DFE.js +5 -0
  39. package/dist/cli-bundle/focused-chunks/{chunk-LYVSW5VM.js → chunk-N7W67YIG.js} +2 -2
  40. package/dist/cli-bundle/focused-chunks/{chunk-37Z4OEAP.js → chunk-NOOZGIXP.js} +2 -2
  41. package/dist/cli-bundle/focused-chunks/{chunk-FZS4H32O.js → chunk-OHIHZ7HS.js} +2 -2
  42. package/dist/cli-bundle/focused-chunks/chunk-ONYQCALA.js +19 -0
  43. package/dist/cli-bundle/focused-chunks/chunk-OVJL6NZE.js +4 -0
  44. package/dist/cli-bundle/focused-chunks/chunk-RAFKLNZX.js +16 -0
  45. package/dist/cli-bundle/focused-chunks/{chunk-IFLWKY2R.js → chunk-VXWATRFL.js} +2 -2
  46. package/dist/cli-bundle/focused-chunks/{chunk-R27IDGHD.js → chunk-XUQPEKRN.js} +2 -2
  47. package/dist/cli-bundle/focused-chunks/{chunk-JOWDB2MH.js → chunk-YO3ZF3FI.js} +2 -2
  48. package/dist/cli-bundle/main.js +18 -17
  49. package/dist/cli-bundle/sdk-authoring.js +1 -1
  50. package/dist/cli-bundle/sdk-contracts.js +2 -2
  51. package/dist/cli-bundle/sdk-core.js +32 -32
  52. package/dist/cli-bundle/sdk-governance.js +1 -1
  53. package/dist/cli-bundle/sdk-graph.js +1 -1
  54. package/dist/cli-bundle/sdk-merge.js +32 -33
  55. package/dist/cli-bundle/sdk-query.js +1 -1
  56. package/dist/cli-bundle/sdk-runtime.js +1 -1
  57. package/dist/cli-bundle/sdk-testing.js +1 -1
  58. package/dist/cli-bundle/sdk.js +6 -5
  59. package/dist/core/diagnostics/remediation.js +17 -2
  60. package/dist/core/extensions/command-metadata-contract.d.ts +10 -0
  61. package/dist/core/extensions/command-metadata-contract.js +23 -0
  62. package/dist/core/extensions/command-visibility-tier.js +12 -2
  63. package/dist/core/extensions/extension-types.d.ts +6 -0
  64. package/dist/core/extensions/extension-types.js +2 -2
  65. package/dist/core/extensions/loader.js +4 -3
  66. package/dist/core/io/body-file.js +7 -2
  67. package/dist/core/io/bulk-ids-input.d.ts +24 -0
  68. package/dist/core/io/bulk-ids-input.js +113 -0
  69. package/dist/core/item/parse.d.ts +22 -2
  70. package/dist/core/item/parse.js +97 -4
  71. package/dist/mcp/server.d.ts +3 -2
  72. package/dist/mcp/server.js +16 -9
  73. package/dist/sdk/agent/refusal-closure-census.d.ts +66 -0
  74. package/dist/sdk/agent/refusal-closure-census.js +144 -0
  75. package/dist/sdk/agent-capability-contracts.d.ts +34 -1
  76. package/dist/sdk/agent-capability-contracts.js +104 -6
  77. package/dist/sdk/annotations.d.ts +2 -0
  78. package/dist/sdk/annotations.js +25 -7
  79. package/dist/sdk/cli-contracts/runtime-contracts.d.ts +10 -0
  80. package/dist/sdk/cli-contracts/runtime-contracts.js +20 -3
  81. package/dist/sdk/cli-contracts/tool-parameter-tables.js +38 -2
  82. package/dist/sdk/cli-contracts/typed-action-inputs.d.ts +4 -1
  83. package/dist/sdk/cli-contracts/typed-action-inputs.js +2 -2
  84. package/dist/sdk/comments.js +15 -7
  85. package/dist/sdk/flag-invocation-contracts.d.ts +2 -0
  86. package/dist/sdk/flag-invocation-contracts.js +28 -3
  87. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +109 -52
  88. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +62 -2
  89. package/dist/sdk/governance/health.js +98 -11
  90. package/dist/sdk/governance/validate.js +15 -9
  91. package/dist/sdk/guide-topics.d.ts +0 -5
  92. package/dist/sdk/guide-topics.js +28 -2
  93. package/dist/sdk/history-compact.d.ts +2 -1
  94. package/dist/sdk/history-compact.js +24 -5
  95. package/dist/sdk/history-mcp.js +11 -10
  96. package/dist/sdk/history-repair.d.ts +24 -0
  97. package/dist/sdk/history-repair.js +145 -4
  98. package/dist/sdk/index.d.ts +1 -0
  99. package/dist/sdk/index.js +3 -2
  100. package/dist/sdk/item-transaction.js +19 -12
  101. package/dist/sdk/lifecycle/append.js +4 -4
  102. package/dist/sdk/lifecycle/close-many.js +16 -7
  103. package/dist/sdk/lifecycle/create.js +29 -21
  104. package/dist/sdk/lifecycle/plan.js +24 -6
  105. package/dist/sdk/lifecycle/update-many.js +15 -8
  106. package/dist/sdk/lifecycle/update.d.ts +2 -0
  107. package/dist/sdk/lifecycle/update.js +34 -24
  108. package/dist/sdk/merge/driver.js +25 -4
  109. package/dist/sdk/merge/receipt-file-boundary.d.ts +21 -0
  110. package/dist/sdk/merge/receipt-file-boundary.js +47 -0
  111. package/dist/sdk/merge/receipts.d.ts +24 -1
  112. package/dist/sdk/merge/receipts.js +342 -36
  113. package/dist/sdk/merge/reconcile.js +117 -47
  114. package/dist/sdk/read-output/continuation.d.ts +15 -2
  115. package/dist/sdk/read-output/continuation.js +86 -6
  116. package/dist/sdk/read-output-contracts.d.ts +4 -1
  117. package/dist/sdk/read-output-contracts.js +9 -6
  118. package/dist/sdk/runtime-extended-actions.js +5 -3
  119. package/dist/sdk/runtime-input.d.ts +4 -0
  120. package/dist/sdk/runtime-input.js +98 -23
  121. package/dist/sdk/runtime-primitives.d.ts +2 -1
  122. package/dist/sdk/runtime-primitives.js +4 -3
  123. package/dist/sdk/runtime.js +7 -20
  124. package/dist/sdk/scheduling-shortcuts.js +5 -3
  125. package/dist/sdk/structured-mutations.js +10 -3
  126. package/dist/sdk/workspace-contracts.d.ts +3 -0
  127. package/dist/sdk/workspace-contracts.js +3 -2
  128. package/docs/MERGE_SAFETY.md +34 -5
  129. package/docs/OUTPUT_PROJECTION_CONTRACTS.md +13 -1
  130. package/docs/README.md +2 -0
  131. package/docs/READ_OUTPUT_CONTRACTS.md +25 -2
  132. package/docs/RELEASING.md +40 -21
  133. package/docs/SCRIPTING.md +62 -3
  134. package/docs/SDK.md +8 -0
  135. package/docs/generated/AGENT_CAPABILITY_ROUTING.md +17 -0
  136. package/docs/generated/AGENT_COMMAND_SURFACE.md +1 -1
  137. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +359 -0
  138. package/marketplace.json +2 -2
  139. package/package.json +1 -1
  140. package/packages/pm-beads/package.json +1 -1
  141. package/packages/pm-calendar/package.json +1 -1
  142. package/packages/pm-command-kit/README.md +9 -11
  143. package/packages/pm-command-kit/extensions/command-kit/index.ts +3 -1
  144. package/packages/pm-command-kit/package.json +1 -1
  145. package/packages/pm-digital-twin/package.json +1 -1
  146. package/packages/pm-governance-audit/package.json +1 -1
  147. package/packages/pm-guide-shell/extensions/guide-shell/index.ts +10 -0
  148. package/packages/pm-guide-shell/package.json +1 -1
  149. package/packages/pm-kanban/package.json +1 -1
  150. package/packages/pm-lifecycle-hooks/package.json +1 -1
  151. package/packages/pm-linked-test-adapters/package.json +1 -1
  152. package/packages/pm-search-advanced/package.json +1 -1
  153. package/packages/pm-templates/package.json +1 -1
  154. package/packages/pm-todos/package.json +1 -1
  155. package/packages/pm-vcs/package.json +1 -1
  156. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  157. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  158. package/scripts/generate-agent-capability-surfaces.mjs +27 -1
  159. package/sdk/public-surface.json +351 -26
  160. package/dist/cli-bundle/chunks/chunk-3ENVHX6C.js +0 -36
  161. package/dist/cli-bundle/chunks/chunk-DW2CGEMU.js +0 -2
  162. package/dist/cli-bundle/chunks/chunk-HZBMLSZI.js +0 -201
  163. package/dist/cli-bundle/chunks/chunk-KXPMB2YL.js +0 -2092
  164. package/dist/cli-bundle/chunks/chunk-TFI5V5N2.js +0 -13
  165. package/dist/cli-bundle/chunks/register-mutation-NX5XE4VX.js +0 -20
  166. package/dist/cli-bundle/focused-chunks/chunk-5S7E72FY.js +0 -16
  167. package/dist/cli-bundle/focused-chunks/chunk-E7X25GC3.js +0 -2
  168. package/dist/cli-bundle/focused-chunks/chunk-FQCSTBWE.js +0 -4
  169. package/dist/cli-bundle/focused-chunks/chunk-MRCNF3U6.js +0 -2
  170. package/dist/cli-bundle/focused-chunks/chunk-OY7QAV4Y.js +0 -51
  171. package/dist/cli-bundle/focused-chunks/chunk-UYRFYCDQ.js +0 -5
  172. package/dist/cli-bundle/focused-chunks/chunk-ZHVA42R5.js +0 -18
@@ -7,16 +7,49 @@
7
7
  * value for the coordinator that performed the merge.
8
8
  */
9
9
 
10
- !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]="7b87b458-164a-5332-8803-6b0695481644")}catch(e){}}();
10
+ !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]="5add49c0-25be-5159-abfc-6c7ae9272f6b")}catch(e){}}();
11
11
  import { execFile } from "node:child_process";
12
12
  import { randomUUID } from "node:crypto";
13
- import { readdir, readFile } from "node:fs/promises";
13
+ import { readdir } from "node:fs/promises";
14
14
  import path from "node:path";
15
15
  import { promisify } from "node:util";
16
- import { ensureDir, pathExists, writeFileAtomic, } from "../../core/fs/fs-utils.js";
16
+ import { ensureDir, isFileAbsentError, pathExists, writeFileAtomic, } from "../../core/fs/fs-utils.js";
17
17
  import { sha256Hex, stableStringify } from "../../core/shared/serialization.js";
18
- import { nowIso } from "../../core/shared/time.js";
18
+ import { isRfc3339DateTime, nowIso } from "../../core/shared/time.js";
19
+ import { readBoundedRegularFile } from "./receipt-file-boundary.js";
19
20
  const execFileAsync = promisify(execFile);
21
+ const RECEIPT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
22
+ const RECEIPT_ITEM_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
23
+ const RECEIPT_FIELD_MAX = 2_048;
24
+ const RECEIPT_FILE_MAX_BYTES = 16 * 1024 * 1024;
25
+ const RECEIPT_VALUE_MAX_DEPTH = 64;
26
+ const RECEIPT_VALUE_MAX_NODES = 100_000;
27
+ const RECEIPT_KEYS = new Set([
28
+ "version",
29
+ "id",
30
+ "item_path",
31
+ "item_id",
32
+ "requested_preference",
33
+ "preferred",
34
+ "conflict_resolution",
35
+ "fields_from_theirs",
36
+ "union_fields",
37
+ "merged_field_hashes",
38
+ "decisions",
39
+ "state",
40
+ "created_at",
41
+ "reconciled_at",
42
+ "value_availability",
43
+ "evidence_source",
44
+ ]);
45
+ const RECEIPT_DECISION_KEYS = new Set([
46
+ "field",
47
+ "base",
48
+ "ours",
49
+ "theirs",
50
+ "retained",
51
+ "discarded",
52
+ ]);
20
53
  async function resolveReceiptDirectory(cwd) {
21
54
  try {
22
55
  const { stdout } = await execFileAsync("git", [
@@ -49,6 +82,200 @@ function durableReceiptDirectory(pmRoot) {
49
82
  function receiptFileName(id) {
50
83
  return `${id}.json`;
51
84
  }
85
+ function isRecord(value) {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value);
87
+ }
88
+ function hasOnlyKeys(value, allowed) {
89
+ return Object.keys(value).every((key) => allowed.has(key));
90
+ }
91
+ function isStringArray(value) {
92
+ return (Array.isArray(value) &&
93
+ value.length <= RECEIPT_FIELD_MAX &&
94
+ value.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 256));
95
+ }
96
+ function hasBoundedJsonStructure(value) {
97
+ const pending = [
98
+ { value, depth: 0 },
99
+ ];
100
+ let nodes = 0;
101
+ while (pending.length > 0) {
102
+ const current = pending.pop();
103
+ nodes += 1;
104
+ if (nodes > RECEIPT_VALUE_MAX_NODES ||
105
+ current.depth > RECEIPT_VALUE_MAX_DEPTH) {
106
+ return false;
107
+ }
108
+ if (Array.isArray(current.value)) {
109
+ for (const entry of current.value) {
110
+ pending.push({ value: entry, depth: current.depth + 1 });
111
+ }
112
+ }
113
+ else if (isRecord(current.value)) {
114
+ for (const entry of Object.values(current.value)) {
115
+ pending.push({ value: entry, depth: current.depth + 1 });
116
+ }
117
+ }
118
+ }
119
+ return true;
120
+ }
121
+ function isMergeDecision(value) {
122
+ return (isRecord(value) &&
123
+ hasOnlyKeys(value, RECEIPT_DECISION_KEYS) &&
124
+ typeof value.field === "string" &&
125
+ value.field.length > 0 &&
126
+ value.field.length <= 256 &&
127
+ Object.hasOwn(value, "base") &&
128
+ Object.hasOwn(value, "ours") &&
129
+ Object.hasOwn(value, "theirs") &&
130
+ Object.hasOwn(value, "retained") &&
131
+ Object.hasOwn(value, "discarded") &&
132
+ hasBoundedJsonStructure(value.base) &&
133
+ hasBoundedJsonStructure(value.ours) &&
134
+ hasBoundedJsonStructure(value.theirs) &&
135
+ hasBoundedJsonStructure(value.retained) &&
136
+ hasBoundedJsonStructure(value.discarded));
137
+ }
138
+ function isMergedFieldHashes(value) {
139
+ if (!isRecord(value))
140
+ return false;
141
+ const entries = Object.entries(value);
142
+ return (entries.length <= RECEIPT_FIELD_MAX &&
143
+ entries.every(([field, hash]) => field.length > 0 &&
144
+ field.length <= 256 &&
145
+ typeof hash === "string" &&
146
+ /^[a-f0-9]{64}$/u.test(hash)));
147
+ }
148
+ function isSafeReceiptItemPath(value, itemId) {
149
+ if (typeof value !== "string" ||
150
+ value.length === 0 ||
151
+ value.length > 4_096 ||
152
+ value.includes("\0") ||
153
+ value.includes("\\") ||
154
+ path.posix.isAbsolute(value)) {
155
+ return false;
156
+ }
157
+ const normalized = path.posix.normalize(value);
158
+ return (normalized !== ".." &&
159
+ !normalized.startsWith("../") &&
160
+ path.posix.basename(normalized, path.posix.extname(normalized)) === itemId);
161
+ }
162
+ function hasValidReceiptIdentity(value) {
163
+ return !hasOnlyKeys(value, RECEIPT_KEYS) || value.version !== 1
164
+ ? false
165
+ : typeof value.id === "string" &&
166
+ RECEIPT_ID_PATTERN.test(value.id) &&
167
+ typeof value.item_id === "string" &&
168
+ RECEIPT_ITEM_ID_PATTERN.test(value.item_id) &&
169
+ isSafeReceiptItemPath(value.item_path, value.item_id);
170
+ }
171
+ function hasValidReceiptCollections(value) {
172
+ return !isStringArray(value.fields_from_theirs) ||
173
+ !isStringArray(value.union_fields) ||
174
+ !Array.isArray(value.decisions) ||
175
+ value.decisions.length > RECEIPT_FIELD_MAX ||
176
+ !value.decisions.every(isMergeDecision)
177
+ ? false
178
+ : value.merged_field_hashes === undefined ||
179
+ isMergedFieldHashes(value.merged_field_hashes);
180
+ }
181
+ function hasValidReceiptEnums(value) {
182
+ const preferences = [undefined, "ours", "theirs"];
183
+ const resolutions = [undefined, "preferred_side", "stable_value_order"];
184
+ const availabilities = [undefined, "clone_local", "hash_only"];
185
+ const sources = [undefined, "clone_local", "durable"];
186
+ return (preferences.includes(value.requested_preference) &&
187
+ preferences.includes(value.preferred) &&
188
+ resolutions.includes(value.conflict_resolution) &&
189
+ (value.state === "pending" || value.state === "reconciled") &&
190
+ availabilities.includes(value.value_availability) &&
191
+ sources.includes(value.evidence_source));
192
+ }
193
+ function hasValidReceiptTimestamps(value) {
194
+ return (typeof value.created_at === "string" &&
195
+ isRfc3339DateTime(value.created_at) &&
196
+ (value.reconciled_at === undefined ||
197
+ (typeof value.reconciled_at === "string" &&
198
+ isRfc3339DateTime(value.reconciled_at))));
199
+ }
200
+ function hasHashOnlyDurableDecisions(receipt) {
201
+ return (receipt.value_availability === "hash_only" &&
202
+ receipt.decisions.every((decision) => decision.base === null &&
203
+ decision.ours === null &&
204
+ decision.theirs === null &&
205
+ isExactPrehashedValue(decision.retained) &&
206
+ isExactPrehashedValue(decision.discarded)));
207
+ }
208
+ function isMergeDecisionReceipt(value, evidenceSource) {
209
+ if (!isRecord(value) ||
210
+ !hasValidReceiptIdentity(value) ||
211
+ !hasValidReceiptCollections(value) ||
212
+ !hasValidReceiptEnums(value) ||
213
+ !hasValidReceiptTimestamps(value)) {
214
+ return false;
215
+ }
216
+ const receipt = value;
217
+ return evidenceSource !== "durable" || hasHashOnlyDurableDecisions(receipt);
218
+ }
219
+ async function readBoundedRegularReceiptFile(receiptPath) {
220
+ return readBoundedRegularFile(receiptPath, RECEIPT_FILE_MAX_BYTES);
221
+ }
222
+ async function prepareReceiptSettlement(params) {
223
+ let raw;
224
+ try {
225
+ raw = await readBoundedRegularReceiptFile(params.receiptPath);
226
+ }
227
+ catch (error) {
228
+ if (isFileAbsentError(error))
229
+ return null;
230
+ throw error;
231
+ }
232
+ if (raw === null) {
233
+ throw new Error(`Receipt ${params.receiptId} settlement source is not a bounded regular file.`);
234
+ }
235
+ let parsed;
236
+ try {
237
+ parsed = JSON.parse(raw);
238
+ }
239
+ catch {
240
+ throw new Error(`Receipt ${params.receiptId} settlement source is not valid JSON.`);
241
+ }
242
+ if (!isMergeDecisionReceipt(parsed, params.evidenceSource) ||
243
+ parsed.id !== params.receiptId ||
244
+ path.basename(params.receiptPath) !== receiptFileName(parsed.id)) {
245
+ throw new Error(`Receipt ${params.receiptId} settlement source failed schema or identity validation.`);
246
+ }
247
+ const { evidence_source: _evidenceSource, ...persistedReceipt } = parsed;
248
+ return {
249
+ path: params.receiptPath,
250
+ fingerprint: receiptProvenanceFingerprint(parsed),
251
+ content: `${JSON.stringify({
252
+ ...persistedReceipt,
253
+ state: "reconciled",
254
+ reconciled_at: params.reconciledAt,
255
+ }, null, 2)}\n`,
256
+ };
257
+ }
258
+ function receiptProvenanceFingerprint(receipt) {
259
+ return sha256Hex(stableStringify({
260
+ id: receipt.id,
261
+ item_id: receipt.item_id,
262
+ item_path: receipt.item_path,
263
+ created_at: receipt.created_at,
264
+ requested_preference: receipt.requested_preference ?? receipt.preferred ?? "ours",
265
+ conflict_resolution: receipt.conflict_resolution ?? "preferred_side",
266
+ fields_from_theirs: receipt.fields_from_theirs,
267
+ union_fields: receipt.union_fields,
268
+ merged_field_hashes: receipt.merged_field_hashes ?? null,
269
+ decisions: summarizeMergeReceipt(receipt).decisions,
270
+ }));
271
+ }
272
+ function mergeReceiptCopyLifecycle(local, durable) {
273
+ if (local.state === "reconciled" && durable.state === "reconciled") {
274
+ return local;
275
+ }
276
+ const { reconciled_at: _reconciledAt, ...pendingLocal } = local;
277
+ return { ...pendingLocal, state: "pending" };
278
+ }
52
279
  /** Convert a raw receipt into privacy-safe history context. */
53
280
  export function summarizeMergeReceipt(receipt) {
54
281
  return {
@@ -81,6 +308,11 @@ function isPrehashedValue(value) {
81
308
  ? hash
82
309
  : undefined;
83
310
  }
311
+ function isExactPrehashedValue(value) {
312
+ return (isRecord(value) &&
313
+ Object.keys(value).length === 1 &&
314
+ isPrehashedValue(value) !== undefined);
315
+ }
84
316
  /** Persist one item-driver outcome in the clone-local Git directory. */
85
317
  export async function writeMergeReceipt(params) {
86
318
  const directory = await resolveReceiptDirectory(params.cwd);
@@ -103,6 +335,9 @@ export async function writeMergeReceipt(params) {
103
335
  conflict_resolution: params.conflictResolution ?? "preferred_side",
104
336
  fields_from_theirs: [...params.fieldsFromTheirs],
105
337
  union_fields: [...params.unionFields],
338
+ ...(params.mergedFieldHashes
339
+ ? { merged_field_hashes: { ...params.mergedFieldHashes } }
340
+ : {}),
106
341
  decisions: structuredClone(params.decisions),
107
342
  state: "pending",
108
343
  created_at: nowIso(),
@@ -131,53 +366,86 @@ export async function writeMergeReceipt(params) {
131
366
  }
132
367
  return receipt;
133
368
  }
134
- async function readReceiptsFromDirectory(directory, options) {
135
- if (!(await pathExists(directory)))
136
- return [];
369
+ async function readReceiptsFromDirectory(directory, evidenceSource) {
370
+ if (!(await pathExists(directory))) {
371
+ return { receipts: [], invalid_evidence_count: 0 };
372
+ }
137
373
  let names;
138
374
  try {
139
375
  names = await readdir(directory);
140
376
  }
141
377
  catch {
142
- return [];
378
+ return { receipts: [], invalid_evidence_count: 1 };
143
379
  }
144
380
  const receipts = [];
381
+ let invalidEvidenceCount = 0;
145
382
  for (const name of names.sort((left, right) => left.localeCompare(right))) {
146
383
  if (!name.endsWith(".json"))
147
384
  continue;
148
385
  try {
149
- const parsed = JSON.parse(await readFile(path.join(directory, name), "utf8"));
150
- const { preferred: legacyPreference, ...receiptWithoutLegacyKey } = parsed;
386
+ const receiptPath = path.join(directory, name);
387
+ const raw = await readBoundedRegularReceiptFile(receiptPath);
388
+ if (raw === null) {
389
+ invalidEvidenceCount += 1;
390
+ continue;
391
+ }
392
+ const parsed = JSON.parse(raw);
393
+ if (!isMergeDecisionReceipt(parsed, evidenceSource) ||
394
+ name !== receiptFileName(parsed.id)) {
395
+ invalidEvidenceCount += 1;
396
+ continue;
397
+ }
398
+ const { preferred: legacyPreference, evidence_source: _serializedEvidenceSource, ...receiptWithoutRuntimeKeys } = parsed;
151
399
  const normalizedReceipt = {
152
- ...receiptWithoutLegacyKey,
400
+ ...receiptWithoutRuntimeKeys,
153
401
  requested_preference: parsed.requested_preference ?? legacyPreference ?? "ours",
154
402
  conflict_resolution: parsed.conflict_resolution ?? "preferred_side",
403
+ evidence_source: evidenceSource,
155
404
  };
156
- if (normalizedReceipt.version === 1 &&
157
- (options.includeReconciled || normalizedReceipt.state === "pending") &&
158
- (options.includeLossless !== false ||
159
- normalizedReceipt.decisions.length > 0)) {
160
- receipts.push(normalizedReceipt);
161
- }
405
+ receipts.push(normalizedReceipt);
162
406
  }
163
407
  catch {
164
- // Malformed evidence remains an integrity concern for a future schema.
408
+ invalidEvidenceCount += 1;
165
409
  }
166
410
  }
167
- return receipts;
411
+ return { receipts, invalid_evidence_count: invalidEvidenceCount };
168
412
  }
169
- /** Read clone-local receipts with explicit reconciled/lossless classification controls. */
170
- export async function listMergeReceipts(cwd, options = {}) {
413
+ /** Inspect valid receipts and count invalid evidence without returning untrusted file contents. */
414
+ export async function inspectMergeReceiptEvidence(cwd, options = {}) {
171
415
  const directory = await resolveReceiptDirectory(cwd);
172
416
  const local = directory === null
173
- ? []
174
- : await readReceiptsFromDirectory(directory, options);
417
+ ? { receipts: [], invalid_evidence_count: 0 }
418
+ : await readReceiptsFromDirectory(directory, "clone_local");
175
419
  const trackerRoot = options.pmRoot ?? path.join(cwd, ".agents", "pm");
176
- const durable = await readReceiptsFromDirectory(durableReceiptDirectory(trackerRoot), options);
177
- const receipts = new Map(durable.map((receipt) => [receipt.id, receipt]));
178
- for (const receipt of local)
179
- receipts.set(receipt.id, receipt);
180
- return [...receipts.values()].sort((left, right) => left.created_at.localeCompare(right.created_at));
420
+ const durable = await readReceiptsFromDirectory(durableReceiptDirectory(trackerRoot), "durable");
421
+ const receipts = new Map(durable.receipts.map((receipt) => [receipt.id, receipt]));
422
+ let divergentCopyCount = 0;
423
+ for (const receipt of local.receipts) {
424
+ const durableCopy = receipts.get(receipt.id);
425
+ if (durableCopy !== undefined &&
426
+ receiptProvenanceFingerprint(durableCopy) !==
427
+ receiptProvenanceFingerprint(receipt)) {
428
+ receipts.delete(receipt.id);
429
+ divergentCopyCount += 1;
430
+ continue;
431
+ }
432
+ receipts.set(receipt.id, durableCopy === undefined
433
+ ? receipt
434
+ : mergeReceiptCopyLifecycle(receipt, durableCopy));
435
+ }
436
+ return {
437
+ receipts: [...receipts.values()]
438
+ .filter((receipt) => (options.includeReconciled || receipt.state === "pending") &&
439
+ (options.includeLossless !== false || receipt.decisions.length > 0))
440
+ .sort((left, right) => left.created_at.localeCompare(right.created_at)),
441
+ invalid_evidence_count: local.invalid_evidence_count +
442
+ durable.invalid_evidence_count +
443
+ divergentCopyCount,
444
+ };
445
+ }
446
+ /** Read clone-local receipts with explicit reconciled/lossless classification controls. */
447
+ export async function listMergeReceipts(cwd, options = {}) {
448
+ return (await inspectMergeReceiptEvidence(cwd, options)).receipts;
181
449
  }
182
450
  /** Split merge provenance into receipts with discarded values and receipts whose composition was lossless. */
183
451
  export function partitionMergeReceipts(receipts) {
@@ -191,19 +459,57 @@ export function partitionMergeReceipts(receipts) {
191
459
  return { pendingDecisions, lossless };
192
460
  }
193
461
  /** Mark a receipt as represented by a committed merge history event. */
194
- export async function markMergeReceiptReconciled(cwd, receipt) {
462
+ export async function markMergeReceiptReconciled(cwd, receipt, options = {}) {
195
463
  const directory = await resolveReceiptDirectory(cwd);
196
464
  if (directory === null) {
465
+ if (options.requireExisting === true) {
466
+ throw new Error(`Receipt ${receipt.id} disappeared before settlement.`);
467
+ }
197
468
  return;
198
469
  }
199
- await writeFileAtomic(path.join(directory, receiptFileName(receipt.id)), `${JSON.stringify({ ...receipt, state: "reconciled", reconciled_at: nowIso() }, null, 2)}\n`);
470
+ const reconciledAt = nowIso();
471
+ const localPath = path.join(directory, receiptFileName(receipt.id));
200
472
  const trackerRoot = await resolveTrackerRootFromItemPath(cwd, receipt.item_path);
201
- if (trackerRoot !== null) {
202
- const durablePath = path.join(durableReceiptDirectory(trackerRoot), receiptFileName(receipt.id));
203
- if (await pathExists(durablePath)) {
204
- const durable = JSON.parse(await readFile(durablePath, "utf8"));
205
- await writeFileAtomic(durablePath, `${JSON.stringify({ ...durable, state: "reconciled", reconciled_at: nowIso() }, null, 2)}\n`);
473
+ const durablePath = trackerRoot === null
474
+ ? null
475
+ : path.join(durableReceiptDirectory(trackerRoot), receiptFileName(receipt.id));
476
+ const prepared = await Promise.all([
477
+ prepareReceiptSettlement({
478
+ receiptPath: localPath,
479
+ receiptId: receipt.id,
480
+ evidenceSource: "clone_local",
481
+ reconciledAt,
482
+ }),
483
+ ...(durablePath === null
484
+ ? []
485
+ : [
486
+ prepareReceiptSettlement({
487
+ receiptPath: durablePath,
488
+ receiptId: receipt.id,
489
+ evidenceSource: "durable",
490
+ reconciledAt,
491
+ }),
492
+ ]),
493
+ ]);
494
+ const writes = prepared.filter((entry) => entry !== null);
495
+ if (writes.length === 0) {
496
+ if (options.requireExisting === true) {
497
+ throw new Error(`Receipt ${receipt.id} disappeared before settlement.`);
206
498
  }
499
+ return;
500
+ }
501
+ const receiptEvidenceSource = receipt.evidence_source ?? "clone_local";
502
+ const trustedReceiptId = receipt.id;
503
+ if (!isMergeDecisionReceipt(receipt, receiptEvidenceSource)) {
504
+ throw new Error(`Receipt ${trustedReceiptId} trusted settlement input failed schema validation.`);
505
+ }
506
+ const expectedFingerprint = receiptProvenanceFingerprint(receipt);
507
+ if (new Set([expectedFingerprint, ...writes.map((write) => write.fingerprint)])
508
+ .size !== 1) {
509
+ throw new Error(`Receipt ${receipt.id} settlement copies disagree on immutable merge provenance.`);
510
+ }
511
+ for (const write of writes) {
512
+ await writeFileAtomic(write.path, write.content);
207
513
  }
208
514
  }
209
515
  /** Report pending or historical clone-local merge decisions. */
@@ -220,4 +526,4 @@ export async function runMergeReceiptReport(options) {
220
526
  };
221
527
  }
222
528
  //# sourceMappingURL=receipts.js.map
223
- //# debugId=7b87b458-164a-5332-8803-6b0695481644
529
+ //# debugId=5add49c0-25be-5159-abfc-6c7ae9272f6b
@@ -1,10 +1,76 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c5620467-6c8c-5702-b742-57fba9e302c5")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c3ec7d9d-f276-5e97-8e97-507925e5d33a")}catch(e){}}();
3
3
  import { EXIT_CODE } from "../../core/shared/constants.js";
4
4
  import { PmCliError } from "../../core/shared/errors.js";
5
+ import { resolvePmRoot } from "../../core/store/paths.js";
5
6
  import { runHistoryRepair, runHistoryRepairAll, } from "../history-repair.js";
6
7
  import { runValidate } from "../governance/validate.js";
7
- import { listMergeReceipts, markMergeReceiptReconciled, partitionMergeReceipts, summarizeMergeReceipt, } from "./receipts.js";
8
+ import { inspectMergeReceiptEvidence, markMergeReceiptReconciled, partitionMergeReceipts, summarizeMergeReceipt, } from "./receipts.js";
9
+ import { findGitWorkspaceRoot } from "./install.js";
10
+ import { mapWithFixedConcurrency } from "../extension/concurrency.js";
11
+ const RECEIPT_ONLY_REPAIR_CONCURRENCY = 4;
12
+ async function runReceiptOnlyRepair(params) {
13
+ try {
14
+ return {
15
+ status: "fulfilled",
16
+ value: await runHistoryRepair(params.id, {
17
+ dryRun: params.dryRun,
18
+ author: params.options.author ?? params.global.author,
19
+ message: params.options.message ??
20
+ "record field-aware branch merge provenance",
21
+ force: params.options.force,
22
+ auditOperation: "merge_reconcile",
23
+ auditContext: params.auditContext,
24
+ forceAuditEntry: true,
25
+ mergeReceiptProof: params.mergeReceiptProof,
26
+ }, params.global),
27
+ };
28
+ }
29
+ catch (reason) {
30
+ return { status: "rejected", reason };
31
+ }
32
+ }
33
+ function appendReceiptOnlyResults(params) {
34
+ params.results.forEach((settled, index) => {
35
+ const id = params.receiptOnlyIds[index];
36
+ if (settled.status === "fulfilled") {
37
+ const outcome = settled.value.changed ? "repaired" : "skipped_clean";
38
+ params.repair.streams.push({
39
+ id,
40
+ outcome,
41
+ entries_rehashed: settled.value.history.entries_rehashed,
42
+ entries_patch_repaired: settled.value.history.entries_patch_repaired,
43
+ reconciled_with_item: settled.value.history.reconciled_with_item,
44
+ ...(settled.value.merge_receipt_proof
45
+ ? { merge_receipt_proof: settled.value.merge_receipt_proof }
46
+ : {}),
47
+ warnings: settled.value.warnings,
48
+ });
49
+ params.repair.totals[outcome] += 1;
50
+ return;
51
+ }
52
+ params.repair.streams.push({
53
+ id,
54
+ outcome: "failed",
55
+ error: settled.reason instanceof Error
56
+ ? settled.reason.message
57
+ : String(settled.reason),
58
+ });
59
+ params.repair.totals.failed += 1;
60
+ });
61
+ }
62
+ async function settleProvenReceipts(params) {
63
+ if (params.dryRun || params.repair.totals.failed > 0)
64
+ return 0;
65
+ const trustedReceiptIds = new Set(params.repair.streams.flatMap((stream) => stream.merge_receipt_proof?.trusted
66
+ ? stream.merge_receipt_proof.receipt_ids
67
+ : []));
68
+ const receiptsToSettle = params.force
69
+ ? params.pendingReceipts
70
+ : params.pendingReceipts.filter((receipt) => trustedReceiptIds.has(receipt.id));
71
+ await Promise.all(receiptsToSettle.map((receipt) => markMergeReceiptReconciled(params.gitWorkspaceRoot ?? process.cwd(), receipt, { requireExisting: true })));
72
+ return receiptsToSettle.length;
73
+ }
8
74
  /**
9
75
  * Preview or apply post-merge history reconciliation and immediately validate
10
76
  * the two merge-critical invariants. The default remains explicit and safe:
@@ -13,9 +79,22 @@ import { listMergeReceipts, markMergeReceiptReconciled, partitionMergeReceipts,
13
79
  */
14
80
  export async function runMergeReconcile(options, global) {
15
81
  const dryRun = options.dryRun === true;
16
- const pendingReceipts = await listMergeReceipts(process.cwd(), {
82
+ const pmRoot = resolvePmRoot(process.cwd(), global.path);
83
+ const gitWorkspaceRoot = await findGitWorkspaceRoot(pmRoot);
84
+ const receiptEvidence = await inspectMergeReceiptEvidence(gitWorkspaceRoot ?? process.cwd(), {
17
85
  includeLossless: true,
86
+ pmRoot,
18
87
  });
88
+ if (receiptEvidence.invalid_evidence_count > 0) {
89
+ throw new PmCliError(`Merge reconciliation refused ${receiptEvidence.invalid_evidence_count} invalid receipt evidence file(s).`, EXIT_CODE.CONFLICT, {
90
+ code: "merge_receipt_evidence_invalid",
91
+ required: "Every clone-local and durable receipt candidate must pass bounded-file, schema, and identity validation.",
92
+ recovery: {
93
+ suggested_retry: "pm health --check-only --full",
94
+ },
95
+ });
96
+ }
97
+ const pendingReceipts = receiptEvidence.receipts;
19
98
  const { pendingDecisions: discardedReceipts } = partitionMergeReceipts(pendingReceipts);
20
99
  if (!dryRun && options.force !== true && discardedReceipts.length > 0) {
21
100
  throw new PmCliError(`Merge reconciliation would accept ${discardedReceipts.length} receipt(s) containing discarded scalar values. Review pm merge report and rerun with --force only after deciding which values to retain or re-apply.`, EXIT_CODE.CONFLICT, {
@@ -29,11 +108,13 @@ export async function runMergeReconcile(options, global) {
29
108
  },
30
109
  });
31
110
  }
32
- const receiptsByItem = pendingReceipts.reduce((groups, receipt) => {
33
- (groups[receipt.item_id] ??= []).push(receipt);
34
- return groups;
35
- }, {});
36
- const auditContextById = Object.fromEntries(Object.entries(receiptsByItem).map(([id, receipts]) => [
111
+ const receiptsByItem = new Map();
112
+ for (const receipt of pendingReceipts) {
113
+ const receipts = receiptsByItem.get(receipt.item_id) ?? [];
114
+ receipts.push(receipt);
115
+ receiptsByItem.set(receipt.item_id, receipts);
116
+ }
117
+ const auditContextById = Object.fromEntries([...receiptsByItem].map(([id, receipts]) => [
37
118
  id,
38
119
  {
39
120
  merge: {
@@ -41,6 +122,10 @@ export async function runMergeReconcile(options, global) {
41
122
  },
42
123
  },
43
124
  ]));
125
+ const mergeReceiptProofById = Object.fromEntries([...receiptsByItem].map(([id, receipts]) => [
126
+ id,
127
+ { gitWorkspaceRoot, receipts },
128
+ ]));
44
129
  const repair = await runHistoryRepairAll({
45
130
  dryRun,
46
131
  author: options.author ?? global.author,
@@ -49,51 +134,36 @@ export async function runMergeReconcile(options, global) {
49
134
  force: options.force,
50
135
  auditOperation: "merge_reconcile",
51
136
  auditContextById,
137
+ ...(pendingReceipts.length > 0 ? { mergeReceiptProofById } : {}),
52
138
  }, global);
53
139
  const representedIds = new Set(repair.streams.map((stream) => stream.id));
54
- const receiptOnlyIds = Object.keys(receiptsByItem).filter((id) => !representedIds.has(id));
55
- const receiptOnlyResults = await Promise.allSettled(receiptOnlyIds.map((id) => runHistoryRepair(id, {
140
+ const receiptOnlyIds = [...receiptsByItem.keys()].filter((id) => !representedIds.has(id));
141
+ const receiptOnlyResults = await mapWithFixedConcurrency(receiptOnlyIds, RECEIPT_ONLY_REPAIR_CONCURRENCY, (id) => runReceiptOnlyRepair({
142
+ id,
56
143
  dryRun,
57
- author: options.author ?? global.author,
58
- message: options.message ?? "record field-aware branch merge provenance",
59
- force: options.force,
60
- auditOperation: "merge_reconcile",
144
+ options,
145
+ global,
61
146
  auditContext: auditContextById[id],
62
- forceAuditEntry: true,
63
- }, global)));
64
- receiptOnlyResults.forEach((settled, index) => {
65
- const id = receiptOnlyIds[index];
66
- if (settled.status === "fulfilled") {
67
- const outcome = settled.value.changed ? "repaired" : "skipped_clean";
68
- repair.streams.push({
69
- id,
70
- outcome,
71
- entries_rehashed: settled.value.history.entries_rehashed,
72
- entries_patch_repaired: settled.value.history.entries_patch_repaired,
73
- reconciled_with_item: settled.value.history.reconciled_with_item,
74
- warnings: settled.value.warnings,
75
- });
76
- repair.totals[outcome] += 1;
77
- }
78
- else {
79
- repair.streams.push({
80
- id,
81
- outcome: "failed",
82
- error: settled.reason instanceof Error
83
- ? settled.reason.message
84
- : String(settled.reason),
85
- });
86
- repair.totals.failed += 1;
87
- }
147
+ mergeReceiptProof: mergeReceiptProofById[id],
148
+ }));
149
+ appendReceiptOnlyResults({
150
+ receiptOnlyIds,
151
+ results: receiptOnlyResults,
152
+ repair,
153
+ });
154
+ const reconciledReceiptCount = await settleProvenReceipts({
155
+ dryRun,
156
+ force: options.force === true,
157
+ pendingReceipts,
158
+ repair,
159
+ gitWorkspaceRoot,
88
160
  });
89
- let reconciledReceiptCount = 0;
90
- if (!dryRun && repair.totals.failed === 0) {
91
- await Promise.all(pendingReceipts.map((receipt) => markMergeReceiptReconciled(process.cwd(), receipt)));
92
- reconciledReceiptCount = pendingReceipts.length;
93
- }
94
161
  const validation = await runValidate({ checkHistoryDrift: true, checkStorageIntegrity: true }, global);
95
162
  const mergeChecksGreen = validation.checks.every((check) => check.status === "ok");
96
- const ok = repair.totals.failed === 0 && mergeChecksGreen;
163
+ const receiptSettlementComplete = dryRun
164
+ ? pendingReceipts.length === 0
165
+ : reconciledReceiptCount === pendingReceipts.length;
166
+ const ok = repair.totals.failed === 0 && mergeChecksGreen && receiptSettlementComplete;
97
167
  const guidance = dryRun
98
168
  ? [
99
169
  "Review repair.streams, then rerun pm merge reconcile without --dry-run to apply audited repairs.",
@@ -123,4 +193,4 @@ export async function runMergeReconcile(options, global) {
123
193
  };
124
194
  }
125
195
  //# sourceMappingURL=reconcile.js.map
126
- //# debugId=c5620467-6c8c-5702-b742-57fba9e302c5
196
+ //# debugId=c3ec7d9d-f276-5e97-8e97-507925e5d33a