tickmarkr 1.90.1 → 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.
- package/dist/cli/commands/doctor.d.ts +26 -22
- package/dist/cli/commands/doctor.js +133 -34
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +34 -1
- package/package.json +1 -1
|
@@ -9,31 +9,35 @@ export type DoctorOpts = {
|
|
|
9
9
|
catalog?: CatalogReadResult;
|
|
10
10
|
catalogNow?: () => Date;
|
|
11
11
|
};
|
|
12
|
+
export declare function runnerIgnoreFinding(cwd: string): {
|
|
13
|
+
verdict: "pass" | "warn";
|
|
14
|
+
detail: string;
|
|
15
|
+
} | undefined;
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
14
|
-
* (
|
|
15
|
-
*
|
|
16
|
-
* scans every suite once per live worktree — duplicate-suite interference red the
|
|
17
|
-
* baseline AND tip-verify on SentioQ's first external run, a class invisible to
|
|
18
|
-
* dogfooding (this repo's own includes are rooted at tests/).
|
|
19
|
-
*
|
|
20
|
-
* Q130s (TRIAL T-OBS-6): the ignore must be <rootDir>-anchored. Jest
|
|
21
|
-
* testPathIgnorePatterns are REGEXES matched against the ABSOLUTE test path; an
|
|
22
|
-
* unanchored "/.tickmarkr/" also matches every suite INSIDE a worktree provisioned at
|
|
23
|
-
* <repo>/.tickmarkr/worktrees.noindex/… — jest discovers nothing there and the whole
|
|
24
|
-
* gate battery fails closed (SentioQ run 2: 10 dispatches, 0 merges). Anchored
|
|
25
|
-
* patterns resolve <rootDir> to the worktree itself and stay inert. Vitest
|
|
26
|
-
* include/exclude globs match root-relative paths, so they cannot self-match, but the
|
|
27
|
-
* prescribed shape is root-anchored anyway.
|
|
17
|
+
* Q134s (doctor --fix): the WRITE half of Q122s/Q130s. Doctor's advisory row cost two
|
|
18
|
+
* repos a hand-edit (SentioQ run 2 paid a whole run first); --fix applies the ignore
|
|
19
|
+
* where a safe textual edit exists and refuses loudly otherwise.
|
|
28
20
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
21
|
+
* Safety contract: only three write shapes, each verified by re-running
|
|
22
|
+
* runnerIgnoreFinding afterward — a write that does not turn the row green is
|
|
23
|
+
* REVERTED and reported as manual. Never touches package.json (jest block → manual).
|
|
24
|
+
* ponytail: text-level edits, same ceilings as the finding; the verify-then-revert
|
|
25
|
+
* loop is what makes them safe to ship.
|
|
34
26
|
*/
|
|
35
|
-
export declare function
|
|
36
|
-
|
|
27
|
+
export declare function applyRunnerIgnore(cwd: string): {
|
|
28
|
+
action: "none" | "wrote" | "manual";
|
|
29
|
+
detail: string;
|
|
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";
|
|
37
41
|
detail: string;
|
|
38
42
|
} | undefined;
|
|
39
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";
|
|
@@ -14,47 +16,49 @@ import { readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog
|
|
|
14
16
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
15
17
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
16
18
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
17
|
-
|
|
18
|
-
* Q122s (TRIAL T-OBS-3): tickmarkr provisions run worktrees INSIDE the repo
|
|
19
|
-
* (.tickmarkr/worktrees.noindex/). A target repo whose test runner collects with
|
|
20
|
-
* repo-wide globs (jest's default `**/__tests__/**` shape, vitest's default include)
|
|
21
|
-
* scans every suite once per live worktree — duplicate-suite interference red the
|
|
22
|
-
* baseline AND tip-verify on SentioQ's first external run, a class invisible to
|
|
23
|
-
* dogfooding (this repo's own includes are rooted at tests/).
|
|
24
|
-
*
|
|
25
|
-
* Q130s (TRIAL T-OBS-6): the ignore must be <rootDir>-anchored. Jest
|
|
26
|
-
* testPathIgnorePatterns are REGEXES matched against the ABSOLUTE test path; an
|
|
27
|
-
* unanchored "/.tickmarkr/" also matches every suite INSIDE a worktree provisioned at
|
|
28
|
-
* <repo>/.tickmarkr/worktrees.noindex/… — jest discovers nothing there and the whole
|
|
29
|
-
* gate battery fails closed (SentioQ run 2: 10 dispatches, 0 merges). Anchored
|
|
30
|
-
* patterns resolve <rootDir> to the worktree itself and stay inert. Vitest
|
|
31
|
-
* include/exclude globs match root-relative paths, so they cannot self-match, but the
|
|
32
|
-
* prescribed shape is root-anchored anyway.
|
|
33
|
-
*
|
|
34
|
-
* Text-level heuristic, advisory only — never enters health/doctor.json or routing.
|
|
35
|
-
* ponytail: ceilings — comments are stripped before judgment, but an include built
|
|
36
|
-
* from variables is still judged by its literal text, and a pattern string containing
|
|
37
|
-
* `//` would be truncated by the line-comment strip. Each costs one advisory row,
|
|
38
|
-
* nothing more.
|
|
39
|
-
*/
|
|
40
|
-
export function runnerIgnoreFinding(cwd) {
|
|
19
|
+
function detectRunner(cwd) {
|
|
41
20
|
const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
|
|
42
|
-
const pkgText = readIf("package.json");
|
|
43
21
|
let pkg = {};
|
|
44
22
|
try {
|
|
45
|
-
pkg = JSON.parse(
|
|
23
|
+
pkg = JSON.parse(readIf("package.json") || "{}");
|
|
46
24
|
}
|
|
47
25
|
catch { /* unparseable manifest: judge config files alone */ }
|
|
48
26
|
const jestConfig = ["jest.config.js", "jest.config.cjs", "jest.config.mjs", "jest.config.ts", "jest.config.json"].find((p) => existsSync(join(cwd, p)));
|
|
49
27
|
const vitestConfig = ["vitest.config.ts", "vitest.config.js", "vitest.config.mts", "vitest.config.mjs", "vitest.config.cts"].find((p) => existsSync(join(cwd, p)));
|
|
50
28
|
const testScript = pkg.scripts?.test ?? "";
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
29
|
+
if (jestConfig !== undefined || pkg.jest !== undefined || /\bjest\b/.test(testScript)) {
|
|
30
|
+
return { kind: "jest", configPath: jestConfig, pkgJestBlock: pkg.jest !== undefined };
|
|
31
|
+
}
|
|
32
|
+
if (vitestConfig !== undefined || /\bvitest\b/.test(testScript)) {
|
|
33
|
+
return { kind: "vitest", configPath: vitestConfig, pkgJestBlock: false };
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
export function runnerIgnoreFinding(cwd) {
|
|
38
|
+
const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
|
|
39
|
+
const runner = detectRunner(cwd);
|
|
40
|
+
if (!runner)
|
|
54
41
|
return undefined;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
42
|
+
const isJest = runner.kind === "jest";
|
|
43
|
+
const jestConfig = isJest ? runner.configPath : undefined;
|
|
44
|
+
const vitestConfig = isJest ? undefined : runner.configPath;
|
|
45
|
+
let pkg = {};
|
|
46
|
+
try {
|
|
47
|
+
pkg = JSON.parse(readIf("package.json") || "{}");
|
|
48
|
+
}
|
|
49
|
+
catch { /* judged above */ }
|
|
50
|
+
// Comment LINES are stripped before judgment: a commented-out ignore must not
|
|
51
|
+
// false-pass, and a prose comment mentioning .tickmarkr must not false-warn the
|
|
52
|
+
// anchor probe. Stripping is LINE-granular on purpose: in-line regex stripping ate
|
|
53
|
+
// glob strings — `'**/*.d.ts'` contains `/*` and later globs contain `*/`, so a
|
|
54
|
+
// naive block-comment regex deleted the middle of real configs and false-PASSED a
|
|
55
|
+
// repo-wide vitest include (caught on Intl-Dossier's config; shipped briefly in
|
|
56
|
+
// 1.90.1). Trailing same-line comments survive — acceptable: they can only make the
|
|
57
|
+
// probe more conservative on lines that also carry code.
|
|
58
|
+
const stripComments = (s) => s.split("\n").filter((l) => {
|
|
59
|
+
const t = l.trimStart();
|
|
60
|
+
return !(t.startsWith("//") || t.startsWith("/*") || t.startsWith("*"));
|
|
61
|
+
}).join("\n");
|
|
58
62
|
const configText = stripComments(isJest
|
|
59
63
|
? `${jestConfig ? readIf(jestConfig) : ""}${pkg.jest ? JSON.stringify(pkg.jest) : ""}`
|
|
60
64
|
: vitestConfig ? readIf(vitestConfig) : "");
|
|
@@ -85,6 +89,87 @@ export function runnerIgnoreFinding(cwd) {
|
|
|
85
89
|
detail: `${isJest ? "jest" : "vitest"} collects repo-wide and will scan .tickmarkr/ run worktrees (duplicate suites, false reds) — ${remedy}`,
|
|
86
90
|
};
|
|
87
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Q134s (doctor --fix): the WRITE half of Q122s/Q130s. Doctor's advisory row cost two
|
|
94
|
+
* repos a hand-edit (SentioQ run 2 paid a whole run first); --fix applies the ignore
|
|
95
|
+
* where a safe textual edit exists and refuses loudly otherwise.
|
|
96
|
+
*
|
|
97
|
+
* Safety contract: only three write shapes, each verified by re-running
|
|
98
|
+
* runnerIgnoreFinding afterward — a write that does not turn the row green is
|
|
99
|
+
* REVERTED and reported as manual. Never touches package.json (jest block → manual).
|
|
100
|
+
* ponytail: text-level edits, same ceilings as the finding; the verify-then-revert
|
|
101
|
+
* loop is what makes them safe to ship.
|
|
102
|
+
*/
|
|
103
|
+
export function applyRunnerIgnore(cwd) {
|
|
104
|
+
const finding = runnerIgnoreFinding(cwd);
|
|
105
|
+
if (!finding)
|
|
106
|
+
return { action: "none", detail: "no recognized test runner" };
|
|
107
|
+
if (finding.verdict === "pass")
|
|
108
|
+
return { action: "none", detail: finding.detail };
|
|
109
|
+
const runner = detectRunner(cwd);
|
|
110
|
+
if (!runner?.configPath) {
|
|
111
|
+
// package.json jest block, or a runner detected only via the test script: hand edit.
|
|
112
|
+
return { action: "manual", detail: finding.detail };
|
|
113
|
+
}
|
|
114
|
+
const file = join(cwd, runner.configPath);
|
|
115
|
+
const original = readFileSync(file, "utf8");
|
|
116
|
+
let text = original;
|
|
117
|
+
if (runner.kind === "jest") {
|
|
118
|
+
// 1) Replace unanchored poison entries in place (the SentioQ run-2 shape).
|
|
119
|
+
text = text.replace(/(["'])\/?\.tickmarkr\/?\1/g, '"<rootDir>/\\\\.tickmarkr/"');
|
|
120
|
+
if (text === original && !/\.tickmarkr/.test(text)) {
|
|
121
|
+
const arr = /testPathIgnorePatterns(["']?)\s*:\s*\[/;
|
|
122
|
+
if (arr.test(text)) {
|
|
123
|
+
// 2) Existing ignore array: prepend the anchored entry.
|
|
124
|
+
text = text.replace(arr, (m) => `${m}"<rootDir>/\\\\.tickmarkr/", `);
|
|
125
|
+
if (runner.configPath.endsWith(".json"))
|
|
126
|
+
text = text.replace(/,(\s*)\]/g, "$1]");
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// 3) No array: add the property, preserving jest's default /node_modules/ ignore.
|
|
130
|
+
const anchor = /(module\.exports\s*=\s*\{|export\s+default\s+\{|export\s+default\s+defineConfig\(\{)/g;
|
|
131
|
+
if ((text.match(anchor) ?? []).length === 1) {
|
|
132
|
+
text = text.replace(anchor, (m) => `${m}\n testPathIgnorePatterns: ["/node_modules/", "<rootDir>/\\\\.tickmarkr/"],`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (!/\.tickmarkr/.test(text)) {
|
|
138
|
+
// vitest: add the glob to every exclude array — test.exclude is the target;
|
|
139
|
+
// coverage.exclude gaining it is correct behavior, not collateral.
|
|
140
|
+
const arr = /exclude\s*:\s*\[/g;
|
|
141
|
+
if (arr.test(text))
|
|
142
|
+
text = text.replace(arr, (m) => `${m}'.tickmarkr/**', `);
|
|
143
|
+
}
|
|
144
|
+
if (text === original)
|
|
145
|
+
return { action: "manual", detail: finding.detail };
|
|
146
|
+
writeFileSync(file, text);
|
|
147
|
+
const after = runnerIgnoreFinding(cwd);
|
|
148
|
+
if (after?.verdict === "pass") {
|
|
149
|
+
return { action: "wrote", detail: `wrote ${runner.configPath} — ${after.detail}` };
|
|
150
|
+
}
|
|
151
|
+
writeFileSync(file, original); // verify failed: never leave a half-fix behind
|
|
152
|
+
return { action: "manual", detail: finding.detail };
|
|
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
|
+
}
|
|
88
173
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
|
|
89
174
|
if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
|
|
90
175
|
const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
|
|
@@ -163,9 +248,23 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
163
248
|
const herdr = HerdrDriver.available();
|
|
164
249
|
rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
|
|
165
250
|
// Q122s: the target repo's runner must not scan tickmarkr's own worktrees (advisory row).
|
|
251
|
+
// Q134s: `doctor --fix` writes the ignore when a safe edit exists (verified, else reverted).
|
|
166
252
|
const runnerIgnore = runnerIgnoreFinding(cwd);
|
|
167
|
-
if (runnerIgnore)
|
|
168
|
-
|
|
253
|
+
if (runnerIgnore) {
|
|
254
|
+
if (_argv.includes("--fix") && runnerIgnore.verdict === "warn") {
|
|
255
|
+
const fixed = applyRunnerIgnore(cwd);
|
|
256
|
+
rows.push(fixed.action === "wrote"
|
|
257
|
+
? alignedStatusRow("pass", "test-runner", fixed.detail)
|
|
258
|
+
: alignedStatusRow("warn", "test-runner", `--fix could not edit safely — apply by hand: ${fixed.detail}`));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
rows.push(alignedStatusRow(runnerIgnore.verdict, "test-runner", runnerIgnore.detail));
|
|
262
|
+
}
|
|
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));
|
|
169
268
|
// v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
|
|
170
269
|
// action-required names the exact one-time command (or dialog) the operator must run once.
|
|
171
270
|
rows.push(legend("workspace trust:"));
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type CommandResult = string | {
|
|
|
5
5
|
};
|
|
6
6
|
export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
|
|
7
7
|
export declare const COMMANDS: CommandMap;
|
|
8
|
-
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval run checked-in fixtures against every channel in isolated temp repos\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open the Fleet Studio TUI (full-screen tabbed cockpit)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume";
|
|
8
|
+
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n eval run checked-in fixtures against every channel in isolated temp repos\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD \u2014 no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n ui open the Fleet Studio TUI (full-screen tabbed cockpit)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume";
|
|
9
9
|
export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
|
|
10
10
|
out: string;
|
|
11
11
|
code: number;
|
package/dist/cli/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { BANNER } from "../brand.js";
|
|
|
28
28
|
export const USAGE = `tickmarkr — spec-driven orchestration harness for AI coding agents
|
|
29
29
|
usage: tickmarkr <command>
|
|
30
30
|
init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs
|
|
31
|
-
doctor re-probe adapters, herdr, auth; print capability matrix
|
|
31
|
+
doctor re-probe adapters, herdr, auth; print capability matrix (--fix writes the test-runner ignore when a safe edit exists)
|
|
32
32
|
fleet interactive fleet editor (fleet --print for CI drift checks)
|
|
33
33
|
compile <src> spec → .tickmarkr/graph.json (fails without acceptance criteria)
|
|
34
34
|
scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -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
|
package/dist/gates/baseline.js
CHANGED
|
@@ -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] =
|
|
169
|
+
out[name] = `${runPrefix} ${name}`;
|
|
137
170
|
}
|
|
138
171
|
return out;
|
|
139
172
|
}
|