@unbrained/pm-cli 2026.10.27 → 2026.10.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +41 -0
  3. package/dist/cli/commander-usage.js +2 -2
  4. package/dist/cli/commands/lifecycle/append.d.ts +4 -0
  5. package/dist/cli/commands/lifecycle/append.js +1 -1
  6. package/dist/cli/commands/lifecycle/create.d.ts +4 -0
  7. package/dist/cli/commands/lifecycle/create.js +1 -1
  8. package/dist/cli/commands/lifecycle/plan.d.ts +3 -0
  9. package/dist/cli/commands/lifecycle/plan.js +1 -1
  10. package/dist/cli/commands/lifecycle/update-many.d.ts +4 -0
  11. package/dist/cli/commands/lifecycle/update-many.js +1 -1
  12. package/dist/cli/commands/lifecycle/update.d.ts +4 -0
  13. package/dist/cli/commands/lifecycle/update.js +1 -1
  14. package/dist/cli/error-guidance.js +2 -2
  15. package/dist/cli/help-content.js +2 -2
  16. package/dist/cli/register-list-query.js +3 -3
  17. package/dist/cli-bundle/bundle-manifest.json +1 -1
  18. package/dist/cli-bundle/chunks/chunk-34MP7WKK.js +6 -0
  19. package/dist/cli-bundle/chunks/{chunk-25MTQAHX.js → chunk-3KMYHYJV.js} +2 -2
  20. package/dist/cli-bundle/chunks/chunk-42BHHNTL.js +128 -0
  21. package/dist/cli-bundle/chunks/{chunk-NNIYQHYT.js → chunk-4BQD5SW2.js} +46 -46
  22. package/dist/cli-bundle/chunks/{chunk-OQOEBWHU.js → chunk-EHBGHX55.js} +2 -2
  23. package/dist/cli-bundle/chunks/{chunk-2XUDAQK5.js → chunk-GRABKNFL.js} +2 -2
  24. package/dist/cli-bundle/chunks/chunk-I5O3XZOA.js +2 -0
  25. package/dist/cli-bundle/chunks/{chunk-34OQP43K.js → chunk-LEIVUGDH.js} +3 -3
  26. package/dist/cli-bundle/chunks/{chunk-57YPOOED.js → chunk-LWW4F4G2.js} +2 -2
  27. package/dist/cli-bundle/chunks/chunk-MVL27TAH.js +2 -0
  28. package/dist/cli-bundle/chunks/{chunk-UHKN6TJM.js → chunk-OH7WPD42.js} +44 -44
  29. package/dist/cli-bundle/chunks/chunk-OOD2A7AF.js +20 -0
  30. package/dist/cli-bundle/chunks/chunk-Q5V7P6NM.js +2 -0
  31. package/dist/cli-bundle/chunks/chunk-QLPFYBXN.js +2 -0
  32. package/dist/cli-bundle/chunks/{chunk-ONC4BBQP.js → chunk-RZQNFHXO.js} +2 -2
  33. package/dist/cli-bundle/chunks/chunk-S2U7ECUJ.js +2 -0
  34. package/dist/cli-bundle/chunks/{chunk-32QK43XG.js → chunk-SA5NYLPE.js} +2 -2
  35. package/dist/cli-bundle/chunks/{chunk-ER7RLJW5.js → chunk-TH5KPAN4.js} +2 -2
  36. package/dist/cli-bundle/chunks/{chunk-EKV5GP6P.js → chunk-UUQX3PFD.js} +57 -57
  37. package/dist/cli-bundle/chunks/chunk-VHLAVTJN.js +31 -0
  38. package/dist/cli-bundle/chunks/{chunk-HIX2OMEZ.js → chunk-XAOVJG52.js} +2 -2
  39. package/dist/cli-bundle/chunks/chunk-XPQ4RTSF.js +2 -0
  40. package/dist/cli-bundle/chunks/{chunk-X5TMR7KC.js → chunk-Y6NC3X3E.js} +2 -2
  41. package/dist/cli-bundle/chunks/{indexed-receipt-blobs-63WYG2PY.js → indexed-receipt-blobs-LHLE7PSG.js} +2 -2
  42. package/dist/cli-bundle/chunks/item-transaction-WIOBZW4E.js +2 -0
  43. package/dist/cli-bundle/chunks/{mutation-admission-L3A6FSZ7.js → mutation-admission-TRGAVS7N.js} +2 -2
  44. package/dist/cli-bundle/chunks/{register-list-query-YSQ7UTFI.js → register-list-query-BH4F6M7Z.js} +6 -6
  45. package/dist/cli-bundle/chunks/register-mutation-5V2NANIS.js +20 -0
  46. package/dist/cli-bundle/chunks/register-operations-XSFRMUFX.js +2 -0
  47. package/dist/cli-bundle/chunks/{register-setup-3WAXNDMY.js → register-setup-QPWSVHFZ.js} +2 -2
  48. package/dist/cli-bundle/chunks/{settlement-hook-CRW6PVQK.js → settlement-hook-MN3Y7LVX.js} +2 -2
  49. package/dist/cli-bundle/chunks/{stage-I46WD4EN.js → stage-RTB6RAAO.js} +2 -2
  50. package/dist/cli-bundle/focused-chunks/chunk-2V6NMNYN.js +26 -0
  51. package/dist/cli-bundle/focused-chunks/{chunk-YW5GHQN2.js → chunk-3SRX77ES.js} +2 -2
  52. package/dist/cli-bundle/focused-chunks/{chunk-YLVHHWHN.js → chunk-4PW5XWVI.js} +2 -2
  53. package/dist/cli-bundle/focused-chunks/{chunk-ZH7GTEWL.js → chunk-ASHGXMX4.js} +3 -3
  54. package/dist/cli-bundle/focused-chunks/{chunk-XK3XMZM4.js → chunk-BCIUAEWX.js} +2 -2
  55. package/dist/cli-bundle/focused-chunks/{chunk-KTXU5WU6.js → chunk-BPE6JTKL.js} +3 -3
  56. package/dist/cli-bundle/focused-chunks/{chunk-MVY6JDIH.js → chunk-CKBN2D42.js} +11 -11
  57. package/dist/cli-bundle/focused-chunks/chunk-FRFNGMAK.js +2 -0
  58. package/dist/cli-bundle/focused-chunks/chunk-I642DXOT.js +22 -0
  59. package/dist/cli-bundle/focused-chunks/chunk-K7DKG5HM.js +91 -0
  60. package/dist/cli-bundle/focused-chunks/chunk-NMQBXU7N.js +44 -0
  61. package/dist/cli-bundle/focused-chunks/{chunk-WDKZB3V6.js → chunk-PGM3ENGZ.js} +2 -2
  62. package/dist/cli-bundle/focused-chunks/{chunk-R2MTHS2H.js → chunk-POJPJX64.js} +2 -2
  63. package/dist/cli-bundle/focused-chunks/{chunk-YQKLKRMW.js → chunk-QFNV4JTA.js} +2 -2
  64. package/dist/cli-bundle/focused-chunks/{chunk-4TXZBGOR.js → chunk-QVJCQMZY.js} +2 -2
  65. package/dist/cli-bundle/focused-chunks/chunk-R2TPPPVU.js +2 -0
  66. package/dist/cli-bundle/focused-chunks/{chunk-B266DDBJ.js → chunk-RGQEJFXO.js} +2 -2
  67. package/dist/cli-bundle/focused-chunks/{chunk-BH7RE2ZY.js → chunk-RKPUQDWF.js} +2 -2
  68. package/dist/cli-bundle/focused-chunks/chunk-RYEZSZJ7.js +2 -0
  69. package/dist/cli-bundle/focused-chunks/{chunk-E5C24MCV.js → chunk-S23ZUASN.js} +2 -2
  70. package/dist/cli-bundle/focused-chunks/{chunk-SYL73MEI.js → chunk-SAT7IAEB.js} +3 -3
  71. package/dist/cli-bundle/focused-chunks/{chunk-TGUOEOKP.js → chunk-VFWCSQAG.js} +2 -2
  72. package/dist/cli-bundle/focused-chunks/chunk-VJVJ2ZX2.js +2 -0
  73. package/dist/cli-bundle/focused-chunks/{chunk-Y57A4ZDI.js → chunk-YQUZL2CK.js} +54 -54
  74. package/dist/cli-bundle/focused-chunks/{chunk-SM2KGD4J.js → chunk-YU7QOYYV.js} +18 -18
  75. package/dist/cli-bundle/focused-chunks/{indexed-receipt-blobs-GAFC52LQ.js → indexed-receipt-blobs-VK2363JB.js} +2 -2
  76. package/dist/cli-bundle/focused-chunks/item-transaction-ZFDKJIP7.js +2 -0
  77. package/dist/cli-bundle/focused-chunks/{mutation-admission-ENBFMV2X.js → mutation-admission-7VUODEPJ.js} +2 -2
  78. package/dist/cli-bundle/focused-chunks/{settlement-hook-PUYJJDFL.js → settlement-hook-OK6A7PG2.js} +2 -2
  79. package/dist/cli-bundle/focused-chunks/{stage-MAWZTM2H.js → stage-S5RP5SZH.js} +2 -2
  80. package/dist/cli-bundle/main.js +21 -21
  81. package/dist/cli-bundle/sdk-authoring.js +1 -1
  82. package/dist/cli-bundle/sdk-contracts.js +2 -2
  83. package/dist/cli-bundle/sdk-core.js +1 -42
  84. package/dist/cli-bundle/sdk-governance.js +1 -1
  85. package/dist/cli-bundle/sdk-graph.js +1 -1
  86. package/dist/cli-bundle/sdk-merge.js +61 -61
  87. package/dist/cli-bundle/sdk-query.js +1 -1
  88. package/dist/cli-bundle/sdk-runtime.js +1 -1
  89. package/dist/cli-bundle/sdk-testing.js +1 -1
  90. package/dist/cli-bundle/sdk.js +11 -11
  91. package/dist/core/history/operation-contract.d.ts +2 -2
  92. package/dist/core/history/operation-contract.js +1 -1
  93. package/dist/core/item/literal-mutation-input.d.ts +12 -0
  94. package/dist/core/item/literal-mutation-input.js +2 -0
  95. package/dist/core/item/parse.d.ts +2 -0
  96. package/dist/core/item/parse.js +2 -2
  97. package/dist/core/item/priority.js +1 -1
  98. package/dist/core/output/output.js +2 -2
  99. package/dist/core/shared/errors.d.ts +2 -0
  100. package/dist/core/shared/errors.js +1 -1
  101. package/dist/core/store/item-store.js +1 -1
  102. package/dist/sdk/agent/command-recovery.d.ts +5 -0
  103. package/dist/sdk/agent/command-recovery.js +1 -1
  104. package/dist/sdk/cli-contracts/flag-contracts.js +1 -1
  105. package/dist/sdk/cli-contracts/flag-lexicon-contracts.js +1 -1
  106. package/dist/sdk/cli-contracts/registration-helpers.js +1 -1
  107. package/dist/sdk/cli-contracts/tool-parameter-tables.js +1 -1
  108. package/dist/sdk/cli-contracts/tool-schema.js +1 -1
  109. package/dist/sdk/command-line.d.ts +6 -0
  110. package/dist/sdk/command-line.js +1 -1
  111. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +1 -1
  112. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +1 -1
  113. package/dist/sdk/index.d.ts +1 -1
  114. package/dist/sdk/index.js +1 -1
  115. package/dist/sdk/item-transaction.d.ts +33 -2
  116. package/dist/sdk/item-transaction.js +1 -1
  117. package/dist/sdk/lifecycle/append.js +1 -1
  118. package/dist/sdk/lifecycle/cli-input.d.ts +4 -0
  119. package/dist/sdk/lifecycle/cli-input.js +2 -0
  120. package/dist/sdk/lifecycle/close.js +1 -1
  121. package/dist/sdk/lifecycle/create.js +1 -1
  122. package/dist/sdk/lifecycle/legacy-none-tokens.js +1 -1
  123. package/dist/sdk/lifecycle/reopen.js +1 -1
  124. package/dist/sdk/lifecycle/update-many.js +1 -1
  125. package/dist/sdk/lifecycle/update.js +2 -2
  126. package/dist/sdk/output/table-rows.d.ts +9 -1
  127. package/dist/sdk/output/table-rows.js +1 -1
  128. package/dist/sdk/public-surface.json +1 -1
  129. package/dist/sdk/query/history.d.ts +6 -0
  130. package/dist/sdk/query/history.js +1 -1
  131. package/dist/sdk/runtime/actions.js +1 -1
  132. package/dist/sdk/runtime/mutation-value-validation.d.ts +16 -0
  133. package/dist/sdk/runtime/mutation-value-validation.js +2 -0
  134. package/dist/sdk/runtime-input.js +1 -1
  135. package/dist/sdk/runtime-primitives.d.ts +1 -0
  136. package/dist/sdk/runtime-primitives.js +1 -1
  137. package/dist/sdk/runtime.d.ts +14 -0
  138. package/dist/sdk/runtime.js +1 -1
  139. package/dist/sdk/structured-mutations.js +1 -1
  140. package/dist/sdk/test/command-resolution.d.ts +5 -0
  141. package/dist/sdk/test/command-resolution.js +3 -0
  142. package/dist/sdk/test/execution.js +2 -2
  143. package/dist/sdk/workspace-transaction-gc.js +1 -1
  144. package/dist/sdk/workspace-transaction.d.ts +54 -0
  145. package/dist/sdk/workspace-transaction.js +2 -2
  146. package/docs/COMMANDS.md +33 -1
  147. package/docs/OUTPUT_PROJECTION_CONTRACTS.md +24 -2
  148. package/docs/SDK.md +165 -0
  149. package/docs/TESTING.md +36 -2
  150. package/docs/generated/FLAG_LEXICON_BUDGETS.md +1 -1
  151. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +6 -3
  152. package/marketplace.json +1 -1
  153. package/package.json +1 -1
  154. package/packages/pm-beads/package.json +1 -1
  155. package/packages/pm-calendar/package.json +1 -1
  156. package/packages/pm-command-kit/package.json +1 -1
  157. package/packages/pm-digital-twin/package.json +1 -1
  158. package/packages/pm-governance-audit/package.json +1 -1
  159. package/packages/pm-guide-shell/package.json +1 -1
  160. package/packages/pm-kanban/package.json +1 -1
  161. package/packages/pm-lifecycle-hooks/package.json +1 -1
  162. package/packages/pm-linked-test-adapters/package.json +1 -1
  163. package/packages/pm-search-advanced/package.json +1 -1
  164. package/packages/pm-templates/package.json +1 -1
  165. package/packages/pm-todos/package.json +1 -1
  166. package/packages/pm-vcs/package.json +1 -1
  167. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  168. package/plugins/pm-claude/package.json +1 -1
  169. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  170. package/plugins/pm-codex/package.json +1 -1
  171. package/plugins/pm-codex/plugin.json +1 -1
  172. package/runtime-dependencies.json +1 -1
  173. package/dist/cli-bundle/chunks/chunk-4D25HF3V.js +0 -2
  174. package/dist/cli-bundle/chunks/chunk-5PTMQBKJ.js +0 -30
  175. package/dist/cli-bundle/chunks/chunk-F26MRQZY.js +0 -6
  176. package/dist/cli-bundle/chunks/chunk-H434DKYU.js +0 -2
  177. package/dist/cli-bundle/chunks/chunk-O3LPT34G.js +0 -20
  178. package/dist/cli-bundle/chunks/chunk-Q55KZDNF.js +0 -2
  179. package/dist/cli-bundle/chunks/chunk-QQYLNHZK.js +0 -128
  180. package/dist/cli-bundle/chunks/chunk-SGFQQXFD.js +0 -2
  181. package/dist/cli-bundle/chunks/chunk-XERBUA6A.js +0 -2
  182. package/dist/cli-bundle/chunks/register-mutation-6HRXYASW.js +0 -20
  183. package/dist/cli-bundle/chunks/register-operations-4O3YOYXF.js +0 -2
  184. package/dist/cli-bundle/focused-chunks/chunk-HWD63G54.js +0 -2
  185. package/dist/cli-bundle/focused-chunks/chunk-J67PFM3O.js +0 -91
  186. package/dist/cli-bundle/focused-chunks/chunk-NLZUI234.js +0 -26
  187. package/dist/cli-bundle/focused-chunks/chunk-TAFMNTA5.js +0 -2
  188. package/dist/cli-bundle/focused-chunks/chunk-U4UBQBS7.js +0 -2
  189. package/dist/cli-bundle/focused-chunks/chunk-YC6NCPLK.js +0 -22
  190. package/dist/cli-bundle/focused-chunks/chunk-ZA6AEYIM.js +0 -2
@@ -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]="4750005e-fc78-5b48-8bba-18790c376a9f")}catch{}})();import{readJsonPathValue,splitJsonPathSegments}from"./json-path.js";import{detectEmptyLinkedTestRun,findLinkedTestExecutionReceipt}from"./execution-receipts.js";import{seedLinkedTestWorkspaceSnapshot}from"./workspace-snapshot.js";import{updateLinkedTest,parseLinkedTestUpdateOptions,assertLinkedTestEditUsage}from"./update.js";import{admitLinkedTestRequest,assertLinkedTestCommand}from"./entry.js";import{assertInitializedTracker}from"../environment/tracker-preflight.js";import{spawn}from"node:child_process";import{cp,mkdir,mkdtemp,open,readdir,rm,symlink}from"node:fs/promises";import{tmpdir}from"node:os";import path from"node:path";import{getActiveExtensionRegistrations}from"../../core/extensions/index.js";import{isFileMissingError,pathExists,readFileIfExists}from"../../core/fs/fs-utils.js";import{getWorkspaceHistoryPath,writeWorkspaceJsonWithHistory}from"../../core/history/workspace-history.js";import{resolveItemTypeRegistry}from"../../core/item/type-registry.js";import{createStdinTokenResolver,parseCsvKv,parseOptionalNumber}from"../../core/item/parse.js";import{EXIT_CODE}from"../../core/shared/constants.js";import{resolveAuthor}from"../../core/shared/author.js";import{PmCliError}from"../../core/shared/errors.js";import{stableValueEquals}from"../../core/shared/serialization.js";import{nowIso}from"../../core/shared/time.js";import{locateItem,mutateItem,readLocatedItem}from"../../core/store/item-store.js";import{getSettingsPath,ITEM_FILE_EXTENSIONS,resolveGlobalPmRoot,resolvePmRoot}from"../../core/store/paths.js";import{readSettings}from"../../core/store/settings.js";import{appendTrackedTestRunSummary,buildTrackedTestRunHistory}from"../../core/test/item-test-run-tracking.js";import{runInit}from"../init.js";import{looksLikeStructuredLinkedTestEntry,normalizeStructuredLinkedTestEntry}from"./entry.js";import{LINKED_TEST_PM_CONTEXT_MODE_VALUES as PM_CONTEXT_MODE_VALUES,LINKED_TEST_PROTECTED_ENV_KEYS,parseLinkedTestAssertionEqualsMap,parseLinkedTestAssertionGteMap,parseLinkedTestBoolean as parseLinkedTestBooleanValue,parseLinkedTestContextMode as parseLinkedTestContextModeValue,parseLinkedTestEnvClear as parseLinkedTestEnvClearValue,parseLinkedTestEnvSet as parseLinkedTestEnvSetValue,parseLinkedTestJsonEntries,parseLinkedTestMinLines,parseLinkedTestRegexList,parseLinkedTestStringList,parseLinkedTestWorkspaceContextMode}from"./parsers.js";import{parseOnlyIndexValue,resolveLinkedTestRunSelection}from"../../core/test/run-selectors.js";import{SCOPE_VALUES}from"../../types/index.js";import{diffTestRunMeasurements,parseTestRunMeasurements,queryTestRunMeasurementsBelow}from"./measurements.js";import{withHostEnvironmentBoundary}from"../environment/host-environment-errors.js";import{SOURCE_CONTEXT_ACCESS_ENV}from"../environment/source-context.js";import{assertTestMeasurementExecution,assertTestMeasurementRun}from"./prerequisites.js";import{acknowledgeLinkedTests,attachLinkedTestMutationProvenance,resolveLinkedTestSourceRef,resolveLinkedTestSourceWorkspaceRoot,resolveLinkedTestTrust}from"./trust.js";const TEST_OUTPUT_MAX_BUFFER_BYTES=20*1024*1024,DEFAULT_LINKED_TEST_TIMEOUT_FORCE_KILL_DELAY_MS=3e3,DEFAULT_LINKED_TEST_HEARTBEAT_INTERVAL_MS=1e4,DEFAULT_LINKED_TEST_PIPE_CLOSE_GRACE_MS=5e3,MAX_LINKED_TEST_COMMAND_LABEL_LENGTH=120,LINKED_TEST_TRACKER_DIRS_TO_SKIP=new Set(["locks","extensions","runtime"]),LINKED_TEST_ITEM_COUNT_DIRS_TO_SKIP=new Set(["history","index","search","extensions","locks","runtime"]),LINKED_TEST_INFRA_COLLISION_PATTERNS=[/eaddrinuse/i,/address already in use/i,/port\s+\d+\s+is already in use/i,/web server[^.\n]*already running/i,/failed to listen on/i],LINKED_TEST_LOCK_ACQUISITION_PATTERN=/\b(?:could not|failed to|unable to)\s+acquire\b[^\n.]*\block\b/i,LINKED_TEST_LOCK_CONTENTION_CONTEXT_PATTERN=/\b(?:already running|timed?\s*out|timeout|held by another|lock\s+is\s+held)\b/i,PM_TRACKER_READ_SUBCOMMANDS=new Set(["activity","calendar","context","ctx","deps","get","health","history","list","list-all","list-blocked","list-canceled","list-closed","list-draft","list-in-progress","list-open","search","stats","test-all","validate"]);function readPositiveIntegerEnv(name,fallback){const raw=process.env[name];if(typeof raw!="string"||raw.trim().length===0)return fallback;const parsed=Number.parseInt(raw,10);return!Number.isFinite(parsed)||parsed<=0?fallback:parsed}function linkedTestTimeoutForceKillDelayMs(){return readPositiveIntegerEnv("PM_LINKED_TEST_TIMEOUT_FORCE_KILL_DELAY_MS",DEFAULT_LINKED_TEST_TIMEOUT_FORCE_KILL_DELAY_MS)}function linkedTestHeartbeatIntervalMs(){return readPositiveIntegerEnv("PM_LINKED_TEST_HEARTBEAT_INTERVAL_MS",DEFAULT_LINKED_TEST_HEARTBEAT_INTERVAL_MS)}function linkedTestPipeCloseGraceMs(){return readPositiveIntegerEnv("PM_LINKED_TEST_PIPE_CLOSE_GRACE_MS",DEFAULT_LINKED_TEST_PIPE_CLOSE_GRACE_MS)}function resolveTrackedRunId(kind){const fromEnv=process.env.PM_BACKGROUND_TEST_RUN_ID?.trim();return fromEnv&&fromEnv.length>0?fromEnv:`${kind}-local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`}function summarizeRunResultStatuses(results){let passed=0,failed=0,skipped=0;for(const entry of results){if(entry.status==="passed"){passed+=1;continue}if(entry.status==="failed"||entry.status==="refused"){failed+=1;continue}skipped+=1}return{passed,failed,skipped}}function summarizeContextPreflight(runResults){let checkedPmCommands=0,trackerReadCommands=0,mismatches=0,autoRemediated=0;for(const result of runResults){const context=result.execution_context;!context||context.is_pm_command!==!0||(checkedPmCommands+=1,context.is_pm_tracker_read_command===!0&&(trackerReadCommands+=1),context.mismatch_detected===!0&&(mismatches+=1),context.auto_pm_context_applied===!0&&(autoRemediated+=1))}return{checked_pm_commands:checkedPmCommands,tracker_read_commands:trackerReadCommands,mismatches,auto_remediated:autoRemediated}}function ensureScope(raw){const value=raw??"project";if(!SCOPE_VALUES.includes(value))throw new PmCliError(`Invalid scope "${raw}"`,EXIT_CODE.USAGE);return value}function parsePmContextMode(raw){if(!raw)return"schema";const normalized=raw.trim().toLowerCase();if(PM_CONTEXT_MODE_VALUES.includes(normalized))return normalized;throw new PmCliError(`Invalid --pm-context value "${raw}". Expected one of: ${PM_CONTEXT_MODE_VALUES.join(", ")}`,EXIT_CODE.USAGE)}function parseWorkspaceContextMode(raw){return parseLinkedTestWorkspaceContextMode(raw??"source","--workspace-context")}function resolveLinkedTestWorkspaceContextMode(linkedTest,runLevelMode,overrideLinkedMode){return overrideLinkedMode?runLevelMode:parseLinkedTestWorkspaceContextMode(linkedTest.workspace_context_mode,"linked test")??runLevelMode}function resolveLinkedTestRequestedContextMode(linkedTest,runLevelMode,overrideLinkedPmContext){return overrideLinkedPmContext||typeof linkedTest.pm_context_mode!="string"||linkedTest.pm_context_mode.trim().length===0?runLevelMode:parsePmContextMode(linkedTest.pm_context_mode)}function resolveLinkedTestEffectiveContextMode(requestedMode,isPmTrackerReadCommand){return requestedMode==="auto"?isPmTrackerReadCommand?"tracker":"schema":requestedMode}function hasLinkedTestAssertions(linkedTest){return Object.entries(linkedTest).filter(([key])=>key.startsWith("assert_")).some(([,value])=>value==null?!1:Array.isArray(value)?value.length>0:typeof value=="object"?Object.keys(value).length>0:typeof value=="string"?value.trim().length>0:!0)}function buildPmContextMismatchHint(params){const{executionContext,runLevelPmContextMode,linkedOverridePmContextMode}=params;return!executionContext.is_pm_tracker_read_command||!executionContext.mismatch_detected?"":runLevelPmContextMode==="tracker"&&linkedOverridePmContextMode==="schema"?" Linked test metadata pm_context_mode=schema overrides run-level --pm-context tracker. Set pm_context_mode=tracker (or auto) on the linked test, or remove the override, to run against seeded tracker data.":executionContext.pm_context_mode==="schema"?" Use --auto-pm-context to route PM tracker-read linked commands through seeded tracker data automatically. Alternatively, use --pm-context tracker for the whole run.":""}function mergeEnvSetDirectives(entries,optionName){const merged={};if(!entries)return merged;for(const entry of entries){const parsed=parseLinkedTestEnvSetValue(entry,optionName);if(parsed)for(const[key,value]of Object.entries(parsed))merged[key]=value}return merged}function mergeEnvClearDirectives(entries,optionName){if(!entries)return[];const values=[];for(const entry of entries){const parsed=parseLinkedTestEnvClearValue(entry,optionName);parsed&&values.push(...parsed)}return[...new Set(values)]}import{BUN_GLOBAL_FLAGS_WITH_VALUE,NPM_GLOBAL_FLAGS_WITH_VALUE,PNPM_GLOBAL_FLAGS_WITH_VALUE,SCRIPT_RUN_FLAGS_WITH_VALUE,SCRIPT_RUN_SUBCOMMANDS,YARN_GLOBAL_FLAGS_WITH_VALUE,firstPmSubcommand,extractPmInvocationArgsFromSegment,isPmCliPackageToken,isPmCliScriptToken,isPmExecutableToken,parseLauncherSubcommand,parseNpmExecCommand,parseNpxCommand,parsePnpmDlxCommand,resolvePmSubcommandContext,splitNormalizedCommandSegments,stripLeadingEnvAssignments}from"./linked-command-detection.js";import{extractReferencedPmItemIdsFromCommand}from"./linked-command-detection.js";function commandInvokesPmCli(command){const normalizedCommand=normalizeCommandForValidation(command);return splitNormalizedCommandSegments(normalizedCommand).some(segment=>extractPmInvocationArgsFromSegment(segment)!==null)}function commandInvokesPmTrackerReadCommand(command){const normalizedCommand=normalizeCommandForValidation(command);return splitNormalizedCommandSegments(normalizedCommand).some(segment=>{const invocationArgs=extractPmInvocationArgsFromSegment(segment);if(!invocationArgs)return!1;const context=resolvePmSubcommandContext(invocationArgs);return context?PM_TRACKER_READ_SUBCOMMANDS.has(context.subcommand):!1})}function resolveDirectRunnerSubcommand(parsed){if(parsed)return SCRIPT_RUN_SUBCOMMANDS.has(parsed.subcommand)?parseLauncherSubcommand(parsed.args,SCRIPT_RUN_FLAGS_WITH_VALUE)?.subcommand:parsed.subcommand}function firstDirectTestRunnerSubcommand(executable,args){if(executable==="npx"||executable==="bunx")return parseNpxCommand(args)?.command;if(executable==="pnpm")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,PNPM_GLOBAL_FLAGS_WITH_VALUE));if(executable==="npm")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,NPM_GLOBAL_FLAGS_WITH_VALUE));if(executable==="yarn")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,YARN_GLOBAL_FLAGS_WITH_VALUE));if(executable==="bun")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,BUN_GLOBAL_FLAGS_WITH_VALUE))}function isDirectTestRunnerSubcommand(token){return token?token==="vitest"||token==="test"||token.startsWith("test:"):!1}function parsedLauncherInvokesRecursiveTestAll(parsed){return!parsed||!isPmExecutableToken(parsed.command)&&!isPmCliPackageToken(parsed.command)?!1:firstPmSubcommand(parsed.args)==="test-all"}const SHELL_COMMAND_FLAG_PATTERNS=new Map([...["sh","bash","dash","zsh"].map(executable=>[executable,/^(?:-c|-[a-z]*c[a-z]*)$/u]),...["cmd","cmd.exe"].map(executable=>[executable,/^\/[ck]$/u]),...["powershell","powershell.exe","pwsh","pwsh.exe"].map(executable=>[executable,/^-(?:c|command)$/u])]),PACKAGE_EXEC_EXECUTABLES=new Set(["npx","bunx"]);function segmentInvokesRecursiveTestAll(segment){const rawTokens=segment.split(" ").filter(token=>token.length>0),tokens=stripLeadingEnvAssignments(rawTokens);if(tokens.length===0)return!1;const[executable,...args]=tokens,shellCommandFlagPattern=SHELL_COMMAND_FLAG_PATTERNS.get(executable);if(shellCommandFlagPattern){const commandFlagIndex=args.findIndex(arg=>shellCommandFlagPattern.test(arg));return commandFlagIndex>=0?segmentInvokesRecursiveTestAll(args.slice(commandFlagIndex+1).join(" ")):!1}return isPmExecutableToken(executable)||isPmCliScriptToken(executable)?firstPmSubcommand(args)==="test-all":executable==="node"&&args.length>0&&isPmCliScriptToken(args[0])?firstPmSubcommand(args.slice(1))==="test-all":PACKAGE_EXEC_EXECUTABLES.has(executable)?parsedLauncherInvokesRecursiveTestAll(parseNpxCommand(args)):executable==="pnpm"?parsedLauncherInvokesRecursiveTestAll(parsePnpmDlxCommand(args)):executable==="npm"?parsedLauncherInvokesRecursiveTestAll(parseNpmExecCommand(args)):!1}function invokesRecursiveTestAllCommand(command){const normalized=normalizeCommandForValidation(command);return splitNormalizedCommandSegments(normalized).some(segment=>segmentInvokesRecursiveTestAll(segment))}function assertNoRecursiveTestAllCommand(command){if(invokesRecursiveTestAllCommand(command))throw new PmCliError('Linked test commands must not invoke "pm test-all"; this creates recursive orchestration.',EXIT_CODE.USAGE)}function normalizeCommandForValidation(command){return command.trim().replaceAll("\\","/").replaceAll('"',"").replaceAll("'","").replaceAll(/\s+/g," ").toLowerCase()}function commandUsesSandboxRunner(normalizedCommand){return normalizedCommand.includes("node scripts/run-tests.mjs ")||normalizedCommand.endsWith("node scripts/run-tests.mjs")||normalizedCommand.includes("node ./scripts/run-tests.mjs ")||normalizedCommand.endsWith("node ./scripts/run-tests.mjs")}function segmentInvokesDirectTestRunner(normalizedSegment){const rawTokens=normalizedSegment.split(" ").filter(token=>token.length>0),tokens=stripLeadingEnvAssignments(rawTokens);if(tokens.length===0)return!1;const[executable,...args]=tokens,shellCommandFlagPattern=SHELL_COMMAND_FLAG_PATTERNS.get(executable);if(shellCommandFlagPattern){const commandFlagIndex=args.findIndex(argument=>shellCommandFlagPattern.test(argument));return commandFlagIndex>=0&&segmentInvokesDirectTestRunner(args.slice(commandFlagIndex+1).join(" "))}return executable==="vitest"||executable.endsWith("/vitest")||executable.endsWith("/vitest.mjs")?!0:executable==="node"?nodeArgsInvokeDirectTestRunner(args):packageManagerInvokesDirectTestRunner(executable,args)}function nodeArgsInvokeDirectTestRunner(args){return args.includes("--test")||args.some(arg=>arg==="vitest"||arg==="vitest.mjs"||arg.endsWith("/vitest")||arg.endsWith("/vitest.mjs"))}function packageManagerInvokesDirectTestRunner(executable,args){return executable==="npx"||executable==="bunx"?isDirectTestRunnerSubcommand(parseNpxCommand(args)?.command):executable==="pnpm"||executable==="npm"?isDirectTestRunnerSubcommand((executable==="pnpm"?parsePnpmDlxCommand(args):parseNpmExecCommand(args))?.command)||isDirectTestRunnerSubcommand(firstDirectTestRunnerSubcommand(executable,args)):(executable==="yarn"||executable==="bun")&&isDirectTestRunnerSubcommand(firstDirectTestRunnerSubcommand(executable,args))}function classifyLinkedTestCommandSafety(command){const normalized=normalizeCommandForValidation(command),segments=splitNormalizedCommandSegments(normalized),directRunner=segments.some(segment=>!commandUsesSandboxRunner(segment)&&segmentInvokesDirectTestRunner(segment)),sandboxRunner=segments.some(commandUsesSandboxRunner);return{accepted:!0,command_kind:directRunner?"direct_runner":sandboxRunner?"sandbox_runner":"other",tracker_isolation:"runtime_injected",trust_boundary:"provenance",advisory:directRunner?"Accepted with runtime-injected tracker isolation. Prefer the repository sandbox runner when available for reproducible build and coverage setup.":"Accepted with runtime-injected tracker isolation and provenance checks before execution."}}function getRuntimeSafetySkipReason(command){if(invokesRecursiveTestAllCommand(command))return'Linked test command skipped: Linked test commands must not invoke "pm test-all"; this creates recursive orchestration.'}function parseAddEntries(raw){return raw?raw.map(parseAddEntry):[]}function parseAddEntry(entry){const trimmed=entry.trim(),kv=looksLikeStructuredLinkedTestEntry(trimmed)?normalizeStructuredLinkedTestEntry(parseCsvKv(entry,"--add"),"--add"):{command:trimmed},command=trimLinkedTestEntryField(kv.command);if(!command)throw new PmCliError("--add requires command=<value> or a bare command (path=<value> is optional metadata)",EXIT_CODE.USAGE);return assertNoRecursiveTestAllCommand(command),{command,path:trimLinkedTestEntryField(kv.path),scope:ensureScope(kv.scope),timeout_seconds:parseLinkedTestTimeoutSeconds(trimLinkedTestEntryField(kv.timeout_seconds),trimLinkedTestEntryField(kv.timeout)),pm_context_mode:parseLinkedTestContextModeValue(trimLinkedTestEntryField(kv.pm_context_mode),"--add"),workspace_context_mode:parseLinkedTestWorkspaceContextMode(trimLinkedTestEntryField(kv.workspace_context_mode),"--add"),env_set:parseLinkedTestEnvSetValue(trimLinkedTestEntryField(kv.env_set),"--add"),env_clear:parseLinkedTestEnvClearValue(trimLinkedTestEntryField(kv.env_clear),"--add"),shared_host_safe:parseLinkedTestBooleanValue(trimLinkedTestEntryField(kv.shared_host_safe),"--add","shared_host_safe"),assert_stdout_contains:parseLinkedTestStringList(trimLinkedTestEntryField(kv.assert_stdout_contains)),assert_stdout_regex:parseLinkedTestRegexList(trimLinkedTestEntryField(kv.assert_stdout_regex),"--add","assert_stdout_regex"),assert_stderr_contains:parseLinkedTestStringList(trimLinkedTestEntryField(kv.assert_stderr_contains)),assert_stderr_regex:parseLinkedTestRegexList(trimLinkedTestEntryField(kv.assert_stderr_regex),"--add","assert_stderr_regex"),assert_stdout_min_lines:parseLinkedTestMinLines(trimLinkedTestEntryField(kv.assert_stdout_min_lines),"--add"),assert_json_field_equals:parseLinkedTestAssertionEqualsMap(trimLinkedTestEntryField(kv.assert_json_field_equals),"--add"),assert_json_field_gte:parseLinkedTestAssertionGteMap(trimLinkedTestEntryField(kv.assert_json_field_gte),"--add"),note:trimLinkedTestEntryField(kv.note)}}function trimLinkedTestEntryField(value){const trimmed=value?.trim();return trimmed&&trimmed.length>0?trimmed:void 0}function parseLinkedTestTimeoutSeconds(timeoutSecondsRaw,timeoutAliasRaw){if(timeoutSecondsRaw&&timeoutAliasRaw&&timeoutSecondsRaw!==timeoutAliasRaw)throw new PmCliError("--add timeout and timeout_seconds must match when both are provided",EXIT_CODE.USAGE);const timeoutRaw=timeoutSecondsRaw??timeoutAliasRaw;if(timeoutRaw===void 0)return;const timeout=Math.floor(parseOptionalNumber(timeoutRaw,"timeout_seconds"));if(timeout<=0)throw new PmCliError("--add timeout_seconds must be a positive integer",EXIT_CODE.USAGE);return timeout}function parseAddJsonEntries(raw){return raw?raw.flatMap(entry=>{const parsed=parseLinkedTestJsonEntries(entry,"--add-json");for(const linkedTest of parsed){const command=linkedTest.command;if(!command)throw new PmCliError("--add-json requires a non-empty command string",EXIT_CODE.USAGE);assertNoRecursiveTestAllCommand(command)}return parsed}):[]}function parseRemoveEntries(raw){return raw?raw.map(entry=>{const trimmed=entry.trim();if(!trimmed)throw new PmCliError("--remove requires command or path value",EXIT_CODE.USAGE);const identityPrefix=/^(command|path)\s*=/i.exec(trimmed);if(identityPrefix){const value=trimmed.slice(identityPrefix[0].length).trim();if(!value)throw new PmCliError("--remove requires a non-empty command=<value> or path=<value>",EXIT_CODE.USAGE);return{kind:identityPrefix[1].toLowerCase(),value}}if(/^[A-Za-z_][\w-]*=\S+$/u.test(trimmed))throw new PmCliError("--remove structured selectors require command=<value> or path=<value>",EXIT_CODE.USAGE);if(/^(?:[-*+]\s+)?(?:path|command)\s*:/i.test(trimmed)||trimmed.startsWith("```")){const kv=parseCsvKv(trimmed,"--remove"),kind=kv.path!==void 0?"path":"command",value=kv.path??kv.command;if(!value?.trim())throw new PmCliError("--remove requires command=<value> and/or path=<value>",EXIT_CODE.USAGE);return{kind,value:value.trim()}}return{kind:"either",value:trimmed}}):[]}function parseRemoveIndexes(raw){return raw?[...new Set(raw.map(value=>{const parsed=typeof value=="number"?value:Number.parseInt(value,10);if(!Number.isInteger(parsed)||parsed<1||String(parsed)!==String(value).trim())throw new PmCliError("--remove-index must be a positive integer",EXIT_CODE.USAGE);return parsed}))]:[]}function closeLinkedTestStdin(child){try{child.stdin?.end()}catch{}}function summarizeLinkedTestCommand(command){const normalized=command.trim().replaceAll(/\s+/g," ");return normalized.length<=MAX_LINKED_TEST_COMMAND_LABEL_LENGTH?normalized:`${normalized.slice(0,MAX_LINKED_TEST_COMMAND_LABEL_LENGTH-3)}...`}function shouldEmitLinkedTestProgress(mode){return mode==="off"?!1:mode==="always"?!0:process.stderr.isTTY===!0}function emitLinkedTestProgress(message){try{process.stderr.write(`${message}
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]="c4a9251c-4eeb-5643-b249-54ed51368540")}catch{}})();import{readJsonPathValue,splitJsonPathSegments}from"./json-path.js";import{detectEmptyLinkedTestRun,findLinkedTestExecutionReceipt}from"./execution-receipts.js";import{seedLinkedTestWorkspaceSnapshot}from"./workspace-snapshot.js";import{detectLinkedTestCommandNotFound}from"./command-resolution.js";import{updateLinkedTest,parseLinkedTestUpdateOptions,assertLinkedTestEditUsage}from"./update.js";import{admitLinkedTestRequest,assertLinkedTestCommand}from"./entry.js";import{assertInitializedTracker}from"../environment/tracker-preflight.js";import{spawn}from"node:child_process";import{cp,mkdir,mkdtemp,open,readdir,rm,symlink}from"node:fs/promises";import{tmpdir}from"node:os";import path from"node:path";import{getActiveExtensionRegistrations}from"../../core/extensions/index.js";import{isFileMissingError,pathExists,readFileIfExists}from"../../core/fs/fs-utils.js";import{getWorkspaceHistoryPath,writeWorkspaceJsonWithHistory}from"../../core/history/workspace-history.js";import{resolveItemTypeRegistry}from"../../core/item/type-registry.js";import{createStdinTokenResolver,parseCsvKv,parseOptionalNumber}from"../../core/item/parse.js";import{EXIT_CODE}from"../../core/shared/constants.js";import{resolveAuthor}from"../../core/shared/author.js";import{PmCliError}from"../../core/shared/errors.js";import{stableValueEquals}from"../../core/shared/serialization.js";import{nowIso}from"../../core/shared/time.js";import{locateItem,mutateItem,readLocatedItem}from"../../core/store/item-store.js";import{getSettingsPath,ITEM_FILE_EXTENSIONS,resolveGlobalPmRoot,resolvePmRoot}from"../../core/store/paths.js";import{readSettings}from"../../core/store/settings.js";import{appendTrackedTestRunSummary,buildTrackedTestRunHistory}from"../../core/test/item-test-run-tracking.js";import{runInit}from"../init.js";import{looksLikeStructuredLinkedTestEntry,normalizeStructuredLinkedTestEntry}from"./entry.js";import{LINKED_TEST_PM_CONTEXT_MODE_VALUES as PM_CONTEXT_MODE_VALUES,LINKED_TEST_PROTECTED_ENV_KEYS,parseLinkedTestAssertionEqualsMap,parseLinkedTestAssertionGteMap,parseLinkedTestBoolean as parseLinkedTestBooleanValue,parseLinkedTestContextMode as parseLinkedTestContextModeValue,parseLinkedTestEnvClear as parseLinkedTestEnvClearValue,parseLinkedTestEnvSet as parseLinkedTestEnvSetValue,parseLinkedTestJsonEntries,parseLinkedTestMinLines,parseLinkedTestRegexList,parseLinkedTestStringList,parseLinkedTestWorkspaceContextMode}from"./parsers.js";import{parseOnlyIndexValue,resolveLinkedTestRunSelection}from"../../core/test/run-selectors.js";import{SCOPE_VALUES}from"../../types/index.js";import{diffTestRunMeasurements,parseTestRunMeasurements,queryTestRunMeasurementsBelow}from"./measurements.js";import{withHostEnvironmentBoundary}from"../environment/host-environment-errors.js";import{SOURCE_CONTEXT_ACCESS_ENV}from"../environment/source-context.js";import{assertTestMeasurementExecution,assertTestMeasurementRun}from"./prerequisites.js";import{acknowledgeLinkedTests,attachLinkedTestMutationProvenance,resolveLinkedTestSourceRef,resolveLinkedTestSourceWorkspaceRoot,resolveLinkedTestTrust}from"./trust.js";const TEST_OUTPUT_MAX_BUFFER_BYTES=20*1024*1024,DEFAULT_LINKED_TEST_TIMEOUT_FORCE_KILL_DELAY_MS=3e3,DEFAULT_LINKED_TEST_HEARTBEAT_INTERVAL_MS=1e4,DEFAULT_LINKED_TEST_PIPE_CLOSE_GRACE_MS=5e3,MAX_LINKED_TEST_COMMAND_LABEL_LENGTH=120,LINKED_TEST_TRACKER_DIRS_TO_SKIP=new Set(["locks","extensions","runtime"]),LINKED_TEST_ITEM_COUNT_DIRS_TO_SKIP=new Set(["history","index","search","extensions","locks","runtime"]),LINKED_TEST_INFRA_COLLISION_PATTERNS=[/eaddrinuse/i,/address already in use/i,/port\s+\d+\s+is already in use/i,/web server[^.\n]*already running/i,/failed to listen on/i],LINKED_TEST_LOCK_ACQUISITION_PATTERN=/\b(?:could not|failed to|unable to)\s+acquire\b[^\n.]*\block\b/i,LINKED_TEST_LOCK_CONTENTION_CONTEXT_PATTERN=/\b(?:already running|timed?\s*out|timeout|held by another|lock\s+is\s+held)\b/i,PM_TRACKER_READ_SUBCOMMANDS=new Set(["activity","calendar","context","ctx","deps","get","health","history","list","list-all","list-blocked","list-canceled","list-closed","list-draft","list-in-progress","list-open","search","stats","test-all","validate"]);function readPositiveIntegerEnv(name,fallback){const raw=process.env[name];if(typeof raw!="string"||raw.trim().length===0)return fallback;const parsed=Number.parseInt(raw,10);return!Number.isFinite(parsed)||parsed<=0?fallback:parsed}function linkedTestTimeoutForceKillDelayMs(){return readPositiveIntegerEnv("PM_LINKED_TEST_TIMEOUT_FORCE_KILL_DELAY_MS",DEFAULT_LINKED_TEST_TIMEOUT_FORCE_KILL_DELAY_MS)}function linkedTestHeartbeatIntervalMs(){return readPositiveIntegerEnv("PM_LINKED_TEST_HEARTBEAT_INTERVAL_MS",DEFAULT_LINKED_TEST_HEARTBEAT_INTERVAL_MS)}function linkedTestPipeCloseGraceMs(){return readPositiveIntegerEnv("PM_LINKED_TEST_PIPE_CLOSE_GRACE_MS",DEFAULT_LINKED_TEST_PIPE_CLOSE_GRACE_MS)}function resolveTrackedRunId(kind){const fromEnv=process.env.PM_BACKGROUND_TEST_RUN_ID?.trim();return fromEnv&&fromEnv.length>0?fromEnv:`${kind}-local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`}function summarizeRunResultStatuses(results){let passed=0,failed=0,skipped=0;for(const entry of results){if(entry.status==="passed"){passed+=1;continue}if(entry.status==="failed"||entry.status==="refused"){failed+=1;continue}skipped+=1}return{passed,failed,skipped}}function summarizeContextPreflight(runResults){let checkedPmCommands=0,trackerReadCommands=0,mismatches=0,autoRemediated=0;for(const result of runResults){const context=result.execution_context;!context||context.is_pm_command!==!0||(checkedPmCommands+=1,context.is_pm_tracker_read_command===!0&&(trackerReadCommands+=1),context.mismatch_detected===!0&&(mismatches+=1),context.auto_pm_context_applied===!0&&(autoRemediated+=1))}return{checked_pm_commands:checkedPmCommands,tracker_read_commands:trackerReadCommands,mismatches,auto_remediated:autoRemediated}}function ensureScope(raw){const value=raw??"project";if(!SCOPE_VALUES.includes(value))throw new PmCliError(`Invalid scope "${raw}"`,EXIT_CODE.USAGE);return value}function parsePmContextMode(raw){if(!raw)return"schema";const normalized=raw.trim().toLowerCase();if(PM_CONTEXT_MODE_VALUES.includes(normalized))return normalized;throw new PmCliError(`Invalid --pm-context value "${raw}". Expected one of: ${PM_CONTEXT_MODE_VALUES.join(", ")}`,EXIT_CODE.USAGE)}function parseWorkspaceContextMode(raw){return parseLinkedTestWorkspaceContextMode(raw??"source","--workspace-context")}function resolveLinkedTestWorkspaceContextMode(linkedTest,runLevelMode,overrideLinkedMode){return overrideLinkedMode?runLevelMode:parseLinkedTestWorkspaceContextMode(linkedTest.workspace_context_mode,"linked test")??runLevelMode}function resolveLinkedTestRequestedContextMode(linkedTest,runLevelMode,overrideLinkedPmContext){return overrideLinkedPmContext||typeof linkedTest.pm_context_mode!="string"||linkedTest.pm_context_mode.trim().length===0?runLevelMode:parsePmContextMode(linkedTest.pm_context_mode)}function resolveLinkedTestEffectiveContextMode(requestedMode,isPmTrackerReadCommand){return requestedMode==="auto"?isPmTrackerReadCommand?"tracker":"schema":requestedMode}function hasLinkedTestAssertions(linkedTest){return Object.entries(linkedTest).filter(([key])=>key.startsWith("assert_")).some(([,value])=>value==null?!1:Array.isArray(value)?value.length>0:typeof value=="object"?Object.keys(value).length>0:typeof value=="string"?value.trim().length>0:!0)}function buildPmContextMismatchHint(params){const{executionContext,runLevelPmContextMode,linkedOverridePmContextMode}=params;return!executionContext.is_pm_tracker_read_command||!executionContext.mismatch_detected?"":runLevelPmContextMode==="tracker"&&linkedOverridePmContextMode==="schema"?" Linked test metadata pm_context_mode=schema overrides run-level --pm-context tracker. Set pm_context_mode=tracker (or auto) on the linked test, or remove the override, to run against seeded tracker data.":executionContext.pm_context_mode==="schema"?" Use --auto-pm-context to route PM tracker-read linked commands through seeded tracker data automatically. Alternatively, use --pm-context tracker for the whole run.":""}function mergeEnvSetDirectives(entries,optionName){const merged={};if(!entries)return merged;for(const entry of entries){const parsed=parseLinkedTestEnvSetValue(entry,optionName);if(parsed)for(const[key,value]of Object.entries(parsed))merged[key]=value}return merged}function mergeEnvClearDirectives(entries,optionName){if(!entries)return[];const values=[];for(const entry of entries){const parsed=parseLinkedTestEnvClearValue(entry,optionName);parsed&&values.push(...parsed)}return[...new Set(values)]}import{BUN_GLOBAL_FLAGS_WITH_VALUE,NPM_GLOBAL_FLAGS_WITH_VALUE,PNPM_GLOBAL_FLAGS_WITH_VALUE,SCRIPT_RUN_FLAGS_WITH_VALUE,SCRIPT_RUN_SUBCOMMANDS,YARN_GLOBAL_FLAGS_WITH_VALUE,firstPmSubcommand,extractPmInvocationArgsFromSegment,isPmCliPackageToken,isPmCliScriptToken,isPmExecutableToken,parseLauncherSubcommand,parseNpmExecCommand,parseNpxCommand,parsePnpmDlxCommand,resolvePmSubcommandContext,splitNormalizedCommandSegments,stripLeadingEnvAssignments}from"./linked-command-detection.js";import{extractReferencedPmItemIdsFromCommand}from"./linked-command-detection.js";function commandInvokesPmCli(command){const normalizedCommand=normalizeCommandForValidation(command);return splitNormalizedCommandSegments(normalizedCommand).some(segment=>extractPmInvocationArgsFromSegment(segment)!==null)}function commandInvokesPmTrackerReadCommand(command){const normalizedCommand=normalizeCommandForValidation(command);return splitNormalizedCommandSegments(normalizedCommand).some(segment=>{const invocationArgs=extractPmInvocationArgsFromSegment(segment);if(!invocationArgs)return!1;const context=resolvePmSubcommandContext(invocationArgs);return context?PM_TRACKER_READ_SUBCOMMANDS.has(context.subcommand):!1})}function resolveDirectRunnerSubcommand(parsed){if(parsed)return SCRIPT_RUN_SUBCOMMANDS.has(parsed.subcommand)?parseLauncherSubcommand(parsed.args,SCRIPT_RUN_FLAGS_WITH_VALUE)?.subcommand:parsed.subcommand}function firstDirectTestRunnerSubcommand(executable,args){if(executable==="npx"||executable==="bunx")return parseNpxCommand(args)?.command;if(executable==="pnpm")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,PNPM_GLOBAL_FLAGS_WITH_VALUE));if(executable==="npm")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,NPM_GLOBAL_FLAGS_WITH_VALUE));if(executable==="yarn")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,YARN_GLOBAL_FLAGS_WITH_VALUE));if(executable==="bun")return resolveDirectRunnerSubcommand(parseLauncherSubcommand(args,BUN_GLOBAL_FLAGS_WITH_VALUE))}function isDirectTestRunnerSubcommand(token){return token?token==="vitest"||token==="test"||token.startsWith("test:"):!1}function parsedLauncherInvokesRecursiveTestAll(parsed){return!parsed||!isPmExecutableToken(parsed.command)&&!isPmCliPackageToken(parsed.command)?!1:firstPmSubcommand(parsed.args)==="test-all"}const SHELL_COMMAND_FLAG_PATTERNS=new Map([...["sh","bash","dash","zsh"].map(executable=>[executable,/^(?:-c|-[a-z]*c[a-z]*)$/u]),...["cmd","cmd.exe"].map(executable=>[executable,/^\/[ck]$/u]),...["powershell","powershell.exe","pwsh","pwsh.exe"].map(executable=>[executable,/^-(?:c|command)$/u])]),PACKAGE_EXEC_EXECUTABLES=new Set(["npx","bunx"]);function segmentInvokesRecursiveTestAll(segment){const rawTokens=segment.split(" ").filter(token=>token.length>0),tokens=stripLeadingEnvAssignments(rawTokens);if(tokens.length===0)return!1;const[executable,...args]=tokens,shellCommandFlagPattern=SHELL_COMMAND_FLAG_PATTERNS.get(executable);if(shellCommandFlagPattern){const commandFlagIndex=args.findIndex(arg=>shellCommandFlagPattern.test(arg));return commandFlagIndex>=0?segmentInvokesRecursiveTestAll(args.slice(commandFlagIndex+1).join(" ")):!1}return isPmExecutableToken(executable)||isPmCliScriptToken(executable)?firstPmSubcommand(args)==="test-all":executable==="node"&&args.length>0&&isPmCliScriptToken(args[0])?firstPmSubcommand(args.slice(1))==="test-all":PACKAGE_EXEC_EXECUTABLES.has(executable)?parsedLauncherInvokesRecursiveTestAll(parseNpxCommand(args)):executable==="pnpm"?parsedLauncherInvokesRecursiveTestAll(parsePnpmDlxCommand(args)):executable==="npm"?parsedLauncherInvokesRecursiveTestAll(parseNpmExecCommand(args)):!1}function invokesRecursiveTestAllCommand(command){const normalized=normalizeCommandForValidation(command);return splitNormalizedCommandSegments(normalized).some(segment=>segmentInvokesRecursiveTestAll(segment))}function assertNoRecursiveTestAllCommand(command){if(invokesRecursiveTestAllCommand(command))throw new PmCliError('Linked test commands must not invoke "pm test-all"; this creates recursive orchestration.',EXIT_CODE.USAGE)}function normalizeCommandForValidation(command){return command.trim().replaceAll("\\","/").replaceAll('"',"").replaceAll("'","").replaceAll(/\s+/g," ").toLowerCase()}function commandUsesSandboxRunner(normalizedCommand){return normalizedCommand.includes("node scripts/run-tests.mjs ")||normalizedCommand.endsWith("node scripts/run-tests.mjs")||normalizedCommand.includes("node ./scripts/run-tests.mjs ")||normalizedCommand.endsWith("node ./scripts/run-tests.mjs")}function segmentInvokesDirectTestRunner(normalizedSegment){const rawTokens=normalizedSegment.split(" ").filter(token=>token.length>0),tokens=stripLeadingEnvAssignments(rawTokens);if(tokens.length===0)return!1;const[executable,...args]=tokens,shellCommandFlagPattern=SHELL_COMMAND_FLAG_PATTERNS.get(executable);if(shellCommandFlagPattern){const commandFlagIndex=args.findIndex(argument=>shellCommandFlagPattern.test(argument));return commandFlagIndex>=0&&segmentInvokesDirectTestRunner(args.slice(commandFlagIndex+1).join(" "))}return executable==="vitest"||executable.endsWith("/vitest")||executable.endsWith("/vitest.mjs")?!0:executable==="node"?nodeArgsInvokeDirectTestRunner(args):packageManagerInvokesDirectTestRunner(executable,args)}function nodeArgsInvokeDirectTestRunner(args){return args.includes("--test")||args.some(arg=>arg==="vitest"||arg==="vitest.mjs"||arg.endsWith("/vitest")||arg.endsWith("/vitest.mjs"))}function packageManagerInvokesDirectTestRunner(executable,args){return executable==="npx"||executable==="bunx"?isDirectTestRunnerSubcommand(parseNpxCommand(args)?.command):executable==="pnpm"||executable==="npm"?isDirectTestRunnerSubcommand((executable==="pnpm"?parsePnpmDlxCommand(args):parseNpmExecCommand(args))?.command)||isDirectTestRunnerSubcommand(firstDirectTestRunnerSubcommand(executable,args)):(executable==="yarn"||executable==="bun")&&isDirectTestRunnerSubcommand(firstDirectTestRunnerSubcommand(executable,args))}function classifyLinkedTestCommandSafety(command){const normalized=normalizeCommandForValidation(command),segments=splitNormalizedCommandSegments(normalized),directRunner=segments.some(segment=>!commandUsesSandboxRunner(segment)&&segmentInvokesDirectTestRunner(segment)),sandboxRunner=segments.some(commandUsesSandboxRunner);return{accepted:!0,command_kind:directRunner?"direct_runner":sandboxRunner?"sandbox_runner":"other",tracker_isolation:"runtime_injected",trust_boundary:"provenance",advisory:directRunner?"Accepted with runtime-injected tracker isolation. Prefer the repository sandbox runner when available for reproducible build and coverage setup.":"Accepted with runtime-injected tracker isolation and provenance checks before execution."}}function getRuntimeSafetySkipReason(command){if(invokesRecursiveTestAllCommand(command))return'Linked test command skipped: Linked test commands must not invoke "pm test-all"; this creates recursive orchestration.'}function parseAddEntries(raw){return raw?raw.map(parseAddEntry):[]}function parseAddEntry(entry){const trimmed=entry.trim(),kv=looksLikeStructuredLinkedTestEntry(trimmed)?normalizeStructuredLinkedTestEntry(parseCsvKv(entry,"--add"),"--add"):{command:trimmed},command=trimLinkedTestEntryField(kv.command);if(!command)throw new PmCliError("--add requires command=<value> or a bare command (path=<value> is optional metadata)",EXIT_CODE.USAGE);return assertNoRecursiveTestAllCommand(command),{command,path:trimLinkedTestEntryField(kv.path),scope:ensureScope(kv.scope),timeout_seconds:parseLinkedTestTimeoutSeconds(trimLinkedTestEntryField(kv.timeout_seconds),trimLinkedTestEntryField(kv.timeout)),pm_context_mode:parseLinkedTestContextModeValue(trimLinkedTestEntryField(kv.pm_context_mode),"--add"),workspace_context_mode:parseLinkedTestWorkspaceContextMode(trimLinkedTestEntryField(kv.workspace_context_mode),"--add"),env_set:parseLinkedTestEnvSetValue(trimLinkedTestEntryField(kv.env_set),"--add"),env_clear:parseLinkedTestEnvClearValue(trimLinkedTestEntryField(kv.env_clear),"--add"),shared_host_safe:parseLinkedTestBooleanValue(trimLinkedTestEntryField(kv.shared_host_safe),"--add","shared_host_safe"),assert_stdout_contains:parseLinkedTestStringList(trimLinkedTestEntryField(kv.assert_stdout_contains)),assert_stdout_regex:parseLinkedTestRegexList(trimLinkedTestEntryField(kv.assert_stdout_regex),"--add","assert_stdout_regex"),assert_stderr_contains:parseLinkedTestStringList(trimLinkedTestEntryField(kv.assert_stderr_contains)),assert_stderr_regex:parseLinkedTestRegexList(trimLinkedTestEntryField(kv.assert_stderr_regex),"--add","assert_stderr_regex"),assert_stdout_min_lines:parseLinkedTestMinLines(trimLinkedTestEntryField(kv.assert_stdout_min_lines),"--add"),assert_json_field_equals:parseLinkedTestAssertionEqualsMap(trimLinkedTestEntryField(kv.assert_json_field_equals),"--add"),assert_json_field_gte:parseLinkedTestAssertionGteMap(trimLinkedTestEntryField(kv.assert_json_field_gte),"--add"),note:trimLinkedTestEntryField(kv.note)}}function trimLinkedTestEntryField(value){const trimmed=value?.trim();return trimmed&&trimmed.length>0?trimmed:void 0}function parseLinkedTestTimeoutSeconds(timeoutSecondsRaw,timeoutAliasRaw){if(timeoutSecondsRaw&&timeoutAliasRaw&&timeoutSecondsRaw!==timeoutAliasRaw)throw new PmCliError("--add timeout and timeout_seconds must match when both are provided",EXIT_CODE.USAGE);const timeoutRaw=timeoutSecondsRaw??timeoutAliasRaw;if(timeoutRaw===void 0)return;const timeout=Math.floor(parseOptionalNumber(timeoutRaw,"timeout_seconds"));if(timeout<=0)throw new PmCliError("--add timeout_seconds must be a positive integer",EXIT_CODE.USAGE);return timeout}function parseAddJsonEntries(raw){return raw?raw.flatMap(entry=>{const parsed=parseLinkedTestJsonEntries(entry,"--add-json");for(const linkedTest of parsed){const command=linkedTest.command;if(!command)throw new PmCliError("--add-json requires a non-empty command string",EXIT_CODE.USAGE);assertNoRecursiveTestAllCommand(command)}return parsed}):[]}function parseRemoveEntries(raw){return raw?raw.map(entry=>{const trimmed=entry.trim();if(!trimmed)throw new PmCliError("--remove requires command or path value",EXIT_CODE.USAGE);const identityPrefix=/^(command|path)\s*=/i.exec(trimmed);if(identityPrefix){const value=trimmed.slice(identityPrefix[0].length).trim();if(!value)throw new PmCliError("--remove requires a non-empty command=<value> or path=<value>",EXIT_CODE.USAGE);return{kind:identityPrefix[1].toLowerCase(),value}}if(/^[A-Za-z_][\w-]*=\S+$/u.test(trimmed))throw new PmCliError("--remove structured selectors require command=<value> or path=<value>",EXIT_CODE.USAGE);if(/^(?:[-*+]\s+)?(?:path|command)\s*:/i.test(trimmed)||trimmed.startsWith("```")){const kv=parseCsvKv(trimmed,"--remove"),kind=kv.path!==void 0?"path":"command",value=kv.path??kv.command;if(!value?.trim())throw new PmCliError("--remove requires command=<value> and/or path=<value>",EXIT_CODE.USAGE);return{kind,value:value.trim()}}return{kind:"either",value:trimmed}}):[]}function parseRemoveIndexes(raw){return raw?[...new Set(raw.map(value=>{const parsed=typeof value=="number"?value:Number.parseInt(value,10);if(!Number.isInteger(parsed)||parsed<1||String(parsed)!==String(value).trim())throw new PmCliError("--remove-index must be a positive integer",EXIT_CODE.USAGE);return parsed}))]:[]}function closeLinkedTestStdin(child){try{child.stdin?.end()}catch{}}function summarizeLinkedTestCommand(command){const normalized=command.trim().replaceAll(/\s+/g," ");return normalized.length<=MAX_LINKED_TEST_COMMAND_LABEL_LENGTH?normalized:`${normalized.slice(0,MAX_LINKED_TEST_COMMAND_LABEL_LENGTH-3)}...`}function shouldEmitLinkedTestProgress(mode){return mode==="off"?!1:mode==="always"?!0:process.stderr.isTTY===!0}function emitLinkedTestProgress(message){try{process.stderr.write(`${message}
2
2
  `)}catch{}}function beginLinkedTestProgress(context,mode){if(!shouldEmitLinkedTestProgress(mode))return null;const commandLabel=summarizeLinkedTestCommand(context.command),startAt=Date.now();emitLinkedTestProgress(`[pm test] linked-test ${context.index}/${context.total} start timeout_ms=${context.timeoutMs} command="${commandLabel}"`);const heartbeat=setInterval(()=>{const elapsedMs=Date.now()-startAt;emitLinkedTestProgress(`[pm test] linked-test ${context.index}/${context.total} running elapsed_ms=${elapsedMs} command="${commandLabel}"`)},linkedTestHeartbeatIntervalMs());return heartbeat.unref?.(),heartbeat}function endLinkedTestProgress(context,executionResult,startedAt,mode){if(!shouldEmitLinkedTestProgress(mode))return;const commandLabel=summarizeLinkedTestCommand(context.command),elapsedMs=Date.now()-startedAt,statusLabel=executionResult.timedOut||executionResult.maxBufferExceeded||executionResult.exitCode!==0?"failed":"passed",reasonTokens=[];executionResult.timedOut&&reasonTokens.push("reason=timeout"),executionResult.maxBufferExceeded&&reasonTokens.push("reason=max_buffer"),executionResult.outputTruncated&&reasonTokens.push("output=truncated"),executionResult.signal&&reasonTokens.push(`signal=${executionResult.signal}`);const exitLabel=executionResult.exitCode===null?"null":String(executionResult.exitCode),reasonSuffix=reasonTokens.length>0?` ${reasonTokens.join(" ")}`:"";emitLinkedTestProgress(`[pm test] linked-test ${context.index}/${context.total} end status=${statusLabel} exit_code=${exitLabel} elapsed_ms=${elapsedMs}${reasonSuffix} command="${commandLabel}"`)}async function killProcessTree(pid){if(!(!Number.isInteger(pid)||pid<=0)){if(process.platform==="win32"){await new Promise(resolve=>{const killer=spawn("taskkill",["/pid",String(pid),"/T","/F"],{stdio:"ignore",windowsHide:!0});killer.on("error",()=>resolve()),killer.on("close",()=>resolve())});return}try{process.kill(-pid,"SIGKILL");return}catch{}try{process.kill(pid,"SIGKILL")}catch{}}}function clearLinkedTestTimers(timers){timers.heartbeat&&clearInterval(timers.heartbeat),timers.timedOutTimer&&(clearTimeout(timers.timedOutTimer),timers.timedOutTimer=null),timers.forceKillTimer&&(clearTimeout(timers.forceKillTimer),timers.forceKillTimer=null)}function createLinkedTestTerminationRequester(child,timers){let terminationRequested=!1;return async()=>{if(terminationRequested)return;terminationRequested=!0;const pid=child.pid;if(!pid||pid<=0){try{child.kill("SIGTERM")}catch{}return}if(process.platform==="win32"){await killProcessTree(pid);return}try{process.kill(-pid,"SIGTERM")}catch{try{child.kill("SIGTERM")}catch{}}timers.forceKillTimer=setTimeout(()=>{killProcessTree(pid)},linkedTestTimeoutForceKillDelayMs()),timers.forceKillTimer.unref?.()}}function waitForLinkedTestChildClose(child){return new Promise(resolve=>{let settled=!1,pipeCloseGraceTimer=null;const settle=(value,destroyPipes=!1)=>{settled||(settled=!0,pipeCloseGraceTimer&&(clearTimeout(pipeCloseGraceTimer),pipeCloseGraceTimer=null),destroyPipes&&(child.stdin?.destroy(),child.stdout?.destroy(),child.stderr?.destroy()),resolve(value))};child.on("exit",(exitCode,exitSignal)=>{settled||(pipeCloseGraceTimer=setTimeout(()=>{settle({code:exitCode,signal:exitSignal},!0)},linkedTestPipeCloseGraceMs()),pipeCloseGraceTimer.unref?.())}),child.on("close",(closeCode,closeSignal)=>{settle({code:closeCode,signal:closeSignal})})})}function createLinkedTestChild(command,env,stdoutFd,stderrFd,cwd){return spawn(command,{cwd,env,shell:!0,windowsHide:!0,detached:process.platform!=="win32",stdio:["pipe",stdoutFd,stderrFd]})}async function readLinkedTestCapture(capturePath){const handle=await open(capturePath,"r");try{const size=(await handle.stat()).size,retainedBytes=Math.min(size,TEST_OUTPUT_MAX_BUFFER_BYTES),buffer=Buffer.alloc(retainedBytes);return retainedBytes>0&&await handle.read(buffer,0,retainedBytes,0),{text:buffer.toString("utf8"),truncated:size>TEST_OUTPUT_MAX_BUFFER_BYTES}}finally{await handle.close()}}async function runLinkedTestCommand(command,timeoutMs,env,progressContext,progressMode,cwd=process.cwd()){const startedAt=Date.now(),captureRoot=await mkdtemp(path.join(tmpdir(),"pm-linked-test-output-"));try{const stdoutPath=path.join(captureRoot,"stdout.log"),stderrPath=path.join(captureRoot,"stderr.log"),[stdoutHandle,stderrHandle]=await Promise.all([open(stdoutPath,"w"),open(stderrPath,"w")]);let child;try{child=createLinkedTestChild(command,env,stdoutHandle.fd,stderrHandle.fd,cwd)}catch(error){throw await Promise.all([stdoutHandle.close(),stderrHandle.close()]),error}const childClose=waitForLinkedTestChildClose(child);let spawnError;child.on("error",error=>{spawnError=error.message}),await Promise.all([stdoutHandle.close(),stderrHandle.close()]),closeLinkedTestStdin(child);const timers={heartbeat:beginLinkedTestProgress(progressContext,progressMode),forceKillTimer:null,timedOutTimer:null},requestTermination=createLinkedTestTerminationRequester(child,timers);let timedOut=!1;timers.timedOutTimer=setTimeout(()=>{timedOut=!0,requestTermination()},timeoutMs),timers.timedOutTimer.unref?.();const{code,signal}=await childClose;clearLinkedTestTimers(timers);const[stdout,stderr]=await Promise.all([readLinkedTestCapture(stdoutPath),readLinkedTestCapture(stderrPath)]),outputTruncated=stdout.truncated||stderr.truncated,truncationNotice=outputTruncated?`
3
3
  [pm linked-test output truncated at ${TEST_OUTPUT_MAX_BUFFER_BYTES} bytes per stream; the child process continued to completion]
4
- `:"",executionResult={stdout:`${stdout.text}${truncationNotice}`,stderr:stderr.text,exitCode:code,signal,timedOut,maxBufferExceeded:!1,outputTruncated,spawnError};return endLinkedTestProgress(progressContext,executionResult,startedAt,progressMode),executionResult}finally{await rm(captureRoot,{recursive:!0,force:!0})}}function formatLinkedTestExecutionError(result,timeoutMs){const details=[];result.maxBufferExceeded&&details.push(`Linked test output exceeded maxBuffer=${TEST_OUTPUT_MAX_BUFFER_BYTES} bytes. Reduce output volume or split the command.`),result.timedOut&&timeoutMs>0&&details.push(`Linked test timed out after ${timeoutMs}ms.`);const signalMessage=result.signal?`Linked test command terminated by signal ${result.signal}.`:void 0,baseMessage=result.spawnError?.trim()||signalMessage||"Linked test command failed.";return details.length===0?baseMessage:`${baseMessage} ${details.join(" ")}`}function hasInfraCollisionSignal(result){const combined=[result.spawnError??"",result.stderr,result.stdout].join(`
4
+ `:"";!spawnError&&await detectLinkedTestCommandNotFound(command,{exitCode:code,stderr:stderr.text},env,cwd)&&(spawnError="Linked test executable was not found. Install the tool or update the linked command before retrying.");const executionResult={stdout:`${stdout.text}${truncationNotice}`,stderr:stderr.text,exitCode:code,signal,timedOut,maxBufferExceeded:!1,outputTruncated,spawnError};return endLinkedTestProgress(progressContext,executionResult,startedAt,progressMode),executionResult}finally{await rm(captureRoot,{recursive:!0,force:!0})}}function formatLinkedTestExecutionError(result,timeoutMs){const details=[];result.maxBufferExceeded&&details.push(`Linked test output exceeded maxBuffer=${TEST_OUTPUT_MAX_BUFFER_BYTES} bytes. Reduce output volume or split the command.`),result.timedOut&&timeoutMs>0&&details.push(`Linked test timed out after ${timeoutMs}ms.`);const signalMessage=result.signal?`Linked test command terminated by signal ${result.signal}.`:void 0,baseMessage=result.spawnError?.trim()||signalMessage||"Linked test command failed.";return details.length===0?baseMessage:`${baseMessage} ${details.join(" ")}`}function hasInfraCollisionSignal(result){const combined=[result.spawnError??"",result.stderr,result.stdout].join(`
5
5
  `);return LINKED_TEST_INFRA_COLLISION_PATTERNS.some(pattern=>pattern.test(combined))||combined.split(/\r?\n/u).some(line=>LINKED_TEST_LOCK_ACQUISITION_PATTERN.test(line)&&LINKED_TEST_LOCK_CONTENTION_CONTEXT_PATTERN.test(line))}function classifyLinkedTestFailure(result){return hasInfraCollisionSignal(result)?"infra_collision":result.timedOut?"timeout":result.maxBufferExceeded?"max_buffer":result.spawnError?"spawn_error":result.signal?"signal":"assertion_failure"}function createEmptyFailureCategoryCounts(){return{infra_collision:0,trust_refusal:0,config_refusal:0,assertion_failure:0,empty_run:0,timeout:0,max_buffer:0,spawn_error:0,signal:0}}function countFailureCategories(runResults){const counts=createEmptyFailureCategoryCounts();for(const result of runResults)result.status!=="failed"&&result.status!=="refused"||!result.failure_category||(counts[result.failure_category]+=1);return counts}function applyEnvDirectiveStage(env,directives){for(const[key,value]of Object.entries(directives.env_set))LINKED_TEST_PROTECTED_ENV_KEYS.has(key.toUpperCase())||(env[key]=value);for(const key of directives.env_clear)LINKED_TEST_PROTECTED_ENV_KEYS.has(key.toUpperCase())||delete env[key]}function applySharedHostSafeDefaults(env){env.PORT===void 0&&(env.PORT="0"),env.HOST===void 0&&(env.HOST="127.0.0.1"),env.PM_SHARED_HOST_SAFE===void 0&&(env.PM_SHARED_HOST_SAFE="1"),env.PLAYWRIGHT_HTML_OPEN===void 0&&(env.PLAYWRIGHT_HTML_OPEN="never"),env.PW_TEST_HTML_REPORT_OPEN===void 0&&(env.PW_TEST_HTML_REPORT_OPEN="never")}function resolveEffectiveLinkedTestDirectives(runtimeDirectives,linkedTest){const envSet={...runtimeDirectives.env_set,...linkedTest.env_set},envClear=[...new Set([...runtimeDirectives.env_clear,...linkedTest.env_clear??[]])],sharedHostSafe=linkedTest.shared_host_safe??runtimeDirectives.shared_host_safe;return{env_set:envSet,env_clear:envClear,shared_host_safe:sharedHostSafe}}function resolveRuntimeDirectives(envSetEntries,envClearEntries,sharedHostSafe){return{env_set:mergeEnvSetDirectives(envSetEntries,"--env-set"),env_clear:mergeEnvClearDirectives(envClearEntries,"--env-clear"),shared_host_safe:sharedHostSafe===!0}}async function copyIntoSandboxIfPresent(sourcePath,targetPath,recursive=!1,makeDirectory=mkdir){if(await pathExists(sourcePath))try{await withHostEnvironmentBoundary("seed_linked_test",async()=>{if(await makeDirectory(path.dirname(targetPath),{recursive:!0}),recursive){await cp(sourcePath,targetPath,{recursive:!0,force:!0});return}await cp(sourcePath,targetPath,{force:!0})},{why:"Linked tests need an isolated PM context without mutating the source tracker.",nextSteps:["Reclaim capacity on the filesystem that hosts the linked-test temporary directory."]})}catch(error){if(typeof error=="object"&&error!==null&&"code"in error&&isFileMissingError(error))return;throw error}}async function seedLinkedTestSandbox(sandboxPmPath,sandboxGlobalPath,sourceRoots,auditSettings=!1){for(const[sourceRoot,sandboxRoot]of[[sourceRoots.projectPmRoot,sandboxPmPath],[sourceRoots.globalPmRoot,sandboxGlobalPath]])if(auditSettings){const raw=await readFileIfExists(getSettingsPath(sourceRoot));if(raw!==null){const settings=await readSettings(sandboxRoot);await writeWorkspaceJsonWithHistory({pmRoot:sandboxRoot,filePath:getSettingsPath(sandboxRoot),raw,op:"settings:write",author:resolveAuthor(void 0,settings.author_default),lockTtlSeconds:settings.locks.ttl_seconds,lockWaitMs:settings.locks.wait_ms,recordCreation:!0,message:"Seed linked-test schema settings history"})}}else await copyIntoSandboxIfPresent(getSettingsPath(sourceRoot),getSettingsPath(sandboxRoot));await copyIntoSandboxIfPresent(path.join(sourceRoots.projectPmRoot,"extensions"),path.join(sandboxPmPath,"extensions"),!0),await copyIntoSandboxIfPresent(path.join(sourceRoots.globalPmRoot,"extensions"),path.join(sandboxGlobalPath,"extensions"),!0)}async function seedLinkedTestTrackerData(sourceRoot,sandboxRoot){if(!await pathExists(sourceRoot))return;const entries=await readdir(sourceRoot,{withFileTypes:!0});for(const entry of entries){if(LINKED_TEST_TRACKER_DIRS_TO_SKIP.has(entry.name))continue;const sourcePath=path.join(sourceRoot,entry.name),targetPath=path.join(sandboxRoot,entry.name);if(entry.isDirectory()){await copyIntoSandboxIfPresent(sourcePath,targetPath,!0);continue}entry.isFile()&&await copyIntoSandboxIfPresent(sourcePath,targetPath)}}async function seedMissingLinkedTestSettingsHistory(sourceRoot,sandboxRoot){const sourceSettingsRaw=await readFileIfExists(getSettingsPath(sourceRoot));if(sourceSettingsRaw===null||await pathExists(getWorkspaceHistoryPath(sourceRoot)))return;const sandboxSettings=await readSettings(sandboxRoot);await rm(getWorkspaceHistoryPath(sandboxRoot),{force:!0}),await rm(getSettingsPath(sandboxRoot),{force:!0}),await writeWorkspaceJsonWithHistory({pmRoot:sandboxRoot,filePath:getSettingsPath(sandboxRoot),raw:sourceSettingsRaw,op:"settings:write",author:resolveAuthor(void 0,sandboxSettings.author_default),lockTtlSeconds:sandboxSettings.locks.ttl_seconds,lockWaitMs:sandboxSettings.locks.wait_ms,recordCreation:!0,message:"Seed linked-test tracker settings history"})}async function countLinkedTestItemFiles(pmRoot){if(!await pathExists(pmRoot))return 0;let total=0;const entries=await readdir(pmRoot,{withFileTypes:!0});for(const entry of entries){if(!entry.isDirectory()||LINKED_TEST_ITEM_COUNT_DIRS_TO_SKIP.has(entry.name))continue;const folderPath=path.join(pmRoot,entry.name);let files;try{files=await readdir(folderPath,{withFileTypes:!0})}catch{continue}for(const file of files)file.isFile()&&ITEM_FILE_EXTENSIONS.some(extension=>file.name.toLowerCase().endsWith(extension))&&(total+=1)}return total}function resolveLinkedTestFailureExitCode(execution){const rawExitCode=typeof execution.exitCode=="number"?execution.exitCode:1;return(execution.timedOut||execution.maxBufferExceeded)&&rawExitCode===0?1:rawExitCode}function parseAssertionLiteral(raw){const trimmed=raw.trim();if(trimmed==="true")return!0;if(trimmed==="false")return!1;if(trimmed==="null")return null;const numeric=Number(trimmed);if(trimmed.length>0&&Number.isFinite(numeric))return numeric;if(trimmed.startsWith("{")&&trimmed.endsWith("}")||trimmed.startsWith("[")&&trimmed.endsWith("]"))try{return JSON.parse(trimmed)}catch{}return trimmed}function compareAssertionValues(actual,expected){return typeof actual=="object"&&actual!==null&&typeof expected=="object"&&expected!==null?stableValueEquals(actual,expected):Object.is(actual,expected)}function evaluateLinkedTestAssertions(linkedTest,stdout,stderr){const failures=[];failures.push(...evaluateContainsAssertions("stdout",stdout,linkedTest.assert_stdout_contains)),failures.push(...evaluateRegexAssertions("stdout",stdout,linkedTest.assert_stdout_regex)),failures.push(...evaluateContainsAssertions("stderr",stderr,linkedTest.assert_stderr_contains)),failures.push(...evaluateRegexAssertions("stderr",stderr,linkedTest.assert_stderr_regex)),failures.push(...evaluateStdoutLineCountAssertion(stdout,linkedTest.assert_stdout_min_lines));const jsonEqualsAssertions=linkedTest.assert_json_field_equals??{},jsonGteAssertions=linkedTest.assert_json_field_gte??{};if(!(Object.keys(jsonEqualsAssertions).length>0||Object.keys(jsonGteAssertions).length>0))return failures;let parsedJson;try{parsedJson=JSON.parse(stdout)}catch(error){return failures.push(`stdout is not valid JSON for assert_json_field_* checks: ${error instanceof Error?error.message:String(error)}`),failures}for(const[fieldPath,expectedRaw]of Object.entries(jsonEqualsAssertions)){const resolved=readJsonPathValue(parsedJson,fieldPath);if(!resolved.found){failures.push(`assert_json_field_equals missing path "${fieldPath}"`);continue}const expected=parseAssertionLiteral(expectedRaw);compareAssertionValues(resolved.value,expected)||failures.push(`assert_json_field_equals mismatch at "${fieldPath}" (expected=${JSON.stringify(expected)} actual=${JSON.stringify(resolved.value)})`)}for(const[fieldPath,expectedMinimum]of Object.entries(jsonGteAssertions)){const resolved=readJsonPathValue(parsedJson,fieldPath);if(!resolved.found){failures.push(`assert_json_field_gte missing path "${fieldPath}"`);continue}if(typeof resolved.value!="number"||!Number.isFinite(resolved.value)){failures.push(`assert_json_field_gte path "${fieldPath}" resolved to non-numeric value`);continue}resolved.value<expectedMinimum&&failures.push(`assert_json_field_gte failed at "${fieldPath}" (expected >= ${expectedMinimum}, actual ${resolved.value})`)}return failures}function evaluateContainsAssertions(streamName,output,expectedValues){const failures=[];for(const expected of expectedValues??[])output.includes(expected)||failures.push(`${streamName} missing required text: "${expected}"`);return failures}function evaluateRegexAssertions(streamName,output,patterns){const failures=[];for(const pattern of patterns??[])try{new RegExp(pattern,"m").test(output)||failures.push(`${streamName} failed regex assertion: /${pattern}/m`)}catch(error){failures.push(`${streamName} regex assertion is invalid: /${pattern}/ (${error instanceof Error?error.message:String(error)})`)}return failures}function evaluateStdoutLineCountAssertion(stdout,minimum){if(typeof minimum!="number")return[];const lineCount=stdout.split(/\r?\n/).map(line=>line.trim()).filter(line=>line.length>0).length;return lineCount<minimum?[`stdout line count ${lineCount} is below required minimum ${minimum}`]:[]}function commandUsesTestNameFilter(command){const normalized=normalizeCommandForValidation(command);return/(?:^|\s)(?:--test-name-pattern|--testnamepattern)(?:=|\s)/u.test(normalized)?!0:splitNormalizedCommandSegments(normalized).some(segment=>/(?:^|\s)-t(?:=|\s)/u.test(segment)&&segmentInvokesDirectTestRunner(segment))}function createLinkedTestSandboxLayout(sandboxRoot){return{root:sandboxRoot,schemaProjectPmPath:path.join(sandboxRoot,"schema","project",".agents","pm"),schemaGlobalPmPath:path.join(sandboxRoot,"schema","global"),trackerProjectPmPath:path.join(sandboxRoot,"tracker","project",".agents","pm"),trackerGlobalPmPath:path.join(sandboxRoot,"tracker","global"),workspaceSnapshotRoot:path.join(sandboxRoot,"workspace","project"),workspaceIsolatedRoot:path.join(sandboxRoot,"workspace","isolated")}}function linkedTestsRequireWorkspaceSnapshot(tests,runLevelMode,overrideLinkedMode){return tests.some(linkedTest=>resolveLinkedTestWorkspaceContextMode(linkedTest,runLevelMode,overrideLinkedMode)==="snapshot")}async function bindSnapshotTracker(snapshotRoot,sandboxPmPath){const agentsRoot=path.join(snapshotRoot,".agents"),target=path.join(agentsRoot,"pm");await mkdir(agentsRoot,{recursive:!0}),await rm(target,{recursive:!0,force:!0}),await symlink(sandboxPmPath,target,process.platform==="win32"?"junction":"dir")}async function initializeLinkedTestSandboxes(layout,initialize=runInit,includeTrackerData=!0){const initOptions={defaults:!0,agentGuidance:"skip"};await initialize(void 0,{path:layout.schemaProjectPmPath},initOptions),await initialize(void 0,{path:layout.schemaGlobalPmPath},initOptions),includeTrackerData&&(await initialize(void 0,{path:layout.trackerProjectPmPath},initOptions),await initialize(void 0,{path:layout.trackerGlobalPmPath},initOptions))}async function seedLinkedTestSandboxesFromSource(layout,sourceRoots,includeTrackerData){sourceRoots&&(await seedLinkedTestSandbox(layout.schemaProjectPmPath,layout.schemaGlobalPmPath,sourceRoots,!0),includeTrackerData&&(await seedLinkedTestSandbox(layout.trackerProjectPmPath,layout.trackerGlobalPmPath,sourceRoots),await seedLinkedTestTrackerData(sourceRoots.projectPmRoot,layout.trackerProjectPmPath),await seedLinkedTestTrackerData(sourceRoots.globalPmRoot,layout.trackerGlobalPmPath),await seedMissingLinkedTestSettingsHistory(sourceRoots.projectPmRoot,layout.trackerProjectPmPath),await seedMissingLinkedTestSettingsHistory(sourceRoots.globalPmRoot,layout.trackerGlobalPmPath)))}function linkedTestsRequireTrackerData(tests,runLevelPmContextMode,options){return tests.some(linkedTest=>{const command=linkedTest.command??"",isPmTrackerReadCommand=command.length>0&&commandInvokesPmTrackerReadCommand(command),requestedPmContextMode=options?.autoPmContext===!0&&isPmTrackerReadCommand?"auto":resolveLinkedTestRequestedContextMode(linkedTest,runLevelPmContextMode,options?.overrideLinkedPmContext===!0);return resolveLinkedTestEffectiveContextMode(requestedPmContextMode,isPmTrackerReadCommand)==="tracker"})}async function countLinkedTestSandboxItems(layout,sourceRoots){return{sourceProjectItemCount:sourceRoots?await countLinkedTestItemFiles(sourceRoots.projectPmRoot):0,sourceGlobalItemCount:sourceRoots?await countLinkedTestItemFiles(sourceRoots.globalPmRoot):0,schemaProjectItemCount:await countLinkedTestItemFiles(layout.schemaProjectPmPath),schemaGlobalItemCount:await countLinkedTestItemFiles(layout.schemaGlobalPmPath),trackerProjectItemCount:await countLinkedTestItemFiles(layout.trackerProjectPmPath),trackerGlobalItemCount:await countLinkedTestItemFiles(layout.trackerGlobalPmPath)}}function buildLinkedTestExecutionContext(params){const trackerMode=params.effectivePmContextMode==="tracker",selectedSandboxProjectPmPath=trackerMode?params.layout.trackerProjectPmPath:params.layout.schemaProjectPmPath,selectedSandboxGlobalPmPath=trackerMode?params.layout.trackerGlobalPmPath:params.layout.schemaGlobalPmPath,selectedSandboxProjectItemCount=trackerMode?params.counts.trackerProjectItemCount:params.counts.schemaProjectItemCount,selectedSandboxGlobalItemCount=trackerMode?params.counts.trackerGlobalItemCount:params.counts.schemaGlobalItemCount,workingDirectory=params.workspaceContextMode==="snapshot"?params.layout.workspaceSnapshotRoot:params.workspaceContextMode==="isolated"?params.layout.workspaceIsolatedRoot:params.sourceWorkspaceRoot,exposedSourceWorkspaceRoot=params.workspaceContextMode==="isolated"?"":workingDirectory;return{requested_pm_context_mode:params.requestedPmContextMode,pm_context_mode:params.effectivePmContextMode,auto_pm_context_applied:params.autoPmContextApplied,is_pm_command:params.isPmCommand,is_pm_tracker_read_command:params.isPmTrackerReadCommand,source_project_pm_path:params.sourceRoots?.projectPmRoot??"",sandbox_project_pm_path:selectedSandboxProjectPmPath,source_global_pm_path:params.sourceRoots?.globalPmRoot??"",sandbox_global_pm_path:selectedSandboxGlobalPmPath,source_project_item_count:params.counts.sourceProjectItemCount,sandbox_project_item_count:selectedSandboxProjectItemCount,source_global_item_count:params.counts.sourceGlobalItemCount,sandbox_global_item_count:selectedSandboxGlobalItemCount,mismatch_detected:params.isPmCommand&&(params.counts.sourceProjectItemCount!==selectedSandboxProjectItemCount||params.counts.sourceGlobalItemCount!==selectedSandboxGlobalItemCount),project_extensions_seeded:!!params.sourceRoots,global_extensions_seeded:!!params.sourceRoots,requested_workspace_context_mode:params.requestedWorkspaceContextMode,workspace_context_mode:params.workspaceContextMode,working_directory:workingDirectory,source_workspace_root:exposedSourceWorkspaceRoot,trust:params.trust}}async function resolveLinkedTestCommandContext(params){const linkedOverridePmContextMode=typeof params.linkedTest.pm_context_mode=="string"&&params.linkedTest.pm_context_mode.trim().length>0?parsePmContextMode(params.linkedTest.pm_context_mode):void 0,command=typeof params.linkedTest.command=="string"&&params.linkedTest.command.length>0?params.linkedTest.command:void 0,isPmCommand=command?commandInvokesPmCli(command):!1,isPmTrackerReadCommand=isPmCommand&&command?commandInvokesPmTrackerReadCommand(command):!1,autoPmContextApplied=params.options?.autoPmContext===!0&&isPmTrackerReadCommand,requestedPmContextMode=autoPmContextApplied?"auto":resolveLinkedTestRequestedContextMode(params.linkedTest,params.runLevelPmContextMode,params.options?.overrideLinkedPmContext===!0),effectivePmContextMode=resolveLinkedTestEffectiveContextMode(requestedPmContextMode,isPmTrackerReadCommand),selectedSandboxProjectPmPath=effectivePmContextMode==="tracker"?params.layout.trackerProjectPmPath:params.layout.schemaProjectPmPath;params.workspaceContextMode==="snapshot"&&await bindSnapshotTracker(params.layout.workspaceSnapshotRoot,selectedSandboxProjectPmPath);const trust=params.options?.pmRoot?await resolveLinkedTestTrust(params.options.pmRoot,params.linkedTest,params.currentSourceRef):await resolveLinkedTestTrust(params.layout.root,params.linkedTest,params.currentSourceRef);return{linkedOverridePmContextMode,executionContext:buildLinkedTestExecutionContext({layout:params.layout,counts:params.counts,sourceRoots:params.sourceRoots,isPmCommand,isPmTrackerReadCommand,requestedPmContextMode,effectivePmContextMode,autoPmContextApplied,requestedWorkspaceContextMode:params.requestedWorkspaceContextMode,workspaceContextMode:params.workspaceContextMode,sourceWorkspaceRoot:params.sourceWorkspaceRoot,trust})}}function buildLinkedTestSkippedResult(linkedTest,executionContext,error){return{command:linkedTest.command,path:linkedTest.path,status:"skipped",execution_context:executionContext,error}}function buildLinkedTestAssertionFailureResult(linkedTest,executionContext,error,output){return{command:linkedTest.command,path:linkedTest.path,status:"failed",exit_code:1,failure_category:"assertion_failure",execution_context:executionContext,...output?{stdout:output.stdout,stderr:output.stderr}:{},error}}function buildLinkedTestTrustRefusalResult(linkedTest,executionContext,error){return{command:linkedTest.command,path:linkedTest.path,status:"failed",exit_code:1,failure_category:"trust_refusal",execution_context:executionContext,error}}function resolveLinkedTestTrustPreflightResult(params){if(params.executionContext.trust.trusted)return null;const overrideRequested=params.options?.allowUntrustedLinkedTests===!0,policyEnabled=params.options?.untrustedLinkedTestsPolicyEnabled===!0;if(overrideRequested&&policyEnabled)return null;const recovery=policyEnabled?"Retry with --allow-untrusted-linked-tests for this invocation, or acknowledge the current commands with --acknowledge-linked-tests.":"Set testing.allow_untrusted_linked_tests=true, then retry with --allow-untrusted-linked-tests; alternatively acknowledge the current commands with --acknowledge-linked-tests.";return buildLinkedTestTrustRefusalResult(params.linkedTest,params.executionContext,`Linked test command is not trusted by this clone (source_ref=${params.executionContext.trust.source_ref??"unknown"}, current_ref=${params.executionContext.trust.current_source_ref??"unknown"}). ${recovery}`)}function resolveLinkedTestSafetyPreflightResult(params){if(!params.linkedTest.command)return buildLinkedTestSkippedResult(params.linkedTest,params.executionContext,"No command configured for this linked test.");const runtimeSafetySkipReason=getRuntimeSafetySkipReason(params.linkedTest.command);return runtimeSafetySkipReason?buildLinkedTestSkippedResult(params.linkedTest,params.executionContext,runtimeSafetySkipReason):null}function resolveUnboundPmContextFailure(params){return params.executionContext.pm_context_mode!=="none"?null:params.executionContext.workspace_context_mode==="source"?buildLinkedTestConfigurationRefusalResult(params.linkedTest,params.executionContext,"pm_context_mode=none requires workspace_context_mode=isolated or snapshot so implicit tracker discovery cannot reach the source workspace."):params.executionContext.is_pm_command?buildLinkedTestConfigurationRefusalResult(params.linkedTest,params.executionContext,"pm_context_mode=none is for self-isolating SDK tests; direct PM commands require schema, tracker, or auto context."):null}function buildLinkedTestConfigurationRefusalResult(linkedTest,executionContext,error){return{command:linkedTest.command,path:linkedTest.path,status:"refused",exit_code:null,failure_category:"config_refusal",execution_context:executionContext,error}}function resolvePmContextMismatchFailure(params){const failOnMismatchByDefault=params.executionContext.pm_context_mode==="schema"&&params.executionContext.is_pm_tracker_read_command&&params.executionContext.mismatch_detected,failOnMismatchByFlag=params.options?.failOnContextMismatch===!0&&params.executionContext.is_pm_command&&params.executionContext.mismatch_detected;if(!failOnMismatchByDefault&&!failOnMismatchByFlag)return null;const mismatchHint=buildPmContextMismatchHint({executionContext:params.executionContext,runLevelPmContextMode:params.runLevelPmContextMode,linkedOverridePmContextMode:params.linkedOverridePmContextMode}),mismatchPrefix=params.options?.checkContext===!0?"Linked test preflight PM context mismatch detected":"Linked test PM context mismatch detected";return buildLinkedTestConfigurationRefusalResult(params.linkedTest,params.executionContext,`${mismatchPrefix} (source_project_items=${params.executionContext.source_project_item_count}, sandbox_project_items=${params.executionContext.sandbox_project_item_count}).${mismatchHint}`)}function resolveLinkedTestPreflightResult(params){return resolveLinkedTestSafetyPreflightResult(params)||resolveLinkedTestTrustPreflightResult(params)||resolveUnboundPmContextFailure(params)||resolvePmContextMismatchFailure(params)||(params.options?.requireAssertionsForPm===!0&&params.executionContext.is_pm_command&&!hasLinkedTestAssertions(params.linkedTest)?buildLinkedTestConfigurationRefusalResult(params.linkedTest,params.executionContext,"Linked PM command requires assertions when --require-assertions-for-pm is enabled."):null)}function buildLinkedTestExecutionEnv(params){const effectiveDirectives=resolveEffectiveLinkedTestDirectives(params.runtimeDirectives,params.linkedTest),executionEnv={...process.env,SENTRY_ENVIRONMENT:"test"};return applyEnvDirectiveStage(executionEnv,params.runtimeDirectives),applyEnvDirectiveStage(executionEnv,{env_set:params.linkedTest.env_set??{},env_clear:params.linkedTest.env_clear??[]}),effectiveDirectives.shared_host_safe&&applySharedHostSafeDefaults(executionEnv),executionEnv.FORCE_COLOR="0",params.executionContext.pm_context_mode==="none"?delete executionEnv.PM_PATH:executionEnv.PM_PATH=params.executionContext.sandbox_project_pm_path,executionEnv.PM_GLOBAL_PATH=params.executionContext.sandbox_global_pm_path,params.executionContext.workspace_context_mode!=="source"&&(delete executionEnv.PM_BUILD_CONSUMER_LEASE,delete executionEnv.PM_BUILD_CONSUMER_LEASE_ROOT),params.executionContext.workspace_context_mode==="isolated"?(delete executionEnv.PM_SOURCE_WORKSPACE_ROOT,delete executionEnv.PM_SOURCE_PM_PATH,delete executionEnv[SOURCE_CONTEXT_ACCESS_ENV]):(executionEnv.PM_SOURCE_WORKSPACE_ROOT=params.executionContext.source_workspace_root,executionEnv.PM_SOURCE_PM_PATH=params.executionContext.workspace_context_mode==="snapshot"?params.executionContext.sandbox_project_pm_path:params.executionContext.source_project_pm_path,executionEnv[SOURCE_CONTEXT_ACCESS_ENV]="read_only"),executionEnv}function buildLinkedTestPassedResult(linkedTest,executionContext,execution,receipt){return{command:linkedTest.command,path:linkedTest.path,status:"passed",exit_code:0,execution_context:executionContext,stdout:execution.stdout,stderr:execution.stderr,...receipt?{execution_receipt:receipt}:{}}}function buildLinkedTestEmptyRunResult(params){return{command:params.linkedTest.command,path:params.linkedTest.path,status:"failed",exit_code:1,failure_category:"empty_run",execution_context:params.executionContext,stdout:params.execution.stdout,stderr:params.execution.stderr,error:`Linked test reported an empty test run (${params.emptyRunSignal.code}). Update the test selection so at least one test executes; unfiltered commands may opt out by omitting --fail-on-empty-test-run.`}}function buildLinkedTestCommandFailureResult(params){return{command:params.linkedTest.command,path:params.linkedTest.path,status:"failed",exit_code:resolveLinkedTestFailureExitCode(params.execution),failure_category:classifyLinkedTestFailure(params.execution),execution_context:params.executionContext,stdout:params.execution.stdout,stderr:params.execution.stderr,error:formatLinkedTestExecutionError(params.execution,params.timeoutMs)}}function buildLinkedTestPassedExecutionResult(params){const receipt=findLinkedTestExecutionReceipt(params.execution.stdout,params.execution.stderr);if(params.options?.failOnEmptyTestRun===!0||commandUsesTestNameFilter(params.linkedTest.command??"")){const emptyRunSignal=detectEmptyLinkedTestRun(params.execution.stdout,params.execution.stderr);if(emptyRunSignal)return buildLinkedTestEmptyRunResult({...params,emptyRunSignal});if(commandUsesTestNameFilter(params.linkedTest.command??"")&&!receipt)return buildLinkedTestEmptyRunResult({...params,emptyRunSignal:{code:"missing_positive_execution_receipt"}})}const assertionFailures=evaluateLinkedTestAssertions(params.linkedTest,params.execution.stdout,params.execution.stderr);return assertionFailures.length>0?buildLinkedTestAssertionFailureResult(params.linkedTest,params.executionContext,`Linked test assertion(s) failed: ${assertionFailures.join("; ")}`,params.execution):buildLinkedTestPassedResult(params.linkedTest,params.executionContext,params.execution,receipt)}async function runSingleLinkedTest(params){const command=params.linkedTest.command??"",timeoutMs=(params.linkedTest.timeout_seconds??params.defaultTimeoutSeconds??120)*1e3,execution=await runLinkedTestCommand(command,timeoutMs,buildLinkedTestExecutionEnv(params),{index:params.index+1,total:params.total,timeoutMs,command},params.progressMode,params.executionContext.working_directory);return execution.exitCode===0&&!execution.timedOut&&!execution.maxBufferExceeded?buildLinkedTestPassedExecutionResult({...params,execution}):buildLinkedTestCommandFailureResult({...params,execution,timeoutMs})}async function runLinkedTests(tests,defaultTimeoutSeconds,options){for(const test of tests)assertLinkedTestCommand(test.command);const results=[],sandboxRoot=await mkdtemp(path.join(tmpdir(),"pm-linked-test-")),layout=createLinkedTestSandboxLayout(sandboxRoot),runLevelPmContextMode=parsePmContextMode(options?.pmContext),runLevelWorkspaceContextMode=parseWorkspaceContextMode(options?.workspaceContext),progressMode=options?.progress===!0?"always":"auto",runtimeDirectives=resolveRuntimeDirectives(options?.envSet,options?.envClear,options?.sharedHostSafe),sourceRoots=options?.sourceRoots,sourceWorkspaceRoot=resolveLinkedTestSourceWorkspaceRoot(),includeTrackerData=linkedTestsRequireTrackerData(tests,runLevelPmContextMode,options),includeWorkspaceSnapshot=linkedTestsRequireWorkspaceSnapshot(tests,runLevelWorkspaceContextMode,options?.overrideLinkedWorkspaceContext===!0),currentSourceRef=await resolveLinkedTestSourceRef(sourceWorkspaceRoot);try{await initializeLinkedTestSandboxes(layout,runInit,includeTrackerData),await mkdir(layout.workspaceIsolatedRoot,{recursive:!0}),await seedLinkedTestSandboxesFromSource(layout,sourceRoots,includeTrackerData),includeWorkspaceSnapshot&&await seedLinkedTestWorkspaceSnapshot(sourceWorkspaceRoot,layout.workspaceSnapshotRoot,sourceRoots?.projectPmRoot,includeTrackerData);const counts=await countLinkedTestSandboxItems(layout,sourceRoots);for(let index=0;index<tests.length;index+=1){const linkedTest=tests[index],workspaceContextMode=resolveLinkedTestWorkspaceContextMode(linkedTest,runLevelWorkspaceContextMode,options?.overrideLinkedWorkspaceContext===!0),commandContext=await resolveLinkedTestCommandContext({linkedTest,layout,counts,sourceRoots,runLevelPmContextMode,options,requestedWorkspaceContextMode:runLevelWorkspaceContextMode,workspaceContextMode,currentSourceRef,sourceWorkspaceRoot}),preflightResult=resolveLinkedTestPreflightResult({linkedTest,executionContext:commandContext.executionContext,runLevelPmContextMode,linkedOverridePmContextMode:commandContext.linkedOverridePmContextMode,options});if(preflightResult){results.push(preflightResult);continue}results.push(await runSingleLinkedTest({linkedTest,index,total:tests.length,defaultTimeoutSeconds,runtimeDirectives,progressMode,executionContext:commandContext.executionContext,options}))}}finally{await rm(layout.root,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}return results}async function readLinkedTestItem(params){const{id,pmRoot,settings,typeToFolder}=params,located=await locateItem(pmRoot,id,settings.id_prefix,settings.item_format,typeToFolder);if(!located)throw new PmCliError(`Item ${id} not found`,EXIT_CODE.NOT_FOUND);const loaded=await readLocatedItem(located,{schema:settings.schema});return{itemId:located.id,tests:loaded.document.metadata.tests??[],testRuns:loaded.document.metadata.test_runs??[],changed:!1,removed:0}}function linkedTestsHaveSameIdentity(left,right){return left.command===right.command&&left.path===right.path&&left.scope===right.scope&&left.pm_context_mode===right.pm_context_mode&&left.workspace_context_mode===right.workspace_context_mode}function appendMissingLinkedTests(current,additions){const next=[...current];for(const addition of additions)next.some(entry=>linkedTestsHaveSameIdentity(entry,addition))||next.push(addition);return next}function removeLinkedTestsBySelector(current,removals,removeIndexes){if(removals.length===0&&removeIndexes.length===0)return{tests:current,removed:0};const indexSet=new Set(removeIndexes),tests=current.filter((entry,index)=>indexSet.has(index+1)?!1:!removals.some(selector=>selector.kind==="command"?entry.command===selector.value:selector.kind==="path"?entry.path===selector.value:entry.command===selector.value||entry.path===selector.value));return{tests,removed:current.length-tests.length}}function applyLinkedTestMutations(previous,adds,removes,removeIndexes){return removeLinkedTestsBySelector(appendMissingLinkedTests(previous,adds),removes,removeIndexes)}function hasTestRuntimeDirectiveFlags(options){return[options.timeout!==void 0,options.progress===!0,(options.envSet?.length??0)>0,(options.envClear?.length??0)>0,options.sharedHostSafe===!0,options.pmContext!==void 0,options.overrideLinkedPmContext===!0,options.failOnContextMismatch===!0,options.failOnSkipped===!0,options.failOnEmptyTestRun===!0,options.requireAssertionsForPm===!0,options.checkContext===!0,options.autoPmContext===!0,options.workspaceContext!==void 0,options.overrideLinkedWorkspaceContext===!0,options.allowUntrustedLinkedTests===!0].some(Boolean)}function assertTestRunFlagUsage(options){if(hasTestRuntimeDirectiveFlags(options)&&options.run!==!0)throw new PmCliError("--timeout, --progress, --env-set, --env-clear, --shared-host-safe, --pm-context, --override-linked-pm-context, --workspace-context, --override-linked-workspace-context, --allow-untrusted-linked-tests, --fail-on-context-mismatch, --fail-on-skipped, --fail-on-empty-test-run, --require-assertions-for-pm, --check-context, and --auto-pm-context require --run",EXIT_CODE.USAGE);if(options.acknowledgeLinkedTests===!0&&options.run===!0)throw new PmCliError("--acknowledge-linked-tests records trust without executing commands; run it separately before --run",EXIT_CODE.USAGE);if((options.match!==void 0||options.onlyIndex!==void 0||options.onlyLast===!0)&&options.run!==!0)throw new PmCliError("--match, --only-index, and --only-last require --run",EXIT_CODE.USAGE)}async function resolveLinkedTestItem(params){const{id,options,pmRoot,settings,typeToFolder,adds,removes,removeIndexes}=params;if(adds.length===0&&removes.length===0&&removeIndexes.length===0)return readLinkedTestItem({id,pmRoot,settings,typeToFolder});let removed=0;const result=await mutateItem({pmRoot,settings,id,op:adds.length>0?"tests_add":"tests_remove",author:resolveAuthor(options.author,settings.author_default),message:options.message,force:options.force,skipNoop:!0,mutate(document){const previous=document.metadata.tests??[],mutation=applyLinkedTestMutations(previous,adds,removes,removeIndexes);if(admitLinkedTestRequest(()=>resolveTestRunOptions(options,mutation.tests)),removed=mutation.removed,(removes.length>0||removeIndexes.length>0)&&removed===0)throw new PmCliError("No linked tests matched the requested removal selector",EXIT_CODE.NOT_FOUND,{code:"linked_test_remove_no_match",unmatched:[...removes.map(entry=>`${entry.kind}=${entry.value}`),...removeIndexes.map(index=>`index=${index}`)],required:"List linked tests and retry with an exact command/path or a current 1-based --remove-index.",examples:[`pm test ${id} --list`,`pm test ${id} --remove-index 1`]});return document.metadata.tests=mutation.tests,{changedFields:stableValueEquals(previous,mutation.tests)?[]:["tests"]}}});return{itemId:result.item.id,tests:result.item.tests??[],testRuns:result.item.test_runs??[],changed:result.changedFields.length>0,removed}}function resolveTestRunOptions(options,tests){const pmContextMode=parsePmContextMode(options.pmContext);parseWorkspaceContextMode(options.workspaceContext),resolveRuntimeDirectives(options.envSet,options.envClear,options.sharedHostSafe);const selectorInput={match:options.match,onlyIndex:options.onlyIndex===void 0?void 0:parseOnlyIndexValue(options.onlyIndex),onlyLast:options.onlyLast},defaultTimeoutSeconds=options.timeout===void 0?void 0:parseOptionalNumber(options.timeout,"--timeout");if(defaultTimeoutSeconds!==void 0&&defaultTimeoutSeconds<=0)throw new PmCliError(`--timeout must be a positive number (got "${options.timeout}")`,EXIT_CODE.USAGE);assertTestRunFlagUsage(options);const runSelection=options.run===!0?resolveLinkedTestRunSelection(tests,selectorInput):void 0;if(runSelection)for(const test of runSelection.selected)assertLinkedTestCommand(test.command);return{defaultTimeoutSeconds,pmContextMode,runSelection,testsToRun:runSelection?.selected??tests}}async function executeSelectedLinkedTests(params){const{options,runOptions,pmRoot}=params;return options.run!==!0?[]:await runLinkedTests(runOptions.testsToRun,runOptions.defaultTimeoutSeconds,{progress:options.progress,envSet:options.envSet,envClear:options.envClear,sharedHostSafe:options.sharedHostSafe,pmContext:runOptions.pmContextMode,overrideLinkedPmContext:options.overrideLinkedPmContext,failOnContextMismatch:options.failOnContextMismatch,failOnEmptyTestRun:options.failOnEmptyTestRun===!0||(options.measure?.length??0)>0,requireAssertionsForPm:options.requireAssertionsForPm,checkContext:options.checkContext,autoPmContext:options.autoPmContext,workspaceContext:options.workspaceContext,overrideLinkedWorkspaceContext:options.overrideLinkedWorkspaceContext,pmRoot,allowUntrustedLinkedTests:options.allowUntrustedLinkedTests,untrustedLinkedTestsPolicyEnabled:params.untrustedLinkedTestsPolicyEnabled,sourceRoots:{projectPmRoot:pmRoot,globalPmRoot:resolveGlobalPmRoot(process.cwd())}})}function buildTestWarnings(options,runSelection,runResults){const warnings=[];if(runSelection&&runSelection.selector!==null&&warnings.push(`linked_test_selection:${runSelection.selector}=${runSelection.requested};selected=${runSelection.selected_count};skipped=${runSelection.skipped_count};indexes=${runSelection.selected_indexes.join(",")}`),options.run===!0&&options.checkContext===!0){const preflight=summarizeContextPreflight(runResults);warnings.push(`context_preflight:checked_pm_commands=${preflight.checked_pm_commands};tracker_read_commands=${preflight.tracker_read_commands};mismatches=${preflight.mismatches};auto_remediated=${preflight.auto_remediated}`)}return warnings}function buildTrackedTestRunSummary(params){const summary=summarizeRunResultStatuses(params.runResults),parsedAttempt=Number.parseInt(process.env.PM_BACKGROUND_TEST_RUN_ATTEMPT?.trim()??"",10),resumedFrom=process.env.PM_BACKGROUND_TEST_RUN_RESUMED_FROM?.trim(),recordedAt=nowIso(),measurements=parseTestRunMeasurements(params.options.measure,recordedAt);return{run_id:resolveTrackedRunId("test"),kind:"test",status:summary.failed>0||params.failOnSkippedTriggered?"failed":"passed",started_at:params.runStartedAt,finished_at:recordedAt,recorded_at:recordedAt,attempt:Number.isFinite(parsedAttempt)&&parsedAttempt>=1?parsedAttempt:void 0,resumed_from:resumedFrom&&resumedFrom.length>0?resumedFrom:void 0,passed:summary.passed,failed:summary.failed,skipped:summary.skipped,fail_on_skipped_triggered:params.failOnSkippedTriggered?!0:void 0,measurements:measurements.length>0?measurements:void 0,executions:params.runResults.filter(result=>result.command?.trim()).slice(0,32).map(result=>({command:result.command.trim(),requested_pm_context_mode:result.execution_context?.requested_pm_context_mode,pm_context_mode:result.execution_context?.pm_context_mode,workspace_context_mode:result.execution_context?.workspace_context_mode,trust_reason:result.execution_context?.trust.reason}))}}async function recordTestRunSummary(params){const{options,pmRoot,settings,itemId,runStartedAt,runResults,failOnSkippedTriggered,warnings}=params;if(options.run!==!0||!runStartedAt)return;const entry=buildTrackedTestRunSummary({options,runStartedAt,runResults,failOnSkippedTriggered});if(settings.testing.record_results_to_items!==!0)return{entry,receipt:{recorded:!1,reason:"tracking_disabled",run_id:entry.run_id,recovery_command:"pm config project set test-result-tracking --policy enabled"}};try{return await appendTrackedTestRunSummary({pmRoot,settings,itemId,author:resolveAuthor(options.author,settings.author_default),message:`Track test run summary (${entry.run_id})`,entry}),{entry,receipt:{recorded:!0,reason:"recorded",run_id:entry.run_id}}}catch(error){return warnings.push(`test_result_tracking_failed:${itemId}:${error instanceof Error?error.message:String(error)}`),{entry,receipt:{recorded:!1,reason:"write_failed",run_id:entry.run_id}}}}function buildTestMeasurementProjection(options,runs,recordedRun){let metricBelow;if(options.metricBelow!==void 0){const separator=options.metricBelow.indexOf("="),name=options.metricBelow.slice(0,separator).trim(),thresholdText=options.metricBelow.slice(separator+1).trim(),threshold=Number(thresholdText);if(separator<=0||name.length===0||thresholdText.length===0||!Number.isFinite(threshold))throw new PmCliError("--metric-below must use name=value",EXIT_CODE.USAGE);metricBelow=queryTestRunMeasurementsBelow(runs,name,threshold)}return{measurements:recordedRun?.measurements,metric_below:metricBelow,metric_diff:options.metricDiff===void 0?void 0:diffTestRunMeasurements(runs,options.metricDiff)}}async function maybeAcknowledgeLinkedTests(options,pmRoot,tests){if(options.acknowledgeLinkedTests===!0)return acknowledgeLinkedTests(pmRoot,tests,nowIso())}function projectTestResult(input){const{item,runResults,failureCategories,runSelection,failOnSkippedTriggered,warnings,recording,measurementProjection}=input;return{ok:runResults.every(entry=>entry.status!=="failed"&&entry.status!=="refused")&&failOnSkippedTriggered!==!0,id:item.itemId,tests:item.tests,run_results:runResults,failure_categories:failureCategories,selection:runSelection?{selector:runSelection.selector,requested:runSelection.requested,selected_indexes:runSelection.selected_indexes,selected_count:runSelection.selected_count,skipped_count:runSelection.skipped_count}:void 0,fail_on_skipped_triggered:failOnSkippedTriggered?!0:void 0,warnings:warnings.length>0?warnings:void 0,changed:item.changed||recording?.receipt.recorded===!0,...recording?{evidence_recording:recording.receipt}:{},count:item.tests.length,...item.removed>0?{removed:item.removed}:{},...measurementProjection}}async function runTest(id,options,global2){const edit=parseLinkedTestUpdateOptions(options);if(edit)return assertLinkedTestEditUsage(options),assertTestRunFlagUsage(options),assertTestMeasurementRun(id,options,global2,resolvePmRoot(process.cwd(),global2.path)),{...await updateLinkedTest(id,{...edit,author:options.author,message:options.message,force:options.force},global2),run_results:[],failure_categories:countFailureCategories([])};const stdinResolver=createStdinTokenResolver(),pmRoot=resolvePmRoot(process.cwd(),global2.path);await assertInitializedTracker(pmRoot);const settings=await readSettings(pmRoot);assertTestMeasurementRun(id,options,global2,pmRoot),parseTestRunMeasurements(options.measure,"validation"),buildTestMeasurementProjection(options,[],void 0);const typeRegistry=resolveItemTypeRegistry(settings,getActiveExtensionRegistrations()),resolvedAdds=await stdinResolver.resolveList(options.add,"--add"),resolvedAddJsons=await stdinResolver.resolveList(options.addJson,"--add-json"),resolvedRemoves=await stdinResolver.resolveList(options.remove,"--remove"),parsedAdds=[...parseAddEntries(resolvedAdds),...parseAddJsonEntries(resolvedAddJsons)],adds=await attachLinkedTestMutationProvenance(parsedAdds,resolveAuthor(options.author,settings.author_default),nowIso())??[],removes=parseRemoveEntries(resolvedRemoves),removeIndexes=parseRemoveIndexes(options.removeIndex),item=await resolveLinkedTestItem({id,options,pmRoot,settings,typeToFolder:typeRegistry.type_to_folder,adds,removes,removeIndexes}),runOptions=admitLinkedTestRequest(()=>resolveTestRunOptions(options,item.tests)),trustAcknowledgement=await maybeAcknowledgeLinkedTests(options,pmRoot,item.tests),runStartedAt=options.run===!0?nowIso():void 0,runResults=await executeSelectedLinkedTests({options,runOptions,pmRoot,untrustedLinkedTestsPolicyEnabled:settings.testing.allow_untrusted_linked_tests});assertTestMeasurementExecution(id,options,global2,pmRoot,runResults);const failureCategories=countFailureCategories(runResults),failOnSkippedTriggered=options.run===!0&&options.failOnSkipped===!0&&runResults.some(entry=>entry.status==="skipped"),warnings=buildTestWarnings(options,runOptions.runSelection,runResults);trustAcknowledgement&&warnings.push(`linked_test_trust_acknowledged:${trustAcknowledgement.acknowledged}`);const recording=await recordTestRunSummary({options,pmRoot,settings,itemId:item.itemId,runStartedAt,runResults,failOnSkippedTriggered,warnings}),runs=recording?.receipt.recorded?buildTrackedTestRunHistory([...item.testRuns,recording.entry]):item.testRuns,measurementProjection=buildTestMeasurementProjection(options,runs,recording?.entry);return projectTestResult({item,runResults,failureCategories,runSelection:runOptions.runSelection,failOnSkippedTriggered,warnings,recording,measurementProjection})}const _testOnlyTestCommand={buildLinkedTestExecutionContext,buildPmContextMismatchHint,commandInvokesPmCli,commandInvokesPmTrackerReadCommand,commandUsesTestNameFilter,copyIntoSandboxIfPresent,countLinkedTestItemFiles,ensureScope,evaluateLinkedTestAssertions,endLinkedTestProgress,extractPmInvocationArgsFromSegment,firstDirectTestRunnerSubcommand,formatLinkedTestExecutionError,hasLinkedTestAssertions,initializeLinkedTestSandboxes,linkedTestsRequireTrackerData,parseAddJsonEntries,parseLinkedTestTimeoutSeconds,parsePmContextMode,readJsonPathValue,resolveAuthor,resolveDirectRunnerSubcommand,resolveLinkedTestEffectiveContextMode,resolveLinkedTestFailureExitCode,resolveLinkedTestRequestedContextMode,resolveTestRunOptions,resolveTrackedRunId,runLinkedTestCommand,seedMissingLinkedTestSettingsHistory,seedLinkedTestSandbox,seedLinkedTestTrackerData,segmentInvokesRecursiveTestAll,splitJsonPathSegments,summarizeRunResultStatuses};export{_testOnlyTestCommand,classifyLinkedTestCommandSafety,classifyLinkedTestFailure,countFailureCategories,extractReferencedPmItemIdsFromCommand,resolveLinkedTestFailureExitCode,runLinkedTests,runTest,summarizeContextPreflight};
6
6
  //# sourceMappingURL=execution.js.map
@@ -1,2 +1,2 @@
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]="9c0640f0-505c-5f4a-bb7a-daa3cc775239")}catch{}})();import{isFileMissingError}from"../core/fs/fs-utils.js";import fs from"node:fs/promises";import path from"node:path";const MILLISECONDS_PER_DAY=1440*60*1e3,TERMINAL_JOURNAL_STATUSES=new Set(["committed","compensated"]);function readJournalStatusAndTimestamp(raw){let parsed;try{parsed=JSON.parse(raw)}catch{return{status:null,updatedAt:null}}if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))return{status:null,updatedAt:null};const record=parsed;return{status:typeof record.status=="string"?record.status:null,updatedAt:typeof record.updatedAt=="string"?record.updatedAt:null}}function classifyWorkspaceTransactionJournal(file,status,updatedAt,nowMs,retentionMs){const updatedAtMs=updatedAt===null?Number.NaN:Date.parse(updatedAt);if(status===null||!Number.isFinite(updatedAtMs))return{entry:{file,status,age_seconds:null,stale:!1,reason:"unparseable"},remove:!1};const ageMs=nowMs-updatedAtMs;return TERMINAL_JOURNAL_STATUSES.has(status)?ageMs>retentionMs?{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!0},remove:!0}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"fresh"},remove:!1}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"active"},remove:!1}}async function processWorkspaceTransactionJournal(absolutePath,relativePath,fileName,options){const warnings=options.hooks?.onRead?await options.hooks.onRead(absolutePath):[],journal=readJournalStatusAndTimestamp(await fs.readFile(absolutePath,"utf8")),decision=classifyWorkspaceTransactionJournal(relativePath,journal.status,journal.updatedAt,options.nowMs,options.retentionMs);return decision.entry.reason==="unparseable"&&warnings.push(`transaction_journal_unparseable:${fileName}`),decision.remove&&!options.dryRun&&(await fs.rm(absolutePath,{force:!0}),options.hooks?.onWrite&&warnings.push(...await options.hooks.onWrite(absolutePath))),{decision,warnings}}async function runWorkspaceTransactionGc(pmRoot,options){const{dryRun,hooks}=options,nowMs=options.now??Date.now(),retentionMs=Math.max(0,options.retentionDays)*MILLISECONDS_PER_DAY,journalsDir=path.join(pmRoot,"transactions","sdk"),result={scanned:0,removed:[],retained:[],warnings:[],entries:[]};let files;try{files=(await fs.readdir(journalsDir)).filter(file=>file.endsWith(".json")).sort((left,right)=>left.localeCompare(right))}catch(error){if(isFileMissingError(error))return result;throw error}for(const file of files){const absPath=path.join(journalsDir,file),relPath=`transactions/sdk/${file}`,fileResult=await processWorkspaceTransactionJournal(absPath,relPath,file,{dryRun,hooks,nowMs,retentionMs});result.warnings.push(...fileResult.warnings),result.scanned+=1;const{decision}=fileResult;if(!decision.remove){result.retained.push(relPath),result.entries.push(decision.entry);continue}result.removed.push(relPath),result.entries.push(decision.entry)}return result}export{runWorkspaceTransactionGc};
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]="ff5e2370-3b70-5f71-9492-72906f642c85")}catch{}})();import{isFileMissingError}from"../core/fs/fs-utils.js";import fs from"node:fs/promises";import path from"node:path";const MILLISECONDS_PER_DAY=1440*60*1e3,TERMINAL_JOURNAL_STATUSES=new Set(["committed","compensated"]);function readJournalStatusAndTimestamp(raw){let parsed;try{parsed=JSON.parse(raw)}catch{return{status:null,updatedAt:null}}if(typeof parsed!="object"||parsed===null||Array.isArray(parsed))return{status:null,updatedAt:null};const record=parsed;return{status:record.abandonment!==void 0?"abandoned":typeof record.status=="string"?record.status:null,updatedAt:typeof record.updatedAt=="string"?record.updatedAt:null}}function classifyWorkspaceTransactionJournal(file,status,updatedAt,nowMs,retentionMs){const updatedAtMs=updatedAt===null?Number.NaN:Date.parse(updatedAt);if(status===null||!Number.isFinite(updatedAtMs))return{entry:{file,status,age_seconds:null,stale:!1,reason:"unparseable"},remove:!1};const ageMs=nowMs-updatedAtMs;return TERMINAL_JOURNAL_STATUSES.has(status)?ageMs>retentionMs?{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!0},remove:!0}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"fresh"},remove:!1}:{entry:{file,status,age_seconds:Math.floor(ageMs/1e3),stale:!1,reason:"active"},remove:!1}}async function processWorkspaceTransactionJournal(absolutePath,relativePath,fileName,options){const warnings=options.hooks?.onRead?await options.hooks.onRead(absolutePath):[],journal=readJournalStatusAndTimestamp(await fs.readFile(absolutePath,"utf8")),decision=classifyWorkspaceTransactionJournal(relativePath,journal.status,journal.updatedAt,options.nowMs,options.retentionMs);return decision.entry.reason==="unparseable"&&warnings.push(`transaction_journal_unparseable:${fileName}`),decision.remove&&!options.dryRun&&(await fs.rm(absolutePath,{force:!0}),options.hooks?.onWrite&&warnings.push(...await options.hooks.onWrite(absolutePath))),{decision,warnings}}async function runWorkspaceTransactionGc(pmRoot,options){const{dryRun,hooks}=options,nowMs=options.now??Date.now(),retentionMs=Math.max(0,options.retentionDays)*MILLISECONDS_PER_DAY,journalsDir=path.join(pmRoot,"transactions","sdk"),result={scanned:0,removed:[],retained:[],warnings:[],entries:[]};let files;try{files=(await fs.readdir(journalsDir)).filter(file=>file.endsWith(".json")).sort((left,right)=>left.localeCompare(right))}catch(error){if(isFileMissingError(error))return result;throw error}for(const file of files){const absPath=path.join(journalsDir,file),relPath=`transactions/sdk/${file}`,fileResult=await processWorkspaceTransactionJournal(absPath,relPath,file,{dryRun,hooks,nowMs,retentionMs});result.warnings.push(...fileResult.warnings),result.scanned+=1;const{decision}=fileResult;if(!decision.remove){result.retained.push(relPath),result.entries.push(decision.entry);continue}result.removed.push(relPath),result.entries.push(decision.entry)}return result}export{runWorkspaceTransactionGc};
2
2
  //# sourceMappingURL=workspace-transaction-gc.js.map
@@ -49,6 +49,8 @@ transactionId:string;
49
49
  author:string;
50
50
  /** Ordered idempotent mutations; compensations run in reverse order. */
51
51
  steps:readonly WorkspaceTransactionStep[];
52
+ /** JSON plan retained for adapter-owned recovery; callers must not store secrets. */
53
+ recoveryData?:WorkspaceTransactionJsonValue;
52
54
  /** Lock lifetime in seconds; size this above the longest expected attempt. */
53
55
  lockTtlSeconds?:number;
54
56
  /** Maximum time to wait for the workspace writer lock. */
@@ -76,6 +78,43 @@ export declare class WorkspaceTransactionInterruptedError extends Error {
76
78
  /** Create one explicit crash-boundary interruption. */
77
79
  constructor(message?:string);
78
80
  }
81
+ /** Durable coordinator lifecycle, including explicitly retired identities. */
82
+ export type WorkspaceTransactionJournalStatus = "applying" | "compensating" | "compensated" | "committed" | "abandoned";
83
+ /** Durable coordinator receipt; step lists describe journaled progress, not a fresh domain inspection. */
84
+ export interface WorkspaceTransactionJournal {
85
+ /** Version 2 fences older coordinators once explicit retirement begins. */
86
+ schemaVersion:1 | 2;
87
+ /** Stable transaction identity. */
88
+ transactionId:string;
89
+ /** Actor who prepared the attempt. */
90
+ author:string;
91
+ /** Last durable lifecycle state. */
92
+ status:WorkspaceTransactionJournalStatus;
93
+ /** One-based attempt number. */
94
+ attempt:number;
95
+ /** Initial preparation timestamp. */
96
+ createdAt:string;
97
+ /** Latest journal write timestamp. */
98
+ updatedAt:string;
99
+ /** Ordered payload-bound step identifiers. */
100
+ stepIds:string[];
101
+ /** Steps durably admitted before forward execution. */
102
+ startedStepIds:string[];
103
+ /** Steps recorded as applied. */
104
+ completedStepIds:string[];
105
+ /** Recorded results for completed steps. */
106
+ results:Record<string,WorkspaceTransactionJsonValue>;
107
+ /** Captured pre-write recovery receipts. */
108
+ compensationData:Record<string,WorkspaceTransactionJsonValue>;
109
+ /** Adapter-owned original plan, absent in legacy journals. */
110
+ recoveryData?:WorkspaceTransactionJsonValue;
111
+ /** Explicit retirement rationale and optional replacement identity. */
112
+ abandonment?:{
113
+ reason:string;
114
+ author:string;
115
+ supersededBy?:string;
116
+ };
117
+ }
79
118
  /** Validate transaction identity before preview staging or acquiring any writer lock. */
80
119
  export declare function validateWorkspaceTransactionIdentity(options:Pick<CommitWorkspaceTransactionOptions,"pmRoot" | "transactionId" | "author">):Pick<CommitWorkspaceTransactionOptions,"pmRoot" | "transactionId" | "author">;
81
120
  /**
@@ -85,3 +124,18 @@ export declare function validateWorkspaceTransactionIdentity(options:Pick<Commit
85
124
  * replay without deleting or rewriting immutable domain histories.
86
125
  */
87
126
  export declare function commitWorkspaceTransaction(options:CommitWorkspaceTransactionOptions):Promise<WorkspaceTransactionCommitResult>;
127
+ /** Read an atomically published journal without changing tracker state; undefined means no retained receipt. */
128
+ export declare function readWorkspaceTransaction(pmRoot:string,transactionId:string):Promise<WorkspaceTransactionJournal | undefined>;
129
+ /** SHA-256 identity of the ordered, payload-bound step identifiers. */
130
+ export declare function workspaceTransactionPlanDigest(journal:Pick<WorkspaceTransactionJournal,"stepIds">):string;
131
+ /** Explicit recovery request; the adapter reconstructs compensation from the retained original plan under the coordinator lock. */
132
+ export interface AbandonWorkspaceTransactionOptions extends Omit<CommitWorkspaceTransactionOptions,"steps" | "recoveryData"> {
133
+ /** Required human-readable recovery rationale. */
134
+ reason:string;
135
+ /** Optional fresh identity chosen by the caller for its replacement plan; this does not reserve it. */
136
+ supersededBy?:string;
137
+ /** Rebuild original steps without applying forward mutations. */
138
+ resolveSteps(journal:WorkspaceTransactionJournal):readonly WorkspaceTransactionStep[];
139
+ }
140
+ /** Compensate interrupted owned effects and permanently retire the id; committed work is never undone. */
141
+ export declare function abandonWorkspaceTransaction(options:AbandonWorkspaceTransactionOptions):Promise<WorkspaceTransactionJournal>;
@@ -1,3 +1,3 @@
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]="d913251a-fc99-5767-b19d-ae9473adc138")}catch{}})();import{mkdir,readFile}from"node:fs/promises";import path from"node:path";import{acquireLock}from"../core/lock/lock.js";import{isFileMissingError,writeFileAtomic}from"../core/fs/fs-utils.js";import{nowIso}from"../core/shared/time.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";class WorkspaceTransactionInterruptedError extends Error{constructor(message="Workspace transaction interrupted"){super(message),this.name="WorkspaceTransactionInterruptedError"}}const WORKSPACE_TRANSACTION_LOCK_ID="sdk-workspace-transaction",TRANSACTION_ID_PATTERN=/^[a-zA-Z0-9._-]+$/,DEFAULT_LOCK_TTL_SECONDS=30,DEFAULT_LOCK_WAIT_MS=3e3;function requiredIdentifier(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized||!TRANSACTION_ID_PATTERN.test(normalized))throw new TypeError(`${label} must match [a-zA-Z0-9._-]+`);return normalized}function requiredText(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized)throw new TypeError(`${label} must be non-empty`);return normalized}function validateWorkspaceTransactionIdentity(options){return{pmRoot:requiredText(options.pmRoot,"Transaction pmRoot"),transactionId:requiredIdentifier(options.transactionId,"Transaction id"),author:requiredText(options.author,"Transaction author")}}function positiveInteger(value,fallback,label){const resolved=value??fallback;if(!Number.isInteger(resolved)||resolved<=0)throw new TypeError(`${label} must be a positive integer`);return resolved}function journalPath(pmRoot,transactionId){return path.join(pmRoot,"transactions","sdk",`${transactionId}.json`)}async function writeJournal(pmRoot,journal){journal.updatedAt=nowIso();const target=journalPath(pmRoot,journal.transactionId);await mkdir(path.dirname(target),{recursive:!0}),await writeFileAtomic(target,`${JSON.stringify(journal,null,2)}
2
- `)}function hasValidJournalMetadata(journal){return Number.isInteger(journal.attempt)&&typeof journal.author=="string"&&typeof journal.createdAt=="string"&&typeof journal.updatedAt=="string"}function createResultMap(source){return Object.assign(Object.create(null),source)}function cloneJournalValue(value,label){try{const serialized=JSON.stringify(value,(_key,nestedValue)=>{if(typeof nestedValue=="number"&&!Number.isFinite(nestedValue))throw new TypeError(`${label} contains a non-finite number`);if(nestedValue===void 0||typeof nestedValue=="function"||typeof nestedValue=="symbol")throw new TypeError(`${label} contains a non-JSON value`);return nestedValue});return JSON.parse(serialized)}catch(error){throw new TypeError(`${label} must be JSON serializable`,{cause:error})}}function cloneOptionalJournalValue(value,label){return value===void 0?void 0:cloneJournalValue(value,label)}function setJournalEntry(target,stepId,value,label){const cloned=cloneOptionalJournalValue(value,label);cloned===void 0?delete target[stepId]:target[stepId]=cloned}function isJournalMap(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function hasUniqueKnownStepIds(value,stepIds){return Array.isArray(value)&&new Set(value).size===value.length&&value.every(stepId=>typeof stepId=="string"&&stepIds.includes(stepId))}function hasValidJournalLifecycle(journal,stepIds){if(!hasUniqueKnownStepIds(journal.startedStepIds,stepIds)||!hasUniqueKnownStepIds(journal.completedStepIds,stepIds)||!isJournalMap(journal.results)||!isJournalMap(journal.compensationData))return!1;const startedStepIds=journal.startedStepIds,started=new Set(startedStepIds),completedStepIds=journal.completedStepIds,completed=new Set(completedStepIds);return Object.keys(journal.compensationData).some(stepId=>!started.has(stepId))||Object.keys(journal.results).some(stepId=>!completed.has(stepId))?!1:journal.status==="committed"?stepIds.every(stepId=>completed.has(stepId)):journal.status!=="compensated"||completed.size===0&&started.size===0}function invalidJournal(transactionId,detail){return new PmCliError(`Workspace transaction ${transactionId} journal is invalid (${detail})`,EXIT_CODE.CONFLICT,{code:"transaction_journal_invalid",required:"A valid durable journal is required before transaction recovery can continue.",why:"Corrupt journal data cannot establish which steps applied or still require compensation.",nextSteps:["Preserve the journal and inspect its integrity or restore a verified backup before retrying.","Do not switch to a new transaction id to bypass an unreadable journal; previously applied work may remain."]})}function journalPlanMismatch(transactionId){return new PmCliError(`Workspace transaction ${transactionId} journal does not match the supplied plan`,EXIT_CODE.CONFLICT,{code:"transaction_plan_mismatch",required:"Replay the exact plan that started this transaction, or start a new transaction with a different id.",why:"A journal records the plan it is resuming; applying a different plan under the same id could skip or duplicate steps.",nextSteps:["Re-run with the original document to resume or replay it.","Pass --transaction-id <new-id> (or a new transactionId over MCP) to commit the changed plan as its own transaction.","Run pm ops gc --scope transactions --dry-run to inspect abandoned journals before pruning them."]})}function parseJournal(raw,transactionId,stepIds){let parsed;try{parsed=JSON.parse(raw)}catch{throw invalidJournal(transactionId,"invalid JSON")}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw invalidJournal(transactionId,"expected an object");const journal=parsed;if(!(journal.schemaVersion===1&&journal.transactionId===transactionId))throw invalidJournal(transactionId,"schema version or transaction id does not match the journal file");const recordedStepIds=journal.stepIds;if(!Array.isArray(recordedStepIds)||!hasUniqueKnownStepIds(recordedStepIds,recordedStepIds)||!hasValidJournalLifecycle(journal,recordedStepIds)||!hasValidJournalMetadata(journal)||typeof journal.status!="string"||!["applying","compensating","compensated","committed"].includes(journal.status))throw invalidJournal(transactionId,"invalid lifecycle, metadata, status or step ids");if(!(journal.status==="compensated"||recordedStepIds.length===stepIds.length&&recordedStepIds.every((stepId,index)=>stepId===stepIds[index])))throw journalPlanMismatch(transactionId);return{...journal,results:createResultMap(journal.results),compensationData:createResultMap(journal.compensationData)}}async function prepareJournalForApply(options,transactionId,author,stepIds,existing){let journal=existing;const recovered=journal!==void 0;if(journal?.status==="compensating"&&await compensateAppliedSteps(options,journal),journal===void 0||journal.status==="compensated"){const timestamp=nowIso();journal={schemaVersion:1,transactionId,author,status:"applying",attempt:(journal?.attempt??0)+1,createdAt:journal?.createdAt??timestamp,updatedAt:timestamp,stepIds,startedStepIds:[],completedStepIds:[],results:createResultMap(),compensationData:createResultMap()},await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"prepared")}return{journal,recovered}}async function applyPreparedTransaction(options,journal,recovered){try{await discoverAppliedSteps(journal,options.steps),await writeJournal(options.pmRoot,journal);const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of options.steps){if(completed.has(step.id))continue;const inspection=await step.inspect();let result=cloneOptionalJournalValue(inspection.result,`Step ${step.id} result`);if(inspection.state!=="applied"){const compensationData=await step.prepareCompensation?.();setJournalEntry(journal.compensationData,step.id,compensationData,`Step ${step.id} compensation data`),started.add(step.id),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),await writeJournal(options.pmRoot,journal);const appliedResult=await step.apply();result=cloneOptionalJournalValue(appliedResult,`Step ${step.id} result`)}await emitTransition(options,journal,"step_applied",step.id),completed.add(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),setJournalEntry(journal.results,step.id,result,`Step ${step.id} result`),await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_recorded",step.id)}return await emitTransition(options,journal,"committing"),journal.status="committed",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"committed"),{transactionId:journal.transactionId,status:"committed",recovered,results:createResultMap(journal.results)}}catch(error){if(error instanceof WorkspaceTransactionInterruptedError||journal.status==="committed")throw error;try{await compensateAppliedSteps(options,journal)}catch(compensationError){throw new AggregateError([error,compensationError],`Workspace transaction failed and compensation failed: ${String(compensationError)}`,{cause:compensationError})}throw error}}async function loadJournal(pmRoot,transactionId,stepIds){try{return parseJournal(await readFile(journalPath(pmRoot,transactionId),"utf8"),transactionId,stepIds)}catch(error){if(error!==null&&typeof error=="object"&&"code"in error&&isFileMissingError(error))return;throw error}}async function emitTransition(options,journal,transition,stepId){await options.onTransition?.({transactionId:journal.transactionId,attempt:journal.attempt,transition,...stepId===void 0?{}:{stepId}})}async function discoverAppliedSteps(journal,steps){const completed=new Set,results=createResultMap();for(const step of steps){const inspection=await step.inspect();inspection.state==="applied"&&(completed.add(step.id),setJournalEntry(results,step.id,inspection.result,`Step ${step.id} result`))}journal.results=results,journal.completedStepIds=steps.map(step=>step.id).filter(stepId=>completed.has(stepId))}async function compensateAppliedSteps(options,journal){journal.status="compensating",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensating");const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of[...options.steps].reverse()){if(!started.has(step.id)&&!completed.has(step.id))continue;let shouldCompensate;try{const state=(await step.inspect()).state;shouldCompensate=started.has(step.id)&&(state==="applied"||state==="pending"&&step.compensatePending===!0)}catch{shouldCompensate=started.has(step.id)}shouldCompensate&&(await emitTransition(options,journal,"step_compensating",step.id),await step.compensate(journal.compensationData[step.id])),completed.delete(step.id),started.delete(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),delete journal.results[step.id],delete journal.compensationData[step.id],await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_compensated",step.id)}journal.status="compensated",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensated")}async function commitWorkspaceTransaction(options){const{pmRoot,transactionId,author}=validateWorkspaceTransactionIdentity(options),lockTtlSeconds=positiveInteger(options.lockTtlSeconds,DEFAULT_LOCK_TTL_SECONDS,"Transaction lock TTL"),lockWaitMs=positiveInteger(options.lockWaitMs,DEFAULT_LOCK_WAIT_MS,"Transaction lock wait"),steps=[...options.steps];if(steps.length===0)throw new TypeError("Workspace transaction requires at least one step");const stepIds=steps.map(step=>{const stepId=requiredIdentifier(step.id,"Transaction step id");if(stepId!==step.id)throw new TypeError("Transaction step id must not contain surrounding whitespace");return stepId});if(new Set(stepIds).size!==stepIds.length)throw new TypeError("Workspace transaction step ids must be unique");const stableOptions={...options,pmRoot,transactionId,author,steps,lockTtlSeconds,lockWaitMs},release=await acquireLock(pmRoot,WORKSPACE_TRANSACTION_LOCK_ID,lockTtlSeconds,author,!1,!1,lockWaitMs);try{const existing=await loadJournal(pmRoot,transactionId,stepIds),recovered=existing!==void 0;if(existing?.status==="committed")return{transactionId,status:"committed",recovered:!0,results:createResultMap(existing.results)};const prepared=await prepareJournalForApply(stableOptions,transactionId,author,stepIds,existing);return await applyPreparedTransaction(stableOptions,prepared.journal,recovered||prepared.recovered)}finally{await release()}}export{WorkspaceTransactionInterruptedError,commitWorkspaceTransaction,validateWorkspaceTransactionIdentity};
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]="a8daf042-f74c-5f51-b7e8-f8ff4fb8fe8e")}catch{}})();import{mkdir,readFile}from"node:fs/promises";import path from"node:path";import{createHash}from"node:crypto";import{appendWorkspaceAuditEvent,getWorkspaceHistoryPath,WORKSPACE_HISTORY_ID}from"../core/history/workspace-history.js";import{readHistoryEntries}from"./history-read.js";import{acquireLock}from"../core/lock/lock.js";import{isFileMissingError,writeFileAtomic}from"../core/fs/fs-utils.js";import{nowIso}from"../core/shared/time.js";import{EXIT_CODE}from"../core/shared/constants.js";import{PmCliError}from"../core/shared/errors.js";class WorkspaceTransactionInterruptedError extends Error{constructor(message="Workspace transaction interrupted"){super(message),this.name="WorkspaceTransactionInterruptedError"}}const WORKSPACE_TRANSACTION_LOCK_ID="sdk-workspace-transaction",TRANSACTION_ID_PATTERN=/^[a-zA-Z0-9._-]+$/,DEFAULT_LOCK_TTL_SECONDS=30,DEFAULT_LOCK_WAIT_MS=3e3;function requiredIdentifier(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized||!TRANSACTION_ID_PATTERN.test(normalized))throw new TypeError(`${label} must match [a-zA-Z0-9._-]+`);return normalized}function requiredText(value,label){if(typeof value!="string")throw new TypeError(`${label} must be a string`);const normalized=value.trim();if(!normalized)throw new TypeError(`${label} must be non-empty`);return normalized}function validateWorkspaceTransactionIdentity(options){return{pmRoot:requiredText(options.pmRoot,"Transaction pmRoot"),transactionId:requiredIdentifier(options.transactionId,"Transaction id"),author:requiredText(options.author,"Transaction author")}}function positiveInteger(value,fallback,label){const resolved=value??fallback;if(!Number.isInteger(resolved)||resolved<=0)throw new TypeError(`${label} must be a positive integer`);return resolved}function journalPath(pmRoot,transactionId){return path.join(pmRoot,"transactions","sdk",`${transactionId}.json`)}async function writeJournal(pmRoot,journal){journal.updatedAt=nowIso();const target=journalPath(pmRoot,journal.transactionId);await mkdir(path.dirname(target),{recursive:!0}),await writeFileAtomic(target,`${JSON.stringify(journal,null,2)}
2
+ `)}function hasValidJournalMetadata(journal){return Number.isInteger(journal.attempt)&&typeof journal.author=="string"&&typeof journal.createdAt=="string"&&typeof journal.updatedAt=="string"&&hasValidAbandonment(journal)}function hasValidAbandonment(journal){const retirement=journal.abandonment;return retirement===void 0?journal.schemaVersion===1&&journal.status!=="abandoned":journal.schemaVersion!==2||retirement===null||typeof retirement!="object"||journal.status==="committed"?!1:typeof retirement.reason=="string"&&retirement.reason.trim().length>0&&typeof retirement.author=="string"&&retirement.author.trim().length>0&&(retirement.supersededBy===void 0||typeof retirement.supersededBy=="string"&&TRANSACTION_ID_PATTERN.test(retirement.supersededBy)&&retirement.supersededBy!==journal.transactionId)}function createResultMap(source){return Object.assign(Object.create(null),source)}function cloneJournalValue(value,label){try{const serialized=JSON.stringify(value,(_key,nestedValue)=>{if(typeof nestedValue=="number"&&!Number.isFinite(nestedValue))throw new TypeError(`${label} contains a non-finite number`);if(nestedValue===void 0||typeof nestedValue=="function"||typeof nestedValue=="symbol")throw new TypeError(`${label} contains a non-JSON value`);return nestedValue});return JSON.parse(serialized)}catch(error){throw new TypeError(`${label} must be JSON serializable`,{cause:error})}}function cloneOptionalJournalValue(value,label){return value===void 0?void 0:cloneJournalValue(value,label)}function setJournalEntry(target,stepId,value,label){const cloned=cloneOptionalJournalValue(value,label);cloned===void 0?delete target[stepId]:target[stepId]=cloned}function isJournalMap(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)}function hasUniqueKnownStepIds(value,stepIds){return Array.isArray(value)&&new Set(value).size===value.length&&value.every(stepId=>typeof stepId=="string"&&stepIds.includes(stepId))}function hasValidJournalLifecycle(journal,stepIds){if(!hasUniqueKnownStepIds(journal.startedStepIds,stepIds)||!hasUniqueKnownStepIds(journal.completedStepIds,stepIds)||!isJournalMap(journal.results)||!isJournalMap(journal.compensationData))return!1;const startedStepIds=journal.startedStepIds,started=new Set(startedStepIds),completedStepIds=journal.completedStepIds,completed=new Set(completedStepIds);return Object.keys(journal.compensationData).some(stepId=>!started.has(stepId))||Object.keys(journal.results).some(stepId=>!completed.has(stepId))?!1:journal.status==="committed"?stepIds.every(stepId=>completed.has(stepId)):!["compensated","abandoned"].includes(journal.status??"")||completed.size===0&&started.size===0}function invalidJournal(transactionId,detail){return new PmCliError(`Workspace transaction ${transactionId} journal is invalid (${detail})`,EXIT_CODE.CONFLICT,{code:"transaction_journal_invalid",required:"A valid durable journal is required before transaction recovery can continue.",why:"Corrupt journal data cannot establish which steps applied or still require compensation.",nextSteps:["Preserve the journal and inspect its integrity or restore a verified backup before retrying.","Do not switch to a new transaction id to bypass an unreadable journal; previously applied work may remain."]})}function journalPlanMismatch(transactionId){return new PmCliError(`Workspace transaction ${transactionId} journal does not match the supplied plan`,EXIT_CODE.CONFLICT,{code:"transaction_plan_mismatch",required:"Replay the original plan or safely abandon the interrupted transaction before replacing it.",why:"A journal records the plan it is resuming; applying a different plan under the same id could skip or duplicate steps.",nextSteps:["Re-run with the original document to resume or replay it.","For item batches use client.itemTransactions.status(transactionId), then client.itemTransactions.abandon(transactionId, { reason }) before committing a changed plan with a new transactionId.","Run pm ops gc --scope transactions --dry-run to inspect retention; active and explicitly abandoned journals remain protected."]})}function matchesJournalSteps(journal,recorded,requested){return requested===void 0||journal.abandonment!==void 0||journal.status==="compensated"||recorded.length===requested.length&&recorded.every((id,index)=>id===requested[index])}function parseJournal(raw,transactionId,stepIds){let parsed;try{parsed=JSON.parse(raw)}catch{throw invalidJournal(transactionId,"invalid JSON")}if(!parsed||typeof parsed!="object"||Array.isArray(parsed))throw invalidJournal(transactionId,"expected an object");const journal=parsed;if(!((journal.schemaVersion===1||journal.schemaVersion===2)&&journal.transactionId===transactionId))throw invalidJournal(transactionId,"schema version or transaction id does not match the journal file");const recordedStepIds=journal.stepIds;if(!Array.isArray(recordedStepIds)||!hasUniqueKnownStepIds(recordedStepIds,recordedStepIds)||!hasValidJournalLifecycle(journal,recordedStepIds)||!hasValidJournalMetadata(journal)||typeof journal.status!="string"||!["applying","compensating","compensated","committed","abandoned"].includes(journal.status))throw invalidJournal(transactionId,"invalid lifecycle, metadata, status or step ids");if(!matchesJournalSteps(journal,recordedStepIds,stepIds))throw journalPlanMismatch(transactionId);return{...journal,results:createResultMap(journal.results),compensationData:createResultMap(journal.compensationData)}}async function prepareJournalForApply(options,transactionId,author,stepIds,existing){let journal=existing;const recovered=journal!==void 0;if(journal?.status==="compensating"&&await compensateAppliedSteps(options,journal),journal===void 0||journal.status==="compensated"){const timestamp=nowIso();journal={schemaVersion:1,transactionId,author,status:"applying",attempt:(journal?.attempt??0)+1,createdAt:journal?.createdAt??timestamp,updatedAt:timestamp,stepIds,startedStepIds:[],completedStepIds:[],results:createResultMap(),compensationData:createResultMap(),...options.recoveryData===void 0?{}:{recoveryData:cloneJournalValue(options.recoveryData,"Transaction recovery data")}},await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"prepared")}return{journal,recovered}}async function applyPreparedTransaction(options,journal,recovered){try{await discoverAppliedSteps(journal,options.steps),await writeJournal(options.pmRoot,journal);const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of options.steps){if(completed.has(step.id))continue;const inspection=await step.inspect();let result=cloneOptionalJournalValue(inspection.result,`Step ${step.id} result`);if(inspection.state!=="applied"){const compensationData=await step.prepareCompensation?.();setJournalEntry(journal.compensationData,step.id,compensationData,`Step ${step.id} compensation data`),started.add(step.id),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),await writeJournal(options.pmRoot,journal);const appliedResult=await step.apply();result=cloneOptionalJournalValue(appliedResult,`Step ${step.id} result`)}await emitTransition(options,journal,"step_applied",step.id),completed.add(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),setJournalEntry(journal.results,step.id,result,`Step ${step.id} result`),await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_recorded",step.id)}return await emitTransition(options,journal,"committing"),journal.status="committed",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"committed"),{transactionId:journal.transactionId,status:"committed",recovered,results:createResultMap(journal.results)}}catch(error){if(error instanceof WorkspaceTransactionInterruptedError||journal.status==="committed")throw error;try{await compensateAppliedSteps(options,journal)}catch(compensationError){throw new AggregateError([error,compensationError],`Workspace transaction failed and compensation failed: ${String(compensationError)}`,{cause:compensationError})}throw error}}async function loadJournal(pmRoot,transactionId,stepIds){try{return parseJournal(await readFile(journalPath(pmRoot,transactionId),"utf8"),transactionId,stepIds)}catch(error){if(error!==null&&typeof error=="object"&&"code"in error&&isFileMissingError(error))return;throw error}}async function emitTransition(options,journal,transition,stepId){await options.onTransition?.({transactionId:journal.transactionId,attempt:journal.attempt,transition,...stepId===void 0?{}:{stepId}})}async function discoverAppliedSteps(journal,steps){const completed=new Set,results=createResultMap();for(const step of steps){const inspection=await step.inspect();inspection.state==="applied"&&(completed.add(step.id),setJournalEntry(results,step.id,inspection.result,`Step ${step.id} result`))}journal.results=results,journal.completedStepIds=steps.map(step=>step.id).filter(stepId=>completed.has(stepId))}async function compensateAppliedSteps(options,journal){journal.status="compensating",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensating");const completed=new Set(journal.completedStepIds),started=new Set(journal.startedStepIds);for(const step of[...options.steps].reverse()){if(!started.has(step.id)&&!completed.has(step.id))continue;let shouldCompensate;try{const state=(await step.inspect()).state;shouldCompensate=started.has(step.id)&&(state==="applied"||state==="pending"&&step.compensatePending===!0)}catch{shouldCompensate=started.has(step.id)}shouldCompensate&&(await emitTransition(options,journal,"step_compensating",step.id),await step.compensate(journal.compensationData[step.id])),completed.delete(step.id),started.delete(step.id),journal.completedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>completed.has(stepId)),journal.startedStepIds=options.steps.map(candidate=>candidate.id).filter(stepId=>started.has(stepId)),delete journal.results[step.id],delete journal.compensationData[step.id],await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"step_compensated",step.id)}journal.status="compensated",await writeJournal(options.pmRoot,journal),await emitTransition(options,journal,"compensated")}async function commitWorkspaceTransaction(options){const{pmRoot,transactionId,author}=validateWorkspaceTransactionIdentity(options),lockTtlSeconds=positiveInteger(options.lockTtlSeconds,DEFAULT_LOCK_TTL_SECONDS,"Transaction lock TTL"),lockWaitMs=positiveInteger(options.lockWaitMs,DEFAULT_LOCK_WAIT_MS,"Transaction lock wait"),steps=[...options.steps];if(steps.length===0)throw new TypeError("Workspace transaction requires at least one step");const stepIds=steps.map(step=>{const stepId=requiredIdentifier(step.id,"Transaction step id");if(stepId!==step.id)throw new TypeError("Transaction step id must not contain surrounding whitespace");return stepId});if(new Set(stepIds).size!==stepIds.length)throw new TypeError("Workspace transaction step ids must be unique");const stableOptions={...options,pmRoot,transactionId,author,steps,lockTtlSeconds,lockWaitMs},release=await acquireLock(pmRoot,WORKSPACE_TRANSACTION_LOCK_ID,lockTtlSeconds,author,!1,!1,lockWaitMs);try{const existing=await loadJournal(pmRoot,transactionId,stepIds);if(existing?.status==="abandoned"||existing?.abandonment!==void 0)throw retiredTransaction(existing);const recovered=existing!==void 0;if(existing?.status==="committed")return{transactionId,status:"committed",recovered:!0,results:createResultMap(existing.results)};const prepared=await prepareJournalForApply(stableOptions,transactionId,author,stepIds,existing);return await applyPreparedTransaction(stableOptions,prepared.journal,recovered||prepared.recovered)}finally{await release()}}function retiredTransaction(journal){const pending=journal.status!=="abandoned",supersededBy=journal.abandonment.supersededBy,retry=supersededBy===void 0?`Retry abandonment without supersededBy to finish retirement of ${JSON.stringify(journal.transactionId)}.`:`Retry abandonment with supersededBy ${JSON.stringify(supersededBy)} to finish retirement of ${JSON.stringify(journal.transactionId)}.`;return new PmCliError(pending?`Transaction ${journal.transactionId} retirement is incomplete`:`Transaction ${journal.transactionId} has been abandoned`,EXIT_CODE.CONFLICT,{code:"transaction_abandoned",required:pending?retry:"Use a new transactionId for the replacement plan.",why:pending?"Forward replay is fenced, but the recorded retirement has not finished. Preserve its replacement identity.":"An explicitly retired transaction identity cannot be resumed.",...pending?{retirement:{state:"pending",superseded_by:supersededBy??null},nextSteps:[retry]}:{}})}async function readWorkspaceTransaction(pmRoot,transactionId){const identity=validateWorkspaceTransactionIdentity({pmRoot,transactionId,author:"reader"});return loadJournal(identity.pmRoot,identity.transactionId)}function workspaceTransactionPlanDigest(journal){return createHash("sha256").update(JSON.stringify(journal.stepIds)).digest("hex")}async function abandonWorkspaceTransaction(options){const identity=validateWorkspaceTransactionIdentity(options),reason=requiredText(options.reason,"Abandonment reason"),supersededBy=options.supersededBy===void 0?void 0:requiredIdentifier(options.supersededBy,"Replacement transaction id");if(supersededBy===identity.transactionId)throw new TypeError("Replacement transaction id must differ from the abandoned id");const lockTtlSeconds=positiveInteger(options.lockTtlSeconds,DEFAULT_LOCK_TTL_SECONDS,"Transaction lock TTL"),lockWaitMs=positiveInteger(options.lockWaitMs,DEFAULT_LOCK_WAIT_MS,"Transaction lock wait"),release=await acquireLock(identity.pmRoot,WORKSPACE_TRANSACTION_LOCK_ID,lockTtlSeconds,identity.author,!1,!1,lockWaitMs);try{const journal=await loadJournal(identity.pmRoot,identity.transactionId);if(journal===void 0)throw new PmCliError(`Transaction ${identity.transactionId} was not found`,EXIT_CODE.NOT_FOUND);if(journal.status==="committed")throw new PmCliError(`Transaction ${identity.transactionId} is committed and cannot be abandoned`,EXIT_CODE.CONFLICT,{code:"transaction_committed",required:"Plan a separate explicit corrective mutation.",why:"Recovery cannot silently undo committed work."});if(journal.abandonment!==void 0&&journal.abandonment.supersededBy!==supersededBy)throw retiredTransaction(journal);if(journal.status==="abandoned")return journal;const steps=options.resolveSteps(structuredClone(journal));if(JSON.stringify(steps.map(step=>step.id))!==JSON.stringify(journal.stepIds))throw journalPlanMismatch(identity.transactionId);if(journal.abandonment===void 0){if(supersededBy!==void 0&&await loadJournal(identity.pmRoot,supersededBy)!==void 0)throw new PmCliError(`Replacement transaction ${supersededBy} already exists`,EXIT_CODE.CONFLICT);journal.schemaVersion=2,journal.abandonment={reason,author:identity.author,...supersededBy===void 0?{}:{supersededBy}},await writeJournal(identity.pmRoot,journal)}return journal.status!=="compensated"&&await compensateAppliedSteps({...options,...identity,steps},journal),await auditTransactionAbandonment(journal,identity.pmRoot,lockTtlSeconds,lockWaitMs),journal.status="abandoned",await writeJournal(identity.pmRoot,journal),journal}finally{await release()}}async function auditTransactionAbandonment(journal,pmRoot,lockTtlSeconds,lockWaitMs){const planDigest=workspaceTransactionPlanDigest(journal);if((await readHistoryEntries(getWorkspaceHistoryPath(pmRoot),WORKSPACE_HISTORY_ID)).some(entry=>entry.op==="transaction_abandon"&&entry.context?.transaction_id===journal.transactionId&&entry.context?.attempt===journal.attempt&&entry.context?.plan_digest===planDigest))return;const abandonment=journal.abandonment;await appendWorkspaceAuditEvent({pmRoot,author:abandonment.author,lockTtlSeconds,lockWaitMs,op:"transaction_abandon",message:abandonment.reason,context:{transaction_id:journal.transactionId,attempt:journal.attempt,plan_digest:planDigest,...abandonment.supersededBy===void 0?{}:{superseded_by:abandonment.supersededBy}}})}export{WorkspaceTransactionInterruptedError,abandonWorkspaceTransaction,commitWorkspaceTransaction,readWorkspaceTransaction,validateWorkspaceTransactionIdentity,workspaceTransactionPlanDigest};
3
3
  //# sourceMappingURL=workspace-transaction.js.map
package/docs/COMMANDS.md CHANGED
@@ -664,6 +664,22 @@ Every transition into a terminal status stamps two distinct ISO timestamps: `clo
664
664
 
665
665
  `pm create --blocked-by <id>` and `pm update --blocked-by <id>` keep the scalar `blocked_by` field as the latest/primary blocker for compatibility. When the value resolves to a real item, the command also appends a `blocked_by` dependency edge so `pm deps` and reverse dependency scans see the full blocker graph. Repeating `pm update --blocked-by` with a different blocker preserves the prior blocker edge and appends the new one; repeating the same blocker is deduplicated. Use `pm update <id> --unset blocked-by` to clear the scalar and all `blocked_by` dependency edges.
666
666
 
667
+ Unsupported `pm block` and `pm unblock` invocations return usage errors with
668
+ `pm update --help` and these explicit mutation templates
669
+ ([pm-gh1607](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1607.toon); tracker record excluded from npm):
670
+
671
+ ```bash
672
+ pm update <item-id> --blocked-by <blocker-id> --blocked-reason "waiting"
673
+ pm update <item-id> --unset blocked-by --unset blocked-reason --unblock-note "ready"
674
+ ```
675
+
676
+ Inspect the actual blockers before applying either template. The unblock recipe
677
+ clears all `blocked_by` relationships and stale blocker metadata while preserving
678
+ other relationship kinds. `--unblock-note` alone only records a note.
679
+ The help retry and mutation examples preserve an explicit `--pm-path` and
680
+ `--no-extensions` from the failed invocation
681
+ ([pm-lifecycle-example-scope](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/tasks/pm-lifecycle-example-scope.toon); tracker record excluded from npm).
682
+
667
683
  When closing a blocker, `pm close` scans reverse `blocked_by` edges and auto-unblocks dependent items only when every resolvable blocker is now terminal. Each unblocked item is updated through the normal audited mutation path, gets an `unblock_note`, and the close result reports compact `auto_unblocked:<id>:resolved_blockers=<ids>` warnings. Items with another active blocker remain blocked.
668
684
 
669
685
  Over MCP, single-item mutations (`pm_create`, `pm_update`, `pm_close`, and
@@ -1166,6 +1182,22 @@ unowned hooks. Default installation remains hook-free. See
1166
1182
 
1167
1183
  ## History and Recovery
1168
1184
 
1185
+ Compact and provenance history default to newest-first, including reads with
1186
+ `--limit`. Full entries default to oldest-first. Use `--order oldest_first` or
1187
+ `--order newest_first` to choose the same direction for any projection and its
1188
+ value diffs. `--limit N` selects the latest N matching entries before ordering.
1189
+ The `order` receipt declares direction and `total` counts matches before limits.
1190
+ Physical indices and durable versions retain their original meaning. Use
1191
+ `--output-cursor` with the returned continuation to traverse the selected order.
1192
+ Verification always checks the full authoritative chain.
1193
+
1194
+ Compatibility: a compact/provenance latest-N read used to flip to chronological
1195
+ order. Chronological consumers should explicitly use, for example,
1196
+ `pm history pm-a1b2 --diff --limit 20 --order oldest_first`.
1197
+ Full reads keep their chronological default.
1198
+ Tracked by [pm-gh1617](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1617.toon)
1199
+ (tracker record excluded from npm).
1200
+
1169
1201
  After a branch merge that touched tracker history, preview and apply the
1170
1202
  one-command reconciliation gate:
1171
1203
 
@@ -1247,7 +1279,7 @@ extension/package activation are recorded in
1247
1279
  workspace auditing and is created on the first audited singleton mutation.
1248
1280
  `pm get --at` is the read-only counterpart: it reconstructs a recorded version through the shared restore replay kernel and labels the result so consumers cannot confuse historical state with current state. Checkpoint baselines produced by `history-compact` remain valid reconstruction roots.
1249
1281
 
1250
- `--diff` replays the history chain and emits, per entry, a `changes` array of `{ field, before, after }` field-level value transitions (alongside the `changed_fields` name list) — so you can see exactly what each field changed from and to without comparing snapshots. It is independent of the compact/full projection. `--field <name>` narrows the diff to a single field's transitions (implying `--diff`), answering "when did `<field>` change?" — e.g. `pm history <id> --diff --field status`.
1282
+ `--diff` replays the history chain and emits, per entry, a `changes` array of `{ field, before, after }` field-level value transitions (alongside the `changed_fields` name list) — so you can see exactly what each field changed from and to without comparing snapshots. It is independent of the compact/full projection. Provenance filters and limits select the same physical entries for the diff; excluded intermediate events still contribute to accurate before/after values. `--field <name>` narrows the diff to a single field's transitions (implying `--diff`), answering "when did `<field>` change?" — e.g. `pm history <id> --diff --field status`.
1251
1283
 
1252
1284
  `pm stats` reports item and history totals plus a screen-sized lifecycle table.
1253
1285
  Every non-empty item type is one row with `total`, `open`, `in_progress`,
@@ -120,8 +120,9 @@ primary rows while a rich result can name
120
120
  `checks.0.details.missing_resolution_rows` as a continuation row. This prevents
121
121
  an inner diagnostic array from disabling `--output-limit` on the outer checks.
122
122
  `toon_encoding: "tabular_when_uniform"` declares that an array of flat objects
123
- with one shared key set renders as a length-marked TOON table; mixed, nested,
124
- or heterogeneous arrays retain the expanded representation. Quoted,
123
+ with one shared key set can render as a length-marked TOON table. Nested or
124
+ heterogeneous object rows can also use a smaller table with explicit sibling
125
+ encoding metadata; otherwise they retain the expanded representation. Quoted,
125
126
  separator-bearing, and multiline values use the canonical TOON encoder and
126
127
  round-trip through the strict decoder.
127
128
  Commands without a row collection, including a dependency tree or leaf `get`,
@@ -463,3 +464,24 @@ Continuation and compaction slice these receipts with the rows. Self-describing
463
464
  focus rows need no extra receipt. Semantic receipts are preserved during
464
465
  explanatory-string compaction, and corrupt alphabet or row-count mismatches are
465
466
  rejected.
467
+
468
+
469
+ ### Compact nested table metadata
470
+
471
+ Tracked by [pm-gh1603](https://github.com/unbraind/pm-cli/blob/main/.agents/pm/issues/pm-gh1603.toon)
472
+ (tracker record excluded from npm). The SDK `encodePmTableRows(rows, { compact: true })`
473
+ can declare `list_columns` (column to delimiter) and `null_columns` alongside
474
+ `json_columns` and `absent`. Omitted null columns also carry `column_order`
475
+ to restore their original first-occurrence positions. `decodePmTableRows` restores
476
+ these facts exactly.
477
+ A safe string-array column uses `;` between nonempty members; an empty cell means
478
+ an empty array. Any empty member, delimiter collision, mixed type or nested value
479
+ uses the existing JSON-column encoding instead. Null columns are omitted only
480
+ when every row explicitly contains null; missing fields remain distinct.
481
+
482
+ The TOON host selects compact tables only when they reduce rendered bytes.
483
+ Agenda rows therefore share null-field declarations and tag arrays avoid repeated
484
+ JSON escaping. JSON output and SDK query results retain their original values.
485
+ Consumers of TOON tables must honor all declared encoding metadata or use the
486
+ current SDK decoder. Whole-envelope budgets measure the actual serialized form,
487
+ including decoding metadata.