tickmarkr 2.7.1 → 2.7.3

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.
@@ -7,7 +7,7 @@ import { basename, join } from "node:path";
7
7
  import { DEFAULT_EVIDENCE_QUOTA_BYTES } from "../config/config.js";
8
8
  import { dependencyLinkRefusal, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
9
9
  import { executionSignal } from "../run/execution-budget.js";
10
- import { isVitestTestCommand, manifestFileCount } from "./test-manifest.js";
10
+ import { isVitestTestCommand, manifestChildEnv, manifestFileCount } from "./test-manifest.js";
11
11
  export const EVIDENCE_TAIL_BYTES = 16 * 1024;
12
12
  /** The default per-run quota; `gates.evidenceQuotaBytes` in config overrides it (OBS-1140). */
13
13
  export const EVIDENCE_RUN_QUOTA_BYTES = DEFAULT_EVIDENCE_QUOTA_BYTES;
@@ -726,7 +726,11 @@ export async function captureBaseline(cwd, commands, opts = {}) {
726
726
  continue;
727
727
  }
728
728
  const evidence = beginGateEvidence(cwd, name, cmd, opts.evidence);
729
- const r = await sh(cmd, cwd, CAPTURE_CEILING_MS, { env: opts.evidence?.env, onReceipt: receipt => { evidence.observe(receipt); opts.onReceipt?.(receipt); } });
729
+ // Queue row 104 (D-1644): a manifested test command is captured under the test gate's own runner environment
730
+ // (node_modules/.bin first), so a bare repo-local runner resolves here exactly as it does in the gate — built on the
731
+ // caller's own evidence environment when it supplies one (D-1659), never in place of it.
732
+ const env = (name === "test" || name === "tipTest") && isVitestTestCommand(cmd, cwd) ? manifestChildEnv(cwd, opts.evidence?.env) : opts.evidence?.env;
733
+ const r = await sh(cmd, cwd, CAPTURE_CEILING_MS, { env, onReceipt: receipt => { evidence.observe(receipt); opts.onReceipt?.(receipt); } });
730
734
  (base.evidenceReceipts ??= {})[name] = evidence.finish(r.stdout, r.stderr);
731
735
  // ponytail: strip the executing cwd so repo-root capture and worktree compare fingerprint identically; /private-vs-/tmp symlink variance is out of scope
732
736
  // ponytail: a capture that was itself killed records the ceiling as its "measurement", which
@@ -15,11 +15,11 @@ import { disallowedBy, observedSeat } from "../route/preference.js";
15
15
  import { marginalCostRank } from "../route/router.js";
16
16
  import { carriedAuthorVendors, gateReviewerFloor, pickReviewer, reviewGate } from "./review.js";
17
17
  import { scopeGate } from "./scope.js";
18
- import { discoverTestManifest, evaluateManifestedTest, isVitestTestCommand, VITEST_CACHE_ENV, worktreeVitestCache } from "./test-manifest.js";
18
+ import { discoverTestManifest, evaluateManifestedTest, isVitestTestCommand, manifestChildEnv } from "./test-manifest.js";
19
19
  import { RUNNER_INFRA_DIAGNOSTIC_RE, timeoutShaped } from "./timeout-shaped.js";
20
20
  import { executionSignal } from "../run/execution-budget.js";
21
21
  import { failureDisposition } from "../run/recovery.js";
22
- import { dependencyLinkRefusal, FORK_CAP_ENV, preserveWorktree, producerFields, readCapacity, ROUTING_ENV_SEAMS, shGit, resolvedCapacity, sameCapacity, SUITE_PARENT_ENV, verificationProtocol } from "../run/git.js";
22
+ import { dependencyLinkRefusal, preserveWorktree, producerFields, readCapacity, shGit, resolvedCapacity, sameCapacity, verificationProtocol } from "../run/git.js";
23
23
  import { withJudgeInvocationEvidence } from "../run/journal.js";
24
24
  import { computeVerificationIdentity, verificationIdentityKey, formatReusedRow, getVerdictStore, isInfraResult, resolveStateDir, reusedIdentity, } from "./cache.js";
25
25
  const productionLoadProvider = () => loadavg()[0] ?? 0;
@@ -285,11 +285,7 @@ export function diagnosticAdmission(baseline, selected) {
285
285
  * discovery receives (test-manifest.ts manifestEnvironment), so it compares with the one a verdict
286
286
  * certified. Undefined when the runner cannot list — an unlisted manifest certifies nothing. */
287
287
  async function listFullManifest(cmd, worktree) {
288
- const env = { ...process.env, PATH: `${join(worktree, "node_modules/.bin")}:${process.env.PATH ?? ""}`,
289
- [VITEST_CACHE_ENV]: worktreeVitestCache(worktree),
290
- [FORK_CAP_ENV]: String(resolvedCapacity().forkCap), [SUITE_PARENT_ENV]: String(process.pid) };
291
- for (const key of [...ROUTING_ENV_SEAMS, "VITEST", "TEST", "VITEST_WORKER_ID", "VITEST_POOL_ID"])
292
- delete env[key];
288
+ const env = manifestChildEnv(worktree);
293
289
  const dir = mkdtempSync(join(tmpdir(), "tickmarkr-full-manifest-"));
294
290
  try {
295
291
  return (await discoverTestManifest(cmd, worktree, { dir, nonce: randomUUID(), env })).files;
@@ -206,6 +206,9 @@ export interface ManifestGateOutcome {
206
206
  exitCode: number;
207
207
  reportPath: string;
208
208
  }
209
+ /** Queue row 104 (D-1644): THE child environment of a manifested test command — its listing and run here, the full
210
+ * manifest re-listing (run-gates.ts) and its baseline capture (baseline.ts) — so every one resolves the same runner. */
211
+ export declare function manifestChildEnv(cwd: string, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
209
212
  export interface DiscoveredManifest {
210
213
  files: string[];
211
214
  listing: string;
@@ -504,17 +504,22 @@ export function runManifestedTest(cmd, cwd, opts) {
504
504
  * `npm_config_ignore_scripts` from the environment over every npmrc); the verdict records it so a
505
505
  * verdict measured with `pretest` hooks is never compared to one without. */
506
506
  function manifestEnvironment(cwd) {
507
- const env = { ...process.env, PATH: `${join(cwd, "node_modules/.bin")}:${process.env.PATH ?? ""}`,
507
+ const env = manifestBaseEnv(cwd);
508
+ return { env: scrubbedManifestEnv(env), verification: verificationProtocol(env, cwd) };
509
+ }
510
+ function manifestBaseEnv(cwd, base = process.env) {
511
+ return { ...base, PATH: `${join(cwd, "node_modules/.bin")}:${base.PATH ?? ""}`,
508
512
  [VITEST_CACHE_ENV]: worktreeVitestCache(cwd),
509
513
  [FORK_CAP_ENV]: String(resolvedCapacity().forkCap), [SUITE_PARENT_ENV]: String(process.pid) };
510
- const verification = verificationProtocol(env, cwd);
511
- for (const key of ROUTING_ENV_SEAMS)
512
- delete env[key];
513
- // A gate can itself be tested under Vitest. Do not give the child the outer worker identity.
514
- for (const key of Object.keys(env))
515
- if (["VITEST", "TEST", "VITEST_WORKER_ID", "VITEST_POOL_ID"].includes(key))
516
- delete env[key];
517
- return { env, verification };
514
+ }
515
+ // A gate can itself be tested under Vitest. Do not give the child the outer worker identity.
516
+ const OUTER_RUNNER_KEYS = new Set([...ROUTING_ENV_SEAMS, "VITEST", "TEST", "VITEST_WORKER_ID", "VITEST_POOL_ID"]);
517
+ const scrubbedManifestEnv = (env) => Object.fromEntries(Object.entries(env).filter(([key]) => !OUTER_RUNNER_KEYS.has(key)));
518
+ /** Queue row 104 (D-1644): THE child environment of a manifested test command — its listing and run here, the full
519
+ * manifest re-listing (run-gates.ts) and its baseline capture (baseline.ts) — so every one resolves the same runner. */
520
+ export function manifestChildEnv(cwd, base) {
521
+ // built on `base` when a caller supplies its own environment (captureBaseline's evidence env), else on process.env
522
+ return scrubbedManifestEnv(manifestBaseEnv(cwd, base));
518
523
  }
519
524
  /** OBS-1044: THE discovery seam — the files the runner would collect under this exact invocation,
520
525
  * from its own listing and never from a stdout summary. The gate and the baseline capture share it,
@@ -4,7 +4,7 @@ import { type DriverChoice } from "../drivers/index.js";
4
4
  import { type ExecutorDriver, type Slot } from "../drivers/types.js";
5
5
  import { type Baseline, type BaselineProvenance, type GateEvidenceOptions } from "../gates/baseline.js";
6
6
  import type { GateResult } from "../gates/types.js";
7
- import { type RunGraph } from "../graph/schema.js";
7
+ import { type RunGraph, type Task } from "../graph/schema.js";
8
8
  import { type PreserveProducer } from "./git.js";
9
9
  import { Journal, type JournalEvent } from "./journal.js";
10
10
  export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
@@ -144,6 +144,19 @@ export declare const gateSatisfied: (g: GateResult) => boolean;
144
144
  * preference (ranked last, never excluded) — every review-pool-demotion except the launch-caused ones, which
145
145
  * never demote a seat (live or replayed). plan.ts previews a resume with this same fold. */
146
146
  export declare function replayedReviewerDemotions(events: readonly JournalEvent[]): Set<string>;
147
+ /** C: a journaled test red whose failing-file attribution is nonempty, trustworthy and complete (repair
148
+ * selection's own rule: a behavioral red on a real subject from a full suite or a screen that names
149
+ * every failure), POSITIVELY certified complete by its runner, uncontradicted by the files its own text
150
+ * names, and wholly outside a declared files[]. Empty files[] is unrestricted and never qualifies;
151
+ * every other shape keeps its replay. */
152
+ export declare function outsideScopeRed(t: Task, taskId: string, data: Record<string, unknown>, commit: string): boolean;
153
+ /** OBS-1106 residual, D-607: what ONE isolated rerun of a timeout-shaped red's attributed `files` says about that
154
+ * red. Lifted unchanged out of adjudicateInfraShapedRed (queue row 102) so its closed outcome table runs on this
155
+ * function in-process. Pure: it reads only its arguments; the caller classifies the rerun first. */
156
+ export declare function reobservationOutcome(files: readonly string[], rerun: GateResult): {
157
+ outcome: "reproduced" | "passed" | "ambiguous";
158
+ selection: string[] | undefined;
159
+ };
147
160
  /**
148
161
  * T4 (OBS-265): the journal with the review objections a round did NOT hinge on removed. Judge and
149
162
  * review are now launched together, so a round can journal a failed review that the serial walk would
@@ -250,6 +263,8 @@ export declare function verifyIntegrationTipCached(intWt: string, commands: Reco
250
263
  signal?: AbortSignal;
251
264
  evidence?: GateEvidenceOptions;
252
265
  }): Promise<boolean>;
266
+ type WorkerProcessTree = "empty" | "running" | "unmeasurable";
267
+ export declare function observeWorkerProcessTree(marker: string, cwd: string): Promise<WorkerProcessTree>;
253
268
  type SuitePidProbe = (pid: number) => number | undefined;
254
269
  /** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
255
270
  * rules remain testable on hosts that forbid process inspection; production supplies cwd and the
@@ -34,7 +34,7 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
34
34
  import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
35
35
  import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
36
36
  import { runEnvironment } from "./environment.js";
37
- import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
37
+ import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
38
38
  import { MASK } from "./redact.js";
39
39
  import { runInteractiveSeed } from "./interactive-seed.js";
40
40
  import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
@@ -409,7 +409,7 @@ const RUNNER_CERTIFICATE_RE = /runner-level diagnostic: never-started ([^;\s]+);
409
409
  * every failure), POSITIVELY certified complete by its runner, uncontradicted by the files its own text
410
410
  * names, and wholly outside a declared files[]. Empty files[] is unrestricted and never qualifies;
411
411
  * every other shape keeps its replay. */
412
- function outsideScopeRed(t, taskId, data, commit) {
412
+ export function outsideScopeRed(t, taskId, data, commit) {
413
413
  if (!t.files.length || data.gate !== "test" || data.pass !== false || data.skipped === true || data.commit !== commit
414
414
  // a failed producer — an infra row, a dirty-tree refusal, a recovery the runner could not finish —
415
415
  // never attributes anything, whatever files it lists
@@ -456,6 +456,21 @@ const observedSelection = (g) => {
456
456
  /** D-607: on the manifest path only the reporter's own attribution counts — a `FAIL path` line inside
457
457
  * captured stdout is text, not attribution. Headline lexing is for runners without a reporter. */
458
458
  const rerunFailingFiles = (g) => metaFiles(g.meta?.manifest) ? [...new Set(metaFiles(g.meta?.failingFiles) ?? [])].sort() : attributedFailingFiles(g);
459
+ /** OBS-1106 residual, D-607: what ONE isolated rerun of a timeout-shaped red's attributed `files` says about that
460
+ * red. Lifted unchanged out of adjudicateInfraShapedRed (queue row 102) so its closed outcome table runs on this
461
+ * function in-process. Pure: it reads only its arguments; the caller classifies the rerun first. */
462
+ export function reobservationOutcome(files, rerun) {
463
+ // Reproduced only by failure evidence the rerun ATTRIBUTES to one of exactly these files, from an
464
+ // OBSERVED selection that was exactly these files (D-607): a path merely printed in output, a
465
+ // positional filter that also collected another file, or a runner that ignored the file
466
+ // arguments and reported no manifest of its selection, says nothing about the original red.
467
+ const failing = rerunFailingFiles(rerun);
468
+ const selection = observedSelection(rerun);
469
+ const isolated = selection !== undefined && selection.length === files.length
470
+ && files.every((file) => selection.includes(file)) && failing.every((file) => files.includes(file));
471
+ const outcome = rerun.pass ? "passed" : isolated && !isInfraResult(rerun) && failing.length ? "reproduced" : "ambiguous";
472
+ return { outcome, selection };
473
+ }
459
474
  // v1.85 T3: the gates whose failure IS a deterministic measurement — a machine re-ran a command over a
460
475
  // tree and printed the same bytes. Those are the failures the fingerprint cap governs (the ruling names
461
476
  // it a "deterministic-gate" cap): a third identical answer to a question already answered twice is the
@@ -1222,8 +1237,9 @@ async function gateCommitSubject(base, head, wt) {
1222
1237
  // OBS-737 needs. A readable process table with no marker root is measured empty; a failed or empty
1223
1238
  // table is unmeasurable. Descendants are closed over PPID because not every child retains the
1224
1239
  // dispatch-script marker in its own argv.
1225
- async function observeWorkerProcessTree(marker, cwd) {
1226
- const snapshot = await shGit("ps -Awwo pid=,ppid=,command=", cwd, 15_000);
1240
+ export async function observeWorkerProcessTree(marker, cwd) {
1241
+ // queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
1242
+ const snapshot = await shell("ps -Awwo pid=,ppid=,command=", cwd, 15_000);
1227
1243
  if (snapshot.code !== 0)
1228
1244
  return "unmeasurable";
1229
1245
  const rows = [];
@@ -1312,7 +1328,8 @@ export const resetLiveSuiteCountForTests = () => { liveSuiteCountForTests = unde
1312
1328
  export async function liveSuiteCount(repoRoot) {
1313
1329
  if (liveSuiteCountForTests)
1314
1330
  return liveSuiteCountForTests(repoRoot);
1315
- const snapshot = await shGit("ps -Aww -o pid=,ppid=,state=,command=", repoRoot, 15_000);
1331
+ // queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
1332
+ const snapshot = await shell("ps -Aww -o pid=,ppid=,state=,command=", repoRoot, 15_000);
1316
1333
  if (snapshot.code !== 0)
1317
1334
  return 0;
1318
1335
  // row 103: one batched async probe per snapshot — a synchronous lsof/ps per pid held this loop 44–46 s
@@ -3080,15 +3097,9 @@ export async function runDaemon(repoRoot, opts = {}) {
3080
3097
  rerun = await withCommandContext(t.id, () => reobserveTestFiles(wt, commands.test, baseline, files, journal.dir))
3081
3098
  .catch((error) => ({ gate: "test", pass: false, details: `infra: rerun failed: ${error instanceof Error ? error.message : String(error)}`, meta: { infra: true } }));
3082
3099
  classifyInfraResult(rerun);
3083
- // Reproduced only by failure evidence the rerun ATTRIBUTES to one of exactly these files, from an
3084
- // OBSERVED selection that was exactly these files (D-607): a path merely printed in output, a
3085
- // positional filter that also collected another file, or a runner that ignored the file
3086
- // arguments and reported no manifest of its selection, says nothing about the original red.
3087
- const failing = rerunFailingFiles(rerun);
3088
- selection = observedSelection(rerun);
3089
- const isolated = selection !== undefined && selection.length === files.length
3090
- && files.every((file) => selection.includes(file)) && failing.every((file) => files.includes(file));
3091
- outcome = rerun.pass ? "passed" : isolated && !isInfraResult(rerun) && failing.length ? "reproduced" : "ambiguous";
3100
+ const judged = reobservationOutcome(files, rerun);
3101
+ outcome = judged.outcome;
3102
+ selection = judged.selection;
3092
3103
  }
3093
3104
  journal.append("gate-reobserved", t.id, {
3094
3105
  gate: "test", attempt, commit: subject, fingerprint, ...(observation ? { observation } : {}), files, outcome,
package/dist/run/stall.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { isAbsolute, relative, sep } from "node:path";
4
- import { activeShellIdentities, processIdentity, resolveActiveShellIdentities, shGit, SUITE_PARENT_ENV } from "./git.js";
4
+ import { activeShellIdentities, processIdentity, resolveActiveShellIdentities, shell, SUITE_PARENT_ENV } from "./git.js";
5
5
  // OBS-82: normalize known presentation tokens before measuring transcript extent or filtering an
6
6
  // LLM-bound transcript. This remains a closed allowlist — ANSI/VT escapes, braille-range spinner
7
7
  // glyphs, and elapsed-time tokens bound to time-unit suffixes. Every other byte passes through
@@ -71,7 +71,8 @@ export async function reapOwnedProcessGroup(group, cwd, ownership) {
71
71
  return reapWorkerProcesses(group, cwd, ownership);
72
72
  if (group === undefined)
73
73
  return null;
74
- const own = await shGit(`ps -o pgid= -p ${process.pid}`, cwd, 5_000);
74
+ // queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
75
+ const own = await shell(`ps -o pgid= -p ${process.pid}`, cwd, 5_000);
75
76
  // An in-process driver fixture (or a non-isolating host) can share the daemon's
76
77
  // group. It is not an owned worker group: let the driver's close retire that slot.
77
78
  if (own.code !== 0 || !/^\d+$/.test(own.stdout.trim()) || Number(own.stdout.trim()) === group)
@@ -87,7 +88,7 @@ export async function reapOwnedProcessGroup(group, cwd, ownership) {
87
88
  // SIGKILL delivery and process retirement are asynchronous. Bound the confirmation
88
89
  // grace, retaining names if a process is still present after it.
89
90
  for (let probe = 0; probe < 5; probe++) {
90
- const snapshot = await shGit("ps -Awwo pid=,pgid=,stat=", cwd, 5_000);
91
+ const snapshot = await shell("ps -Awwo pid=,pgid=,stat=", cwd, 5_000);
91
92
  if (snapshot.code !== 0)
92
93
  return null;
93
94
  survivors = snapshot.stdout.split("\n").flatMap((line) => {
@@ -377,9 +378,9 @@ let linuxClockTickMs;
377
378
  function linuxProcessCpuMs(pid, cwd) {
378
379
  if (!existsSync("/proc/self/stat"))
379
380
  return Promise.resolve(undefined);
380
- // shGit, not sh: the accountant samples at 100ms cadence and must not run the operator's login
381
- // profile (nvm/pyenv/direnv side effects included) on every sample.
382
- linuxClockTickMs ??= shGit("getconf CLK_TCK", cwd, 15_000).then((r) => {
381
+ // shell (login false), not sh: the accountant samples at 100ms cadence and must not run the operator's login
382
+ // profile (nvm/pyenv/direnv side effects included). Not shGit either: getconf is not git (queue row 113).
383
+ linuxClockTickMs ??= shell("getconf CLK_TCK", cwd, 15_000).then((r) => {
383
384
  const ticks = r.code === 0 ? Number(r.stdout.trim()) : Number.NaN;
384
385
  return Number.isFinite(ticks) && ticks > 0 ? 1_000 / ticks : undefined;
385
386
  });
@@ -403,12 +404,21 @@ function linuxProcessCpuMs(pid, cwd) {
403
404
  // finds that root and its descendants. An empty tree is measurable zero; a failed or unparseable
404
405
  // snapshot is undefined because missing evidence can never prove inactivity.
405
406
  async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
406
- const snapshot = await shGit("ps -Awwo pid=,ppid=,time=,command=", cwd, 15_000);
407
+ // queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
408
+ // ONE snapshot carries the group column too (it was a second `ps -Awwo pid=,pgid=` per sample).
409
+ const snapshot = await shell("ps -Awwo pid=,ppid=,pgid=,time=,command=", cwd, 15_000);
407
410
  if (snapshot.code !== 0)
408
411
  return undefined;
409
412
  const rows = [];
413
+ const grouped = [];
410
414
  for (const line of snapshot.stdout.split("\n")) {
411
- const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
415
+ // Group membership reads EVERY row, as the separate group snapshot did; a CPU row also needs a parseable time.
416
+ const ids = /^\s*(\d+)\s+(\d+)\s+(\d+)(?:\s|$)/.exec(line);
417
+ if (!ids)
418
+ continue;
419
+ if (group !== undefined && Number(ids[3]) === group)
420
+ grouped.push(ids[1]);
421
+ const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)$/.exec(line);
412
422
  if (!m)
413
423
  continue;
414
424
  const cpu = parsePsCpu(m[3]);
@@ -420,16 +430,8 @@ async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
420
430
  const tree = new Set(rows.filter((p) => p.cmd.includes(marker)).map((p) => p.pid));
421
431
  // CPU presentation matching is intentionally broader than authority to signal a process.
422
432
  const owned = new Set(rows.filter((p) => p.cmd === `bash ${marker}` || p.cmd === `/bin/bash ${marker}`).map((p) => p.pid));
423
- if (group !== undefined) {
424
- const groups = await shGit("ps -Awwo pid=,pgid=", cwd, 15_000);
425
- if (groups.code !== 0)
426
- return undefined;
427
- for (const line of groups.stdout.split("\n")) {
428
- const row = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
429
- if (row && Number(row[2]) === group)
430
- tree.add(row[1]);
431
- }
432
- }
433
+ for (const pid of grouped)
434
+ tree.add(pid);
433
435
  // ps output is not topologically ordered; relax the parent -> child closure until stable.
434
436
  for (let grew = true; grew;) {
435
437
  grew = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",