tickmarkr 1.90.0 → 1.90.2

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.
@@ -9,20 +9,23 @@ export type DoctorOpts = {
9
9
  catalog?: CatalogReadResult;
10
10
  catalogNow?: () => Date;
11
11
  };
12
- /**
13
- * Q122s (TRIAL T-OBS-3): tickmarkr provisions run worktrees INSIDE the repo
14
- * (.tickmarkr/worktrees.noindex/). A target repo whose test runner collects with
15
- * repo-wide globs (jest's default `**​/__tests__/**` shape, vitest's default include)
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
- * Text-level heuristic, advisory only — never enters health/doctor.json or routing.
21
- * ponytail: ceilings — a commented-out ignore false-passes; an include built from
22
- * variables is judged by its literal text. Both cost one advisory row, nothing more.
23
- */
24
12
  export declare function runnerIgnoreFinding(cwd: string): {
25
13
  verdict: "pass" | "warn";
26
14
  detail: string;
27
15
  } | undefined;
16
+ /**
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.
20
+ *
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.
26
+ */
27
+ export declare function applyRunnerIgnore(cwd: string): {
28
+ action: "none" | "wrote" | "manual";
29
+ detail: string;
30
+ };
28
31
  export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
@@ -14,37 +14,63 @@ import { readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog
14
14
  const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
15
15
  const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
16
16
  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
- * Text-level heuristic, advisory only — never enters health/doctor.json or routing.
26
- * ponytail: ceilings — a commented-out ignore false-passes; an include built from
27
- * variables is judged by its literal text. Both cost one advisory row, nothing more.
28
- */
29
- export function runnerIgnoreFinding(cwd) {
17
+ function detectRunner(cwd) {
30
18
  const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
31
- const pkgText = readIf("package.json");
32
19
  let pkg = {};
33
20
  try {
34
- pkg = JSON.parse(pkgText || "{}");
21
+ pkg = JSON.parse(readIf("package.json") || "{}");
35
22
  }
36
23
  catch { /* unparseable manifest: judge config files alone */ }
37
24
  const jestConfig = ["jest.config.js", "jest.config.cjs", "jest.config.mjs", "jest.config.ts", "jest.config.json"].find((p) => existsSync(join(cwd, p)));
38
25
  const vitestConfig = ["vitest.config.ts", "vitest.config.js", "vitest.config.mts", "vitest.config.mjs", "vitest.config.cts"].find((p) => existsSync(join(cwd, p)));
39
26
  const testScript = pkg.scripts?.test ?? "";
40
- const isJest = jestConfig !== undefined || pkg.jest !== undefined || /\bjest\b/.test(testScript);
41
- const isVitest = !isJest && (vitestConfig !== undefined || /\bvitest\b/.test(testScript));
42
- if (!isJest && !isVitest)
27
+ if (jestConfig !== undefined || pkg.jest !== undefined || /\bjest\b/.test(testScript)) {
28
+ return { kind: "jest", configPath: jestConfig, pkgJestBlock: pkg.jest !== undefined };
29
+ }
30
+ if (vitestConfig !== undefined || /\bvitest\b/.test(testScript)) {
31
+ return { kind: "vitest", configPath: vitestConfig, pkgJestBlock: false };
32
+ }
33
+ return undefined;
34
+ }
35
+ export function runnerIgnoreFinding(cwd) {
36
+ const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
37
+ const runner = detectRunner(cwd);
38
+ if (!runner)
43
39
  return undefined;
44
- const configText = isJest
40
+ const isJest = runner.kind === "jest";
41
+ const jestConfig = isJest ? runner.configPath : undefined;
42
+ const vitestConfig = isJest ? undefined : runner.configPath;
43
+ let pkg = {};
44
+ try {
45
+ pkg = JSON.parse(readIf("package.json") || "{}");
46
+ }
47
+ catch { /* judged above */ }
48
+ // Comment LINES are stripped before judgment: a commented-out ignore must not
49
+ // false-pass, and a prose comment mentioning .tickmarkr must not false-warn the
50
+ // anchor probe. Stripping is LINE-granular on purpose: in-line regex stripping ate
51
+ // glob strings — `'**/*.d.ts'` contains `/*` and later globs contain `*/`, so a
52
+ // naive block-comment regex deleted the middle of real configs and false-PASSED a
53
+ // repo-wide vitest include (caught on Intl-Dossier's config; shipped briefly in
54
+ // 1.90.1). Trailing same-line comments survive — acceptable: they can only make the
55
+ // probe more conservative on lines that also carry code.
56
+ const stripComments = (s) => s.split("\n").filter((l) => {
57
+ const t = l.trimStart();
58
+ return !(t.startsWith("//") || t.startsWith("/*") || t.startsWith("*"));
59
+ }).join("\n");
60
+ const configText = stripComments(isJest
45
61
  ? `${jestConfig ? readIf(jestConfig) : ""}${pkg.jest ? JSON.stringify(pkg.jest) : ""}`
46
- : vitestConfig ? readIf(vitestConfig) : "";
62
+ : vitestConfig ? readIf(vitestConfig) : "");
47
63
  if (/\.tickmarkr|worktrees\.noindex/.test(configText)) {
64
+ // Q130s self-match probe: erase every <rootDir>-anchored token (through the end of
65
+ // its quoted string); any surviving mention is a pattern that would also match
66
+ // inside a provisioned worktree (jest regex-vs-absolute-path semantics only).
67
+ const unanchored = configText.replace(/<rootDir>[^"'`\s]*/g, "");
68
+ if (isJest && /\.tickmarkr|worktrees\.noindex/.test(unanchored)) {
69
+ return {
70
+ verdict: "warn",
71
+ detail: `jest ignore for .tickmarkr/ is not <rootDir>-anchored — testPathIgnorePatterns are regexes against ABSOLUTE paths, so inside run worktrees (provisioned under .tickmarkr/worktrees.noindex/) it matches EVERY suite and the test gate fails closed; use "<rootDir>/\\\\.tickmarkr/" in ${jestConfig ?? "the package.json jest block"}`,
72
+ };
73
+ }
48
74
  return { verdict: "pass", detail: `${isJest ? "jest" : "vitest"} config ignores .tickmarkr/ — run worktrees stay out of the suite` };
49
75
  }
50
76
  // Rooted collection globs never reach .tickmarkr/…; only repo-wide `**/`-style patterns (or
@@ -54,13 +80,75 @@ export function runnerIgnoreFinding(cwd) {
54
80
  return { verdict: "pass", detail: `${isJest ? "jest" : "vitest"} collection globs are rooted — run worktrees are not collectable` };
55
81
  }
56
82
  const remedy = isJest
57
- ? `add "/.tickmarkr/" to testPathIgnorePatterns in ${jestConfig ?? "the package.json jest block"}`
58
- : `add "**/.tickmarkr/**" to test.exclude in ${vitestConfig ?? "a vitest config"}`;
83
+ ? `add "<rootDir>/\\\\.tickmarkr/" to testPathIgnorePatterns in ${jestConfig ?? "the package.json jest block"}`
84
+ : `add ".tickmarkr/**" to test.exclude in ${vitestConfig ?? "a vitest config"}`;
59
85
  return {
60
86
  verdict: "warn",
61
87
  detail: `${isJest ? "jest" : "vitest"} collects repo-wide and will scan .tickmarkr/ run worktrees (duplicate suites, false reds) — ${remedy}`,
62
88
  };
63
89
  }
90
+ /**
91
+ * Q134s (doctor --fix): the WRITE half of Q122s/Q130s. Doctor's advisory row cost two
92
+ * repos a hand-edit (SentioQ run 2 paid a whole run first); --fix applies the ignore
93
+ * where a safe textual edit exists and refuses loudly otherwise.
94
+ *
95
+ * Safety contract: only three write shapes, each verified by re-running
96
+ * runnerIgnoreFinding afterward — a write that does not turn the row green is
97
+ * REVERTED and reported as manual. Never touches package.json (jest block → manual).
98
+ * ponytail: text-level edits, same ceilings as the finding; the verify-then-revert
99
+ * loop is what makes them safe to ship.
100
+ */
101
+ export function applyRunnerIgnore(cwd) {
102
+ const finding = runnerIgnoreFinding(cwd);
103
+ if (!finding)
104
+ return { action: "none", detail: "no recognized test runner" };
105
+ if (finding.verdict === "pass")
106
+ return { action: "none", detail: finding.detail };
107
+ const runner = detectRunner(cwd);
108
+ if (!runner?.configPath) {
109
+ // package.json jest block, or a runner detected only via the test script: hand edit.
110
+ return { action: "manual", detail: finding.detail };
111
+ }
112
+ const file = join(cwd, runner.configPath);
113
+ const original = readFileSync(file, "utf8");
114
+ let text = original;
115
+ if (runner.kind === "jest") {
116
+ // 1) Replace unanchored poison entries in place (the SentioQ run-2 shape).
117
+ text = text.replace(/(["'])\/?\.tickmarkr\/?\1/g, '"<rootDir>/\\\\.tickmarkr/"');
118
+ if (text === original && !/\.tickmarkr/.test(text)) {
119
+ const arr = /testPathIgnorePatterns(["']?)\s*:\s*\[/;
120
+ if (arr.test(text)) {
121
+ // 2) Existing ignore array: prepend the anchored entry.
122
+ text = text.replace(arr, (m) => `${m}"<rootDir>/\\\\.tickmarkr/", `);
123
+ if (runner.configPath.endsWith(".json"))
124
+ text = text.replace(/,(\s*)\]/g, "$1]");
125
+ }
126
+ else {
127
+ // 3) No array: add the property, preserving jest's default /node_modules/ ignore.
128
+ const anchor = /(module\.exports\s*=\s*\{|export\s+default\s+\{|export\s+default\s+defineConfig\(\{)/g;
129
+ if ((text.match(anchor) ?? []).length === 1) {
130
+ text = text.replace(anchor, (m) => `${m}\n testPathIgnorePatterns: ["/node_modules/", "<rootDir>/\\\\.tickmarkr/"],`);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ else if (!/\.tickmarkr/.test(text)) {
136
+ // vitest: add the glob to every exclude array — test.exclude is the target;
137
+ // coverage.exclude gaining it is correct behavior, not collateral.
138
+ const arr = /exclude\s*:\s*\[/g;
139
+ if (arr.test(text))
140
+ text = text.replace(arr, (m) => `${m}'.tickmarkr/**', `);
141
+ }
142
+ if (text === original)
143
+ return { action: "manual", detail: finding.detail };
144
+ writeFileSync(file, text);
145
+ const after = runnerIgnoreFinding(cwd);
146
+ if (after?.verdict === "pass") {
147
+ return { action: "wrote", detail: `wrote ${runner.configPath} — ${after.detail}` };
148
+ }
149
+ writeFileSync(file, original); // verify failed: never leave a half-fix behind
150
+ return { action: "manual", detail: finding.detail };
151
+ }
64
152
  export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
65
153
  if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
66
154
  const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
@@ -139,9 +227,19 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
139
227
  const herdr = HerdrDriver.available();
140
228
  rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
141
229
  // Q122s: the target repo's runner must not scan tickmarkr's own worktrees (advisory row).
230
+ // Q134s: `doctor --fix` writes the ignore when a safe edit exists (verified, else reverted).
142
231
  const runnerIgnore = runnerIgnoreFinding(cwd);
143
- if (runnerIgnore)
144
- rows.push(alignedStatusRow(runnerIgnore.verdict, "test-runner", runnerIgnore.detail));
232
+ if (runnerIgnore) {
233
+ if (_argv.includes("--fix") && runnerIgnore.verdict === "warn") {
234
+ const fixed = applyRunnerIgnore(cwd);
235
+ rows.push(fixed.action === "wrote"
236
+ ? alignedStatusRow("pass", "test-runner", fixed.detail)
237
+ : alignedStatusRow("warn", "test-runner", `--fix could not edit safely — apply by hand: ${fixed.detail}`));
238
+ }
239
+ else {
240
+ rows.push(alignedStatusRow(runnerIgnore.verdict, "test-runner", runnerIgnore.detail));
241
+ }
242
+ }
145
243
  // v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
146
244
  // action-required names the exact one-time command (or dialog) the operator must run once.
147
245
  rows.push(legend("workspace trust:"));
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.90.0",
3
+ "version": "1.90.2",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",