tickmarkr 2.1.1 → 2.1.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/adapters/claude-code.js +18 -3
- package/dist/adapters/pi.d.ts +6 -0
- package/dist/adapters/pi.js +99 -18
- package/dist/cli/commands/beat.js +28 -8
- package/dist/cli/commands/init.js +29 -2
- package/dist/cli/commands/status.js +17 -15
- package/dist/cli/commands/verify.js +9 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/gates/baseline.d.ts +10 -0
- package/dist/gates/baseline.js +46 -3
- package/dist/run/git.d.ts +5 -0
- package/dist/run/git.js +52 -4
- package/dist/run/lock.d.ts +1 -0
- package/dist/run/lock.js +23 -13
- package/dist/run/supervision.d.ts +9 -3
- package/dist/run/supervision.js +77 -21
- package/dist/tui/cockpit/derive.js +5 -10
- package/package.json +2 -2
- package/skills/tickmarkr-auto/SKILL.md +19 -2
- package/skills/tickmarkr-loop/SKILL.md +19 -2
- package/skills/tickmarkr-overseer/SKILL.md +108 -14
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +79 -30
|
@@ -209,7 +209,7 @@ export const claudeCode = {
|
|
|
209
209
|
channels: (cfg) => channelsFromConfig("claude-code", cfg),
|
|
210
210
|
// v1.65 T3: every flag the command builders below hardcode — doctor checks `claude --help` still
|
|
211
211
|
// lists each (all present on claude 2.x, verified 2026-07-22). Advisory only, never routing.
|
|
212
|
-
hardcodedFlags: { binary: "claude", flags: ["-p", "--model", "--permission-mode", "--strict-mcp-config", "--mcp-config", "--output-format", "-r"] },
|
|
212
|
+
hardcodedFlags: { binary: "claude", flags: ["-p", "--model", "--permission-mode", "--strict-mcp-config", "--mcp-config", "--output-format", "-r", "--prompt-suggestions"] },
|
|
213
213
|
// --strict-mcp-config --mcp-config '{"mcpServers":{}}': pin the MCP surface to empty so fresh-worktree
|
|
214
214
|
// workers/gates don't load project .mcp.json servers (herdr scrapes dialogs as idle — v1.4 incident,
|
|
215
215
|
// memory tickmarkr-worker-mcp-dialog-stall). Live-verified 2026-07-10 on claude 2.1.205 (operator check):
|
|
@@ -223,10 +223,25 @@ export const claudeCode = {
|
|
|
223
223
|
// HYG-03 / OBS-137: the residual first-entry dialog is workspace trust, not MCP config loading.
|
|
224
224
|
// Claude's only store is global last-writer-wins ~/.claude.json, so tickmarkr still does not seed it;
|
|
225
225
|
// the daemon safely answers only the exact adapter-declared dialog once per slot.
|
|
226
|
-
|
|
226
|
+
//
|
|
227
|
+
// v2.1.3 T7 --prompt-suggestions false: claude paints a PREDICTED next prompt into the editor after
|
|
228
|
+
// a turn. On a pane it is pixel-identical to a seat's own unsubmitted draft, so every occupancy
|
|
229
|
+
// reader downstream — the input-box occupiedMatch above, the stall watchdog that sweeps for a
|
|
230
|
+
// parked turn — reads the vendor's ghost text as work in progress and waits out its window on a
|
|
231
|
+
// worker that is idle. Suppressed at the SOURCE instead: the two builders that seed a real terminal
|
|
232
|
+
// are exactly these (interactive, resume); the -p builder has no editor to paint into and is out of
|
|
233
|
+
// scope. Live-checked against claude 2.1.246: the flag parses and `false` is an allowed choice
|
|
234
|
+
// ("Allowed choices are true, false, 1, 0, yes, no, on, off").
|
|
235
|
+
// Placement is NOT free: --mcp-config above is variadic and eats the next positional (2026-07-10
|
|
236
|
+
// live check ate the prompt), and --prompt-suggestions takes an OPTIONAL value — appended directly
|
|
237
|
+
// before the prompt it would swallow it the same way. So the setting's value is always followed by
|
|
238
|
+
// another flag, never by the prompt positional.
|
|
239
|
+
interactiveCommand: (promptFile, model) => `claude --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --prompt-suggestions false --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
227
240
|
trustDialog: CLAUDE_TRUST_DIALOG,
|
|
228
241
|
inputBox: CLAUDE_INPUT_BOX,
|
|
229
|
-
|
|
242
|
+
// A resumed attempt lands in the same painted editor, so it carries the same ghost-text suppression
|
|
243
|
+
// and the same value-then-flag placement.
|
|
244
|
+
resumeCommand: (sessionId, promptFile, model) => `claude -r ${shq(sessionId)} --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --prompt-suggestions false --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
230
245
|
invoke(task, _cwd, a, ctx) {
|
|
231
246
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
232
247
|
},
|
package/dist/adapters/pi.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "./types.js";
|
|
2
2
|
export declare function parsePiModels(raw: string): string[];
|
|
3
|
+
export interface ServedModelDrift {
|
|
4
|
+
pinned: string;
|
|
5
|
+
served: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function readPiServedModels(): ServedModelDrift[];
|
|
8
|
+
export declare function servedModelNote(drifts?: ServedModelDrift[]): string;
|
|
3
9
|
export declare const pi: WorkerAdapter;
|
package/dist/adapters/pi.js
CHANGED
|
@@ -32,6 +32,92 @@ export function parsePiModels(raw) {
|
|
|
32
32
|
.map(([provider, model]) => `${provider}/${model}`)
|
|
33
33
|
.filter((id) => MODEL_ID_RE.test(id));
|
|
34
34
|
}
|
|
35
|
+
// Newest-first by mtime, capped — mtime picks WHICH files to scan, never a record's cursor.
|
|
36
|
+
// Shared by collectUsage (spend) and readPiServedModels (v2.1.3 T7): same store, same bounds.
|
|
37
|
+
function sessionFileStats(dir) {
|
|
38
|
+
return readdirSync(dir)
|
|
39
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
40
|
+
.map((f) => {
|
|
41
|
+
const path = join(dir, f);
|
|
42
|
+
try {
|
|
43
|
+
return { f: path, m: statSync(path).mtimeMs };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined; // a stat failure just drops that file
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter((x) => x !== undefined);
|
|
50
|
+
}
|
|
51
|
+
function newestSessionFiles(entries) {
|
|
52
|
+
return [...entries].sort((a, b) => b.m - a.m).slice(0, MAX_SESSION_FILES).map((x) => x.f);
|
|
53
|
+
}
|
|
54
|
+
// ponytail: three distinct pairs is the note's ceiling — raise it if a real store ever shows more.
|
|
55
|
+
const MAX_DRIFT_PAIRS = 3;
|
|
56
|
+
export function readPiServedModels() {
|
|
57
|
+
const root = join(homedir(), ".pi", "agent", "sessions");
|
|
58
|
+
let files;
|
|
59
|
+
try {
|
|
60
|
+
files = newestSessionFiles(readdirSync(root).flatMap((slug) => {
|
|
61
|
+
try {
|
|
62
|
+
return sessionFileStats(join(root, slug));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return []; // not a directory / unreadable ⇒ skip
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return []; // no store ⇒ nothing to report
|
|
71
|
+
}
|
|
72
|
+
const out = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = readFileSync(file, "utf8").slice(0, MAX_SESSION_BYTES);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const line of text.split("\n")) {
|
|
83
|
+
if (!line.trim())
|
|
84
|
+
continue;
|
|
85
|
+
let recRaw;
|
|
86
|
+
try {
|
|
87
|
+
recRaw = JSON.parse(line);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (recRaw === null || typeof recRaw !== "object" || Array.isArray(recRaw))
|
|
93
|
+
continue;
|
|
94
|
+
const rec = recRaw;
|
|
95
|
+
if (rec.type !== "message" || rec.message?.role !== "assistant")
|
|
96
|
+
continue;
|
|
97
|
+
const pinned = rec.message?.model;
|
|
98
|
+
const served = rec.message?.responseModel;
|
|
99
|
+
// absent responseModel === the two agree. Never a mismatch, never an alarm.
|
|
100
|
+
if (typeof served !== "string" || typeof pinned !== "string" || served === pinned)
|
|
101
|
+
continue;
|
|
102
|
+
if (!MODEL_ID_RE.test(served) || !MODEL_ID_RE.test(pinned))
|
|
103
|
+
continue;
|
|
104
|
+
const key = `${pinned}\u0000${served}`;
|
|
105
|
+
if (seen.has(key))
|
|
106
|
+
continue;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
out.push({ pinned, served });
|
|
109
|
+
if (out.length >= MAX_DRIFT_PAIRS)
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
// The probe's note clause, or "" when the store reports no discrepancy at all.
|
|
116
|
+
export function servedModelNote(drifts = readPiServedModels()) {
|
|
117
|
+
if (drifts.length === 0)
|
|
118
|
+
return "";
|
|
119
|
+
return `served-model drift: ${drifts.map((d) => `pinned ${d.pinned} served ${d.served}`).join(", ")}`;
|
|
120
|
+
}
|
|
35
121
|
export const pi = {
|
|
36
122
|
id: "pi",
|
|
37
123
|
// FLEET-04: cross-vendor review honesty — GLM's provider (pi's own label is "zai"; either is
|
|
@@ -42,10 +128,17 @@ export const pi = {
|
|
|
42
128
|
const h = probeVersion("pi");
|
|
43
129
|
if (!h.installed)
|
|
44
130
|
return h;
|
|
131
|
+
// The store is local and independently useful: preserve its advisory even when the network/auth
|
|
132
|
+
// capability call below fails. A failed model listing must not erase evidence of a substitution.
|
|
133
|
+
const drift = servedModelNote();
|
|
45
134
|
const r = spawnSync("pi", ["--list-models"], { encoding: "utf8", timeout: 15000 });
|
|
46
|
-
if (r.error || r.status !== 0)
|
|
47
|
-
return h;
|
|
48
|
-
|
|
135
|
+
if (r.error || r.status !== 0) {
|
|
136
|
+
return drift ? { ...h, note: `${h.note ? `${h.note}; ` : ""}${drift}` } : h;
|
|
137
|
+
}
|
|
138
|
+
// v2.1.3 T7: the probe is the production caller that already reads this store, and doctor/fleet
|
|
139
|
+
// already render its note — so the served-model discrepancy is reported where it is already read.
|
|
140
|
+
const note = `auth verified via pi --list-models (free; auth-filtered by pi)${drift ? `; ${drift}` : ""}`;
|
|
141
|
+
return { ...h, servable: parsePiModels(r.stdout || ""), note };
|
|
49
142
|
},
|
|
50
143
|
channels: (cfg) => channelsFromConfig("pi", cfg),
|
|
51
144
|
// v1.65 T3: every flag the command builders below hardcode — verified in `pi --help` 2026-07-22.
|
|
@@ -84,25 +177,13 @@ export const pi = {
|
|
|
84
177
|
const real = realpathSync(cwd);
|
|
85
178
|
const slug = "-" + real.replaceAll("/", "-") + "--";
|
|
86
179
|
const dir = join(homedir(), ".pi", "agent", "sessions", slug);
|
|
87
|
-
const files =
|
|
88
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
89
|
-
.map((f) => {
|
|
90
|
-
try {
|
|
91
|
-
return { f, m: statSync(join(dir, f)).mtimeMs };
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
.filter((x) => x !== undefined)
|
|
98
|
-
.sort((a, b) => b.m - a.m)
|
|
99
|
-
.slice(0, MAX_SESSION_FILES);
|
|
180
|
+
const files = newestSessionFiles(sessionFileStats(dir));
|
|
100
181
|
let input = 0, output = 0, kept = false;
|
|
101
182
|
let cacheRead, cacheWrite;
|
|
102
|
-
for (const
|
|
183
|
+
for (const f of files) {
|
|
103
184
|
let text;
|
|
104
185
|
try {
|
|
105
|
-
text = readFileSync(
|
|
186
|
+
text = readFileSync(f, "utf8").slice(0, MAX_SESSION_BYTES);
|
|
106
187
|
}
|
|
107
188
|
catch {
|
|
108
189
|
continue;
|
|
@@ -20,31 +20,51 @@ import { SUPERVISION_BEAT_MS, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSuper
|
|
|
20
20
|
// evidence: stop calling it and the tier ages into STALE on its own.
|
|
21
21
|
export async function beat(argv, cwd = process.cwd()) {
|
|
22
22
|
const standDown = argv.includes("--stand-down");
|
|
23
|
-
const
|
|
23
|
+
const seat = seatOf(argv);
|
|
24
|
+
const named = argv.find((a, i) => !a.startsWith("--") && argv[i - 1] !== "--seat");
|
|
24
25
|
if (!isTier(named)) {
|
|
25
|
-
throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> [--stand-down] — got ${named ? `\`${named}\`` : "no tier"}`);
|
|
26
|
+
throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity> [--stand-down] — got ${named ? `\`${named}\`` : "no tier"}`);
|
|
27
|
+
}
|
|
28
|
+
// SUP-05: NO SEAT, NO BEAT — and the refusal comes before any write, so a refused invocation leaves
|
|
29
|
+
// the tier exactly as it found it. This verb is one-shot: the pid it would otherwise record has
|
|
30
|
+
// already exited by the time anyone reads the record, so tier + pid + instant is a beat nobody can
|
|
31
|
+
// attribute to a seat. Measured 2026-08-26: a consult seat ran the documented loop verbatim and the
|
|
32
|
+
// board read that tier ARMED with no seat of that tier having armed anything, and a seatless ARMED
|
|
33
|
+
// reads as coverage — worse than ABSENT, because ABSENT sends someone to look.
|
|
34
|
+
if (!seat) {
|
|
35
|
+
throw new Error(`${named} needs --seat <identity> — a beat that names no seat arms a tier nobody occupies` +
|
|
36
|
+
" (pass the seat's own pane id or agent name)");
|
|
26
37
|
}
|
|
27
38
|
if (standDown)
|
|
28
|
-
return standDownTier(cwd, named);
|
|
39
|
+
return standDownTier(cwd, named, seat);
|
|
29
40
|
// Clear a stand-down marker left by an earlier session BEFORE beating: a valid marker outranks every
|
|
30
41
|
// beat that does not strictly follow it, and two writes landing in the same millisecond do not. An
|
|
31
42
|
// uncleared marker would render DISARMED while this verb claimed ARMED, so the removal is unguarded
|
|
32
43
|
// too — `force` makes the ordinary "no marker" case a no-op, and anything else is a real failure.
|
|
33
44
|
rmSync(supervisionStandDownPath(cwd, named), { force: true, recursive: true });
|
|
34
|
-
beatSupervision(cwd, named);
|
|
35
|
-
return `${named} ARMED — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
|
|
45
|
+
beatSupervision(cwd, named, seat);
|
|
46
|
+
return `${named} ARMED as ${seat} — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
|
|
47
|
+
}
|
|
48
|
+
/** `--seat <identity>` or `--seat=<identity>`; blank and missing are the same answer — none. */
|
|
49
|
+
function seatOf(argv) {
|
|
50
|
+
const inline = argv.find((a) => a.startsWith("--seat="))?.slice("--seat=".length);
|
|
51
|
+
const spaced = argv[argv.indexOf("--seat") + 1];
|
|
52
|
+
const seat = (inline ?? (argv.includes("--seat") ? spaced : undefined))?.trim();
|
|
53
|
+
return seat && !seat.startsWith("--") ? seat : undefined;
|
|
36
54
|
}
|
|
37
55
|
// Stand-down is a RECORDED act, not a silence: the marker tells a reader this watcher left on purpose,
|
|
38
56
|
// so the tier reads DISARMED rather than ageing out as a death. Published atomically — written aside,
|
|
39
57
|
// renamed over — because a torn marker is rejected by the reader, and a rejected stand-down reports a
|
|
40
58
|
// deliberate hand-off as a death.
|
|
41
|
-
function standDownTier(repoRoot, tier) {
|
|
59
|
+
function standDownTier(repoRoot, tier, seat) {
|
|
42
60
|
tickmarkrDir(repoRoot); // the write path DOES create — markers land inside the gitignored state dir
|
|
43
61
|
const p = supervisionStandDownPath(repoRoot, tier);
|
|
44
62
|
const tmp = `${p}.${process.pid}.tmp`;
|
|
45
63
|
mkdirSync(dirname(p), { recursive: true });
|
|
46
|
-
|
|
64
|
+
// The marker names the seat for the same reason the beat does: "someone stood this tier down" is not
|
|
65
|
+
// a hand-off anyone can act on, and on a seat tier the reader rejects an anonymous one outright.
|
|
66
|
+
writeFileSync(tmp, JSON.stringify({ tier, seat, pid: process.pid, disarmedAt: new Date().toISOString() }) + "\n");
|
|
47
67
|
renameSync(tmp, p);
|
|
48
|
-
return `${tier} DISARMED —
|
|
68
|
+
return `${tier} DISARMED — ${seat} handed off; status reads it stood down, not dead`;
|
|
49
69
|
}
|
|
50
70
|
const isTier = (v) => SUPERVISION_TIERS.includes(v ?? "");
|
|
@@ -105,11 +105,11 @@ Outside multi-agent environments, run the loop directly.
|
|
|
105
105
|
|
|
106
106
|
### Version preflight
|
|
107
107
|
|
|
108
|
-
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version\`, read \`package.json\` version, and if the
|
|
108
|
+
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version\`, read \`package.json\` version, and stop if the versions differ anywhere (major, minor, or patch); the binary and repository must agree on the entire version, so binary \`2.1.0\` versus repository \`2.1.1\` is a stop. Tell the operator to update or link the correct binary. Never proceed on hope — stale binaries silently skip daemon gates. Also verify no run is live in this repository before starting one: lead with this repository's \`.tickmarkr/graph.lock\`, read its holder pid, and treat it as live until \`kill -0 <pid>\` proves that holder dead. Never require a machine-wide process pattern to be empty: a lawful run in another repository — or the probing shell's own argv — can match. If you use a process probe as secondary evidence, exclude the probing process, resolve every candidate's own cwd (for example with \`lsof -a -p <pid> -d cwd\`), and count only candidates whose cwd is this repository root.
|
|
109
109
|
|
|
110
110
|
### Tip-verify-before-green
|
|
111
111
|
|
|
112
|
-
A run is green only when the run-end event exists in the journal
|
|
112
|
+
A run is green only when ALL of these hold: the run-end event exists in the journal, tip verify is not "failed", AND the run-end summary's \`failed\`, \`human\`, \`blocked\` and \`pending\` buckets are every one of them empty. The shorter "run-end plus tip verify" form is NOT sufficient: a run ending \`done=[T1,T3,T4] human=[T2] tipVerify=passed\` is a closed PARTIAL run — three of four delivered, one parked — and calling it green is how a park becomes invisible. Never report green to the operator, tab titles, or records until every clause holds; when one does not, name the bucket that is not empty.
|
|
113
113
|
|
|
114
114
|
### Verified handoffs
|
|
115
115
|
|
|
@@ -203,6 +203,24 @@ function skillDrift(cwd) {
|
|
|
203
203
|
}
|
|
204
204
|
// Name what drifted. "stale" alone sends the reader to diff three directories by hand, and the file that
|
|
205
205
|
// matters is usually the one they never knew shipped.
|
|
206
|
+
// The GENERATED GUIDANCE BLOCK goes stale by exactly the shape skillDrift exists for: the package
|
|
207
|
+
// moves, the rendered copy does not, and nothing in the repo changes to hint at it. Worse than a skill,
|
|
208
|
+
// because a doc the host reads as law can be gitignored — this project's own AGENTS.md kept two
|
|
209
|
+
// superseded preflight rules (patch-version drift tolerated, a machine-wide process match required)
|
|
210
|
+
// through the very release that fixed the tracked copy, and a consultation was launched under the stale
|
|
211
|
+
// text with nobody able to see it. Same rule as skillDrift: a doc that was never written is not drift.
|
|
212
|
+
const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
213
|
+
if (!existsSync(docPath))
|
|
214
|
+
return false;
|
|
215
|
+
const current = readFileSync(docPath, "utf8");
|
|
216
|
+
const begin = current.indexOf(DOCS_BEGIN);
|
|
217
|
+
if (begin < 0)
|
|
218
|
+
return false; // no block installed here — --docs writes one, that is not drift
|
|
219
|
+
const end = current.indexOf(DOCS_END, begin);
|
|
220
|
+
if (end < 0)
|
|
221
|
+
return true; // a truncated block is stale by definition
|
|
222
|
+
return current.slice(begin, end + DOCS_END.length) !== AGENT_DOCS.trimEnd();
|
|
223
|
+
});
|
|
206
224
|
const describeDrift = (d) => [
|
|
207
225
|
d.missing.length ? `missing ${d.missing.join(", ")}` : "",
|
|
208
226
|
d.modified.length ? `modified ${d.modified.join(", ")}` : "",
|
|
@@ -361,6 +379,15 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
361
379
|
notes.push(`stale ${join(d.dir, "SKILL.md")} — ${describeDrift(d)}; run tickmarkr init --agent --force to refresh it`);
|
|
362
380
|
}
|
|
363
381
|
}
|
|
382
|
+
// Same reasoning, one layer up — and the refresh command is `--agent --force`, the same one the skill
|
|
383
|
+
// note names. `--docs` governs only the APPEND path for a repo with no block yet; replacing an existing
|
|
384
|
+
// block is gated on force (see the marked-region branch in installAgentFiles). Naming `--docs` here
|
|
385
|
+
// would send the reader to a flag that silently does nothing for the case they are in.
|
|
386
|
+
if (!(values.agent && values.force)) {
|
|
387
|
+
for (const docPath of docsDrift(cwd)) {
|
|
388
|
+
notes.push(`stale ${docPath} — its tickmarkr guidance block does not match this version; run tickmarkr init --agent --force to refresh it`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
364
391
|
const input = io.input ?? process.stdin;
|
|
365
392
|
const output = io.output ?? process.stdout;
|
|
366
393
|
const interactive = input.isTTY === true && output.isTTY === true && !(values.yes ?? false);
|
|
@@ -8,6 +8,7 @@ import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../.
|
|
|
8
8
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
9
9
|
import { foldActivity } from "../../run/activity.js";
|
|
10
10
|
import { Journal, engagementComparable, isQualityFailureParkKind, recordedTaskFailureKind, runHasEnded, } from "../../run/journal.js";
|
|
11
|
+
import { isPidLive } from "../../run/lock.js";
|
|
11
12
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
12
13
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
13
14
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
@@ -617,6 +618,12 @@ const daemonPid = (events) => {
|
|
|
617
618
|
}
|
|
618
619
|
return undefined;
|
|
619
620
|
};
|
|
621
|
+
const daemonLiveness = (events) => {
|
|
622
|
+
const pid = daemonPid(events);
|
|
623
|
+
if (pid === undefined)
|
|
624
|
+
return { state: "unknown" };
|
|
625
|
+
return { state: isPidLive(pid) ? "alive" : "dead", pid };
|
|
626
|
+
};
|
|
620
627
|
const terminalFailureCause = (events) => {
|
|
621
628
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
622
629
|
const e = events[i];
|
|
@@ -792,27 +799,19 @@ const keepPanesSetting = (cwd) => {
|
|
|
792
799
|
return DEFAULT_CONFIG.visibility.keepPanes;
|
|
793
800
|
}
|
|
794
801
|
};
|
|
795
|
-
const liveness = (events, now = Date.now()) => {
|
|
802
|
+
const liveness = (events, daemon, now = Date.now()) => {
|
|
796
803
|
const last = events.at(-1);
|
|
797
804
|
if (!last)
|
|
798
805
|
return "last event unknown · daemon pid unknown";
|
|
799
806
|
const age = fmtAge(now - Date.parse(last.ts));
|
|
800
|
-
const pid = daemonPid(events);
|
|
801
807
|
const cause = terminalFailureCause(events);
|
|
802
|
-
if (
|
|
808
|
+
if (daemon.state === "unknown")
|
|
803
809
|
return `last event ${age} ago · daemon pid unknown${cause ? ` · ${cause}` : ""}`;
|
|
804
810
|
// a dead pid after run-end is a clean exit, not a crash — "dead" is only alarming (red) while
|
|
805
811
|
// the run is still incomplete (operator's crash indicator)
|
|
806
812
|
const ended = events.some((e) => e.event === "run-end");
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
process.kill(pid, 0);
|
|
810
|
-
state = "alive";
|
|
811
|
-
} // no throw ⇒ alive
|
|
812
|
-
catch (k) {
|
|
813
|
-
state = k.code === "ESRCH" ? (ended ? "finished" : "dead") : "alive";
|
|
814
|
-
} // EPERM ⇒ alive
|
|
815
|
-
return `last event ${age} ago · daemon pid ${pid} ${state}${cause ? ` · ${cause}` : ""}`;
|
|
813
|
+
const state = daemon.state === "alive" ? "alive" : ended ? "finished" : "dead";
|
|
814
|
+
return `last event ${age} ago · daemon pid ${daemon.pid} ${state}${cause ? ` · ${cause}` : ""}`;
|
|
816
815
|
};
|
|
817
816
|
// Resume's shared comparator remains the fail-closed baseline. Status may additionally accept the
|
|
818
817
|
// daemon's audited graph-rehash release, but only when its `from` names that baseline identity (or
|
|
@@ -899,6 +898,9 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
899
898
|
const record = readRunRecord(cwd, g, namedRunId);
|
|
900
899
|
const runId = record?.runId;
|
|
901
900
|
const events = record?.events ?? [];
|
|
901
|
+
// The header and every live-phase consumer share this exact process reading. Proved death retires
|
|
902
|
+
// stale phase markers; unknown identity deliberately does not, preserving pre-identity journals.
|
|
903
|
+
const daemon = daemonLiveness(events);
|
|
902
904
|
const comparable = record?.comparable ?? false;
|
|
903
905
|
const rehashAt = record?.rehashAt;
|
|
904
906
|
const assignments = new Map();
|
|
@@ -972,7 +974,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
972
974
|
? workerPaneLocators(events, runId, keepPanesSetting(cwd))
|
|
973
975
|
: new Map();
|
|
974
976
|
const taskIds = new Set(g.tasks.map((task) => task.id));
|
|
975
|
-
const phases = comparable ? livePhases(events) : new Map();
|
|
977
|
+
const phases = comparable && daemon.state !== "dead" ? livePhases(events) : new Map();
|
|
976
978
|
for (const taskId of phases.keys())
|
|
977
979
|
if (!taskIds.has(taskId))
|
|
978
980
|
phases.delete(taskId);
|
|
@@ -1022,7 +1024,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1022
1024
|
});
|
|
1023
1025
|
const zone = journalRowsOnly ? `${divider}zone ${localZoneLabel(zoneReference)}` : "";
|
|
1024
1026
|
const header = runId
|
|
1025
|
-
? `tickmarkr status${divider}run ${runId}${zone}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, now).replaceAll(" · ", divider)}${verify ? `${divider}${verify}` : ""}${divider}${tipPhase ? `${done}/${total} tasks done${divider}run not verified` : `${done}/${total} done`}`
|
|
1027
|
+
? `tickmarkr status${divider}run ${runId}${zone}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, daemon, now).replaceAll(" · ", divider)}${verify ? `${divider}${verify}` : ""}${divider}${tipPhase ? `${done}/${total} tasks done${divider}run not verified` : `${done}/${total} done`}`
|
|
1026
1028
|
: `tickmarkr status${zone}${divider}no runs yet${divider}${done}/${total} done`;
|
|
1027
1029
|
const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
1028
1030
|
return {
|
|
@@ -1046,7 +1048,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1046
1048
|
const tally = tipPhase
|
|
1047
1049
|
? `${progressTone(`${progressGlyph} ${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
1048
1050
|
: done === total && total > 0 ? ok(`${done}/${total} done`) : information(`${done}/${total} done`);
|
|
1049
|
-
const live = liveness(events, now)
|
|
1051
|
+
const live = liveness(events, daemon, now)
|
|
1050
1052
|
.replace(/\bdead\b/u, fail("dead"))
|
|
1051
1053
|
.replace(/\bfinished\b/u, dim("finished"))
|
|
1052
1054
|
.replace(/\balive\b/u, running("alive"))
|
|
@@ -104,7 +104,15 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
104
104
|
}
|
|
105
105
|
const wantAcceptance = acceptance.length > 0 && !values["no-acceptance"];
|
|
106
106
|
const wantReview = !values["no-review"];
|
|
107
|
-
|
|
107
|
+
// A gate that enforced nothing must not print a green row. `files` has exactly three sources —
|
|
108
|
+
// explicit --files, a compiled task's own files[], or nothing — and only the third leaves the
|
|
109
|
+
// allowlist empty, where scopeGate passes as "no file scope declared — unrestricted"
|
|
110
|
+
// (scope.ts:41). An honest `details` string is no defence: the ROW is what gets quoted, and
|
|
111
|
+
// quoting it launders a check that never ran. So scope filters on availability exactly as
|
|
112
|
+
// acceptance and review do below — the report omits the gate rather than crediting one that
|
|
113
|
+
// gated no allowlist. (Narrowing the empty allowlist to the changed set is the same green row by
|
|
114
|
+
// another mechanism, and hides the same fact.)
|
|
115
|
+
const gates = GATE_NAMES.filter((g) => (g !== "acceptance" || wantAcceptance) && (g !== "review" || wantReview) && (g !== "scope" || files.length > 0));
|
|
108
116
|
const task = {
|
|
109
117
|
id: "VERIFY", title: "standalone verification", goal, shape: "implement", complexity: 5,
|
|
110
118
|
deps: [], files, context: [], acceptance: acceptance.length ? acceptance : ["(deterministic verification only)"],
|
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 (--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 auto|herdr|subprocess|orca --route-strict; orca runs only when named)\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 beat <tier> record one supervision beat for orchestrator|overseer|watch (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop\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 auto|herdr|subprocess|orca --route-strict; orca runs only when named)\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 beat <tier> record one supervision beat for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop\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
|
@@ -43,7 +43,7 @@ usage: tickmarkr <command>
|
|
|
43
43
|
profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)
|
|
44
44
|
ui open the Fleet Studio TUI (full-screen tabbed cockpit)
|
|
45
45
|
unlock remove a stale/garbage run lock (refuses if the holder is alive)
|
|
46
|
-
beat <tier> record one supervision beat for orchestrator|overseer|watch (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop
|
|
46
|
+
beat <tier> record one supervision beat for orchestrator|orchestrator-context|overseer|overseer-context|watch, --seat <identity> required (--stand-down to hand off); a supervising seat's own watcher loop calls it, and status reads the tier STALE once the beats stop
|
|
47
47
|
approve <id> <task> release a park (--uphold sides with the reviewer and funds a fixed attempt; --by <name> --reason <text>); takes effect on resume`;
|
|
48
48
|
// pure, testable dispatcher: resolves a command, forwards argv, shapes the result — no side effects.
|
|
49
49
|
// unknown/missing cmd → USAGE (exit 1 if a cmd was typed, 0 for bare `tickmarkr`); a handler throw becomes
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface BaselineCommand {
|
|
|
12
12
|
missingCommand?: boolean;
|
|
13
13
|
/** What this command actually took at capture, on a pristine tree. Absent in pre-v1.90 baselines. */
|
|
14
14
|
durationMs?: number;
|
|
15
|
+
/** Sum of the per-file durations named by the runner; null when its output names none. */
|
|
16
|
+
fileDurationSumMs?: number | null;
|
|
17
|
+
/** fileDurationSumMs / durationMs — average implied file concurrency, not a configured fork count. */
|
|
18
|
+
impliedParallelism?: number | null;
|
|
19
|
+
/** The slowest per-file entry named by the runner; null when per-file timing is unavailable. */
|
|
20
|
+
longestFile?: BaselineFileDuration | null;
|
|
15
21
|
/** The ceiling that measurement implies, persisted so every later battery uses the same number. */
|
|
16
22
|
ceilingMs?: number;
|
|
17
23
|
/**
|
|
@@ -20,6 +26,10 @@ export interface BaselineCommand {
|
|
|
20
26
|
*/
|
|
21
27
|
infra?: true;
|
|
22
28
|
}
|
|
29
|
+
export interface BaselineFileDuration {
|
|
30
|
+
file: string;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
23
33
|
export interface Baseline {
|
|
24
34
|
commands: Record<string, BaselineCommand>;
|
|
25
35
|
warnings?: BaselineWarning[];
|
package/dist/gates/baseline.js
CHANGED
|
@@ -24,6 +24,12 @@ const SUMMARY_FAIL_RE = /^\s*(?:Tests?\s+(?:Files?\s+)?(?:\d+|#)\s+failed|#\s+fa
|
|
|
24
24
|
const ERROR_ANCHOR_RE = /^\s*(?:Error|[A-Za-z_$][\w$]*Error):\s+\S/;
|
|
25
25
|
const TSC_ERROR_RE = /^\s*\S.*\((?:\d+|#),(?:\d+|#)\):\s+error\s+[A-Z]+(?:\d+|#):/i; // tsc
|
|
26
26
|
const LINTER_ERROR_RE = /^\s*(?:\d+|#):(?:\d+|#)\s+error\s+\S/; // eslint stylish
|
|
27
|
+
// T5: package.json selecting oxlint's stylish format is this repository's local remedy because the
|
|
28
|
+
// existing LINTER_ERROR_RE already reads it. PATH_FIRST_LINTER_ERROR_RE is the shipped fix: it reads
|
|
29
|
+
// the default `path:line:column: error …` form for every user without requiring their script to change.
|
|
30
|
+
// Keep the path at line start — accepting a position mid-sentence turns ordinary runner prose into a
|
|
31
|
+
// manufactured fresh failure.
|
|
32
|
+
const PATH_FIRST_LINTER_ERROR_RE = /^\s*\S+:(?:\d+|#):(?:\d+|#):\s+error\b(?:\s+\S)?/;
|
|
27
33
|
// The failure shapes non-Vitest runners name their tests with: pytest's short summary
|
|
28
34
|
// (`FAILED tests/t.py::test_x - AssertionError`, `ERROR tests/t.py::fixture`), go test
|
|
29
35
|
// (`--- FAIL: TestFoo (0.00s)`), TAP / node:test (`not ok 1 - name`, whose summary line lands in
|
|
@@ -97,7 +103,7 @@ const namesFailureEitherForm = (l) => {
|
|
|
97
103
|
return stripped !== undefined && namesFailure(stripped);
|
|
98
104
|
};
|
|
99
105
|
const isFailureShaped = (l) => namesFailure(l) || SUMMARY_FAIL_RE.test(l) || ERROR_ANCHOR_RE.test(l)
|
|
100
|
-
|| TSC_ERROR_RE.test(l) || LINTER_ERROR_RE.test(l) || X_GLYPH_FAIL_RE.test(l);
|
|
106
|
+
|| TSC_ERROR_RE.test(l) || LINTER_ERROR_RE.test(l) || PATH_FIRST_LINTER_ERROR_RE.test(l) || X_GLYPH_FAIL_RE.test(l);
|
|
101
107
|
const VOCAB_RE = /\b(?:error|fail(?:ed|ure|ing)?)\b/i;
|
|
102
108
|
// T9 — the infra/regression discriminator. A runner that died because the MACHINE ran out of
|
|
103
109
|
// processes, file descriptors or memory never finished asking the question, so its nonzero exit is
|
|
@@ -128,6 +134,33 @@ export function classifyFailureOutput(output) {
|
|
|
128
134
|
return lines.some(isInfraLine) ? "infra" : undefined;
|
|
129
135
|
}
|
|
130
136
|
const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
137
|
+
// Vitest's default reporter names file durations as
|
|
138
|
+
// `✓ |project| tests/example.test.ts (12 tests) 1.23s` (❯ for a red file). The runner may be
|
|
139
|
+
// turbo-prefixed, but a test-name timing has no parenthesized file tally and therefore cannot enter
|
|
140
|
+
// this measurement. These are observations of runner output, not a promise that every runner exposes
|
|
141
|
+
// them — callers record null, never zero, when no line matches.
|
|
142
|
+
const FILE_DURATION_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)[✓✔×❯]\s+(?:\|[^|\r\n]+\|\s+)?(\S+)\s+\([^\r\n)]*\)\s+(\d+(?:\.\d+)?)\s*(ms|s|m)\b/;
|
|
143
|
+
const durationUnitMs = (unit) => unit === "m" ? 60_000 : unit === "s" ? 1_000 : 1;
|
|
144
|
+
function fileTiming(output, wallClockMs) {
|
|
145
|
+
const files = [];
|
|
146
|
+
for (const line of output.split("\n")) {
|
|
147
|
+
const match = FILE_DURATION_RE.exec(line.replace(ANSI_RE, ""));
|
|
148
|
+
if (!match)
|
|
149
|
+
continue;
|
|
150
|
+
const durationMs = Number(match[2]) * durationUnitMs(match[3]);
|
|
151
|
+
if (Number.isFinite(durationMs))
|
|
152
|
+
files.push({ file: match[1], durationMs });
|
|
153
|
+
}
|
|
154
|
+
if (!files.length)
|
|
155
|
+
return { fileDurationSumMs: null, impliedParallelism: null, longestFile: null };
|
|
156
|
+
const fileDurationSumMs = files.reduce((sum, entry) => sum + entry.durationMs, 0);
|
|
157
|
+
const longestFile = files.reduce((longest, entry) => entry.durationMs > longest.durationMs ? entry : longest);
|
|
158
|
+
return {
|
|
159
|
+
fileDurationSumMs,
|
|
160
|
+
impliedParallelism: wallClockMs > 0 ? fileDurationSumMs / wallClockMs : null,
|
|
161
|
+
longestFile,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
131
164
|
// A failing command whose output holds no shape any runner here names. The marker is content-free and
|
|
132
165
|
// constant: downstream consumers (tip verify journals fingerprint counts) still see that the command
|
|
133
166
|
// failed, while no line of the output — narration, status strip, stack trace — can enter the set. Being
|
|
@@ -363,16 +396,26 @@ export async function captureBaseline(cwd, commands) {
|
|
|
363
396
|
console.error(`tickmarkr: baseline capture for "${name}" was killed at its ${CAPTURE_CEILING_MS}ms ceiling — `
|
|
364
397
|
+ `it recorded NO fingerprints, so nothing is forgiven and every gate will treat a pre-existing `
|
|
365
398
|
+ `failure as a fresh one. Raise the ceiling or shorten the command.`);
|
|
366
|
-
base.commands[name] = {
|
|
399
|
+
base.commands[name] = {
|
|
400
|
+
infra: true,
|
|
401
|
+
fingerprints: [],
|
|
402
|
+
durationMs,
|
|
403
|
+
fileDurationSumMs: null,
|
|
404
|
+
impliedParallelism: null,
|
|
405
|
+
longestFile: null,
|
|
406
|
+
ceilingMs: effectiveCeilingMs({ durationMs }),
|
|
407
|
+
};
|
|
367
408
|
continue;
|
|
368
409
|
}
|
|
410
|
+
const raw = (r.stdout + "\n" + r.stderr).split(cwd).join("");
|
|
369
411
|
base.commands[name] = {
|
|
370
412
|
exitCode: r.code,
|
|
371
413
|
// a command that exits 0 has no failures to fingerprint — recording any would be a lie the
|
|
372
414
|
// compare step then has to forgive
|
|
373
|
-
fingerprints: r.code === 0 ? [] : fingerprint(
|
|
415
|
+
fingerprints: r.code === 0 ? [] : fingerprint(raw),
|
|
374
416
|
missingCommand: missingConfiguredCommand(cmd, r),
|
|
375
417
|
durationMs,
|
|
418
|
+
...fileTiming(raw, durationMs),
|
|
376
419
|
ceilingMs: effectiveCeilingMs({ durationMs }),
|
|
377
420
|
};
|
|
378
421
|
}
|
package/dist/run/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
2
3
|
export { ROUTING_ENV_SEAMS };
|
|
3
4
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
@@ -42,6 +43,10 @@ export interface ShResult {
|
|
|
42
43
|
timedOut?: boolean;
|
|
43
44
|
durationMs?: number;
|
|
44
45
|
}
|
|
46
|
+
export declare const setSpawnForTests: (fn: typeof spawn) => void;
|
|
47
|
+
export declare const resetSpawnForTests: () => void;
|
|
48
|
+
export declare const SPAWN_ATTEMPT_LIMIT = 4;
|
|
49
|
+
export declare const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
45
50
|
export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
46
51
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
47
52
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|