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.
- package/dist/cli/commands/doctor.d.ts +15 -12
- package/dist/cli/commands/doctor.js +122 -24
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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 "
|
|
58
|
-
: `add "
|
|
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
|
-
|
|
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:"));
|
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)
|