@yemi33/minions 0.1.2424 → 0.1.2425

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 (182) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +6 -4
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/internal-install.md +190 -0
  55. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  56. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  57. package/docs/kb-sweep.md +2 -2
  58. package/docs/keep-processes.md +8 -1
  59. package/docs/live-checkout-mode.md +1 -1
  60. package/docs/managed-spawn.md +21 -3
  61. package/docs/named-agents.md +3 -2
  62. package/docs/plan-lifecycle.md +32 -32
  63. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  64. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  65. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  69. package/docs/qa-runbook-lifecycle.md +67 -51
  70. package/docs/qa-runbooks.md +2 -2
  71. package/docs/runtime-adapters.md +101 -50
  72. package/docs/security.md +28 -0
  73. package/docs/self-improvement.md +47 -13
  74. package/docs/skills.md +29 -0
  75. package/docs/slim-ux/concepts.md +2 -2
  76. package/docs/specs/agent-configurability.md +43 -41
  77. package/docs/specs/agent-rename.md +13 -13
  78. package/docs/team-memory.md +283 -14
  79. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  80. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  81. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  82. package/docs/tutorials/README.md +4 -3
  83. package/docs/visual-evidence-ci.md +103 -0
  84. package/docs/watches.md +35 -30
  85. package/docs/worktree-lifecycle.md +151 -13
  86. package/engine/acp-transport.js +1 -0
  87. package/engine/ado.js +80 -33
  88. package/engine/agent-api-validation.js +571 -0
  89. package/engine/agent-worker-pool.js +7 -6
  90. package/engine/api-contracts/agent-content.js +761 -0
  91. package/engine/api-contracts/cc-ops.js +219 -0
  92. package/engine/api-contracts/config-runtime.js +419 -0
  93. package/engine/api-contracts/core.js +138 -0
  94. package/engine/api-contracts/index.js +506 -0
  95. package/engine/api-contracts/orchestration.js +602 -0
  96. package/engine/api-contracts/pull-requests.js +323 -0
  97. package/engine/api-contracts/qa-process.js +764 -0
  98. package/engine/api-contracts/work-plan-prd.js +494 -0
  99. package/engine/api-validation.js +543 -0
  100. package/engine/cc-api-validation.js +586 -0
  101. package/engine/cc-worker-pool.js +9 -1
  102. package/engine/cleanup.js +393 -271
  103. package/engine/cli.js +65 -7
  104. package/engine/comment-classifier.js +70 -5
  105. package/engine/consolidation.js +123 -5
  106. package/engine/content-api-validation.js +606 -0
  107. package/engine/create-pr-worktree.js +22 -1
  108. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  109. package/engine/dispatch.js +33 -0
  110. package/engine/distribution.js +189 -0
  111. package/engine/execution-model.js +59 -0
  112. package/engine/features.js +12 -0
  113. package/engine/github.js +67 -25
  114. package/engine/harness-context.js +313 -0
  115. package/engine/inbox-store.js +1 -1
  116. package/engine/kb-sweep.js +161 -38
  117. package/engine/keep-process-sweep.js +43 -3
  118. package/engine/lifecycle.js +1066 -165
  119. package/engine/llm.js +4 -0
  120. package/engine/managed-spawn.js +463 -48
  121. package/engine/meeting.js +197 -34
  122. package/engine/memory-retrieval.js +280 -0
  123. package/engine/memory-store.js +821 -38
  124. package/engine/model-discovery.js +17 -3
  125. package/engine/pipeline.js +802 -14
  126. package/engine/plan-prd-validation.js +745 -0
  127. package/engine/playbook.js +206 -17
  128. package/engine/pooled-agent-process.js +6 -1
  129. package/engine/pr-action.js +1 -1
  130. package/engine/pr-issue-validation.js +653 -0
  131. package/engine/pr-resolve.js +75 -13
  132. package/engine/prd-store.js +39 -6
  133. package/engine/preflight.js +7 -2
  134. package/engine/process-utils.js +79 -31
  135. package/engine/projects.js +198 -12
  136. package/engine/promotion.js +371 -0
  137. package/engine/qa-from-prd.js +3 -0
  138. package/engine/qa-process-validation.js +572 -0
  139. package/engine/qa-runbooks.js +41 -37
  140. package/engine/qa-runners/maestro.js +11 -7
  141. package/engine/qa-runners/playwright.js +37 -14
  142. package/engine/qa-runners.js +9 -10
  143. package/engine/qa-runs.js +124 -11
  144. package/engine/qa-sessions.js +139 -40
  145. package/engine/quarantine-refs.js +81 -11
  146. package/engine/queries.js +163 -9
  147. package/engine/review-learning-backfill.js +482 -0
  148. package/engine/review-learning.js +1236 -0
  149. package/engine/runtimes/claude.js +60 -2
  150. package/engine/runtimes/codex.js +57 -16
  151. package/engine/runtimes/contract.js +21 -0
  152. package/engine/runtimes/copilot.js +51 -3
  153. package/engine/runtimes/index.js +1 -0
  154. package/engine/scheduler.js +201 -32
  155. package/engine/settings-validation.js +909 -0
  156. package/engine/shared.js +705 -208
  157. package/engine/spawn-agent.js +48 -21
  158. package/engine/steering-constraints.js +31 -0
  159. package/engine/steering-store.js +10 -2
  160. package/engine/steering.js +14 -2
  161. package/engine/timeout.js +404 -111
  162. package/engine/untrusted-fence.js +24 -10
  163. package/engine/watch-actions.js +7 -2
  164. package/engine/watches.js +353 -51
  165. package/engine/work-item-validation.js +561 -0
  166. package/engine/work-items-store.js +61 -1
  167. package/engine/worktree-gc.js +656 -37
  168. package/engine/worktree-preflight.js +154 -0
  169. package/engine.js +2068 -342
  170. package/package.json +1 -1
  171. package/playbooks/_pr-description-audit.md +88 -41
  172. package/playbooks/build-fix-complex.md +3 -3
  173. package/playbooks/docs.md +2 -2
  174. package/playbooks/fix.md +18 -18
  175. package/playbooks/implement.md +18 -18
  176. package/playbooks/qa-session-draft.md +10 -12
  177. package/playbooks/qa-session-execute.md +16 -12
  178. package/playbooks/qa-validate.md +8 -7
  179. package/playbooks/shared-rules.md +45 -0
  180. package/playbooks/test.md +4 -0
  181. package/prompts/cc-system.md +1 -1
  182. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -8,8 +8,8 @@
8
8
  * Two responsibilities:
9
9
  * 1. `normalizePrRef(input)` — accept any GitHub or ADO PR URL OR the canonical
10
10
  * `github:owner/repo#N` / `ado:org/proj/repo#N` id and return a normalized
11
- * `{ host, slug, repo, number, ... }` struct. Reuses the existing parsers in
12
- * engine/shared.js (`parsePrUrl`, `parseCanonicalPrId`).
11
+ * `{ host, slug, repo, number, ... }` struct. URL forms are parsed from the
12
+ * validated hostname/path; canonical ids reuse `shared.parseCanonicalPrId`.
13
13
  * 2. `fetchPrPayload(refOrInput)` — fetch `{ diff, title, body, comments, author }`
14
14
  * for that PR WITHOUT a configured project or clone, via `gh` (GitHub) and the
15
15
  * ADO REST API (`_apis/git/...`).
@@ -38,6 +38,38 @@ const ADO_API_VERSION = '7.1';
38
38
 
39
39
  // ── Normalization ───────────────────────────────────────────────────────────
40
40
 
41
+ function _parsePrUrl(parsedUrl) {
42
+ const hostname = parsedUrl.hostname.toLowerCase();
43
+ let match;
44
+ if (hostname === 'github.com') {
45
+ match = parsedUrl.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/([0-9]+)(?:\/|$)/i);
46
+ if (!match) return null;
47
+ return {
48
+ scope: `github:${decodeURIComponent(match[1])}/${decodeURIComponent(match[2])}`,
49
+ prNumber: match[3],
50
+ };
51
+ }
52
+ if (hostname === 'dev.azure.com') {
53
+ match = parsedUrl.pathname.match(/^\/([^/]+)\/([^/]+)\/_git\/([^/]+)\/pullrequest\/([0-9]+)(?:\/|$)/i);
54
+ if (!match) return null;
55
+ return {
56
+ scope: `ado:${decodeURIComponent(match[1])}/${decodeURIComponent(match[2])}/${decodeURIComponent(match[3])}`,
57
+ prNumber: match[4],
58
+ };
59
+ }
60
+ if (hostname.endsWith('.visualstudio.com')) {
61
+ const org = hostname.slice(0, -'.visualstudio.com'.length);
62
+ if (!org || org.includes('.')) return null;
63
+ match = parsedUrl.pathname.match(/^\/(?:DefaultCollection\/)?([^/]+)\/_git\/([^/]+)\/pullrequest\/([0-9]+)(?:\/|$)/i);
64
+ if (!match) return null;
65
+ return {
66
+ scope: `ado:${org}/${decodeURIComponent(match[1])}/${decodeURIComponent(match[2])}`,
67
+ prNumber: match[3],
68
+ };
69
+ }
70
+ return null;
71
+ }
72
+
41
73
  /**
42
74
  * Turn a `{ scope, prNumber }` pair (as returned by shared.parsePrUrl /
43
75
  * parseCanonicalPrId) into a normalized projectless PR ref. Returns null when the
@@ -45,21 +77,37 @@ const ADO_API_VERSION = '7.1';
45
77
  */
46
78
  function _normalizeFromScope(scope, number) {
47
79
  if (!scope || typeof scope !== 'string') return null;
48
- const num = Number(number);
49
- if (!Number.isInteger(num) || num <= 0) return null;
80
+ let num;
81
+ try { num = shared.validatePrNum(number); }
82
+ catch { return null; }
50
83
 
51
84
  const sepIdx = scope.indexOf(':');
52
85
  if (sepIdx <= 0) return null;
53
86
  const host = scope.slice(0, sepIdx).toLowerCase();
54
87
  const slug = scope.slice(sepIdx + 1);
55
- const segs = slug.split('/').filter(Boolean);
88
+ const expectedSegments = { github: 2, ado: 3 }[host] || 0;
89
+ const rawSegments = slug.split('/');
90
+ if (!expectedSegments || rawSegments.length !== expectedSegments || rawSegments.some(segment => !segment.trim())) {
91
+ return null;
92
+ }
93
+ let normalizedScope;
94
+ try {
95
+ [normalizedScope] = shared.validateAdditionalRepoScopes([`${host}:${slug}`]) || [];
96
+ } catch {
97
+ return null;
98
+ }
99
+ if (!normalizedScope) return null;
100
+ const normalizedSlug = normalizedScope.slice(host.length + 1);
101
+ const segs = normalizedSlug.split('/');
102
+ if (segs.some(segment => segment === '.' || segment === '..')) return null;
56
103
 
57
104
  if (host === 'github') {
58
- if (segs.length !== 2) return null;
59
105
  const [owner, repo] = segs;
106
+ try { shared.validateGhSlug(`${owner}/${repo}`); }
107
+ catch { return null; }
60
108
  return {
61
109
  host: 'github',
62
- scope,
110
+ scope: normalizedScope,
63
111
  slug: `${owner}/${repo}`,
64
112
  owner,
65
113
  repo,
@@ -68,11 +116,13 @@ function _normalizeFromScope(scope, number) {
68
116
  };
69
117
  }
70
118
  if (host === 'ado') {
71
- if (segs.length !== 3) return null;
119
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(segs[0])
120
+ || segs.some(segment => segment.length > 128
121
+ || /[\u0000-\u001f\u007f#?\\]/.test(segment))) return null;
72
122
  const [org, project, repo] = segs;
73
123
  return {
74
124
  host: 'ado',
75
- scope,
125
+ scope: normalizedScope,
76
126
  slug: `${org}/${project}/${repo}`,
77
127
  org,
78
128
  project,
@@ -95,9 +145,21 @@ function normalizePrRef(input) {
95
145
  return _normalizeFromScope(input.scope || `${input.host}:${input.slug}`, input.number);
96
146
  }
97
147
  const text = String(input == null ? '' : input).trim();
98
- if (!text) return null;
99
- // URL forms first, then the canonical `host:scope#N` id form.
100
- const parsed = shared.parsePrUrl(text) || shared.parseCanonicalPrId(text);
148
+ if (!text || text.length > 2048) return null;
149
+ let parsed = null;
150
+ try {
151
+ if (/^https?:\/\//i.test(text)) {
152
+ const parsedUrl = new URL(text);
153
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)
154
+ || parsedUrl.username
155
+ || parsedUrl.password) return null;
156
+ parsed = _parsePrUrl(parsedUrl);
157
+ } else {
158
+ parsed = shared.parseCanonicalPrId(text);
159
+ }
160
+ } catch {
161
+ return null;
162
+ }
101
163
  if (!parsed) return null;
102
164
  return _normalizeFromScope(parsed.scope, parsed.prNumber);
103
165
  }
@@ -125,7 +187,7 @@ function resolvePrActionRef(input, PrActionError) {
125
187
  : (typeof input === 'string' ? input.trim() : '');
126
188
  if (!rawUrl) throw new PrActionError('url required');
127
189
  const ref = normalizePrRef(rawUrl);
128
- if (!ref) throw new PrActionError(`unrecognized PR reference: ${JSON.stringify(rawUrl.slice(0, 120))}`);
190
+ if (!ref) throw new PrActionError('unrecognized PR reference');
129
191
  return ref;
130
192
  }
131
193
 
@@ -10,7 +10,7 @@ function _toMs(v) {
10
10
  }
11
11
  function _bool01(v) { return v ? 1 : 0; }
12
12
 
13
- function _validatePrdFilename(filename) {
13
+ function validatePrdFilename(filename) {
14
14
  if (typeof filename !== 'string' || !filename.endsWith('.json')
15
15
  || filename.includes('/') || filename.includes('\\') || filename === '.json') {
16
16
  throw new TypeError('PRD filename must be a basename ending in .json');
@@ -187,7 +187,7 @@ function _upsertPrd(db, filename, archived, prd, now) {
187
187
  }
188
188
 
189
189
  function writePrd(filename, prd, { archived = false } = {}) {
190
- _validatePrdFilename(filename);
190
+ validatePrdFilename(filename);
191
191
  if (!prd || typeof prd !== 'object' || Array.isArray(prd)) {
192
192
  throw new TypeError('prd-store.writePrd requires a filename and object');
193
193
  }
@@ -201,7 +201,7 @@ function writePrd(filename, prd, { archived = false } = {}) {
201
201
  }
202
202
 
203
203
  function createPrdUnique(baseFilename, prd) {
204
- _validatePrdFilename(baseFilename);
204
+ validatePrdFilename(baseFilename);
205
205
  if (!prd || typeof prd !== 'object' || Array.isArray(prd)) {
206
206
  throw new TypeError('prd-store.createPrdUnique requires a PRD object');
207
207
  }
@@ -224,7 +224,7 @@ function createPrdUnique(baseFilename, prd) {
224
224
  }
225
225
 
226
226
  function mutatePrd(filename, mutator, { archived = false, defaultValue = {} } = {}) {
227
- _validatePrdFilename(filename);
227
+ validatePrdFilename(filename);
228
228
  if (typeof mutator !== 'function') {
229
229
  throw new TypeError('prd-store.mutatePrd requires a filename and mutator');
230
230
  }
@@ -249,7 +249,7 @@ function mutatePrd(filename, mutator, { archived = false, defaultValue = {} } =
249
249
  }
250
250
 
251
251
  function deletePrd(filename, { archived = false } = {}) {
252
- _validatePrdFilename(filename);
252
+ validatePrdFilename(filename);
253
253
  const { getDb, withTransaction } = require('./db');
254
254
  const db = getDb();
255
255
  const removed = withTransaction(db, () => {
@@ -264,7 +264,7 @@ function deletePrd(filename, { archived = false } = {}) {
264
264
  }
265
265
 
266
266
  function readPrd(filename, { archived = false } = {}) {
267
- _validatePrdFilename(filename);
267
+ validatePrdFilename(filename);
268
268
  const row = require('./db').getDb()
269
269
  .prepare('SELECT data FROM prds WHERE filename=? AND archived=?')
270
270
  .get(filename, archived ? 1 : 0);
@@ -300,6 +300,37 @@ function restorePrdFromArchive(filename) {
300
300
  return restored;
301
301
  }
302
302
 
303
+ // Atomic inverse of restorePrdFromArchive, used only to compensate a failed
304
+ // legacy unarchive after its archived=1 row has already moved into the live
305
+ // bucket. Modern archives remain in place and do not call this helper.
306
+ function movePrdToArchive(filename, replacementPrd) {
307
+ validatePrdFilename(filename);
308
+ if (replacementPrd !== undefined
309
+ && (!replacementPrd || typeof replacementPrd !== 'object' || Array.isArray(replacementPrd))) {
310
+ throw new TypeError('prd-store.movePrdToArchive replacement must be a PRD object');
311
+ }
312
+ const { getDb, withTransaction } = require('./db');
313
+ const db = getDb();
314
+ const archived = withTransaction(db, () => {
315
+ const row = db.prepare('SELECT id, data FROM prds WHERE filename=? AND archived=0').get(filename);
316
+ if (!row) return null;
317
+ const prd = replacementPrd === undefined
318
+ ? _normalizePrdForStorage(JSON.parse(row.data))
319
+ : _normalizePrdForStorage(structuredClone(replacementPrd));
320
+ _upsertPrd(db, filename, 1, prd, Date.now());
321
+ _deletePrdRow(db, row.id);
322
+ return prd;
323
+ });
324
+ if (archived) {
325
+ require('./db-events').emitStateEvent('prds', {
326
+ filename,
327
+ archived: 1,
328
+ compensated: true,
329
+ });
330
+ }
331
+ return archived;
332
+ }
333
+
303
334
  // Idempotent boot sweep for work items that predate PRD FK stamping.
304
335
  // This is set-based, only touches NULL FKs, and prefers live PRDs when a
305
336
  // basename exists in both live and archived buckets.
@@ -359,12 +390,14 @@ function listPrdRows() {
359
390
  }
360
391
 
361
392
  module.exports = {
393
+ validatePrdFilename,
362
394
  writePrd,
363
395
  createPrdUnique,
364
396
  mutatePrd,
365
397
  deletePrd,
366
398
  readPrd,
367
399
  restorePrdFromArchive,
400
+ movePrdToArchive,
368
401
  backfillWorkItemPrdItemIds,
369
402
  listPrdRows,
370
403
  _upsertPrd, // exported for tests
@@ -353,12 +353,17 @@ const _FEATURE_FLAG_DEFAULTS = {
353
353
  * 2. `CC overrides` — only when `ccCli` or `ccModel` is set
354
354
  * 3. `Active fleet flags` — only when any feature flag deviates from default
355
355
  */
356
- function _fleetSummaryResults(config) {
356
+ function _fleetSummaryResults(config, { dispatch } = {}) {
357
357
  const results = [];
358
358
  if (!config || typeof config !== 'object') return results;
359
359
  const engine = config.engine || {};
360
360
  const defaultCli = engine.defaultCli ? String(engine.defaultCli) : 'copilot';
361
- const defaultModel = engine.defaultModel ? String(engine.defaultModel) : '(runtime default)';
361
+ const display = require('./queries').resolveRuntimeModelDisplay({
362
+ runtime: defaultCli,
363
+ configuredModel: engine.defaultModel,
364
+ dispatch,
365
+ });
366
+ const defaultModel = display.model || '(runtime model not observed yet)';
362
367
  results.push({ name: 'Fleet', ok: true, message: `defaultCli=${defaultCli} defaultModel=${defaultModel}` });
363
368
 
364
369
  const ccBits = [];
@@ -24,12 +24,7 @@ const { execSync: _execSync, execFile: _execFile } = require('child_process');
24
24
  function isPidAlive(pid) {
25
25
  if (!Number.isFinite(pid) || pid <= 0) return false;
26
26
  try { process.kill(pid, 0); return true; }
27
- catch (err) {
28
- // Signal 0 returning EPERM means the process exists but this caller cannot
29
- // inspect it. Treating that as dead lets lock recovery delete a live
30
- // holder's lock and breaks cross-process mutual exclusion.
31
- return err && err.code === 'EPERM';
32
- }
27
+ catch { return false; }
33
28
  }
34
29
 
35
30
  // ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
@@ -52,12 +47,13 @@ function unixChildPids(pid) {
52
47
  }
53
48
 
54
49
  function killUnixProcessTree(pid, signal, seen = new Set()) {
55
- if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid)) return;
50
+ if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid)) return false;
56
51
  seen.add(pid);
57
52
  for (const childPid of unixChildPids(pid)) {
58
53
  killUnixProcessTree(childPid, signal, seen);
59
54
  }
60
- try { process.kill(pid, signal); } catch { /* process may be dead */ }
55
+ try { process.kill(pid, signal); return true; }
56
+ catch { return false; }
61
57
  }
62
58
 
63
59
  function unrefTimer(timer) {
@@ -83,12 +79,16 @@ function killGracefully(proc, graceMs = 5000) {
83
79
 
84
80
  function killImmediate(proc) {
85
81
  const pid = normalizeKillPid(proc);
86
- if (!pid) return;
82
+ if (!pid) return false;
87
83
  if (process.platform === 'win32') {
88
- try { _execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
89
- } else {
90
- killUnixProcessTree(pid, 'SIGKILL');
84
+ try {
85
+ _execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true });
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
91
90
  }
91
+ return killUnixProcessTree(pid, 'SIGKILL');
92
92
  }
93
93
 
94
94
  // Decide whether a Windows `tasklist /FI "PID eq <pid>" /NH` output proves the
@@ -289,7 +289,7 @@ function _winListProcesses() {
289
289
  if (ps && ps.length) return ps;
290
290
  try {
291
291
  const out = _execSync(
292
- 'wmic process get Name,ParentProcessId,ProcessId,CommandLine /format:csv',
292
+ 'wmic process get CreationDate,Name,ParentProcessId,ProcessId /format:csv',
293
293
  { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }
294
294
  );
295
295
  return _parseWmicCsv(out);
@@ -299,10 +299,15 @@ function _winListProcesses() {
299
299
  function _unixListProcesses() {
300
300
  try {
301
301
  const out = _execSync(
302
- 'ps -A -o pid=,ppid=,comm=',
303
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }
302
+ 'ps -A -o pid=,ppid=,lstart=,comm=',
303
+ {
304
+ encoding: 'utf8',
305
+ stdio: ['ignore', 'pipe', 'ignore'],
306
+ timeout: 5000,
307
+ env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
308
+ }
304
309
  );
305
- return _parseUnixProcesses(out);
310
+ return _parseUnixProcessesWithStart(out);
306
311
  } catch { return []; }
307
312
  }
308
313
 
@@ -345,6 +350,28 @@ function _parseUnixProcessesWithStart(out) {
345
350
  }).filter(Boolean);
346
351
  }
347
352
 
353
+ function getProcessIdentity(pid, opts = {}) {
354
+ const value = Number(pid);
355
+ if (!Number.isInteger(value) || value <= 0) return null;
356
+ const processes = Array.isArray(opts.processes) ? opts.processes : listAllProcesses();
357
+ const match = processes.find(entry => Number(entry && entry.pid) === value);
358
+ const startedAt = Number(match && match.startedAt);
359
+ return startedAt > 0 ? { pid: value, startedAt } : null;
360
+ }
361
+
362
+ function isProcessIdentityCompatible(identity, recordedAtMs, skewMs = 1000) {
363
+ return !!identity
364
+ && Number.isFinite(recordedAtMs)
365
+ && identity.startedAt <= recordedAtMs + skewMs;
366
+ }
367
+
368
+ function isSameProcessIdentity(left, right) {
369
+ return !!left
370
+ && !!right
371
+ && left.pid === right.pid
372
+ && left.startedAt === right.startedAt;
373
+ }
374
+
348
375
  async function _applyLinuxStartTicks(processes) {
349
376
  if (process.platform !== 'linux') return processes;
350
377
  await Promise.all(processes.map(async (entry) => {
@@ -408,12 +435,13 @@ async function listAllProcessesAsync() {
408
435
  }
409
436
 
410
437
  // W-mq6f2fe0000557fa — orphan-worktree GC: identify OS processes whose cwd
411
- // is at or inside `dir`. Cross-platform with a fail-open contract: on any
412
- // error or timeout we return [] (the caller continues with its existing
413
- // escalation path and an unenriched note).
438
+ // is at or inside `dir`. Cross-platform and fail-open by default; callers that
439
+ // gate deletion may pass `throwOnFailure:true` to fail closed instead.
414
440
  //
415
- // Return shape: [{ pid, cmdline, startedAt, ageMs }, ...]
441
+ // Return shape: [{ pid, cwd, cmdline, startedAt, ageMs }, ...]
416
442
  // - pid: numeric OS pid
443
+ // - cwd: matched current working directory when available (the legacy
444
+ // Windows command-line heuristic omits it)
417
445
  // - cmdline: full command line as a single string (best-effort; possibly truncated)
418
446
  // - startedAt: ms-since-epoch process start time (0 when unknown)
419
447
  // - ageMs: Date.now() - startedAt (0 when startedAt is 0)
@@ -451,10 +479,13 @@ function findProcessesWithCwdInside(dir, opts = {}) {
451
479
  }
452
480
 
453
481
  if (process.platform === 'linux') {
454
- return _findLinuxProcessesWithCwdInside(resolved, timeoutMs, now);
482
+ return _findLinuxProcessesWithCwdInside(resolved, timeoutMs, now, !!opts.throwOnFailure);
455
483
  }
456
- return _findMacProcessesWithCwdInside(resolved, timeoutMs, now);
457
- } catch { return []; }
484
+ return _findMacProcessesWithCwdInside(resolved, timeoutMs, now, !!opts.throwOnFailure);
485
+ } catch (err) {
486
+ if (opts.throwOnFailure) throw err;
487
+ return [];
488
+ }
458
489
  }
459
490
 
460
491
  async function findProcessesWithCwdInsideAsync(dir, opts = {}) {
@@ -529,11 +560,14 @@ function _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now) {
529
560
  return out;
530
561
  }
531
562
 
532
- function _findLinuxProcessesWithCwdInside(resolved, _timeoutMs, now) {
563
+ function _findLinuxProcessesWithCwdInside(resolved, _timeoutMs, now, throwOnFailure = false) {
533
564
  // /proc walk: scan numeric entries, readlink cwd, keep matches.
534
565
  let entries;
535
566
  try { entries = fs.readdirSync('/proc'); }
536
- catch { return []; }
567
+ catch (err) {
568
+ if (throwOnFailure) throw err;
569
+ return [];
570
+ }
537
571
  const prefix = resolved + path.sep;
538
572
  const out = [];
539
573
  for (const e of entries) {
@@ -556,6 +590,7 @@ function _findLinuxProcessesWithCwdInside(resolved, _timeoutMs, now) {
556
590
  } catch { /* stat can fail on race */ }
557
591
  out.push({
558
592
  pid,
593
+ cwd,
559
594
  cmdline,
560
595
  startedAt,
561
596
  ageMs: startedAt > 0 ? Math.max(0, now - startedAt) : 0,
@@ -564,7 +599,7 @@ function _findLinuxProcessesWithCwdInside(resolved, _timeoutMs, now) {
564
599
  return out;
565
600
  }
566
601
 
567
- function _findMacProcessesWithCwdInside(resolved, timeoutMs, now) {
602
+ function _findMacProcessesWithCwdInside(resolved, timeoutMs, now, throwOnFailure = false) {
568
603
  // `lsof -a -d cwd -F pn` emits PID then cwd path. Filter for dir matches,
569
604
  // then run a single `ps` pass to enrich cmdline + start time.
570
605
  let raw;
@@ -572,10 +607,15 @@ function _findMacProcessesWithCwdInside(resolved, timeoutMs, now) {
572
607
  raw = _execSync('lsof -a -d cwd -F pn 2>/dev/null', {
573
608
  encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024,
574
609
  });
575
- } catch { return []; }
610
+ } catch (err) {
611
+ // lsof exits 1 when no process matches; that is a successful empty probe.
612
+ if (err?.status === 1 && !String(err.stdout || '').trim()) return [];
613
+ if (throwOnFailure) throw err;
614
+ return [];
615
+ }
576
616
  if (!raw) return [];
577
617
  const prefix = resolved + path.sep;
578
- const pids = new Set();
618
+ const pids = new Map();
579
619
  let currentPid = null;
580
620
  for (const line of String(raw).split(/\r?\n/)) {
581
621
  if (!line) continue;
@@ -585,12 +625,12 @@ function _findMacProcessesWithCwdInside(resolved, timeoutMs, now) {
585
625
  const n = Number(val);
586
626
  currentPid = Number.isInteger(n) && n > 0 ? n : null;
587
627
  } else if (tag === 'n' && currentPid != null) {
588
- if (val === resolved || val.startsWith(prefix)) pids.add(currentPid);
628
+ if (val === resolved || val.startsWith(prefix)) pids.set(currentPid, val);
589
629
  }
590
630
  }
591
631
  if (pids.size === 0) return [];
592
632
  const out = [];
593
- for (const pid of pids) {
633
+ for (const [pid, cwd] of pids) {
594
634
  let line;
595
635
  try {
596
636
  line = String(_execSync(`ps -p ${pid} -o lstart=,command=`, {
@@ -608,6 +648,7 @@ function _findMacProcessesWithCwdInside(resolved, timeoutMs, now) {
608
648
  }
609
649
  out.push({
610
650
  pid,
651
+ cwd,
611
652
  cmdline,
612
653
  startedAt,
613
654
  ageMs: startedAt > 0 ? Math.max(0, now - startedAt) : 0,
@@ -725,7 +766,10 @@ function findProcessCwdHolders(worktreePath, opts = {}) {
725
766
  `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${scriptPath}"`,
726
767
  { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }
727
768
  );
728
- } catch { raw = ''; /* ETIMEDOUT / spawn fail / write fail → reap nothing */ }
769
+ } catch (err) {
770
+ if (opts.throwOnFailure) throw err;
771
+ raw = ''; // ETIMEDOUT / spawn fail / write fail -> reap nothing
772
+ }
729
773
  finally {
730
774
  if (scriptPath) { try { fs.unlinkSync(scriptPath); } catch { /* best-effort */ } }
731
775
  }
@@ -861,8 +905,12 @@ module.exports = {
861
905
  _psListProcesses,
862
906
  _winListProcesses,
863
907
  _unixListProcesses,
908
+ _parseUnixProcessesWithStart, // exported for testing
864
909
  listAllProcesses,
865
910
  listAllProcessesAsync,
911
+ getProcessIdentity,
912
+ isProcessIdentityCompatible,
913
+ isSameProcessIdentity,
866
914
  findProcessesWithCwdInside,
867
915
  findProcessesWithCwdInsideAsync,
868
916
  _findWindowsProcessesWithCwdInside,