tickmarkr 1.90.2 → 1.90.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.
@@ -28,4 +28,31 @@ export declare function applyRunnerIgnore(cwd: string): {
28
28
  action: "none" | "wrote" | "manual";
29
29
  detail: string;
30
30
  };
31
+ /**
32
+ * Q140s(b) / operator directive (D-OBS-10): the repo's package manager must resolve in
33
+ * THIS process's environment — the same class of environment the daemon spawns gates
34
+ * into. Dossier run 1: pnpm lived only in the operator's interactive PATH; every gate
35
+ * died on `Unable to find package manager binary` / `spawnSync pnpm ENOENT`, the reds
36
+ * were baseline-forgiven, and no suite ever ran. A which(1)-grade check at doctor time
37
+ * turns that into preflight information.
38
+ */
39
+ export declare function packageManagerFinding(cwd: string, probe?: (pm: string) => boolean): {
40
+ verdict: "pass" | "fail";
41
+ detail: string;
42
+ } | undefined;
43
+ /**
44
+ * Q142s: tickmarkr guarded every binary except its own. Exhibit (operator machine,
45
+ * 2026-08-12): an orphaned npm-prefix root at ~/.local held tickmarkr AND tkr at
46
+ * 1.85.0 beside the live 1.90.3 — two seats disagreed about the tool's version and
47
+ * were BOTH right about their own PATH order (a consultant reported "no verify
48
+ * command" — verify shipped in 1.90.0 — while the overseer saw it present). Checks
49
+ * EVERY bin name the package declares: the first sweep of that machine missed `tkr`.
50
+ */
51
+ export declare function selfShadowFinding(ownVersion: string, cwd?: string, resolve?: (bin: string, cwd: string) => {
52
+ resolved?: string;
53
+ all: string[];
54
+ }, versionOf?: (path: string) => string | undefined): {
55
+ verdict: "pass" | "fail";
56
+ detail: string;
57
+ } | undefined;
31
58
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
@@ -1,7 +1,10 @@
1
1
  import { writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { join } from "node:path";
4
- import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
5
+ import { detectPackageManager } from "../../gates/baseline.js";
6
+ import { version } from "./version.js";
7
+ import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
5
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
6
9
  import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
7
10
  import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
@@ -149,6 +152,59 @@ export function applyRunnerIgnore(cwd) {
149
152
  writeFileSync(file, original); // verify failed: never leave a half-fix behind
150
153
  return { action: "manual", detail: finding.detail };
151
154
  }
155
+ /**
156
+ * Q140s(b) / operator directive (D-OBS-10): the repo's package manager must resolve in
157
+ * THIS process's environment — the same class of environment the daemon spawns gates
158
+ * into. Dossier run 1: pnpm lived only in the operator's interactive PATH; every gate
159
+ * died on `Unable to find package manager binary` / `spawnSync pnpm ENOENT`, the reds
160
+ * were baseline-forgiven, and no suite ever ran. A which(1)-grade check at doctor time
161
+ * turns that into preflight information.
162
+ */
163
+ export function packageManagerFinding(cwd, probe = (pm) => spawnSync(pm, ["--version"], { stdio: "ignore", shell: false }).error === undefined) {
164
+ if (!existsSync(join(cwd, "package.json")))
165
+ return undefined;
166
+ const pm = detectPackageManager(cwd);
167
+ if (probe(pm))
168
+ return { verdict: "pass", detail: `${pm} (detected) resolves in this environment` };
169
+ return {
170
+ verdict: "fail",
171
+ detail: `repo uses ${pm} but the binary does not resolve in this environment — every auto-detected gate command will die before running any suite (fix: corepack enable, or install ${pm} on the daemon's PATH)`,
172
+ };
173
+ }
174
+ /**
175
+ * Q142s: tickmarkr guarded every binary except its own. Exhibit (operator machine,
176
+ * 2026-08-12): an orphaned npm-prefix root at ~/.local held tickmarkr AND tkr at
177
+ * 1.85.0 beside the live 1.90.3 — two seats disagreed about the tool's version and
178
+ * were BOTH right about their own PATH order (a consultant reported "no verify
179
+ * command" — verify shipped in 1.90.0 — while the overseer saw it present). Checks
180
+ * EVERY bin name the package declares: the first sweep of that machine missed `tkr`.
181
+ */
182
+ export function selfShadowFinding(ownVersion, cwd = process.cwd(), resolve = resolveShellBinary, versionOf = (p) => {
183
+ const r = spawnSync(p, ["version"], { encoding: "utf8", timeout: 5_000, shell: false });
184
+ return r.status === 0 ? r.stdout.trim().split("\n")[0] : undefined;
185
+ }) {
186
+ const hits = new Map(); // path -> version
187
+ for (const bin of ["tickmarkr", "tkr"]) {
188
+ for (const p of resolve(bin, cwd).all) {
189
+ if (!hits.has(p))
190
+ hits.set(p, versionOf(p) ?? "?");
191
+ }
192
+ }
193
+ if (hits.size === 0)
194
+ return undefined; // no install on PATH (repo checkout) — nothing to shadow
195
+ const stale = [...hits].filter(([, v]) => v !== ownVersion);
196
+ if (stale.length === 0) {
197
+ return { verdict: "pass", detail: `single install root at ${ownVersion} (${[...hits.keys()].join(", ")})` };
198
+ }
199
+ const listing = stale.map(([p, v]) => `${p} → ${v}`).join(", ");
200
+ // One mismatched hit and it's the one running? Impossible — it would report ownVersion.
201
+ // Mismatch therefore means a SECOND root: a shell or daemon with a different PATH
202
+ // order runs a different tickmarkr than this one, silently.
203
+ return {
204
+ verdict: "fail",
205
+ detail: `stale tickmarkr/tkr binaries on PATH: ${listing} (running ${ownVersion}) — a seat with different PATH order silently runs the old binary; remove the stale install root`,
206
+ };
207
+ }
152
208
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
153
209
  if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
154
210
  const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
@@ -240,6 +296,14 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
240
296
  rows.push(alignedStatusRow(runnerIgnore.verdict, "test-runner", runnerIgnore.detail));
241
297
  }
242
298
  }
299
+ // Q140s(b): the repo's package manager must resolve where gates spawn (D-OBS-10).
300
+ const pmRow = packageManagerFinding(cwd);
301
+ if (pmRow)
302
+ rows.push(alignedStatusRow(pmRow.verdict, "package-manager", pmRow.detail));
303
+ // Q142s: tickmarkr's own binary gets the shadow check every adapter binary already had.
304
+ const shadowRow = selfShadowFinding(await version(), cwd);
305
+ if (shadowRow)
306
+ rows.push(alignedStatusRow(shadowRow.verdict, "tickmarkr-binary", shadowRow.detail));
243
307
  // v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
244
308
  // action-required names the exact one-time command (or dialog) the operator must run once.
245
309
  rows.push(legend("workspace trust:"));
@@ -37,6 +37,15 @@ export declare function freshFailures(entry: BaselineCommand | undefined, raw: s
37
37
  failing: string[];
38
38
  unreadable: boolean;
39
39
  };
40
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
41
+ /**
42
+ * Operator directive 2026-08-12 (D-OBS-10): tickmarkr is package-manager agnostic.
43
+ * The `packageManager` field is authoritative (corepack's own contract); lockfiles
44
+ * break ties; npm is only the LAST resort. Dossier exhibit: auto-detected
45
+ * `npm run -s test` on a repo whose engines said `"npm": "please-use-pnpm"` — turbo
46
+ * then failed to resolve pnpm and every gate died on environment, not code.
47
+ */
48
+ export declare function detectPackageManager(repoRoot: string): PackageManager;
40
49
  export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
41
50
  /**
42
51
  * The ceiling a battery runs under: whichever is larger of the shipped constant and the slack applied
@@ -123,17 +123,50 @@ export function freshFailures(entry, raw) {
123
123
  const fresh = current.filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
124
124
  return { failing: fresh.filter((f) => f !== UNRECOGNIZED_FAILURE), unreadable: current.includes(UNRECOGNIZED_FAILURE) };
125
125
  }
126
+ /**
127
+ * Operator directive 2026-08-12 (D-OBS-10): tickmarkr is package-manager agnostic.
128
+ * The `packageManager` field is authoritative (corepack's own contract); lockfiles
129
+ * break ties; npm is only the LAST resort. Dossier exhibit: auto-detected
130
+ * `npm run -s test` on a repo whose engines said `"npm": "please-use-pnpm"` — turbo
131
+ * then failed to resolve pnpm and every gate died on environment, not code.
132
+ */
133
+ export function detectPackageManager(repoRoot) {
134
+ const pkgPath = join(repoRoot, "package.json");
135
+ try {
136
+ const pm = JSON.parse(readFileSync(pkgPath, "utf8")).packageManager;
137
+ const name = pm?.split("@")[0];
138
+ if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm")
139
+ return name;
140
+ }
141
+ catch { /* no or unparseable manifest: lockfiles decide */ }
142
+ if (existsSync(join(repoRoot, "pnpm-lock.yaml")))
143
+ return "pnpm";
144
+ if (existsSync(join(repoRoot, "yarn.lock")))
145
+ return "yarn";
146
+ if (existsSync(join(repoRoot, "bun.lock")) || existsSync(join(repoRoot, "bun.lockb")))
147
+ return "bun";
148
+ return "npm";
149
+ }
150
+ // Silent where the manager supports it everywhere (npm -s; pnpm's global -s), bare
151
+ // where a flag would break a major version (yarn berry rejects run -s; bun is quiet).
152
+ const RUN_PREFIX = {
153
+ npm: "npm run -s",
154
+ pnpm: "pnpm -s run",
155
+ yarn: "yarn run",
156
+ bun: "bun run",
157
+ };
126
158
  export function detectGateCommands(repoRoot, cfg) {
127
159
  const out = {};
128
160
  const pkgPath = join(repoRoot, "package.json");
129
161
  const scripts = existsSync(pkgPath)
130
162
  ? (JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {})
131
163
  : {};
164
+ const runPrefix = RUN_PREFIX[detectPackageManager(repoRoot)];
132
165
  for (const name of ["build", "test", "lint"]) {
133
166
  if (cfg.gates[name])
134
167
  out[name] = cfg.gates[name];
135
168
  else if (scripts[name])
136
- out[name] = `npm run -s ${name}`;
169
+ out[name] = `${runPrefix} ${name}`;
137
170
  }
138
171
  return out;
139
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.90.2",
3
+ "version": "1.90.4",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",