tickmarkr 2.7.2 → 2.7.4

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 { allAdapters, probeAll, readDoctor, rolePools, writeDoctor } from "../..
7
7
  import { channelKey } from "../../adapters/types.js";
8
8
  import { loadConfig } from "../../config/config.js";
9
9
  import { captureBaseline, detectGateCommands, staleFileCountCommands } from "../../gates/baseline.js";
10
- import { getVerdictStore, runnerInputsHash } from "../../gates/cache.js";
10
+ import { getVerdictStore, lockfileKeyBytes, runnerInputsHash } from "../../gates/cache.js";
11
11
  import { modelProvider } from "../../gates/review.js";
12
12
  import { runGates } from "../../gates/run-gates.js";
13
13
  import { getTask, loadGraph } from "../../graph/graph.js";
@@ -92,7 +92,7 @@ export function baselineCachePath(cwd, baseSha, commands) {
92
92
  const lockParts = [];
93
93
  for (const file of LOCKFILES) {
94
94
  const path = join(cwd, file);
95
- lockParts.push(file, existsSync(path) ? readFileSync(path) : "<absent>");
95
+ lockParts.push(file, existsSync(path) ? lockfileKeyBytes(file, readFileSync(path)) : "<absent>");
96
96
  }
97
97
  const commandParts = Object.entries(commands).sort(([a], [b]) => a.localeCompare(b)).map(([name, command]) => `${name}\0${command}\0`);
98
98
  return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}-${runnerInputsHash()}.json`);
@@ -5,6 +5,7 @@ export declare const DEFAULT_VERDICT_CACHE_BOUND = 128;
5
5
  export declare function setVerdictCacheBoundForTests(bound: number | undefined): void;
6
6
  export declare function resetVerdictCacheBoundForTests(): void;
7
7
  export declare const LOCKFILES: readonly ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
8
+ export declare function lockfileKeyBytes(file: string, bytes: Buffer): Buffer | string;
8
9
  export declare function lockfileHash(worktree: string): string;
9
10
  export declare function canonicalJson(obj: unknown): string;
10
11
  export declare function baselineIdentity(baseline?: Baseline): string;
@@ -23,6 +23,32 @@ export const LOCKFILES = [
23
23
  "bun.lock",
24
24
  "bun.lockb",
25
25
  ];
26
+ // Queue row 122: a version-only bump rewrites an npm v2/v3 lockfile's root `version` and `packages[""].version` and nothing
27
+ // a dependency resolves from, so those two fields leave the key; every other byte keys as written. Other lockfile kinds, a
28
+ // v1 or unversioned npm lockfile, and an unparseable one key on their raw bytes, which can only miss, never falsely hit.
29
+ export function lockfileKeyBytes(file, bytes) {
30
+ if (file !== "package-lock.json" && file !== "npm-shrinkwrap.json")
31
+ return bytes;
32
+ let lock;
33
+ try {
34
+ lock = JSON.parse(bytes.toString("utf8"));
35
+ }
36
+ catch {
37
+ return bytes;
38
+ }
39
+ if (!lock || typeof lock !== "object" || Array.isArray(lock))
40
+ return bytes;
41
+ const { version: _version, ...rest } = lock;
42
+ if (rest.lockfileVersion !== 2 && rest.lockfileVersion !== 3)
43
+ return bytes;
44
+ const packages = rest.packages;
45
+ const own = packages?.[""];
46
+ if (own && typeof own === "object" && !Array.isArray(own)) {
47
+ const { version: _ownVersion, ...ownRest } = own;
48
+ rest.packages = { ...packages, "": ownRest };
49
+ }
50
+ return canonicalJson(rest);
51
+ }
26
52
  export function lockfileHash(worktree) {
27
53
  const hash = createHash("sha256");
28
54
  let found = false;
@@ -31,7 +57,7 @@ export function lockfileHash(worktree) {
31
57
  if (existsSync(p)) {
32
58
  found = true;
33
59
  try {
34
- hash.update(file).update("\0").update(readFileSync(p)).update("\0");
60
+ hash.update(file).update("\0").update(lockfileKeyBytes(file, readFileSync(p))).update("\0");
35
61
  }
36
62
  catch {
37
63
  hash.update(file).update("\0unreadable\0");
@@ -263,6 +263,8 @@ export declare function verifyIntegrationTipCached(intWt: string, commands: Reco
263
263
  signal?: AbortSignal;
264
264
  evidence?: GateEvidenceOptions;
265
265
  }): Promise<boolean>;
266
+ type WorkerProcessTree = "empty" | "running" | "unmeasurable";
267
+ export declare function observeWorkerProcessTree(marker: string, cwd: string): Promise<WorkerProcessTree>;
266
268
  type SuitePidProbe = (pid: number) => number | undefined;
267
269
  /** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
268
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";
@@ -1237,8 +1237,9 @@ async function gateCommitSubject(base, head, wt) {
1237
1237
  // OBS-737 needs. A readable process table with no marker root is measured empty; a failed or empty
1238
1238
  // table is unmeasurable. Descendants are closed over PPID because not every child retains the
1239
1239
  // dispatch-script marker in its own argv.
1240
- async function observeWorkerProcessTree(marker, cwd) {
1241
- 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);
1242
1243
  if (snapshot.code !== 0)
1243
1244
  return "unmeasurable";
1244
1245
  const rows = [];
@@ -1327,7 +1328,8 @@ export const resetLiveSuiteCountForTests = () => { liveSuiteCountForTests = unde
1327
1328
  export async function liveSuiteCount(repoRoot) {
1328
1329
  if (liveSuiteCountForTests)
1329
1330
  return liveSuiteCountForTests(repoRoot);
1330
- 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);
1331
1333
  if (snapshot.code !== 0)
1332
1334
  return 0;
1333
1335
  // row 103: one batched async probe per snapshot — a synchronous lsof/ps per pid held this loop 44–46 s
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.2",
3
+ "version": "2.7.4",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",