tickmarkr 1.90.2 → 1.90.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.
@@ -28,4 +28,16 @@ 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;
31
43
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
@@ -1,6 +1,8 @@
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";
5
+ import { detectPackageManager } from "../../gates/baseline.js";
4
6
  import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
5
7
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
6
8
  import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
@@ -149,6 +151,25 @@ export function applyRunnerIgnore(cwd) {
149
151
  writeFileSync(file, original); // verify failed: never leave a half-fix behind
150
152
  return { action: "manual", detail: finding.detail };
151
153
  }
154
+ /**
155
+ * Q140s(b) / operator directive (D-OBS-10): the repo's package manager must resolve in
156
+ * THIS process's environment — the same class of environment the daemon spawns gates
157
+ * into. Dossier run 1: pnpm lived only in the operator's interactive PATH; every gate
158
+ * died on `Unable to find package manager binary` / `spawnSync pnpm ENOENT`, the reds
159
+ * were baseline-forgiven, and no suite ever ran. A which(1)-grade check at doctor time
160
+ * turns that into preflight information.
161
+ */
162
+ export function packageManagerFinding(cwd, probe = (pm) => spawnSync(pm, ["--version"], { stdio: "ignore", shell: false }).error === undefined) {
163
+ if (!existsSync(join(cwd, "package.json")))
164
+ return undefined;
165
+ const pm = detectPackageManager(cwd);
166
+ if (probe(pm))
167
+ return { verdict: "pass", detail: `${pm} (detected) resolves in this environment` };
168
+ return {
169
+ verdict: "fail",
170
+ 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)`,
171
+ };
172
+ }
152
173
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
153
174
  if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
154
175
  const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
@@ -240,6 +261,10 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
240
261
  rows.push(alignedStatusRow(runnerIgnore.verdict, "test-runner", runnerIgnore.detail));
241
262
  }
242
263
  }
264
+ // Q140s(b): the repo's package manager must resolve where gates spawn (D-OBS-10).
265
+ const pmRow = packageManagerFinding(cwd);
266
+ if (pmRow)
267
+ rows.push(alignedStatusRow(pmRow.verdict, "package-manager", pmRow.detail));
243
268
  // v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
244
269
  // action-required names the exact one-time command (or dialog) the operator must run once.
245
270
  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.3",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",