@unbrained/pm-cli 2026.3.12 → 2026.5.1-2

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 (285) hide show
  1. package/.agents/pm/extensions/.managed-extensions.json +42 -0
  2. package/.agents/pm/extensions/beads/index.js +109 -0
  3. package/.agents/pm/extensions/beads/manifest.json +7 -0
  4. package/{dist/cli/commands/beads.js → .agents/pm/extensions/beads/runtime.js} +31 -21
  5. package/.agents/pm/extensions/beads/runtime.ts +702 -0
  6. package/.agents/pm/extensions/todos/index.js +126 -0
  7. package/.agents/pm/extensions/todos/manifest.json +7 -0
  8. package/{dist/extensions/builtins/todos/import-export.js → .agents/pm/extensions/todos/runtime.js} +39 -29
  9. package/.agents/pm/extensions/todos/runtime.ts +568 -0
  10. package/AGENTS.md +196 -92
  11. package/CHANGELOG.md +404 -0
  12. package/CODE_OF_CONDUCT.md +42 -0
  13. package/CONTRIBUTING.md +144 -0
  14. package/PRD.md +512 -164
  15. package/README.md +1053 -2
  16. package/SECURITY.md +51 -0
  17. package/dist/cli/commands/activity.d.ts +5 -0
  18. package/dist/cli/commands/activity.js +66 -3
  19. package/dist/cli/commands/activity.js.map +1 -1
  20. package/dist/cli/commands/aggregate.d.ts +54 -0
  21. package/dist/cli/commands/aggregate.js +181 -0
  22. package/dist/cli/commands/aggregate.js.map +1 -0
  23. package/dist/cli/commands/append.js +4 -1
  24. package/dist/cli/commands/append.js.map +1 -1
  25. package/dist/cli/commands/calendar.d.ts +109 -0
  26. package/dist/cli/commands/calendar.js +797 -0
  27. package/dist/cli/commands/calendar.js.map +1 -0
  28. package/dist/cli/commands/claim.d.ts +5 -1
  29. package/dist/cli/commands/claim.js +42 -21
  30. package/dist/cli/commands/claim.js.map +1 -1
  31. package/dist/cli/commands/close.d.ts +1 -0
  32. package/dist/cli/commands/close.js +54 -5
  33. package/dist/cli/commands/close.js.map +1 -1
  34. package/dist/cli/commands/comments-audit.d.ts +91 -0
  35. package/dist/cli/commands/comments-audit.js +195 -0
  36. package/dist/cli/commands/comments-audit.js.map +1 -0
  37. package/dist/cli/commands/comments.d.ts +1 -0
  38. package/dist/cli/commands/comments.js +70 -21
  39. package/dist/cli/commands/comments.js.map +1 -1
  40. package/dist/cli/commands/completion.d.ts +10 -4
  41. package/dist/cli/commands/completion.js +1184 -137
  42. package/dist/cli/commands/completion.js.map +1 -1
  43. package/dist/cli/commands/config.d.ts +35 -3
  44. package/dist/cli/commands/config.js +968 -13
  45. package/dist/cli/commands/config.js.map +1 -1
  46. package/dist/cli/commands/context.d.ts +86 -0
  47. package/dist/cli/commands/context.js +299 -0
  48. package/dist/cli/commands/context.js.map +1 -0
  49. package/dist/cli/commands/contracts.d.ts +78 -0
  50. package/dist/cli/commands/contracts.js +920 -0
  51. package/dist/cli/commands/contracts.js.map +1 -0
  52. package/dist/cli/commands/create.d.ts +48 -14
  53. package/dist/cli/commands/create.js +1331 -160
  54. package/dist/cli/commands/create.js.map +1 -1
  55. package/dist/cli/commands/dedupe-audit.d.ts +81 -0
  56. package/dist/cli/commands/dedupe-audit.js +330 -0
  57. package/dist/cli/commands/dedupe-audit.js.map +1 -0
  58. package/dist/cli/commands/deps.d.ts +52 -0
  59. package/dist/cli/commands/deps.js +204 -0
  60. package/dist/cli/commands/deps.js.map +1 -0
  61. package/dist/cli/commands/docs.d.ts +19 -0
  62. package/dist/cli/commands/docs.js +212 -13
  63. package/dist/cli/commands/docs.js.map +1 -1
  64. package/dist/cli/commands/extension.d.ts +122 -0
  65. package/dist/cli/commands/extension.js +1850 -0
  66. package/dist/cli/commands/extension.js.map +1 -0
  67. package/dist/cli/commands/files.d.ts +52 -1
  68. package/dist/cli/commands/files.js +455 -13
  69. package/dist/cli/commands/files.js.map +1 -1
  70. package/dist/cli/commands/gc.d.ts +11 -1
  71. package/dist/cli/commands/gc.js +89 -11
  72. package/dist/cli/commands/gc.js.map +1 -1
  73. package/dist/cli/commands/get.d.ts +13 -0
  74. package/dist/cli/commands/get.js +35 -3
  75. package/dist/cli/commands/get.js.map +1 -1
  76. package/dist/cli/commands/health.d.ts +10 -2
  77. package/dist/cli/commands/health.js +774 -23
  78. package/dist/cli/commands/health.js.map +1 -1
  79. package/dist/cli/commands/history.d.ts +20 -0
  80. package/dist/cli/commands/history.js +152 -6
  81. package/dist/cli/commands/history.js.map +1 -1
  82. package/dist/cli/commands/index.d.ts +16 -3
  83. package/dist/cli/commands/index.js +16 -3
  84. package/dist/cli/commands/index.js.map +1 -1
  85. package/dist/cli/commands/init.d.ts +7 -2
  86. package/dist/cli/commands/init.js +137 -5
  87. package/dist/cli/commands/init.js.map +1 -1
  88. package/dist/cli/commands/learnings.d.ts +17 -0
  89. package/dist/cli/commands/learnings.js +129 -0
  90. package/dist/cli/commands/learnings.js.map +1 -0
  91. package/dist/cli/commands/list.d.ts +29 -1
  92. package/dist/cli/commands/list.js +289 -53
  93. package/dist/cli/commands/list.js.map +1 -1
  94. package/dist/cli/commands/normalize.d.ts +51 -0
  95. package/dist/cli/commands/normalize.js +298 -0
  96. package/dist/cli/commands/normalize.js.map +1 -0
  97. package/dist/cli/commands/notes.d.ts +17 -0
  98. package/dist/cli/commands/notes.js +129 -0
  99. package/dist/cli/commands/notes.js.map +1 -0
  100. package/dist/cli/commands/reindex.d.ts +1 -0
  101. package/dist/cli/commands/reindex.js +208 -32
  102. package/dist/cli/commands/reindex.js.map +1 -1
  103. package/dist/cli/commands/restore.js +164 -30
  104. package/dist/cli/commands/restore.js.map +1 -1
  105. package/dist/cli/commands/search.d.ts +14 -1
  106. package/dist/cli/commands/search.js +475 -81
  107. package/dist/cli/commands/search.js.map +1 -1
  108. package/dist/cli/commands/stats.js +26 -10
  109. package/dist/cli/commands/stats.js.map +1 -1
  110. package/dist/cli/commands/templates.d.ts +26 -0
  111. package/dist/cli/commands/templates.js +179 -0
  112. package/dist/cli/commands/templates.js.map +1 -0
  113. package/dist/cli/commands/test-all.d.ts +19 -1
  114. package/dist/cli/commands/test-all.js +161 -13
  115. package/dist/cli/commands/test-all.js.map +1 -1
  116. package/dist/cli/commands/test-runs.d.ts +63 -0
  117. package/dist/cli/commands/test-runs.js +179 -0
  118. package/dist/cli/commands/test-runs.js.map +1 -0
  119. package/dist/cli/commands/test.d.ts +75 -1
  120. package/dist/cli/commands/test.js +1360 -41
  121. package/dist/cli/commands/test.js.map +1 -1
  122. package/dist/cli/commands/update-many.d.ts +57 -0
  123. package/dist/cli/commands/update-many.js +631 -0
  124. package/dist/cli/commands/update-many.js.map +1 -0
  125. package/dist/cli/commands/update.d.ts +30 -0
  126. package/dist/cli/commands/update.js +1393 -84
  127. package/dist/cli/commands/update.js.map +1 -1
  128. package/dist/cli/commands/validate.d.ts +30 -0
  129. package/dist/cli/commands/validate.js +1151 -0
  130. package/dist/cli/commands/validate.js.map +1 -0
  131. package/dist/cli/error-guidance.d.ts +33 -0
  132. package/dist/cli/error-guidance.js +337 -0
  133. package/dist/cli/error-guidance.js.map +1 -0
  134. package/dist/cli/extension-command-options.d.ts +1 -0
  135. package/dist/cli/extension-command-options.js +92 -0
  136. package/dist/cli/extension-command-options.js.map +1 -1
  137. package/dist/cli/help-content.d.ts +20 -0
  138. package/dist/cli/help-content.js +543 -0
  139. package/dist/cli/help-content.js.map +1 -0
  140. package/dist/cli/main.js +3625 -445
  141. package/dist/cli/main.js.map +1 -1
  142. package/dist/core/extensions/index.d.ts +13 -1
  143. package/dist/core/extensions/index.js +108 -1
  144. package/dist/core/extensions/index.js.map +1 -1
  145. package/dist/core/extensions/item-fields.d.ts +2 -0
  146. package/dist/core/extensions/item-fields.js +79 -0
  147. package/dist/core/extensions/item-fields.js.map +1 -0
  148. package/dist/core/extensions/loader.d.ts +322 -9
  149. package/dist/core/extensions/loader.js +911 -20
  150. package/dist/core/extensions/loader.js.map +1 -1
  151. package/dist/core/extensions/runtime-registrations.d.ts +5 -0
  152. package/dist/core/extensions/runtime-registrations.js +51 -0
  153. package/dist/core/extensions/runtime-registrations.js.map +1 -0
  154. package/dist/core/history/history-stream-policy.d.ts +20 -0
  155. package/dist/core/history/history-stream-policy.js +53 -0
  156. package/dist/core/history/history-stream-policy.js.map +1 -0
  157. package/dist/core/history/history.js +90 -1
  158. package/dist/core/history/history.js.map +1 -1
  159. package/dist/core/item/id.js +4 -1
  160. package/dist/core/item/id.js.map +1 -1
  161. package/dist/core/item/index.d.ts +1 -0
  162. package/dist/core/item/index.js +1 -0
  163. package/dist/core/item/index.js.map +1 -1
  164. package/dist/core/item/item-format.d.ts +11 -5
  165. package/dist/core/item/item-format.js +507 -24
  166. package/dist/core/item/item-format.js.map +1 -1
  167. package/dist/core/item/parent-reference-policy.d.ts +6 -0
  168. package/dist/core/item/parent-reference-policy.js +32 -0
  169. package/dist/core/item/parent-reference-policy.js.map +1 -0
  170. package/dist/core/item/parse.d.ts +5 -0
  171. package/dist/core/item/parse.js +216 -19
  172. package/dist/core/item/parse.js.map +1 -1
  173. package/dist/core/item/sprint-release-format.d.ts +6 -0
  174. package/dist/core/item/sprint-release-format.js +33 -0
  175. package/dist/core/item/sprint-release-format.js.map +1 -0
  176. package/dist/core/item/status.d.ts +3 -0
  177. package/dist/core/item/status.js +24 -0
  178. package/dist/core/item/status.js.map +1 -0
  179. package/dist/core/item/type-registry.d.ts +37 -0
  180. package/dist/core/item/type-registry.js +706 -0
  181. package/dist/core/item/type-registry.js.map +1 -0
  182. package/dist/core/lock/lock.d.ts +1 -1
  183. package/dist/core/lock/lock.js +101 -12
  184. package/dist/core/lock/lock.js.map +1 -1
  185. package/dist/core/output/command-aware.d.ts +1 -0
  186. package/dist/core/output/command-aware.js +394 -0
  187. package/dist/core/output/command-aware.js.map +1 -0
  188. package/dist/core/output/output.d.ts +3 -0
  189. package/dist/core/output/output.js +124 -6
  190. package/dist/core/output/output.js.map +1 -1
  191. package/dist/core/schema/runtime-field-filters.d.ts +3 -0
  192. package/dist/core/schema/runtime-field-filters.js +39 -0
  193. package/dist/core/schema/runtime-field-filters.js.map +1 -0
  194. package/dist/core/schema/runtime-field-values.d.ts +8 -0
  195. package/dist/core/schema/runtime-field-values.js +154 -0
  196. package/dist/core/schema/runtime-field-values.js.map +1 -0
  197. package/dist/core/schema/runtime-schema.d.ts +68 -0
  198. package/dist/core/schema/runtime-schema.js +554 -0
  199. package/dist/core/schema/runtime-schema.js.map +1 -0
  200. package/dist/core/search/cache.d.ts +13 -1
  201. package/dist/core/search/cache.js +123 -14
  202. package/dist/core/search/cache.js.map +1 -1
  203. package/dist/core/search/semantic-defaults.d.ts +6 -0
  204. package/dist/core/search/semantic-defaults.js +120 -0
  205. package/dist/core/search/semantic-defaults.js.map +1 -0
  206. package/dist/core/search/vector-stores.js +3 -1
  207. package/dist/core/search/vector-stores.js.map +1 -1
  208. package/dist/core/shared/command-types.d.ts +2 -0
  209. package/dist/core/shared/conflict-markers.d.ts +7 -0
  210. package/dist/core/shared/conflict-markers.js +27 -0
  211. package/dist/core/shared/conflict-markers.js.map +1 -0
  212. package/dist/core/shared/constants.d.ts +15 -4
  213. package/dist/core/shared/constants.js +141 -1
  214. package/dist/core/shared/constants.js.map +1 -1
  215. package/dist/core/shared/errors.d.ts +10 -1
  216. package/dist/core/shared/errors.js +3 -1
  217. package/dist/core/shared/errors.js.map +1 -1
  218. package/dist/core/shared/text-normalization.d.ts +4 -0
  219. package/dist/core/shared/text-normalization.js +33 -0
  220. package/dist/core/shared/text-normalization.js.map +1 -0
  221. package/dist/core/shared/time.d.ts +1 -2
  222. package/dist/core/shared/time.js +98 -11
  223. package/dist/core/shared/time.js.map +1 -1
  224. package/dist/core/store/index.d.ts +1 -0
  225. package/dist/core/store/index.js +1 -0
  226. package/dist/core/store/index.js.map +1 -1
  227. package/dist/core/store/item-format-migration.d.ts +9 -0
  228. package/dist/core/store/item-format-migration.js +87 -0
  229. package/dist/core/store/item-format-migration.js.map +1 -0
  230. package/dist/core/store/item-store.d.ts +13 -4
  231. package/dist/core/store/item-store.js +238 -51
  232. package/dist/core/store/item-store.js.map +1 -1
  233. package/dist/core/store/paths.d.ts +21 -3
  234. package/dist/core/store/paths.js +59 -4
  235. package/dist/core/store/paths.js.map +1 -1
  236. package/dist/core/store/settings.d.ts +14 -1
  237. package/dist/core/store/settings.js +463 -7
  238. package/dist/core/store/settings.js.map +1 -1
  239. package/dist/core/telemetry/consent.d.ts +2 -0
  240. package/dist/core/telemetry/consent.js +79 -0
  241. package/dist/core/telemetry/consent.js.map +1 -0
  242. package/dist/core/telemetry/runtime.d.ts +38 -0
  243. package/dist/core/telemetry/runtime.js +733 -0
  244. package/dist/core/telemetry/runtime.js.map +1 -0
  245. package/dist/core/test/background-runs.d.ts +117 -0
  246. package/dist/core/test/background-runs.js +760 -0
  247. package/dist/core/test/background-runs.js.map +1 -0
  248. package/dist/core/test/item-test-run-tracking.d.ts +9 -0
  249. package/dist/core/test/item-test-run-tracking.js +50 -0
  250. package/dist/core/test/item-test-run-tracking.js.map +1 -0
  251. package/dist/sdk/cli-contracts.d.ts +92 -0
  252. package/dist/sdk/cli-contracts.js +2357 -0
  253. package/dist/sdk/cli-contracts.js.map +1 -0
  254. package/dist/sdk/index.d.ts +34 -0
  255. package/dist/sdk/index.js +23 -0
  256. package/dist/sdk/index.js.map +1 -0
  257. package/dist/types.d.ts +197 -3
  258. package/dist/types.js +48 -1
  259. package/dist/types.js.map +1 -1
  260. package/docs/ARCHITECTURE.md +368 -39
  261. package/docs/EXTENSIONS.md +454 -49
  262. package/docs/RELEASING.md +70 -19
  263. package/docs/SDK.md +123 -0
  264. package/docs/examples/starter-extension/README.md +48 -0
  265. package/docs/examples/starter-extension/index.js +191 -0
  266. package/docs/examples/starter-extension/manifest.json +17 -0
  267. package/docs/examples/starter-extension/package.json +10 -0
  268. package/package.json +41 -14
  269. package/.pi/extensions/pm-cli/index.ts +0 -778
  270. package/dist/cli/commands/beads.d.ts +0 -16
  271. package/dist/cli/commands/beads.js.map +0 -1
  272. package/dist/cli/commands/install.d.ts +0 -18
  273. package/dist/cli/commands/install.js +0 -87
  274. package/dist/cli/commands/install.js.map +0 -1
  275. package/dist/core/extensions/builtins.d.ts +0 -3
  276. package/dist/core/extensions/builtins.js +0 -47
  277. package/dist/core/extensions/builtins.js.map +0 -1
  278. package/dist/extensions/builtins/beads/index.d.ts +0 -8
  279. package/dist/extensions/builtins/beads/index.js +0 -33
  280. package/dist/extensions/builtins/beads/index.js.map +0 -1
  281. package/dist/extensions/builtins/todos/import-export.d.ts +0 -26
  282. package/dist/extensions/builtins/todos/import-export.js.map +0 -1
  283. package/dist/extensions/builtins/todos/index.d.ts +0 -8
  284. package/dist/extensions/builtins/todos/index.js +0 -38
  285. package/dist/extensions/builtins/todos/index.js.map +0 -1
@@ -0,0 +1,760 @@
1
+ import fs from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import path from "node:path";
5
+ import { ensureDir, pathExists, readFileIfExists, writeFileAtomic } from "../fs/fs-utils.js";
6
+ import { PmCliError } from "../shared/errors.js";
7
+ import { EXIT_CODE } from "../shared/constants.js";
8
+ import { nowIso } from "../shared/time.js";
9
+ import { getTestRunRecordPath, getTestRunResultPath, getTestRunStderrPath, getTestRunStdoutPath, getTestRunsPath, getTestRunsRecordsPath, getTestRunsResultsPath, getTestRunsStderrPath, getTestRunsStdoutPath, } from "../store/paths.js";
10
+ const BACKGROUND_RUN_ACTIVE_STATUSES = new Set(["queued", "running"]);
11
+ const BACKGROUND_RUN_TERMINAL_STATUSES = new Set(["passed", "failed", "stopped", "canceled"]);
12
+ const DEFAULT_BACKGROUND_RUN_RESOURCE_SNAPSHOT_INTERVAL_MS = 3000;
13
+ const DEFAULT_BACKGROUND_RUN_FORCE_KILL_DELAY_MS = 3000;
14
+ const DEFAULT_BACKGROUND_RUN_HEARTBEAT_STALE_MS = 30000;
15
+ const DEFAULT_BACKGROUND_RUN_LOG_TAIL_LINES = 100;
16
+ const PROC_STAT_TICKS_PER_SECOND = 100;
17
+ function nowMs() {
18
+ return Date.now();
19
+ }
20
+ function normalizeCommandArgs(args) {
21
+ return args.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
22
+ }
23
+ function summarizeCommandLabel(args) {
24
+ const joined = args.join(" ").replaceAll(/\s+/g, " ").trim();
25
+ if (joined.length <= 180) {
26
+ return joined;
27
+ }
28
+ return `${joined.slice(0, 177)}...`;
29
+ }
30
+ function buildRunId() {
31
+ const timePart = Date.now().toString(36);
32
+ const randomPart = randomBytes(3).toString("hex");
33
+ return `tr-${timePart}-${randomPart}`;
34
+ }
35
+ export function buildBackgroundTestRunFingerprint(kind, commandArgs, pmRoot) {
36
+ const payload = {
37
+ kind,
38
+ command: normalizeCommandArgs(commandArgs),
39
+ pm_root: path.resolve(pmRoot),
40
+ };
41
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
42
+ }
43
+ function isPidRunning(pid) {
44
+ if (!Number.isInteger(pid) || !pid || pid <= 0) {
45
+ return false;
46
+ }
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
53
+ return code === "EPERM";
54
+ }
55
+ }
56
+ async function ensureBackgroundRunStorage(pmRoot) {
57
+ await ensureDir(getTestRunsPath(pmRoot));
58
+ await ensureDir(getTestRunsRecordsPath(pmRoot));
59
+ await ensureDir(getTestRunsStdoutPath(pmRoot));
60
+ await ensureDir(getTestRunsStderrPath(pmRoot));
61
+ await ensureDir(getTestRunsResultsPath(pmRoot));
62
+ }
63
+ async function parseBackgroundRunRecord(raw, recordPath) {
64
+ try {
65
+ const parsed = JSON.parse(raw);
66
+ if (!parsed || typeof parsed !== "object" || typeof parsed.id !== "string") {
67
+ throw new Error("invalid run record payload");
68
+ }
69
+ return parsed;
70
+ }
71
+ catch (error) {
72
+ throw new PmCliError(`Failed to parse background test run record at ${recordPath}: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODE.GENERIC_FAILURE);
73
+ }
74
+ }
75
+ async function writeBackgroundRunRecord(pmRoot, record) {
76
+ const next = {
77
+ ...record,
78
+ updated_at: nowIso(),
79
+ };
80
+ await writeFileAtomic(getTestRunRecordPath(pmRoot, record.id), `${JSON.stringify(next, null, 2)}\n`);
81
+ }
82
+ export async function readBackgroundTestRunRecord(pmRoot, runId) {
83
+ const recordPath = getTestRunRecordPath(pmRoot, runId);
84
+ const raw = await readFileIfExists(recordPath);
85
+ if (!raw) {
86
+ return null;
87
+ }
88
+ return parseBackgroundRunRecord(raw, recordPath);
89
+ }
90
+ async function listBackgroundRunRecordPaths(pmRoot) {
91
+ const recordsRoot = getTestRunsRecordsPath(pmRoot);
92
+ if (!(await pathExists(recordsRoot))) {
93
+ return [];
94
+ }
95
+ const entries = await fs.readdir(recordsRoot, { withFileTypes: true });
96
+ return entries
97
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
98
+ .map((entry) => path.join(recordsRoot, entry.name))
99
+ .sort((left, right) => right.localeCompare(left));
100
+ }
101
+ async function readLinuxRssBytes(pid) {
102
+ if (process.platform !== "linux") {
103
+ return undefined;
104
+ }
105
+ try {
106
+ const raw = await fs.readFile(`/proc/${pid}/status`, "utf8");
107
+ const match = raw.match(/^VmRSS:\s+(\d+)\s+kB$/m);
108
+ if (!match) {
109
+ return undefined;
110
+ }
111
+ const value = Number.parseInt(match[1] ?? "", 10);
112
+ if (!Number.isFinite(value)) {
113
+ return undefined;
114
+ }
115
+ return value * 1024;
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ }
121
+ async function readLinuxCpuSeconds(pid) {
122
+ if (process.platform !== "linux") {
123
+ return {};
124
+ }
125
+ try {
126
+ const raw = await fs.readFile(`/proc/${pid}/stat`, "utf8");
127
+ const closeParenIndex = raw.lastIndexOf(")");
128
+ if (closeParenIndex < 0) {
129
+ return {};
130
+ }
131
+ const remainder = raw.slice(closeParenIndex + 2).trim();
132
+ const parts = remainder.split(/\s+/);
133
+ const utimeTicks = Number.parseInt(parts[11] ?? "", 10);
134
+ const stimeTicks = Number.parseInt(parts[12] ?? "", 10);
135
+ if (!Number.isFinite(utimeTicks) || !Number.isFinite(stimeTicks)) {
136
+ return {};
137
+ }
138
+ return {
139
+ cpu_user_seconds: utimeTicks / PROC_STAT_TICKS_PER_SECOND,
140
+ cpu_system_seconds: stimeTicks / PROC_STAT_TICKS_PER_SECOND,
141
+ };
142
+ }
143
+ catch {
144
+ return {};
145
+ }
146
+ }
147
+ async function buildResourceSnapshot(record) {
148
+ const pid = record.child_pid ?? record.worker_pid;
149
+ if (!isPidRunning(pid)) {
150
+ return undefined;
151
+ }
152
+ const rssBytes = await readLinuxRssBytes(pid);
153
+ const cpu = await readLinuxCpuSeconds(pid);
154
+ const startedAtMs = record.started_at ? Date.parse(record.started_at) : Number.NaN;
155
+ const uptimeSeconds = Number.isFinite(startedAtMs) ? Math.max(0, (nowMs() - startedAtMs) / 1000) : undefined;
156
+ return {
157
+ recorded_at: nowIso(),
158
+ rss_bytes: rssBytes,
159
+ cpu_user_seconds: cpu.cpu_user_seconds,
160
+ cpu_system_seconds: cpu.cpu_system_seconds,
161
+ uptime_seconds: uptimeSeconds,
162
+ };
163
+ }
164
+ function readCount(value) {
165
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
166
+ }
167
+ function evaluateWorkerResult(kind, payload) {
168
+ if (!payload || typeof payload !== "object") {
169
+ return {
170
+ summary: {
171
+ passed: 0,
172
+ failed: 1,
173
+ skipped: 0,
174
+ },
175
+ parsedResult: null,
176
+ };
177
+ }
178
+ const record = payload;
179
+ if (kind === "test-all") {
180
+ const totals = (record.totals ?? {});
181
+ return {
182
+ summary: {
183
+ items: typeof totals.items === "number" ? totals.items : undefined,
184
+ linked_tests: typeof totals.linked_tests === "number" ? totals.linked_tests : undefined,
185
+ passed: readCount(totals.passed),
186
+ failed: readCount(totals.failed),
187
+ skipped: readCount(totals.skipped),
188
+ fail_on_skipped_triggered: record.fail_on_skipped_triggered === true ? true : undefined,
189
+ },
190
+ parsedResult: payload,
191
+ };
192
+ }
193
+ const runResults = Array.isArray(record.run_results) ? record.run_results : [];
194
+ let passed = 0;
195
+ let failed = 0;
196
+ let skipped = 0;
197
+ for (const entry of runResults) {
198
+ if (!entry || typeof entry !== "object") {
199
+ continue;
200
+ }
201
+ const status = entry.status;
202
+ if (status === "passed") {
203
+ passed += 1;
204
+ continue;
205
+ }
206
+ if (status === "failed") {
207
+ failed += 1;
208
+ continue;
209
+ }
210
+ skipped += 1;
211
+ }
212
+ return {
213
+ summary: {
214
+ passed,
215
+ failed,
216
+ skipped,
217
+ fail_on_skipped_triggered: record.fail_on_skipped_triggered === true ? true : undefined,
218
+ },
219
+ parsedResult: payload,
220
+ };
221
+ }
222
+ function parseProgressLine(stderrLine) {
223
+ const line = stderrLine.trim();
224
+ if (line.length === 0) {
225
+ return null;
226
+ }
227
+ const match = line.match(/\[pm test\]\s+linked-test\s+(\d+)\/(\d+)\s+(start|running|end)(?:.*elapsed_ms=(\d+))?/i);
228
+ if (!match) {
229
+ return null;
230
+ }
231
+ const index = Number.parseInt(match[1] ?? "", 10);
232
+ const total = Number.parseInt(match[2] ?? "", 10);
233
+ const elapsed = match[4] ? Number.parseInt(match[4], 10) : undefined;
234
+ return {
235
+ linked_test_index: Number.isFinite(index) ? index : undefined,
236
+ linked_test_total: Number.isFinite(total) ? total : undefined,
237
+ elapsed_ms: Number.isFinite(elapsed) ? elapsed : undefined,
238
+ heartbeat_at: nowIso(),
239
+ phase: match[3]?.toLowerCase() === "end" ? "finished" : "running",
240
+ };
241
+ }
242
+ function splitLines(value) {
243
+ return value
244
+ .split(/\r?\n/)
245
+ .map((entry) => entry.trimEnd())
246
+ .filter((entry) => entry.length > 0);
247
+ }
248
+ function tailLines(value, limit) {
249
+ if (limit <= 0) {
250
+ return [];
251
+ }
252
+ const lines = splitLines(value);
253
+ if (lines.length <= limit) {
254
+ return lines;
255
+ }
256
+ return lines.slice(lines.length - limit);
257
+ }
258
+ async function resolveBackgroundCliEntry(cwd) {
259
+ const configured = process.env.PM_BACKGROUND_CLI_ENTRY?.trim();
260
+ if (configured && configured.length > 0) {
261
+ const resolvedConfigured = path.resolve(cwd, configured);
262
+ if (await pathExists(resolvedConfigured)) {
263
+ return resolvedConfigured;
264
+ }
265
+ }
266
+ const distEntry = path.resolve(cwd, "dist", "cli.js");
267
+ if (await pathExists(distEntry)) {
268
+ return distEntry;
269
+ }
270
+ const argvEntry = process.argv[1]?.trim();
271
+ if (argvEntry && argvEntry.length > 0) {
272
+ const resolvedArgvEntry = path.resolve(cwd, argvEntry);
273
+ if (await pathExists(resolvedArgvEntry)) {
274
+ return resolvedArgvEntry;
275
+ }
276
+ }
277
+ throw new PmCliError("Unable to resolve a CLI entrypoint for background test runs. Build the project or set PM_BACKGROUND_CLI_ENTRY.", EXIT_CODE.GENERIC_FAILURE);
278
+ }
279
+ async function refreshRunIfStale(pmRoot, record) {
280
+ if (!BACKGROUND_RUN_ACTIVE_STATUSES.has(record.status)) {
281
+ return record;
282
+ }
283
+ const workerAlive = isPidRunning(record.worker_pid);
284
+ if (workerAlive) {
285
+ return record;
286
+ }
287
+ if (record.finished_at) {
288
+ return record;
289
+ }
290
+ const next = {
291
+ ...record,
292
+ status: "failed",
293
+ finished_at: nowIso(),
294
+ error: record.error ?? "Background test run worker exited before writing terminal status.",
295
+ };
296
+ await writeBackgroundRunRecord(pmRoot, next);
297
+ return next;
298
+ }
299
+ export async function startBackgroundTestRun(options) {
300
+ await ensureBackgroundRunStorage(options.pmRoot);
301
+ const normalizedArgs = normalizeCommandArgs(options.commandArgs);
302
+ if (normalizedArgs.length === 0) {
303
+ throw new PmCliError("Background test run requires command arguments.", EXIT_CODE.USAGE);
304
+ }
305
+ const fingerprint = buildBackgroundTestRunFingerprint(options.kind, normalizedArgs, options.pmRoot);
306
+ const existingRuns = await listBackgroundTestRuns(options.pmRoot, {});
307
+ for (const existing of existingRuns) {
308
+ const refreshed = await refreshRunIfStale(options.pmRoot, existing);
309
+ if (refreshed.fingerprint !== fingerprint) {
310
+ continue;
311
+ }
312
+ if (!BACKGROUND_RUN_ACTIVE_STATUSES.has(refreshed.status)) {
313
+ continue;
314
+ }
315
+ if (!isPidRunning(refreshed.worker_pid)) {
316
+ continue;
317
+ }
318
+ return {
319
+ started: false,
320
+ run: {
321
+ ...refreshed,
322
+ duplicate_of: refreshed.id,
323
+ },
324
+ duplicate_of: refreshed.id,
325
+ };
326
+ }
327
+ const runId = buildRunId();
328
+ const createdAt = nowIso();
329
+ const stdoutPath = getTestRunStdoutPath(options.pmRoot, runId);
330
+ const stderrPath = getTestRunStderrPath(options.pmRoot, runId);
331
+ const resultPath = getTestRunResultPath(options.pmRoot, runId);
332
+ await fs.writeFile(stdoutPath, "", "utf8");
333
+ await fs.writeFile(stderrPath, "", "utf8");
334
+ const record = {
335
+ id: runId,
336
+ kind: options.kind,
337
+ status: "queued",
338
+ created_at: createdAt,
339
+ updated_at: createdAt,
340
+ requested_by: options.requestedBy,
341
+ fingerprint,
342
+ command_args: normalizedArgs,
343
+ command_label: summarizeCommandLabel(normalizedArgs),
344
+ pm_root: options.pmRoot,
345
+ global_pm_root: options.globalPmRoot,
346
+ target_id: options.targetId,
347
+ status_filter: options.statusFilter,
348
+ attempt: typeof options.attempt === "number" && Number.isFinite(options.attempt) && options.attempt >= 1
349
+ ? Math.floor(options.attempt)
350
+ : 1,
351
+ resumed_from: options.resumedFrom,
352
+ resumed_by: options.resumedBy,
353
+ stdout_path: stdoutPath,
354
+ stderr_path: stderrPath,
355
+ result_path: resultPath,
356
+ progress: {
357
+ phase: "queued",
358
+ message: "Queued for background execution.",
359
+ heartbeat_at: createdAt,
360
+ },
361
+ };
362
+ await writeBackgroundRunRecord(options.pmRoot, record);
363
+ return {
364
+ started: true,
365
+ run: record,
366
+ };
367
+ }
368
+ export async function spawnBackgroundTestRunWorker(options) {
369
+ const record = await readBackgroundTestRunRecord(options.pmRoot, options.runId);
370
+ if (!record) {
371
+ throw new PmCliError(`Background test run ${options.runId} not found`, EXIT_CODE.NOT_FOUND);
372
+ }
373
+ if (BACKGROUND_RUN_TERMINAL_STATUSES.has(record.status)) {
374
+ throw new PmCliError(`Background test run ${record.id} is already terminal (${record.status}).`, EXIT_CODE.CONFLICT);
375
+ }
376
+ const cliEntry = await resolveBackgroundCliEntry(process.cwd());
377
+ const args = [];
378
+ if (options.noExtensions === true) {
379
+ args.push("--no-extensions");
380
+ }
381
+ args.push("--path", record.pm_root, "test-runs-worker", record.id);
382
+ const env = {
383
+ ...process.env,
384
+ PM_PATH: record.pm_root,
385
+ PM_GLOBAL_PATH: record.global_pm_root,
386
+ PM_BACKGROUND_TEST_RUN_ID: record.id,
387
+ PM_BACKGROUND_TEST_RUN_ATTEMPT: String(record.attempt),
388
+ PM_BACKGROUND_TEST_RUN_RESUMED_FROM: record.resumed_from ?? "",
389
+ FORCE_COLOR: "0",
390
+ };
391
+ const child = spawn(process.execPath, [cliEntry, ...args], {
392
+ cwd: process.cwd(),
393
+ env,
394
+ detached: true,
395
+ stdio: "ignore",
396
+ windowsHide: true,
397
+ });
398
+ child.unref();
399
+ const next = {
400
+ ...record,
401
+ status: "queued",
402
+ worker_pid: child.pid && child.pid > 0 ? child.pid : record.worker_pid,
403
+ progress: {
404
+ phase: "queued",
405
+ message: "Worker process started.",
406
+ heartbeat_at: nowIso(),
407
+ },
408
+ };
409
+ await writeBackgroundRunRecord(options.pmRoot, next);
410
+ return next;
411
+ }
412
+ async function appendFileOrdered(queue, filePath, text) {
413
+ await queue;
414
+ await fs.appendFile(filePath, text, "utf8");
415
+ }
416
+ export async function runBackgroundTestRunWorker(pmRoot, runId, noExtensions = false) {
417
+ const loaded = await readBackgroundTestRunRecord(pmRoot, runId);
418
+ if (!loaded) {
419
+ throw new PmCliError(`Background test run ${runId} not found`, EXIT_CODE.NOT_FOUND);
420
+ }
421
+ const record = {
422
+ ...loaded,
423
+ status: "running",
424
+ started_at: loaded.started_at ?? nowIso(),
425
+ worker_pid: process.pid,
426
+ progress: {
427
+ phase: "running",
428
+ message: "Worker started.",
429
+ heartbeat_at: nowIso(),
430
+ },
431
+ };
432
+ const cliEntry = await resolveBackgroundCliEntry(process.cwd());
433
+ const env = {
434
+ ...process.env,
435
+ PM_PATH: record.pm_root,
436
+ PM_GLOBAL_PATH: record.global_pm_root,
437
+ FORCE_COLOR: "0",
438
+ };
439
+ await writeBackgroundRunRecord(pmRoot, record);
440
+ const childArgs = noExtensions ? ["--no-extensions", ...record.command_args] : [...record.command_args];
441
+ const child = spawn(process.execPath, [cliEntry, ...childArgs], {
442
+ cwd: process.cwd(),
443
+ env,
444
+ detached: false,
445
+ stdio: ["ignore", "pipe", "pipe"],
446
+ windowsHide: true,
447
+ });
448
+ record.child_pid = child.pid && child.pid > 0 ? child.pid : undefined;
449
+ let writeQueue = Promise.resolve();
450
+ const scheduleRecordWrite = () => {
451
+ writeQueue = writeQueue.then(async () => {
452
+ await writeBackgroundRunRecord(pmRoot, record);
453
+ }).catch(() => {
454
+ // Keep worker alive even if a single metadata write fails.
455
+ });
456
+ };
457
+ let stdoutWriteQueue = Promise.resolve();
458
+ let stderrWriteQueue = Promise.resolve();
459
+ let stdoutBuffer = "";
460
+ let stopRequested = false;
461
+ const requestStop = async () => {
462
+ if (stopRequested) {
463
+ return;
464
+ }
465
+ stopRequested = true;
466
+ record.stop_requested_at = nowIso();
467
+ record.progress = {
468
+ ...record.progress,
469
+ phase: "stopping",
470
+ message: "Stop requested for background run.",
471
+ heartbeat_at: nowIso(),
472
+ };
473
+ scheduleRecordWrite();
474
+ if (!child.pid || child.pid <= 0) {
475
+ return;
476
+ }
477
+ try {
478
+ child.kill("SIGTERM");
479
+ }
480
+ catch {
481
+ // Child may already be gone.
482
+ }
483
+ const forceKillDelayMs = Number.parseInt(process.env.PM_BACKGROUND_RUN_FORCE_KILL_DELAY_MS ?? "", 10);
484
+ const delay = Number.isFinite(forceKillDelayMs) && forceKillDelayMs > 0
485
+ ? forceKillDelayMs
486
+ : DEFAULT_BACKGROUND_RUN_FORCE_KILL_DELAY_MS;
487
+ const forceTimer = setTimeout(() => {
488
+ try {
489
+ child.kill("SIGKILL");
490
+ }
491
+ catch {
492
+ // Child may already be gone.
493
+ }
494
+ }, delay);
495
+ forceTimer.unref?.();
496
+ };
497
+ const onSignal = () => {
498
+ void requestStop();
499
+ };
500
+ process.on("SIGTERM", onSignal);
501
+ process.on("SIGINT", onSignal);
502
+ const resourceIntervalMs = Number.parseInt(process.env.PM_BACKGROUND_RUN_RESOURCE_INTERVAL_MS ?? "", 10);
503
+ const resourceInterval = Number.isFinite(resourceIntervalMs) && resourceIntervalMs > 0
504
+ ? resourceIntervalMs
505
+ : DEFAULT_BACKGROUND_RUN_RESOURCE_SNAPSHOT_INTERVAL_MS;
506
+ const resourceTimer = setInterval(() => {
507
+ void (async () => {
508
+ record.resource = await buildResourceSnapshot(record);
509
+ if (record.progress) {
510
+ record.progress.heartbeat_at = nowIso();
511
+ }
512
+ scheduleRecordWrite();
513
+ })();
514
+ }, resourceInterval);
515
+ resourceTimer.unref?.();
516
+ child.stdout?.on("data", (chunk) => {
517
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
518
+ stdoutBuffer += text;
519
+ stdoutWriteQueue = appendFileOrdered(stdoutWriteQueue, record.stdout_path, text);
520
+ });
521
+ child.stderr?.on("data", (chunk) => {
522
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
523
+ stderrWriteQueue = appendFileOrdered(stderrWriteQueue, record.stderr_path, text);
524
+ const lines = splitLines(text);
525
+ const latestLine = lines.at(-1);
526
+ if (latestLine) {
527
+ const progressPatch = parseProgressLine(latestLine);
528
+ record.progress = {
529
+ phase: record.progress?.phase ?? "running",
530
+ message: latestLine,
531
+ heartbeat_at: nowIso(),
532
+ linked_test_index: progressPatch?.linked_test_index ?? record.progress?.linked_test_index,
533
+ linked_test_total: progressPatch?.linked_test_total ?? record.progress?.linked_test_total,
534
+ elapsed_ms: progressPatch?.elapsed_ms ?? record.progress?.elapsed_ms,
535
+ };
536
+ if (progressPatch?.phase === "finished" && !stopRequested) {
537
+ record.progress.phase = "running";
538
+ }
539
+ scheduleRecordWrite();
540
+ }
541
+ });
542
+ let exitCode = null;
543
+ let signal = null;
544
+ try {
545
+ ({ code: exitCode, signal } = await new Promise((resolve) => {
546
+ child.on("close", (code, closeSignal) => {
547
+ resolve({ code, signal: closeSignal });
548
+ });
549
+ }));
550
+ }
551
+ finally {
552
+ clearInterval(resourceTimer);
553
+ process.off("SIGTERM", onSignal);
554
+ process.off("SIGINT", onSignal);
555
+ }
556
+ await stdoutWriteQueue;
557
+ await stderrWriteQueue;
558
+ let parsedResult = null;
559
+ if (stdoutBuffer.trim().length > 0) {
560
+ try {
561
+ parsedResult = JSON.parse(stdoutBuffer);
562
+ }
563
+ catch {
564
+ parsedResult = null;
565
+ }
566
+ }
567
+ const evaluated = evaluateWorkerResult(record.kind, parsedResult);
568
+ record.summary = evaluated.summary;
569
+ record.exit_code = typeof exitCode === "number" ? exitCode : undefined;
570
+ record.signal = signal ?? undefined;
571
+ record.finished_at = nowIso();
572
+ record.status = stopRequested
573
+ ? "stopped"
574
+ : (record.exit_code === 0 &&
575
+ (record.summary.failed ?? 0) === 0 &&
576
+ record.summary.fail_on_skipped_triggered !== true)
577
+ ? "passed"
578
+ : "failed";
579
+ record.progress = {
580
+ phase: "finished",
581
+ message: stopRequested ? "Background run stopped." : `Background run finished with status=${record.status}.`,
582
+ heartbeat_at: nowIso(),
583
+ linked_test_index: record.progress?.linked_test_index,
584
+ linked_test_total: record.progress?.linked_test_total,
585
+ elapsed_ms: record.progress?.elapsed_ms,
586
+ };
587
+ record.resource = await buildResourceSnapshot(record);
588
+ if (parsedResult !== null) {
589
+ await writeFileAtomic(record.result_path, `${JSON.stringify(parsedResult, null, 2)}\n`);
590
+ }
591
+ else {
592
+ await writeFileAtomic(record.result_path, `${JSON.stringify({
593
+ parse_error: "Background run output was not valid JSON.",
594
+ stdout_excerpt: tailLines(stdoutBuffer, DEFAULT_BACKGROUND_RUN_LOG_TAIL_LINES),
595
+ }, null, 2)}\n`);
596
+ }
597
+ await writeBackgroundRunRecord(pmRoot, record);
598
+ await writeQueue;
599
+ return record;
600
+ }
601
+ export async function listBackgroundTestRuns(pmRoot, options) {
602
+ const recordPaths = await listBackgroundRunRecordPaths(pmRoot);
603
+ const runs = [];
604
+ for (const recordPath of recordPaths) {
605
+ const raw = await readFileIfExists(recordPath);
606
+ if (!raw) {
607
+ continue;
608
+ }
609
+ const parsed = await parseBackgroundRunRecord(raw, recordPath);
610
+ runs.push(parsed);
611
+ }
612
+ const refreshed = [];
613
+ for (const run of runs) {
614
+ refreshed.push(await refreshRunIfStale(pmRoot, run));
615
+ }
616
+ const filtered = options.status ? refreshed.filter((entry) => entry.status === options.status) : refreshed;
617
+ const sorted = filtered.sort((left, right) => {
618
+ const byUpdated = Date.parse(right.updated_at) - Date.parse(left.updated_at);
619
+ if (Number.isFinite(byUpdated) && byUpdated !== 0) {
620
+ return byUpdated;
621
+ }
622
+ return right.id.localeCompare(left.id);
623
+ });
624
+ const limit = typeof options.limit === "number" && options.limit >= 0 ? options.limit : undefined;
625
+ return limit === undefined ? sorted : sorted.slice(0, limit);
626
+ }
627
+ export async function getBackgroundTestRunStatus(pmRoot, runId) {
628
+ const loaded = await readBackgroundTestRunRecord(pmRoot, runId);
629
+ if (!loaded) {
630
+ throw new PmCliError(`Background test run ${runId} not found`, EXIT_CODE.NOT_FOUND);
631
+ }
632
+ const refreshed = await refreshRunIfStale(pmRoot, loaded);
633
+ const workerAlive = isPidRunning(refreshed.worker_pid);
634
+ const childAlive = isPidRunning(refreshed.child_pid);
635
+ if (refreshed.status === "running" && !childAlive && !refreshed.finished_at && !workerAlive) {
636
+ refreshed.status = "failed";
637
+ refreshed.finished_at = nowIso();
638
+ refreshed.error = refreshed.error ?? "Background run process exited unexpectedly.";
639
+ await writeBackgroundRunRecord(pmRoot, refreshed);
640
+ }
641
+ if (refreshed.status === "running") {
642
+ refreshed.resource = await buildResourceSnapshot(refreshed);
643
+ await writeBackgroundRunRecord(pmRoot, refreshed);
644
+ }
645
+ const heartbeatAt = refreshed.progress?.heartbeat_at;
646
+ const heartbeatAtMs = heartbeatAt ? Date.parse(heartbeatAt) : Number.NaN;
647
+ const lagMs = Number.isFinite(heartbeatAtMs) ? Math.max(0, nowMs() - heartbeatAtMs) : undefined;
648
+ const staleMs = Number.parseInt(process.env.PM_BACKGROUND_RUN_HEARTBEAT_STALE_MS ?? "", 10);
649
+ const staleThresholdMs = Number.isFinite(staleMs) && staleMs > 0 ? staleMs : DEFAULT_BACKGROUND_RUN_HEARTBEAT_STALE_MS;
650
+ const healthState = refreshed.status === "running"
651
+ ? lagMs !== undefined && lagMs > staleThresholdMs
652
+ ? "stale"
653
+ : "healthy"
654
+ : "inactive";
655
+ return {
656
+ run: refreshed,
657
+ health: {
658
+ state: healthState,
659
+ last_heartbeat_at: heartbeatAt,
660
+ heartbeat_lag_ms: lagMs,
661
+ worker_alive: workerAlive,
662
+ child_alive: childAlive,
663
+ },
664
+ };
665
+ }
666
+ export async function stopBackgroundTestRun(pmRoot, runId, force = false) {
667
+ const loaded = await readBackgroundTestRunRecord(pmRoot, runId);
668
+ if (!loaded) {
669
+ throw new PmCliError(`Background test run ${runId} not found`, EXIT_CODE.NOT_FOUND);
670
+ }
671
+ const refreshed = await refreshRunIfStale(pmRoot, loaded);
672
+ if (BACKGROUND_RUN_TERMINAL_STATUSES.has(refreshed.status)) {
673
+ return {
674
+ run: refreshed,
675
+ signal_sent: "none",
676
+ };
677
+ }
678
+ let signalSent = "none";
679
+ const signal = force ? "SIGKILL" : "SIGTERM";
680
+ if (isPidRunning(refreshed.worker_pid) && refreshed.worker_pid) {
681
+ try {
682
+ process.kill(refreshed.worker_pid, signal);
683
+ signalSent = signal;
684
+ }
685
+ catch {
686
+ signalSent = "none";
687
+ }
688
+ }
689
+ if (signalSent === "none") {
690
+ refreshed.status = "stopped";
691
+ refreshed.finished_at = refreshed.finished_at ?? nowIso();
692
+ }
693
+ refreshed.stop_requested_at = nowIso();
694
+ refreshed.progress = {
695
+ phase: "stopping",
696
+ message: signalSent === "none" ? "Run marked stopped." : `Stop requested via ${signalSent}.`,
697
+ heartbeat_at: nowIso(),
698
+ linked_test_index: refreshed.progress?.linked_test_index,
699
+ linked_test_total: refreshed.progress?.linked_test_total,
700
+ elapsed_ms: refreshed.progress?.elapsed_ms,
701
+ };
702
+ await writeBackgroundRunRecord(pmRoot, refreshed);
703
+ return {
704
+ run: refreshed,
705
+ signal_sent: signalSent,
706
+ };
707
+ }
708
+ export async function resumeBackgroundTestRun(pmRoot, runId, requestedBy, noExtensions = false) {
709
+ const loaded = await readBackgroundTestRunRecord(pmRoot, runId);
710
+ if (!loaded) {
711
+ throw new PmCliError(`Background test run ${runId} not found`, EXIT_CODE.NOT_FOUND);
712
+ }
713
+ const refreshed = await refreshRunIfStale(pmRoot, loaded);
714
+ if (!BACKGROUND_RUN_TERMINAL_STATUSES.has(refreshed.status)) {
715
+ throw new PmCliError(`Background test run ${runId} is not terminal and cannot be resumed.`, EXIT_CODE.CONFLICT);
716
+ }
717
+ const started = await startBackgroundTestRun({
718
+ pmRoot: refreshed.pm_root,
719
+ globalPmRoot: refreshed.global_pm_root,
720
+ kind: refreshed.kind,
721
+ commandArgs: refreshed.command_args,
722
+ requestedBy,
723
+ targetId: refreshed.target_id,
724
+ statusFilter: refreshed.status_filter,
725
+ resumedFrom: refreshed.id,
726
+ resumedBy: requestedBy,
727
+ attempt: refreshed.attempt + 1,
728
+ });
729
+ if (!started.started) {
730
+ return started.run;
731
+ }
732
+ const spawned = await spawnBackgroundTestRunWorker({
733
+ pmRoot,
734
+ runId: started.run.id,
735
+ noExtensions,
736
+ });
737
+ const prior = {
738
+ ...refreshed,
739
+ resumed_by: spawned.id,
740
+ };
741
+ await writeBackgroundRunRecord(pmRoot, prior);
742
+ return spawned;
743
+ }
744
+ export async function readBackgroundTestRunLogs(pmRoot, runId, stream, tail) {
745
+ const loaded = await readBackgroundTestRunRecord(pmRoot, runId);
746
+ if (!loaded) {
747
+ throw new PmCliError(`Background test run ${runId} not found`, EXIT_CODE.NOT_FOUND);
748
+ }
749
+ const resolvedTail = typeof tail === "number" && Number.isFinite(tail) && tail >= 0 ? Math.floor(tail) : DEFAULT_BACKGROUND_RUN_LOG_TAIL_LINES;
750
+ const stdoutRaw = stream === "stdout" || stream === "both" ? (await readFileIfExists(loaded.stdout_path)) ?? "" : "";
751
+ const stderrRaw = stream === "stderr" || stream === "both" ? (await readFileIfExists(loaded.stderr_path)) ?? "" : "";
752
+ return {
753
+ run: loaded,
754
+ stream,
755
+ tail: resolvedTail,
756
+ stdout: stream === "stdout" || stream === "both" ? tailLines(stdoutRaw, resolvedTail) : [],
757
+ stderr: stream === "stderr" || stream === "both" ? tailLines(stderrRaw, resolvedTail) : [],
758
+ };
759
+ }
760
+ //# sourceMappingURL=background-runs.js.map