@unbrained/pm-cli 2026.9.5 → 2026.9.6

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 (185) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +32 -2
  3. package/dist/cli/main.js +10 -8
  4. package/dist/cli/register-operations.js +13 -8
  5. package/dist/cli-bundle/bundle-manifest.json +150 -158
  6. package/dist/cli-bundle/chunks/chunk-AZDFJGH6.js +9 -0
  7. package/dist/cli-bundle/chunks/{chunk-WWAEGW4V.js → chunk-BAX2BYUW.js} +66 -66
  8. package/dist/cli-bundle/chunks/{chunk-IBYREGSP.js → chunk-CKOL5U7J.js} +51 -51
  9. package/dist/cli-bundle/chunks/{chunk-ZRAQS3UA.js → chunk-JT6XF6GE.js} +2 -2
  10. package/dist/cli-bundle/chunks/{chunk-HUAQITVW.js → chunk-JX4DH4WC.js} +2 -2
  11. package/dist/cli-bundle/chunks/{chunk-QPZHQY2O.js → chunk-NPN5I67J.js} +2 -2
  12. package/dist/cli-bundle/chunks/{chunk-66VPKFP2.js → chunk-OF76PSDQ.js} +2 -2
  13. package/dist/cli-bundle/chunks/chunk-QUWFABDM.js +232 -0
  14. package/dist/cli-bundle/chunks/{chunk-3HBDTJRK.js → chunk-RWWBOWJ5.js} +2 -2
  15. package/dist/cli-bundle/chunks/chunk-WGAOYSYH.js +40 -0
  16. package/dist/cli-bundle/chunks/{chunk-SF6SM6U5.js → chunk-WI2AYPA7.js} +2 -2
  17. package/dist/cli-bundle/chunks/{register-list-query-AYKGODWT.js → register-list-query-5JL52D33.js} +2 -2
  18. package/dist/cli-bundle/chunks/{register-mutation-YZYG53SQ.js → register-mutation-ZPG7KMVP.js} +2 -2
  19. package/dist/cli-bundle/chunks/register-operations-I5W3KMGB.js +2 -0
  20. package/dist/cli-bundle/chunks/{register-setup-UJCRVGBG.js → register-setup-H4SVAVX7.js} +2 -2
  21. package/dist/cli-bundle/focused-chunks/{chunk-QL66QNWG.js → chunk-3BZEOXLT.js} +2 -2
  22. package/dist/cli-bundle/focused-chunks/chunk-AL5ON6K6.js +2 -0
  23. package/dist/cli-bundle/focused-chunks/chunk-CHC2IDXA.js +6 -0
  24. package/dist/cli-bundle/focused-chunks/chunk-CK6QFAPZ.js +20 -0
  25. package/dist/cli-bundle/focused-chunks/{chunk-WOCIPMW7.js → chunk-EZR3GHFE.js} +2 -2
  26. package/dist/cli-bundle/focused-chunks/chunk-H2WJWK7L.js +56 -0
  27. package/dist/cli-bundle/focused-chunks/{chunk-3ASU6WGI.js → chunk-LIJIA4TY.js} +2 -2
  28. package/dist/cli-bundle/focused-chunks/{chunk-H5FOGAVD.js → chunk-M6PBRFSH.js} +2 -2
  29. package/dist/cli-bundle/focused-chunks/{chunk-B2TTQGFG.js → chunk-NTPY5GSO.js} +2 -2
  30. package/dist/cli-bundle/focused-chunks/chunk-P5O7MP5Y.js +2 -0
  31. package/dist/cli-bundle/focused-chunks/chunk-PDMW2E36.js +177 -0
  32. package/dist/cli-bundle/focused-chunks/{chunk-YEF5PQQ2.js → chunk-PEFABF7E.js} +2 -2
  33. package/dist/cli-bundle/focused-chunks/{chunk-KWWCBWFO.js → chunk-QTVCLMSZ.js} +2 -2
  34. package/dist/cli-bundle/focused-chunks/{chunk-4OSHLNUE.js → chunk-QTZRBUYA.js} +2 -2
  35. package/dist/cli-bundle/focused-chunks/chunk-SGQST3PQ.js +12 -0
  36. package/dist/cli-bundle/focused-chunks/chunk-UKHASMMP.js +10 -0
  37. package/dist/cli-bundle/focused-chunks/chunk-ZPEKQHOC.js +2 -0
  38. package/dist/cli-bundle/main.js +20 -20
  39. package/dist/cli-bundle/sdk-authoring.js +1 -1
  40. package/dist/cli-bundle/sdk-contracts.js +2 -2
  41. package/dist/cli-bundle/sdk-core.js +37 -37
  42. package/dist/cli-bundle/sdk-governance.js +1 -1
  43. package/dist/cli-bundle/sdk-graph.js +1 -1
  44. package/dist/cli-bundle/sdk-merge.js +42 -42
  45. package/dist/cli-bundle/sdk-query.js +1 -1
  46. package/dist/cli-bundle/sdk-runtime.js +1 -1
  47. package/dist/cli-bundle/sdk-testing.js +1 -1
  48. package/dist/cli-bundle/sdk.js +9 -9
  49. package/dist/core/extensions/module-graph-snapshot.js +5 -4
  50. package/dist/core/fs/host-environment-errors.d.ts +27 -0
  51. package/dist/core/fs/host-environment-errors.js +142 -0
  52. package/dist/core/output/mutation-projection.js +12 -5
  53. package/dist/core/store/item-metadata-query-index.js +13 -10
  54. package/dist/core/store/item-metadata-read-work.d.ts +19 -0
  55. package/dist/core/store/item-metadata-read-work.js +44 -0
  56. package/dist/core/store/item-store.js +6 -2
  57. package/dist/core/validate/fix-planning.d.ts +1 -1
  58. package/dist/core/validate/fix-planning.js +2 -2
  59. package/dist/core/validate/missing-link-owners.d.ts +1 -1
  60. package/dist/core/validate/missing-link-owners.js +5 -3
  61. package/dist/core/validate/stale-file-classification.d.ts +6 -4
  62. package/dist/core/validate/stale-file-classification.js +20 -5
  63. package/dist/mcp/server.js +12 -5
  64. package/dist/sdk/cli-contracts/flag-contracts.d.ts +2 -0
  65. package/dist/sdk/cli-contracts/flag-contracts.js +7 -5
  66. package/dist/sdk/cli-contracts/flag-lexicon-contracts.js +4 -3
  67. package/dist/sdk/cli-contracts/grammar-contracts.js +11 -2
  68. package/dist/sdk/cli-contracts/runtime-contracts.js +5 -2
  69. package/dist/sdk/cli-contracts/schemas/assurance-definition.d.ts +254 -5
  70. package/dist/sdk/cli-contracts/schemas/assurance-definition.js +52 -2
  71. package/dist/sdk/cli-contracts/tool-parameter-tables.js +18 -6
  72. package/dist/sdk/cli-contracts/tool-schema.d.ts +2 -2
  73. package/dist/sdk/cli-contracts/tool-schema.js +17 -5
  74. package/dist/sdk/context/usage-ledger.d.ts +41 -0
  75. package/dist/sdk/context/usage-ledger.js +186 -0
  76. package/dist/sdk/context-usage.d.ts +18 -9
  77. package/dist/sdk/context-usage.js +37 -112
  78. package/dist/sdk/dependency-provenance.d.ts +7 -0
  79. package/dist/sdk/dependency-provenance.js +14 -4
  80. package/dist/sdk/duplicates.d.ts +2 -0
  81. package/dist/sdk/duplicates.js +3 -2
  82. package/dist/sdk/environment/host-environment-errors.d.ts +5 -27
  83. package/dist/sdk/environment/host-environment-errors.js +4 -137
  84. package/dist/sdk/extension/install-runtime.js +9 -6
  85. package/dist/sdk/extension/install-snapshot.js +7 -6
  86. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +8 -8
  87. package/dist/sdk/governance/assurance-action.d.ts +3 -2
  88. package/dist/sdk/governance/assurance-action.js +23 -11
  89. package/dist/sdk/governance/assurance-runtime.d.ts +2 -0
  90. package/dist/sdk/governance/assurance-runtime.js +6 -3
  91. package/dist/sdk/governance/defect-recurrence-coverage.d.ts +93 -0
  92. package/dist/sdk/governance/defect-recurrence-coverage.js +189 -0
  93. package/dist/sdk/governance/defect-recurrence-graph.d.ts +33 -0
  94. package/dist/sdk/governance/defect-recurrence-graph.js +121 -0
  95. package/dist/sdk/governance/defect-recurrence.d.ts +8 -0
  96. package/dist/sdk/governance/defect-recurrence.js +118 -17
  97. package/dist/sdk/governance/health.d.ts +17 -2
  98. package/dist/sdk/governance/health.js +31 -10
  99. package/dist/sdk/governance/linked-file-report.d.ts +41 -0
  100. package/dist/sdk/governance/linked-file-report.js +72 -0
  101. package/dist/sdk/governance/validate.d.ts +5 -2
  102. package/dist/sdk/governance/validate.js +21 -37
  103. package/dist/sdk/governance.d.ts +2 -0
  104. package/dist/sdk/governance.js +4 -2
  105. package/dist/sdk/history-repair.d.ts +7 -0
  106. package/dist/sdk/history-repair.js +48 -13
  107. package/dist/sdk/lifecycle/close.d.ts +8 -0
  108. package/dist/sdk/lifecycle/close.js +16 -8
  109. package/dist/sdk/lifecycle/create.js +9 -12
  110. package/dist/sdk/lifecycle/update.js +12 -12
  111. package/dist/sdk/lifecycle-policy.d.ts +3 -0
  112. package/dist/sdk/lifecycle-policy.js +2 -2
  113. package/dist/sdk/merge/abandoned-receipts.d.ts +15 -0
  114. package/dist/sdk/merge/abandoned-receipts.js +47 -0
  115. package/dist/sdk/merge/driver.js +4 -2
  116. package/dist/sdk/merge/index.d.ts +1 -0
  117. package/dist/sdk/merge/index.js +2 -2
  118. package/dist/sdk/merge/receipt-operation.d.ts +15 -0
  119. package/dist/sdk/merge/receipt-operation.js +97 -0
  120. package/dist/sdk/merge/receipt-schema.d.ts +20 -0
  121. package/dist/sdk/merge/receipt-schema.js +398 -0
  122. package/dist/sdk/merge/receipts.d.ts +16 -7
  123. package/dist/sdk/merge/receipts.js +54 -340
  124. package/dist/sdk/merge/reconcile.d.ts +2 -0
  125. package/dist/sdk/merge/reconcile.js +11 -4
  126. package/dist/sdk/public-surface.json +1 -1
  127. package/dist/sdk/query/duplicate-candidates.d.ts +7 -0
  128. package/dist/sdk/query/duplicate-candidates.js +71 -0
  129. package/dist/sdk/query/get.js +7 -7
  130. package/dist/sdk/query.d.ts +2 -0
  131. package/dist/sdk/query.js +4 -2
  132. package/dist/sdk/read-output-session.js +5 -3
  133. package/dist/sdk/runtime.js +4 -2
  134. package/dist/sdk/similarity.d.ts +24 -3
  135. package/dist/sdk/similarity.js +50 -55
  136. package/docs/AGENT_GUIDE.md +23 -2
  137. package/docs/BOUNDED_CONTEXT_READS.md +185 -0
  138. package/docs/CLOSE_EVIDENCE_RECOVERY.md +35 -0
  139. package/docs/COMMANDS.md +30 -8
  140. package/docs/CONTEXT_RELEVANCE.md +16 -4
  141. package/docs/DEFECT_RECURRENCE.md +80 -6
  142. package/docs/EXTENSIONS.md +18 -19
  143. package/docs/MERGE_SAFETY.md +50 -0
  144. package/docs/OUTPUT_PROJECTION_CONTRACTS.md +66 -0
  145. package/docs/PR_REVIEW_LOOP.md +18 -3
  146. package/docs/README.md +18 -1
  147. package/docs/RELEASING.md +6 -2
  148. package/docs/SDK_LIFECYCLE.md +27 -0
  149. package/docs/SDK_RUNTIME_BOUNDARIES.md +11 -0
  150. package/docs/generated/FLAG_LEXICON_BUDGETS.md +1 -1
  151. package/marketplace.json +2 -2
  152. package/package.json +6 -3
  153. package/packages/pm-beads/package.json +1 -1
  154. package/packages/pm-calendar/package.json +1 -1
  155. package/packages/pm-command-kit/package.json +1 -1
  156. package/packages/pm-digital-twin/package.json +1 -1
  157. package/packages/pm-governance-audit/README.md +10 -1
  158. package/packages/pm-governance-audit/extensions/governance-audit/dedupe-audit.ts +50 -65
  159. package/packages/pm-governance-audit/extensions/governance-audit/index.ts +2 -2
  160. package/packages/pm-governance-audit/extensions/governance-audit/sdk.ts +1 -0
  161. package/packages/pm-governance-audit/package.json +1 -1
  162. package/packages/pm-guide-shell/package.json +1 -1
  163. package/packages/pm-kanban/package.json +1 -1
  164. package/packages/pm-lifecycle-hooks/package.json +1 -1
  165. package/packages/pm-linked-test-adapters/package.json +1 -1
  166. package/packages/pm-search-advanced/package.json +1 -1
  167. package/packages/pm-templates/package.json +1 -1
  168. package/packages/pm-todos/package.json +1 -1
  169. package/packages/pm-vcs/package.json +1 -1
  170. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  171. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  172. package/dist/cli-bundle/chunks/chunk-LUI4E32I.js +0 -231
  173. package/dist/cli-bundle/chunks/chunk-SVK5Y64D.js +0 -40
  174. package/dist/cli-bundle/chunks/chunk-V5GBD5KV.js +0 -9
  175. package/dist/cli-bundle/chunks/register-operations-KFSCC7QV.js +0 -2
  176. package/dist/cli-bundle/focused-chunks/chunk-24IG6ONG.js +0 -2
  177. package/dist/cli-bundle/focused-chunks/chunk-3ITLIBKS.js +0 -56
  178. package/dist/cli-bundle/focused-chunks/chunk-3NQD5YDG.js +0 -2
  179. package/dist/cli-bundle/focused-chunks/chunk-65N6JHAN.js +0 -2
  180. package/dist/cli-bundle/focused-chunks/chunk-GUBM3RGY.js +0 -20
  181. package/dist/cli-bundle/focused-chunks/chunk-JX3RCVCX.js +0 -2
  182. package/dist/cli-bundle/focused-chunks/chunk-MV752SUC.js +0 -12
  183. package/dist/cli-bundle/focused-chunks/chunk-PKIPMCFS.js +0 -6
  184. package/dist/cli-bundle/focused-chunks/chunk-RJMR2JIF.js +0 -9
  185. package/dist/cli-bundle/focused-chunks/chunk-WIUPWIZR.js +0 -177
@@ -0,0 +1,7 @@
1
+ import type { PreparedSimilarityText } from "../similarity-scoring.js";
2
+ /** Declared safety bound applies to retained pairs, never silently to recall. */
3
+ export declare const MAX_DUPLICATE_CANDIDATE_PAIRS = 1000000;
4
+ /** Generate every potentially qualifying pair once, with an explicit exhaustive oracle. */
5
+ export declare function collectDuplicateCandidatePairs(items: readonly {
6
+ prepared: PreparedSimilarityText;
7
+ }[], maxPairs?: number, threshold?: number, exhaustive?: boolean): Set<string>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @module sdk/query/duplicate-candidates
3
+ *
4
+ * Lossless prefix filtering for Jaccard joins. A globally ordered prefix of
5
+ * length n-ceil(threshold*n)+1 must intersect the corresponding prefix of
6
+ * every qualifying set. Exact titles and issue codes remain independent
7
+ * signals, including titles with no word tokens.
8
+ */
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]="d425cf45-5b62-5613-a5ce-78da6b0e201b")}catch(e){}}();
11
+ import { EXIT_CODE } from "../../core/shared/constants.js";
12
+ import { PmCliError } from "../../core/shared/errors.js";
13
+ /** Declared safety bound applies to retained pairs, never silently to recall. */
14
+ export const MAX_DUPLICATE_CANDIDATE_PAIRS = 1_000_000;
15
+ /** Generate every potentially qualifying pair once, with an explicit exhaustive oracle. */
16
+ export function collectDuplicateCandidatePairs(items, maxPairs = MAX_DUPLICATE_CANDIDATE_PAIRS, threshold = 0.8, exhaustive = false) {
17
+ const candidates = new Set();
18
+ /** Deduplicate a candidate pair and refuse before publishing an over-budget result. */
19
+ const add = (left, right) => {
20
+ candidates.add(`${left}:${right}`);
21
+ if (candidates.size > maxPairs) {
22
+ throw new PmCliError(`Duplicate sweep exceeded ${maxPairs} candidate pairs; narrow it with statuses or since.`, EXIT_CODE.CONFLICT, { code: "duplicate_sweep_cost_limit", required: "Narrow the duplicate query or increase its similarity threshold. No partial result was returned." });
23
+ }
24
+ };
25
+ if (exhaustive || threshold === 0) {
26
+ for (let right = 0; right < items.length; right += 1) {
27
+ for (let left = 0; left < right; left += 1)
28
+ add(left, right);
29
+ }
30
+ return candidates;
31
+ }
32
+ const frequencies = new Map();
33
+ for (const { prepared } of items) {
34
+ for (const token of prepared.tokens)
35
+ frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
36
+ }
37
+ const exact = new Map();
38
+ const codes = new Map();
39
+ const prefixes = new Map();
40
+ for (const [right, { prepared }] of items.entries()) {
41
+ const tokens = [...prepared.tokens].sort((left, other) => frequencies.get(left) - frequencies.get(other) || left.localeCompare(other));
42
+ // An epsilon expands rather than shrinks the prefix at floating boundaries.
43
+ const prefixLength = tokens.length - Math.ceil(threshold * tokens.length - 1e-12) + 1;
44
+ const signals = [
45
+ { index: exact, keys: [prepared.normalized], lengthFilter: false },
46
+ { index: codes, keys: threshold <= 0.99 ? prepared.issueCodes : [], lengthFilter: false },
47
+ { index: prefixes, keys: tokens.length === 0 ? [""] : tokens.slice(0, prefixLength), lengthFilter: true },
48
+ ];
49
+ for (const signal of signals) {
50
+ appendPostingCandidates(items, right, signal, threshold, add);
51
+ }
52
+ }
53
+ return candidates;
54
+ }
55
+ /** Join one exact/code/prefix posting list, applying the Jaccard length bound only to tokens. */
56
+ function appendPostingCandidates(items, right, signal, threshold, add) {
57
+ const rightLength = items[right].prepared.tokens.length;
58
+ for (const key of signal.keys) {
59
+ const posting = signal.index.get(key) ?? [];
60
+ for (const left of posting) {
61
+ const leftLength = items[left].prepared.tokens.length;
62
+ if (signal.lengthFilter && Math.min(leftLength, rightLength) / Math.max(leftLength, rightLength) + 1e-12 < threshold)
63
+ continue;
64
+ add(left, right);
65
+ }
66
+ posting.push(right);
67
+ signal.index.set(key, posting);
68
+ }
69
+ }
70
+ //# sourceMappingURL=duplicate-candidates.js.map
71
+ //# debugId=d425cf45-5b62-5613-a5ce-78da6b0e201b
@@ -4,7 +4,7 @@
4
4
  * Implements the SDK-owned item query shared by every surface.
5
5
  */
6
6
 
7
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="16112e3d-b640-5114-a2c2-5effc655fe50")}catch(e){}}();
7
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="88732a1d-ae5a-5dfb-93d6-9659cb6f05c3")}catch(e){}}();
8
8
  import { assertInitializedTracker } from "../environment/tracker-preflight.js";
9
9
  import { getActiveExtensionRegistrations, toItemRecord, resolveItemTypeRegistry, resolveRuntimeFieldRegistry, resolveRuntimeStatusRegistry, EXIT_CODE, TYPE_TO_FOLDER, PmCliError, buildItemNotFoundError, listAllItemMetadataLight, locateItem, readLocatedItem, getHistoryPath, resolvePmRoot, readSettings, resolveAuthor, } from "../runtime-primitives.js";
10
10
  import { readHistoryEntries } from "../history-read.js";
@@ -337,14 +337,14 @@ function attachGetLinked(result, context, fields, includeLinked) {
337
337
  docs: includeLinkedDocs ? (context.metadata.docs ?? []) : [],
338
338
  };
339
339
  }
340
- async function buildGetChildrenRollup(context, includeChildren, includeEmpty, includeSample) {
340
+ /** Enumerate hierarchy metadata only when the caller explicitly requested a child rollup. */
341
+ async function buildGetChildrenRollup(context, includeChildren) {
341
342
  if (!includeChildren) {
342
343
  return undefined;
343
344
  }
344
345
  const statusRegistry = resolveRuntimeStatusRegistry(context.settings.schema);
345
346
  const corpus = await listAllItemMetadataLight(context.pmRoot, context.settings.item_format, context.typeToFolder, undefined, context.settings.schema);
346
- const rollup = buildItemChildrenRollup(context.locatedId, corpus, statusRegistry, includeSample ? undefined : 0);
347
- return rollup.count > 0 || includeEmpty ? rollup : undefined;
347
+ return buildItemChildrenRollup(context.locatedId, corpus, statusRegistry);
348
348
  }
349
349
  function attachGetSchedule(result, context, fields, includeSchedule) {
350
350
  if (!includeSchedule) {
@@ -395,7 +395,7 @@ export async function runGet(id, global, options = {}) {
395
395
  const includeChildren = context.historical === undefined &&
396
396
  (projection.fieldProjection
397
397
  ? fieldsIncludeRoot(projection.fields, "children")
398
- : projection.depth !== "brief" &&
398
+ : projection.depth === "deep" &&
399
399
  shouldAutoIncludeGetChildren(context.metadata.type));
400
400
  const includeSchedule = projection.fieldProjection
401
401
  ? fieldsIncludeRoot(projection.fields, "schedule")
@@ -414,7 +414,7 @@ export async function runGet(id, global, options = {}) {
414
414
  result.claim_state = claimState;
415
415
  }
416
416
  attachGetSchedule(result, context, projection.fields, includeSchedule);
417
- const children = await buildGetChildrenRollup(context, includeChildren, projection.fieldProjection, projection.fieldProjection || projection.depth === "deep");
417
+ const children = await buildGetChildrenRollup(context, includeChildren);
418
418
  if (children !== undefined) {
419
419
  result.children = children;
420
420
  }
@@ -448,4 +448,4 @@ export const _testOnlyGetCommand = {
448
448
  shouldAutoIncludeGetChildren,
449
449
  };
450
450
  //# sourceMappingURL=get.js.map
451
- //# debugId=16112e3d-b640-5114-a2c2-5effc655fe50
451
+ //# debugId=88732a1d-ae5a-5dfb-93d6-9659cb6f05c3
@@ -28,3 +28,5 @@ export * from "./context-signal-store.js";
28
28
  export * from "./context-usage.js";
29
29
  export * from "./workspace-memory.js";
30
30
  export * from "./duplicates.js";
31
+ export { analyzeDuplicateItems, type DuplicateItemInput, type DuplicateItemsAnalysis } from "./similarity.js";
32
+ export { measureItemMetadataReadWork, type ItemMetadataReadWork } from "../core/store/item-metadata-read-work.js";
package/dist/sdk/query.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Provides focused read, search, pagination, and context-ranking primitives.
5
5
  */
6
6
 
7
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="214c6bc1-313b-53c1-883a-d2d0536f8aea")}catch(e){}}();
7
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="affe6060-5129-5914-9d4e-46c300d49080")}catch(e){}}();
8
8
  export * from "./query/list.js";
9
9
  export * from "./query/complete-list.js";
10
10
  export * from "./query/search.js";
@@ -30,5 +30,7 @@ export * from "./context-signal-store.js";
30
30
  export * from "./context-usage.js";
31
31
  export * from "./workspace-memory.js";
32
32
  export * from "./duplicates.js";
33
+ export { analyzeDuplicateItems } from "./similarity.js";
34
+ export { measureItemMetadataReadWork } from "../core/store/item-metadata-read-work.js";
33
35
  //# sourceMappingURL=query.js.map
34
- //# debugId=214c6bc1-313b-53c1-883a-d2d0536f8aea
36
+ //# debugId=affe6060-5129-5914-9d4e-46c300d49080
@@ -5,7 +5,7 @@
5
5
  * budgets and resolvable references to item facts served by earlier reads.
6
6
  */
7
7
 
8
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="642f6dec-c0b2-5377-a401-dfa4f8ef6a2a")}catch(e){}}();
8
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bb73f77d-d372-5d68-8ef2-90b208129f21")}catch(e){}}();
9
9
  import { EXIT_CODE } from "../core/shared/constants.js";
10
10
  import { PmCliError } from "../core/shared/errors.js";
11
11
  import { mapReadOutputRows, readOutputRowCollections, } from "./read-output-rows.js";
@@ -119,7 +119,9 @@ function summarizeReadOutputSessionRows(result) {
119
119
  continue;
120
120
  }
121
121
  const itemId = itemIdFromRow(row);
122
- if (itemId !== undefined)
122
+ // Activity can contain workspace pseudo-identities. Preserve those rows
123
+ // in the response, but only carry ids the next invocation can consume.
124
+ if (itemId !== undefined && ITEM_ID_PATTERN.test(itemId))
123
125
  deliveredIds.add(itemId);
124
126
  }
125
127
  }
@@ -204,4 +206,4 @@ export function readOutputSessionRemainingTokens(state) {
204
206
  return Math.max(0, state.token_budget - state.spent_tokens);
205
207
  }
206
208
  //# sourceMappingURL=read-output-session.js.map
207
- //# debugId=642f6dec-c0b2-5377-a401-dfa4f8ef6a2a
209
+ //# debugId=bb73f77d-d372-5d68-8ef2-90b208129f21
@@ -4,7 +4,7 @@
4
4
  * Defines public SDK APIs and package-author helpers for Runtime.
5
5
  */
6
6
 
7
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="099c7cda-4a92-553c-ad60-00e185b5971b")}catch(e){}}();
7
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="422ff60b-97b1-5477-a7b8-ed4f983155e0")}catch(e){}}();
8
8
  export { PM_GITIGNORE_END, PM_GITIGNORE_START, ensurePmGitignore, getPmGitignoreBlock, } from "./workspace.js";
9
9
  export { SEARCH_EXTENSION_FLAG_DEFINITIONS } from "./extension-contracts.js";
10
10
  export * from "./cli-contracts/agent-output-contracts.js";
@@ -1770,11 +1770,13 @@ const SDK_ACTION_HANDLERS = {
1770
1770
  return runGet(id, ctx.global, applyContextIntentProjection("get", ctx.options, [id]));
1771
1771
  },
1772
1772
  search: runMcpSearchAction,
1773
+ /** Normalize MCP duplicate controls before delegating to the shared SDK action. */
1773
1774
  duplicates: (ctx) => {
1774
1775
  const status = typeof ctx.options.status === "string"
1775
1776
  ? [ctx.options.status]
1776
1777
  : readStringArray(ctx.options.status);
1777
1778
  return runDuplicates(ctx.global, {
1779
+ exhaustive: ctx.options.exhaustive === true,
1778
1780
  ...(status.length === 0 ? {} : { status }),
1779
1781
  since: readString(ctx.options, "since"),
1780
1782
  threshold: typeof ctx.options.threshold === "number"
@@ -1924,4 +1926,4 @@ async function loadWorkspaceExtensionRegistrations(pmRoot, settings, cwd) {
1924
1926
  }
1925
1927
  }
1926
1928
  //# sourceMappingURL=runtime.js.map
1927
- //# debugId=099c7cda-4a92-553c-ad60-00e185b5971b
1929
+ //# debugId=422ff60b-97b1-5477-a7b8-ed4f983155e0
@@ -1,3 +1,4 @@
1
+ import { collectDuplicateCandidatePairs } from "./query/duplicate-candidates.js";
1
2
  import type { ItemMetadata } from "../types/index.js";
2
3
  import { type PreparedSimilarityText } from "./similarity-scoring.js";
3
4
  export { jaccardSimilarity, normalizeSimilarityText, prepareSimilarityText, scoreItemSimilarity, scorePreparedItemSimilarity, tokenizeSimilarityText, type ItemSimilarityScore, type PreparedSimilarityText, } from "./similarity-scoring.js";
@@ -51,6 +52,8 @@ export interface SimilarItemsResult {
51
52
  }
52
53
  /** Query controls for one all-status duplicate-cluster sweep. */
53
54
  export interface FindDuplicateClustersOptions extends FindSimilarItemsOptions {
55
+ /** Compare every pair as an exact oracle; retains the declared safety bound. */
56
+ exhaustive?: boolean;
54
57
  /** Lifecycle statuses to include; omit to inspect every status. */
55
58
  statuses?: readonly string[];
56
59
  /** Inclusive ISO timestamp lower bound on item creation. */
@@ -97,6 +100,14 @@ export interface DuplicateClustersResult {
97
100
  cost: {
98
101
  /** Items retained after filters. */
99
102
  item_count: number;
103
+ /** Algorithm used to generate candidates before canonical scoring. */
104
+ algorithm: "prefix_exact" | "exhaustive";
105
+ /** Pairs in the exhaustive population, including pairs pruned without scoring. */
106
+ possible_pairs: number;
107
+ /** Safety ceiling, exceeded only by an explicit refusal. */
108
+ pair_limit: number;
109
+ /** Every qualifying pair is retained by candidate generation. */
110
+ recall_guarantee: "exact";
100
111
  /** Candidate pairs sharing at least one deterministic signal. */
101
112
  candidate_pairs: number;
102
113
  /** Candidate pairs scored. */
@@ -104,7 +115,7 @@ export interface DuplicateClustersResult {
104
115
  };
105
116
  }
106
117
  interface PreparedDuplicateItem {
107
- item: ItemMetadata;
118
+ item: Pick<ItemMetadata, "id" | "title" | "status" | "type">;
108
119
  prepared: PreparedSimilarityText;
109
120
  }
110
121
  /** Create/copy governance envelope attached when likely duplicates exist. */
@@ -131,7 +142,6 @@ export interface SimilarityRecovery {
131
142
  }
132
143
  /** Render compact warning tokens shared by create and copy results. */
133
144
  export declare function similarityAdvisoryWarnings(advisory: SimilarityAdvisory | undefined): string[];
134
- declare function collectDuplicateCandidatePairs(items: PreparedDuplicateItem[], maxPairEvaluations?: number): Set<string>;
135
145
  /** Internal deterministic seams used to prove bounded batch behavior without building a million-pair fixture. */
136
146
  export declare const _testOnlySimilarity: {
137
147
  buildDuplicateClusters: typeof buildDuplicateClusters;
@@ -142,10 +152,21 @@ declare function createDuplicateUnionFind(size: number): {
142
152
  parent: number[];
143
153
  findRoot: (index: number) => number;
144
154
  };
155
+ /** Group unioned endpoints and their scored evidence once, then deterministically rank and limit complete components. */
145
156
  declare function buildDuplicateClusters(items: PreparedDuplicateItem[], matches: DuplicateClusterMatch[], union: ReturnType<typeof createDuplicateUnionFind>, limit: number): DuplicateCluster[];
146
157
  /** Find likely existing duplicates through an index-first bounded read. */
147
158
  export declare function findSimilarItems(candidate: SimilarItemCandidate, options?: FindSimilarItemsOptions): Promise<SimilarItemsResult>;
148
- /** Find connected duplicate clusters with one metadata read and one prepared representation per item. */
159
+ /** Minimal metadata accepted from package-owned or remote project stores. */
160
+ export type DuplicateItemInput = Pick<ItemMetadata, "id" | "title" | "status" | "type">;
161
+ /** Exact in-memory duplicate analysis, independent of a filesystem workspace. */
162
+ export type DuplicateItemsAnalysis = Omit<DuplicateClustersResult, "source" | "filters">;
163
+ /**
164
+ * Cluster caller-owned metadata using the same scorer and safety bounds as the CLI.
165
+ * Candidate filtering is lossless; exhaustive mode provides an independent pair
166
+ * population for recall checks. Inputs are copied, sorted and never mutated.
167
+ */
168
+ export declare function analyzeDuplicateItems(input: readonly DuplicateItemInput[], options?: Pick<FindDuplicateClustersOptions, "threshold" | "limit" | "exhaustive">): DuplicateItemsAnalysis;
169
+ /** Find connected duplicate clusters with one metadata read and shared package analysis. */
149
170
  export declare function findDuplicateClusters(options?: FindDuplicateClustersOptions): Promise<DuplicateClustersResult>;
150
171
  /** Apply off/advisory/strict mutation governance through the shared query. */
151
172
  export declare function evaluateSimilarityGovernance(candidate: SimilarItemCandidate, options: FindSimilarItemsOptions & {
@@ -5,7 +5,7 @@
5
5
  * governance and package-owned duplicate analysis.
6
6
  */
7
7
 
8
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="33a43890-abcf-58fc-bf9d-bf2619d4cf35")}catch(e){}}();
8
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9baf5711-9796-57e8-99e5-8990fd481ef8")}catch(e){}}();
9
9
  import { EXIT_CODE } from "../core/shared/constants.js";
10
10
  import { PmCliError } from "../core/shared/errors.js";
11
11
  import { resolveItemTypeRegistry } from "../core/item/type-registry.js";
@@ -16,6 +16,7 @@ import { resolvePmRoot } from "../core/store/paths.js";
16
16
  import { readSettings } from "../core/store/settings.js";
17
17
  import { readItemMetadataDerivedIndexState } from "../core/store/item-metadata-cache.js";
18
18
  import { querySimilarItemMetadataIndex } from "../core/store/item-metadata-query-index.js";
19
+ import { collectDuplicateCandidatePairs, MAX_DUPLICATE_CANDIDATE_PAIRS } from "./query/duplicate-candidates.js";
19
20
  import { prepareSimilarityText, scoreItemSimilarity, scorePreparedItemSimilarity, tokenizeSimilarityText, } from "./similarity-scoring.js";
20
21
  export { jaccardSimilarity, normalizeSimilarityText, prepareSimilarityText, scoreItemSimilarity, scorePreparedItemSimilarity, tokenizeSimilarityText, } from "./similarity-scoring.js";
21
22
  const DEFAULT_SIMILARITY_LIMIT = 3;
@@ -23,7 +24,6 @@ const MAX_SIMILARITY_LIMIT = 20;
23
24
  const DEFAULT_SIMILARITY_THRESHOLD = 0.8;
24
25
  const DEFAULT_DUPLICATE_CLUSTER_LIMIT = 100;
25
26
  const MAX_DUPLICATE_CLUSTER_LIMIT = 1_000;
26
- const MAX_BATCH_PAIR_EVALUATIONS = 1_000_000;
27
27
  /** Render compact warning tokens shared by create and copy results. */
28
28
  export function similarityAdvisoryWarnings(advisory) {
29
29
  return advisory === undefined
@@ -80,7 +80,8 @@ function validateDuplicateClusterOptions(options) {
80
80
  }
81
81
  return { limit, since };
82
82
  }
83
- async function loadPreparedDuplicateItems(options, since) {
83
+ /** Resolve registered statuses and load the metadata population before applying status and creation-time filters. */
84
+ async function loadDuplicateItems(options, since) {
84
85
  const pmRoot = resolvePmRoot(options.cwd ?? process.cwd(), options.pmRoot);
85
86
  const settings = await readSettings(pmRoot);
86
87
  const typeRegistry = resolveItemTypeRegistry(settings);
@@ -111,43 +112,10 @@ async function loadPreparedDuplicateItems(options, since) {
111
112
  return {
112
113
  items: (await listAllItemMetadataLight(pmRoot, settings.item_format, typeRegistry.type_to_folder, undefined, settings.schema))
113
114
  .filter((item) => (!allowedStatuses || allowedStatuses.has(item.status)) &&
114
- (!since || new Date(item.created_at).getTime() >= since.getTime()))
115
- .sort((left, right) => left.id.localeCompare(right.id))
116
- .map((item) => ({ item, prepared: prepareSimilarityText(item.title) })),
115
+ (!since || new Date(item.created_at).getTime() >= since.getTime())),
117
116
  statuses: effectiveStatuses,
118
117
  };
119
118
  }
120
- function collectDuplicateCandidatePairs(items, maxPairEvaluations = MAX_BATCH_PAIR_EVALUATIONS) {
121
- const candidates = new Set();
122
- const indexes = [
123
- new Map(),
124
- new Map(),
125
- new Map(),
126
- ];
127
- for (const [index, entry] of items.entries()) {
128
- const signals = [
129
- [entry.prepared.normalized],
130
- entry.prepared.issueCodes,
131
- entry.prepared.tokens,
132
- ];
133
- for (const [signalKind, values] of signals.entries()) {
134
- for (const value of values) {
135
- const matches = indexes[signalKind].get(value) ?? [];
136
- for (const other of matches)
137
- candidates.add(`${other}:${index}`);
138
- if (candidates.size > maxPairEvaluations) {
139
- throw new PmCliError(`Duplicate sweep exceeded ${MAX_BATCH_PAIR_EVALUATIONS} candidate pairs; narrow it with statuses or since.`, EXIT_CODE.CONFLICT, {
140
- code: "duplicate_sweep_cost_limit",
141
- required: "Narrow the batch duplicate query until its deterministic candidate set fits the disclosed safety bound.",
142
- });
143
- }
144
- matches.push(index);
145
- indexes[signalKind].set(value, matches);
146
- }
147
- }
148
- }
149
- return candidates;
150
- }
151
119
  /** Internal deterministic seams used to prove bounded batch behavior without building a million-pair fixture. */
152
120
  export const _testOnlySimilarity = {
153
121
  buildDuplicateClusters,
@@ -190,6 +158,7 @@ function scoreDuplicateCandidates(items, candidates, threshold, union) {
190
158
  }
191
159
  return matches;
192
160
  }
161
+ /** Group unioned endpoints and their scored evidence once, then deterministically rank and limit complete components. */
193
162
  function buildDuplicateClusters(items, matches, union, limit) {
194
163
  const components = new Map();
195
164
  for (const index of items.keys()) {
@@ -198,12 +167,19 @@ function buildDuplicateClusters(items, matches, union, limit) {
198
167
  component.push(index);
199
168
  components.set(root, component);
200
169
  }
170
+ const rootsById = new Map(items.map(({ item }, index) => [item.id, union.findRoot(index)]));
171
+ const matchesByRoot = new Map();
172
+ for (const match of matches) {
173
+ // Scored matches have already joined both endpoints in the union above.
174
+ const root = rootsById.get(match.left_id);
175
+ const rows = matchesByRoot.get(root) ?? [];
176
+ rows.push(match);
177
+ matchesByRoot.set(root, rows);
178
+ }
201
179
  return [...components.values()]
202
180
  .filter((component) => component.length > 1)
203
181
  .map((component) => {
204
- const ids = new Set(component.map((index) => items[index].item.id));
205
- const clusterMatches = matches
206
- .filter((match) => ids.has(match.left_id) && ids.has(match.right_id))
182
+ const clusterMatches = (matchesByRoot.get(union.findRoot(component[0])) ?? [])
207
183
  .sort((left, right) => right.score - left.score ||
208
184
  left.left_id.localeCompare(right.left_id) ||
209
185
  left.right_id.localeCompare(right.right_id));
@@ -272,15 +248,24 @@ export async function findSimilarItems(candidate, options = {}) {
272
248
  source: indexed ? "persistent_index" : "metadata_fallback",
273
249
  };
274
250
  }
275
- /** Find connected duplicate clusters with one metadata read and one prepared representation per item. */
276
- export async function findDuplicateClusters(options = {}) {
277
- const { threshold } = validateSimilarityOptions({
278
- ...options,
279
- limit: undefined,
280
- });
281
- const { limit, since } = validateDuplicateClusterOptions(options);
282
- const { items, statuses } = await loadPreparedDuplicateItems(options, since);
283
- const candidates = collectDuplicateCandidatePairs(items);
251
+ /**
252
+ * Cluster caller-owned metadata using the same scorer and safety bounds as the CLI.
253
+ * Candidate filtering is lossless; exhaustive mode provides an independent pair
254
+ * population for recall checks. Inputs are copied, sorted and never mutated.
255
+ */
256
+ export function analyzeDuplicateItems(input, options = {}) {
257
+ const { threshold } = validateSimilarityOptions({ ...options, limit: undefined });
258
+ const { limit } = validateDuplicateClusterOptions(options);
259
+ const ids = new Set();
260
+ for (const item of input) {
261
+ if (!item.id.trim() || ids.has(item.id)) {
262
+ throw new PmCliError("Duplicate analysis requires unique non-empty item ids.", EXIT_CODE.USAGE);
263
+ }
264
+ ids.add(item.id);
265
+ }
266
+ const items = [...input].sort((left, right) => left.id.localeCompare(right.id))
267
+ .map((item) => ({ item, prepared: prepareSimilarityText(item.title) }));
268
+ const candidates = collectDuplicateCandidatePairs(items, MAX_DUPLICATE_CANDIDATE_PAIRS, threshold, options.exhaustive === true);
284
269
  const union = createDuplicateUnionFind(items.length);
285
270
  const matches = scoreDuplicateCandidates(items, candidates, threshold, union);
286
271
  const clusters = buildDuplicateClusters(items, matches, union, limit);
@@ -288,18 +273,28 @@ export async function findDuplicateClusters(options = {}) {
288
273
  clusters,
289
274
  count: clusters.length,
290
275
  threshold,
291
- source: "metadata_scan",
292
- filters: {
293
- statuses: statuses ?? null,
294
- since: options.since ?? null,
295
- },
296
276
  cost: {
297
277
  item_count: items.length,
278
+ algorithm: options.exhaustive === true || threshold === 0 ? "exhaustive" : "prefix_exact",
279
+ possible_pairs: items.length * (items.length - 1) / 2,
280
+ pair_limit: MAX_DUPLICATE_CANDIDATE_PAIRS,
281
+ recall_guarantee: "exact",
298
282
  candidate_pairs: candidates.size,
299
283
  scored_pairs: candidates.size,
300
284
  },
301
285
  };
302
286
  }
287
+ /** Find connected duplicate clusters with one metadata read and shared package analysis. */
288
+ export async function findDuplicateClusters(options = {}) {
289
+ validateSimilarityOptions({ ...options, limit: undefined });
290
+ const { since } = validateDuplicateClusterOptions(options);
291
+ const { items, statuses } = await loadDuplicateItems(options, since);
292
+ return {
293
+ ...analyzeDuplicateItems(items, options),
294
+ source: "metadata_scan",
295
+ filters: { statuses: statuses ?? null, since: options.since ?? null },
296
+ };
297
+ }
303
298
  /** Apply off/advisory/strict mutation governance through the shared query. */
304
299
  export async function evaluateSimilarityGovernance(candidate, options) {
305
300
  if (options.mode === "off")
@@ -329,4 +324,4 @@ export async function evaluateSimilarityGovernance(candidate, options) {
329
324
  return { mode: options.mode, bypassed, result, recovery };
330
325
  }
331
326
  //# sourceMappingURL=similarity.js.map
332
- //# debugId=33a43890-abcf-58fc-bf9d-bf2619d4cf35
327
+ //# debugId=9baf5711-9796-57e8-99e5-8990fd481ef8
@@ -105,10 +105,22 @@ pm learnings <item-id> --add "Durable lesson for future work."
105
105
  pm test <item-id> --run --progress
106
106
  node scripts/run-tests.mjs coverage
107
107
  pm comments <item-id> "Evidence: linked test and coverage passed."
108
- pm close <item-id> "Acceptance criteria met; verification passed." --validate-close warn
108
+ pm close <item-id> "Acceptance criteria met; verification passed." --resolution "Delivered behavior" --expected "Required outcome" --actual "Verified outcome" --validate-close warn
109
109
  pm release <item-id>
110
110
  ```
111
111
 
112
+ If a warning close omitted evidence, use its `recovery.suggested_retry_args`
113
+ to append an evidence update while preserving the original close event. See
114
+ [Close Evidence Recovery](CLOSE_EVIDENCE_RECOVERY.md).
115
+
116
+ For real subprocess tests, set `cwd`, `PM_PATH`, and `PM_GLOBAL_PATH` to the
117
+ fixture's temporary workspace and trackers. This also isolates bootstrap and
118
+ implicit discovery reads. Disable external probes with
119
+ `PM_AGENT_PROBES=0` when testing deterministic process output.
120
+ When testing conflicting ambient context, preserve that deliberate input in the
121
+ fixture. CLI and MCP dispatch scope SDK identity signals to the resolved tracker;
122
+ tests compare complete history bytes while the unrelated tracker's claims change.
123
+
112
124
  ## Token-Minimal Retrieval
113
125
 
114
126
  | Need | Command |
@@ -181,6 +193,12 @@ Use the canonical [guide topic map](README.md#guide-topic-map) when local in-CLI
181
193
 
182
194
  ## Reviewed Delivery Closeout
183
195
 
196
+ Before closing repository defects, set `escape_class` and structured
197
+ `gate_evidence` through `pm update` as described in
198
+ [Defect Evidence](DEFECT_RECURRENCE.md#defect-evidence-on-pm-items).
199
+ Run `pnpm quality:defect-evidence` after the terminal transitions: a static
200
+ gate run before closure cannot validate evidence required only for closed items.
201
+
184
202
  Treat PM evidence, item closure, and the generated changelog as part of the
185
203
  reviewed change. Add all evidence known before merge to the active branch,
186
204
  close and release the item there, regenerate `CHANGELOG.md`, and include those
@@ -219,7 +237,10 @@ positively and explain false or stale findings on their actual review surface.
219
237
  After each push, request the reviewers again and wait for terminal results;
220
238
  provider quota or skipped reviews are limitations, not approvals. Before merge,
221
239
  run `inventory` once more to catch new or edited feedback, and require all
222
- findings to have an explicit disposition.
240
+ findings to have an explicit disposition. Compare each artifact's `revision`
241
+ and pass it as `--revision` when acknowledging feedback, so edited bodies get
242
+ fresh decisions and retrying a handled revision adds no duplicate reply.
243
+ See [Review Loop](PR_REVIEW_LOOP.md) for the revision and reply contracts.
223
244
 
224
245
  ## Documentation Rules for Agents
225
246