tickmarkr 2.6.5 → 2.6.7
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/README.md +7 -3
- package/dist/adapters/claude-code.js +8 -2
- package/dist/adapters/codex-commit-check.d.ts +16 -4
- package/dist/adapters/codex-commit-check.js +150 -26
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +20 -8
- package/dist/adapters/types.d.ts +4 -1
- package/dist/cli/commands/beat.d.ts +6 -1
- package/dist/cli/commands/beat.js +123 -76
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/fleet.js +368 -85
- package/dist/cli/commands/init.js +23 -9
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/verify.js +37 -9
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +10 -5
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/config/fleet-overlay.d.ts +14 -0
- package/dist/config/fleet-overlay.js +154 -25
- package/dist/drivers/orca.d.ts +17 -3
- package/dist/drivers/orca.js +64 -20
- package/dist/gates/baseline.d.ts +9 -0
- package/dist/gates/baseline.js +14 -3
- package/dist/gates/cache.js +1 -1
- package/dist/gates/review.js +15 -0
- package/dist/gates/run-gates.d.ts +20 -7
- package/dist/gates/run-gates.js +345 -210
- package/dist/gates/test-manifest.d.ts +11 -0
- package/dist/gates/test-manifest.js +151 -111
- package/dist/gates/timeout-shaped.d.ts +3 -0
- package/dist/gates/timeout-shaped.js +10 -0
- package/dist/run/beat-lifecycle.d.ts +154 -0
- package/dist/run/beat-lifecycle.js +825 -0
- package/dist/run/daemon.js +202 -126
- package/dist/run/git-trust.d.ts +27 -0
- package/dist/run/git-trust.js +178 -0
- package/dist/run/git.js +34 -14
- package/dist/run/journal.js +15 -3
- package/dist/run/lease.d.ts +23 -0
- package/dist/run/lease.js +232 -38
- package/dist/run/merge.js +2 -2
- package/dist/run/stall.js +20 -1
- package/dist/run/supervision.d.ts +8 -0
- package/dist/run/supervision.js +24 -4
- package/dist/run/verification-job.d.ts +51 -0
- package/dist/run/verification-job.js +137 -0
- package/dist/tui/ink/fleet-app.d.ts +35 -5
- package/dist/tui/ink/fleet-app.js +244 -124
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +19 -10
- package/skills/tickmarkr-loop/SKILL.md +19 -10
- package/skills/tickmarkr-overseer/SKILL.md +25 -16
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +58 -16
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +19 -6
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +121 -64
|
@@ -1,99 +1,146 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, locateSupervisionRoot, newSupervisionArm, publishStandDown, readSupervisionArm, resolveSupervisionRoot, } from "../../run/supervision.js";
|
|
3
|
+
import { BEAT_GENERATION_ENV, beatStatus, defaultBeatDeps, startBeat, stopBeat, withLegacyClaim, } from "../../run/beat-lifecycle.js";
|
|
4
|
+
// Two forms share one strict parser. The lifecycle verbs `start|stop|status <tier>` own a DETACHED loop
|
|
5
|
+
// (src/run/beat-lifecycle.ts); the legacy `<tier>` form keeps its duties: one-shot ticks reuse a durable
|
|
6
|
+
// arm, only --new-arm (or --loop) acknowledges a stand-down, and --status is the read-only status verb.
|
|
7
|
+
// Every refusal is NONZERO: the shipped watcher gates on the exit code with stdout discarded, so a refusal
|
|
8
|
+
// that exits 0 leaves a live watcher believing it is armed while status reads DISARMED — and a loop that is
|
|
9
|
+
// stood down, superseded or cannot write exits nonzero with its reason instead of going silently quiet.
|
|
10
|
+
const VERBS = ["start", "stop", "status"];
|
|
11
|
+
const LEGACY_WRITER_OPTIONS = ["new-arm", "loop", "arm-id", "pct", "threshold-pct", "stand-down"];
|
|
12
|
+
const usage = (got) => `usage: tickmarkr beat <start|stop|status> <tier> --seat <identity> | tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity> ` +
|
|
13
|
+
`[--new-arm] [--loop] [--arm-id <identity> --pct <0..100> --threshold-pct <0..100>] [--stand-down] [--status] — ` +
|
|
14
|
+
`got ${got ? `\`${got}\`` : "no tier"}`;
|
|
15
|
+
export async function beat(argv, cwd = process.cwd(), deps = defaultBeatDeps()) {
|
|
16
|
+
const parsed = parseBeat(argv);
|
|
17
|
+
const { verb, tier, seat, values } = parsed;
|
|
18
|
+
if (verb === "status" || values.status) {
|
|
19
|
+
// Read-only by construction: locating the root never creates it, and a repository without a state
|
|
20
|
+
// dir has, truthfully, no beat recorded.
|
|
21
|
+
const { root, state } = locateSupervisionRoot(cwd);
|
|
22
|
+
return state ? beatStatus(root, tier, seat, deps) : { out: `${tier} ABSENT — no state directory`, code: 0 };
|
|
23
23
|
}
|
|
24
24
|
// SUP-05: NO SEAT, NO BEAT — and the refusal comes before any write, so a refused invocation leaves
|
|
25
|
-
// the tier exactly as it found it.
|
|
26
|
-
//
|
|
27
|
-
// attribute to a seat. Measured 2026-08-26: a consult seat ran the documented loop verbatim and the
|
|
28
|
-
// board read that tier ARMED with no seat of that tier having armed anything, and a seatless ARMED
|
|
29
|
-
// reads as coverage — worse than ABSENT, because ABSENT sends someone to look.
|
|
25
|
+
// the tier exactly as it found it. Tier + writer + instant is a beat nobody can attribute to a seat
|
|
26
|
+
// (measured 2026-08-26), and a seatless ARMED reads as coverage — worse than ABSENT.
|
|
30
27
|
if (!seat) {
|
|
31
|
-
throw new Error(`${
|
|
28
|
+
throw new Error(`${verb ? `${verb} ${tier}` : tier} needs --seat <identity> — a beat that names no seat arms a tier nobody occupies` +
|
|
32
29
|
" (pass the seat's own pane id or agent name)");
|
|
33
30
|
}
|
|
34
31
|
// A beat names repository state, never the caller's incidental directory. Resolution is read-only
|
|
35
32
|
// and happens before every write, so a non-repository invocation cannot create the state it claims.
|
|
36
33
|
const repoRoot = resolveSupervisionRoot(cwd);
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (verb === "start")
|
|
35
|
+
return startBeat(repoRoot, tier, seat, deps);
|
|
36
|
+
if (verb === "stop")
|
|
37
|
+
return stopBeat(repoRoot, tier, seat, deps);
|
|
38
|
+
const generation = deps.env[BEAT_GENERATION_ENV]?.trim() || undefined;
|
|
39
|
+
const writer = (settled = false, arm) => ({ generation, seat, settled, arm, deps });
|
|
40
|
+
if (values["stand-down"]) {
|
|
41
|
+
return withLegacyClaim(repoRoot, tier, writer(), (recheck) => { recheck(); return standDownTier(repoRoot, tier, seat); });
|
|
42
|
+
}
|
|
43
|
+
const loop = values.loop === true;
|
|
44
|
+
const armId = values["arm-id"];
|
|
45
|
+
const pct = percentage(values.pct, "--pct");
|
|
46
|
+
const thresholdPct = percentage(values["threshold-pct"], "--threshold-pct") ?? SUPERVISION_DEFAULT_THRESHOLD_PCT;
|
|
41
47
|
// Context arm ids retain their existing obligation semantics; they do not implicitly re-arm liveness.
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const observe = (arm) => pct === undefined ? undefined : { armId: armId ?? arm?.armId ?? seat, pct, thresholdPct };
|
|
49
|
+
// EVERY mutation — the one-shot tick, the arm, and each later loop tick — runs under the tier claim, and
|
|
50
|
+
// `recheck` re-reads that claim and the tier's ownership immediately before the write: a generation-carrying
|
|
51
|
+
// writer mutates only while its COMPLETE generation (token, seat, pid once settled, and the owner's durable
|
|
52
|
+
// arm id + epoch) is on record; every other writer only while no detached beat owns the tier. `created` marks
|
|
53
|
+
// the arm this same claim just published: an owned re-arm rebinds the owner record to it — only while that arm
|
|
54
|
+
// and the owner checked before it are still the ones on disk — and is checked again as that new generation
|
|
55
|
+
// before its beat. A running loop carries its arm into every check. A claim someone else holds refuses with
|
|
56
|
+
// no bytes changed.
|
|
57
|
+
const tick = (recheck, arm, created = false) => {
|
|
58
|
+
recheck(arm, created);
|
|
59
|
+
if (beatSupervision(repoRoot, tier, seat, observe(arm), { arm, armId, loop }))
|
|
60
|
+
return;
|
|
61
|
+
const current = arm && readSupervisionArm(repoRoot, tier);
|
|
62
|
+
if (arm && (current?.armId !== arm.armId || current.armEpoch !== arm.armEpoch)) {
|
|
63
|
+
throw new Error(`${tier} superseded — a newer arm replaced this loop's arm ${arm.armId}; exiting`);
|
|
49
64
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
throw new Error(`${tier} DISARMED — stood down; use --new-arm to resume beating`);
|
|
66
|
+
};
|
|
67
|
+
if (values["new-arm"] || loop) {
|
|
68
|
+
const arm = await withLegacyClaim(repoRoot, tier, writer(), (recheck) => {
|
|
69
|
+
recheck();
|
|
70
|
+
const created = newSupervisionArm(repoRoot, tier, armId);
|
|
71
|
+
tick(recheck, created, true);
|
|
72
|
+
return created;
|
|
73
|
+
});
|
|
74
|
+
// ponytail: a running loop outlasts a brief claim holder (a repeat start, a status-side stop) for ~5 s of
|
|
75
|
+
// retries, then refuses nonzero; a per-tick deadline if holders ever legitimately run longer.
|
|
76
|
+
const waits = { tries: 20, sleep: deps.sleep };
|
|
77
|
+
while (loop) {
|
|
78
|
+
await deps.sleep(SUPERVISION_BEAT_MS);
|
|
79
|
+
await withLegacyClaim(repoRoot, tier, writer(true, arm), (recheck) => tick(recheck, arm), waits);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
await withLegacyClaim(repoRoot, tier, writer(), (recheck) => tick(recheck));
|
|
84
|
+
return `${tier} 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`;
|
|
60
85
|
}
|
|
61
|
-
/**
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
/** Unknown, duplicate, conflicting or empty options refuse here — before anything is read or written. */
|
|
87
|
+
function parseBeat(argv) {
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = parseArgs({
|
|
91
|
+
args: argv, allowPositionals: true, strict: true, tokens: true,
|
|
92
|
+
options: {
|
|
93
|
+
seat: { type: "string" }, "new-arm": { type: "boolean" }, loop: { type: "boolean" },
|
|
94
|
+
"arm-id": { type: "string" }, pct: { type: "string" }, "threshold-pct": { type: "string" },
|
|
95
|
+
"stand-down": { type: "boolean" }, status: { type: "boolean" },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new Error(`${error.message} — ${usage(undefined).replace(/ — got .*$/, "")}`);
|
|
101
|
+
}
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
for (const token of parsed.tokens) {
|
|
104
|
+
if (token.kind !== "option")
|
|
105
|
+
continue;
|
|
106
|
+
if (seen.has(token.name))
|
|
107
|
+
throw new Error(`duplicate --${token.name} — pass each option once`);
|
|
108
|
+
seen.add(token.name);
|
|
109
|
+
if (typeof token.value === "string" && !token.value.trim())
|
|
110
|
+
throw new Error(`--${token.name} needs a non-empty value`);
|
|
111
|
+
}
|
|
112
|
+
const values = parsed.values;
|
|
113
|
+
const [first, ...rest] = parsed.positionals;
|
|
114
|
+
const verb = VERBS.includes(first ?? "") ? first : undefined;
|
|
115
|
+
const named = verb ? rest[0] : first;
|
|
116
|
+
if (!isTier(named))
|
|
117
|
+
throw new Error(usage(verb && named === undefined ? `${verb} with no tier` : named));
|
|
118
|
+
const extra = (verb ? rest.slice(1) : rest)[0];
|
|
119
|
+
if (extra !== undefined)
|
|
120
|
+
throw new Error(`unexpected argument \`${extra}\` — ${usage(named).replace(/ — got .*$/, "")}`);
|
|
121
|
+
const readOnly = verb !== undefined || values.status;
|
|
122
|
+
const writer = LEGACY_WRITER_OPTIONS.find((name) => values[name] !== undefined);
|
|
123
|
+
if (readOnly && writer)
|
|
124
|
+
throw new Error(`--${writer} conflicts with ${verb ? `beat ${verb}` : "--status"}`);
|
|
125
|
+
if (verb && values.status)
|
|
126
|
+
throw new Error(`--status conflicts with beat ${verb}`);
|
|
127
|
+
const withStandDown = values["stand-down"] && LEGACY_WRITER_OPTIONS.find((name) => name !== "stand-down" && values[name] !== undefined);
|
|
128
|
+
if (withStandDown)
|
|
129
|
+
throw new Error(`--${withStandDown} conflicts with --stand-down`);
|
|
130
|
+
return { ...(verb ? { verb } : {}), tier: named, ...(values.seat ? { seat: values.seat.trim() } : {}), values };
|
|
67
131
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
if (raw === undefined) {
|
|
71
|
-
if (argv.includes(name) || argv.some((a) => a.startsWith(`${name}=`))) {
|
|
72
|
-
throw new Error(`${name} needs a number from 0 through 100`);
|
|
73
|
-
}
|
|
132
|
+
function percentage(raw, name) {
|
|
133
|
+
if (raw === undefined)
|
|
74
134
|
return undefined;
|
|
75
|
-
}
|
|
76
135
|
const value = Number(raw);
|
|
77
136
|
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
78
137
|
throw new Error(`${name} must be a number from 0 through 100`);
|
|
79
138
|
}
|
|
80
139
|
return value;
|
|
81
140
|
}
|
|
82
|
-
|
|
83
|
-
// so the tier reads DISARMED rather than ageing out as a death. Published atomically — written aside,
|
|
84
|
-
// renamed over — because a torn marker is rejected by the reader, and a rejected stand-down reports a
|
|
85
|
-
// deliberate hand-off as a death.
|
|
141
|
+
/** Record an explicit hand-off: the tier reads DISARMED, a stood-down tier, not a dead one. */
|
|
86
142
|
export function standDownTier(repoRoot, tier, seat) {
|
|
87
|
-
|
|
88
|
-
const p = supervisionStandDownPath(repoRoot, tier);
|
|
89
|
-
const tmp = `${p}.${randomUUID()}.tmp`;
|
|
90
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
91
|
-
// The marker names the seat for the same reason the beat does: "someone stood this tier down" is not
|
|
92
|
-
// a hand-off anyone can act on, and on a seat tier the reader rejects an anonymous one outright.
|
|
93
|
-
writeFileSync(tmp, JSON.stringify({
|
|
94
|
-
tier, seat, standDownId: randomUUID(), exitedWriterPid: process.pid, disarmedAt: new Date().toISOString(),
|
|
95
|
-
}) + "\n");
|
|
96
|
-
renameSync(tmp, p);
|
|
143
|
+
publishStandDown(repoRoot, tier, seat);
|
|
97
144
|
return `${tier} DISARMED — ${seat} handed off; status reads it stood down, not dead`;
|
|
98
145
|
}
|
|
99
146
|
const isTier = (v) => SUPERVISION_TIERS.includes(v ?? "");
|
|
@@ -15,6 +15,7 @@ import { HerdrDriver } from "../../drivers/herdr.js";
|
|
|
15
15
|
import { ORCA_FIXTURE_VERSION, parseEnvelope, resolveOrcaCliBinary } from "../../drivers/orca.js";
|
|
16
16
|
import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
17
17
|
import { recordCodexCommit } from "../../adapters/codex-commit-check.js";
|
|
18
|
+
import { codex } from "../../adapters/codex.js";
|
|
18
19
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, observedSeat, preferRanks } from "../../route/preference.js";
|
|
19
20
|
import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
20
21
|
import { sh } from "../../run/git.js";
|
|
@@ -672,7 +673,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
672
673
|
const h = health[a.id];
|
|
673
674
|
// OBS-503: a crash-on-version verdict carries its reason in note — print it instead of the lie.
|
|
674
675
|
const state = !h.installed ? (h.note ?? "not installed") : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
|
|
675
|
-
|
|
676
|
+
// v2.6.6 T9 (K): a Codex sandbox escape is a BLOCKING security warning — never a passing row
|
|
677
|
+
const healthy = h.installed && (a.id !== kimi.id || h.authed) && !(a.id === codex.id && h.codexCommit === "escape");
|
|
676
678
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
677
679
|
});
|
|
678
680
|
const reviewDemotions = recentReviewDemotions(cwd);
|