tickmarkr 2.2.1 → 2.4.0
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 +10 -9
- package/dist/adapters/catalog-remote.d.ts +1 -4
- package/dist/adapters/catalog-remote.js +52 -42
- package/dist/adapters/catalog.js +5 -3
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +8 -5
- package/dist/adapters/model-lints.d.ts +9 -5
- package/dist/adapters/model-lints.js +56 -15
- package/dist/adapters/model-windows.js +11 -0
- package/dist/adapters/prompt.js +1 -0
- package/dist/adapters/qwen.d.ts +5 -0
- package/dist/adapters/qwen.js +153 -0
- package/dist/adapters/types.d.ts +21 -1
- package/dist/adapters/types.js +43 -2
- package/dist/cli/commands/approve.js +5 -4
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/compile.js +32 -6
- package/dist/cli/commands/doctor.d.ts +9 -4
- package/dist/cli/commands/doctor.js +87 -13
- package/dist/cli/commands/fleet.d.ts +4 -0
- package/dist/cli/commands/fleet.js +53 -14
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +45 -7
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +45 -1
- package/dist/cli/commands/verify.d.ts +6 -0
- package/dist/cli/commands/verify.js +145 -25
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/compile/collateral.d.ts +2 -9
- package/dist/compile/collateral.js +17 -18
- package/dist/compile/index.d.ts +4 -1
- package/dist/compile/index.js +41 -7
- package/dist/compile/native.d.ts +4 -2
- package/dist/compile/native.js +58 -9
- package/dist/compile/ownership.js +34 -9
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +52 -6
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +11 -1
- package/dist/drivers/index.d.ts +6 -0
- package/dist/drivers/index.js +19 -4
- package/dist/drivers/orca.d.ts +35 -1
- package/dist/drivers/orca.js +260 -20
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +12 -0
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +47 -11
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +25 -9
- package/dist/gates/review.d.ts +7 -3
- package/dist/gates/review.js +61 -22
- package/dist/gates/run-gates.d.ts +5 -2
- package/dist/gates/run-gates.js +50 -26
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/route/preference.d.ts +4 -0
- package/dist/route/preference.js +40 -0
- package/dist/route/router.js +15 -2
- package/dist/run/consult.d.ts +1 -0
- package/dist/run/consult.js +39 -8
- package/dist/run/daemon.d.ts +16 -0
- package/dist/run/daemon.js +345 -74
- package/dist/run/git.d.ts +3 -0
- package/dist/run/git.js +40 -5
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +73 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/fleet-app.d.ts +4 -0
- package/dist/tui/ink/fleet-app.js +45 -16
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +59 -1
- package/skills/tickmarkr-overseer/SKILL.md +77 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
@@ -8,20 +9,20 @@ import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../
|
|
|
8
9
|
import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
9
10
|
import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
10
11
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
12
|
+
import { orcaHostDetected } from "../../drivers/index.js";
|
|
11
13
|
import { Journal } from "../../run/journal.js";
|
|
12
14
|
import { doctor } from "./doctor.js";
|
|
13
15
|
import { assembleFleetEditor } from "./fleet.js";
|
|
14
16
|
const SCAFFOLD_SPEC = "tickmarkr.spec.md";
|
|
15
17
|
// Operator-approved (2026-07-17) environments footer — no npm install for herdr (npm package
|
|
16
|
-
// "herdr" is a reserved 0.0.0 placeholder as of that date).
|
|
17
|
-
//
|
|
18
|
-
// herdr-else-subprocess and never picks it, so the footer names it beside the other two choices.
|
|
18
|
+
// "herdr" is a reserved 0.0.0 placeholder as of that date). Auto resolves herdr → orca →
|
|
19
|
+
// subprocess; the orca leg is environment-only, using the two markers Orca stamps in its terminals.
|
|
19
20
|
const ENVIRONMENTS_FOOTER = [
|
|
20
21
|
"environments:",
|
|
21
|
-
" herdr — the full cockpit — every worker, judge, and consult is a visible pane you can watch and unblock · https://herdr.dev",
|
|
22
|
-
" orca — visible terminals in the Orca app — an
|
|
22
|
+
" herdr — the full cockpit — every worker, judge, and consult is a visible pane you can watch and unblock · https://herdr.dev · auto picks it first when HERDR_ENV=1",
|
|
23
|
+
" orca — visible terminals in the Orca app — auto picks it inside an Orca terminal after herdr and before subprocess; set driver: orca to force it · https://onorca.dev",
|
|
23
24
|
" claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively",
|
|
24
|
-
" anywhere — no herdr? same fail-closed gates, headless subprocess driver",
|
|
25
|
+
" anywhere — no herdr or Orca terminal? same fail-closed gates, headless subprocess driver (legacy: no herdr? same fail-closed gates, headless subprocess driver when Orca markers are absent)",
|
|
25
26
|
].join("\n");
|
|
26
27
|
/** Latest journal without a run-end event, if any. */
|
|
27
28
|
function activeRunId(cwd) {
|
|
@@ -140,17 +141,6 @@ you were handed the whole picture — list the repository root, open every guida
|
|
|
140
141
|
direction each moves, what makes a criterion real, and why an absence or a source-text grep is never a
|
|
141
142
|
criterion. Read it before authoring or repairing acceptance items.
|
|
142
143
|
- **Editing \`src/gates/\`, \`src/compile/\` or \`src/graph/\`?** Read \`docs/codebase/ARCHITECTURE.md\` first.
|
|
143
|
-
- **EVERY fix gets a ship/no-ship decision, recorded, at the moment it is made.** Ask one question of each
|
|
144
|
-
one: *does a user hit this defect?* If yes, the fix belongs in \`src/**\` or \`skills/**\` — the only trees
|
|
145
|
-
the package carries (\`files: [dist, schema, skills, fixtures]\`). A script, overlay, config entry or
|
|
146
|
-
operator-side workaround that resolves the symptom **locally is not the fix; it is a decision to leave
|
|
147
|
-
every other user broken**, and it must say so in writing and name the condition that removes it.
|
|
148
|
-
**The default answer is SHIP.** A local remedy is the exception and carries the burden of proof.
|
|
149
|
-
Watch for the three shapes this hides in: a fix applied where you happened to be standing rather than
|
|
150
|
-
where the defect lives; an observation filed with a product fix named in its own text and queued
|
|
151
|
-
nowhere; and a local tool that quietly grows into a product feature nobody shipped. **A defect and its
|
|
152
|
-
fix must be recorded in the same place, or the queue silently becomes a list of things everyone assumed
|
|
153
|
-
someone else had shipped.**
|
|
154
144
|
${DOCS_END}
|
|
155
145
|
`;
|
|
156
146
|
// Every applicable host location gets the skills, each paired with its own repository guidance
|
|
@@ -229,7 +219,28 @@ const describeDrift = (d) => [
|
|
|
229
219
|
// should not be asked again just because a copy drifted. Staleness is a different question with a
|
|
230
220
|
// different answer (`--agent --force`), and it is reported by name on every init path below.
|
|
231
221
|
const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
|
|
232
|
-
const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : "auto";
|
|
222
|
+
const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : orcaHostDetected() ? "orca" : "auto";
|
|
223
|
+
function packageName(cwd) {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
226
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function gitTracked(cwd, path) {
|
|
233
|
+
const rel = relative(cwd, path);
|
|
234
|
+
if (rel.startsWith(".."))
|
|
235
|
+
return false;
|
|
236
|
+
try {
|
|
237
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd, stdio: "ignore" });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
233
244
|
async function installAgentFiles(cwd, force, docs, notes) {
|
|
234
245
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
235
246
|
let prompt;
|
|
@@ -257,13 +268,14 @@ async function installAgentFiles(cwd, force, docs, notes) {
|
|
|
257
268
|
notes.push(`kept current ${dest}`);
|
|
258
269
|
continue;
|
|
259
270
|
}
|
|
271
|
+
const tracked = exists && gitTracked(cwd, dest);
|
|
260
272
|
if (exists && !force && !(await confirm(`Overwrite ${dest} (${describeDrift(stale)})?`))) {
|
|
261
|
-
notes.push(`skipped existing ${dest} — ${describeDrift(stale)}; pass --force to overwrite it`);
|
|
273
|
+
notes.push(`skipped existing ${tracked ? "tracked " : ""}${dest} — ${describeDrift(stale)}; pass --force to overwrite it`);
|
|
262
274
|
continue;
|
|
263
275
|
}
|
|
264
276
|
// whole skill dir, not just SKILL.md — the overseer ships its pane-watcher script
|
|
265
277
|
cpSync(fileURLToPath(new URL(`../../../skills/${skill}`, import.meta.url)), join(skillsDir, skill), { recursive: true });
|
|
266
|
-
notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
|
|
278
|
+
notes.push(`${exists ? "overwrote" : "wrote"} ${tracked ? "tracked " : ""}${dest}`);
|
|
267
279
|
}
|
|
268
280
|
const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
|
|
269
281
|
// The block is CANONICAL guidance ("stated once here, restated nowhere"), so a repo that has
|
|
@@ -341,6 +353,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
341
353
|
});
|
|
342
354
|
// banner at START on the visual surface — every init path, not just the wizard, and never trailing the probe (operator report 2026-07-17)
|
|
343
355
|
emitBanner();
|
|
356
|
+
if (packageName(cwd) === "tickmarkr") {
|
|
357
|
+
throw new Error("OBS-584: tickmarkr init refuses to run inside the tickmarkr source repository");
|
|
358
|
+
}
|
|
344
359
|
const gdir = values["global-dir"] ?? globalConfigDir();
|
|
345
360
|
mkdirSync(gdir, { recursive: true });
|
|
346
361
|
const notes = [];
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
2
5
|
import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints, ttyVisual } from "../../adapters/model-lints.js";
|
|
3
6
|
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
4
|
-
import { parseArgs } from "node:util";
|
|
5
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
6
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
7
9
|
import { DEFAULT_CONFIG, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
8
10
|
import { loadGraph } from "../../graph/graph.js";
|
|
9
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
10
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
11
|
-
import { excludedChannels, exclusionLine } from "../../route/preference.js";
|
|
13
|
+
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
12
14
|
import { staffLedEvidence } from "../../route/profile.js";
|
|
13
15
|
import { route, RoutingError } from "../../route/router.js";
|
|
14
16
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
15
|
-
import { modelId } from "../../gates/review.js";
|
|
17
|
+
import { modelId, modelProvider } from "../../gates/review.js";
|
|
16
18
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
17
19
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
18
20
|
import { shq } from "../../adapters/types.js";
|
|
@@ -106,10 +108,15 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
106
108
|
}
|
|
107
109
|
const g = loadGraph(cwd);
|
|
108
110
|
const oracleRefusals = new Map();
|
|
111
|
+
const oracleAdvisories = new Map();
|
|
109
112
|
if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
|
|
110
113
|
const listing = await (opts.listTests ?? listVitestTests)(cwd);
|
|
111
114
|
for (const task of g.tasks) {
|
|
112
115
|
const refusals = [];
|
|
116
|
+
const advisories = [];
|
|
117
|
+
const authoredTest = task.files
|
|
118
|
+
.map((path) => path.replace(/^\.\//, ""))
|
|
119
|
+
.find((path) => /^tests\/.+\.test\.ts$/.test(path) && !/[?*{[]/.test(path) && !existsSync(join(cwd, path)));
|
|
113
120
|
if (listing.status === "failed") {
|
|
114
121
|
if (task.acceptance.some((item) => typeof item === "object" && item.oracle === "test")) {
|
|
115
122
|
refusals.push(`acceptance oracle unresolved — runner listing failed: ${listing.error.split("\n")[0]}`);
|
|
@@ -117,7 +124,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
117
124
|
}
|
|
118
125
|
else {
|
|
119
126
|
for (const audit of auditNamedTestOracles(task.acceptance, listing.tests)) {
|
|
120
|
-
if (audit.matches.length === 0) {
|
|
127
|
+
if (audit.matches.length === 0 && authoredTest) {
|
|
128
|
+
advisories.push(`acceptance oracle ${JSON.stringify(audit.criterion)} not yet written (worker authors ${authoredTest})`);
|
|
129
|
+
}
|
|
130
|
+
else if (audit.matches.length === 0) {
|
|
121
131
|
refusals.push(`acceptance oracle ${JSON.stringify(audit.criterion)} matches zero runner-listed test names`);
|
|
122
132
|
}
|
|
123
133
|
else if (audit.matches.length > 1) {
|
|
@@ -127,6 +137,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
127
137
|
}
|
|
128
138
|
if (refusals.length)
|
|
129
139
|
oracleRefusals.set(task.id, refusals);
|
|
140
|
+
if (advisories.length)
|
|
141
|
+
oracleAdvisories.set(task.id, advisories);
|
|
130
142
|
}
|
|
131
143
|
}
|
|
132
144
|
const inputFindings = await taskInputFindings(g.tasks, cwd);
|
|
@@ -141,7 +153,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
141
153
|
// readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
|
|
142
154
|
const cached = readDoctor(cwd);
|
|
143
155
|
const health = cached ?? (await probeAll(adapters));
|
|
144
|
-
const
|
|
156
|
+
const pools = rolePools(cfg, adapters, health);
|
|
157
|
+
const channels = pools.worker;
|
|
145
158
|
// VIS-04 trust ramp (VALIDATION 13-01-11): preview:true bypasses the routing.learned:off short-circuit so
|
|
146
159
|
// the static-vs-learned comparison renders even when the daemon's learned routing is off. Cold ⇒ undefined ⇒
|
|
147
160
|
// output byte-identical to today.
|
|
@@ -163,6 +176,13 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
163
176
|
harnessLine(resolveHarness(harnessFrom)),
|
|
164
177
|
"",
|
|
165
178
|
];
|
|
179
|
+
const judgeDeny = disallowedBy(cfg.judge, cfg.routing, "judge");
|
|
180
|
+
if (judgeDeny?.by === "deny") {
|
|
181
|
+
lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
|
|
182
|
+
}
|
|
183
|
+
const entrySeats = routingEntrySeatLines(cfg);
|
|
184
|
+
if (entrySeats.length)
|
|
185
|
+
lines.push(...entrySeats, "");
|
|
166
186
|
const derivation = (shape) => {
|
|
167
187
|
const floor = cfg.routing.floors[shape];
|
|
168
188
|
return floor ? ` floor ${floor} ← ${mode.provenance[shape] ?? "config floors"}` : null;
|
|
@@ -223,6 +243,22 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
223
243
|
lints.push(...modelLints(cfg, health, adapters, { tty: ttyVisual() })); // health may be pre-v1.5/probeAll-fallback — no-detection branch covers both
|
|
224
244
|
// v1.54 T3: dead-steering sweep — advisory only, renders with the routing lints, never alters routing.
|
|
225
245
|
lints.push(...preferEntryLints(cfg, health, overlayPreferShapes(cwd)));
|
|
246
|
+
const preferMatches = (entry, pool) => pool.some((c) => entry === c.adapter || entry === `${c.adapter}:${c.model}`);
|
|
247
|
+
const lintInertPrefer = (surface, entries, poolName, pool) => {
|
|
248
|
+
for (const entry of entries ?? []) {
|
|
249
|
+
if (!preferMatches(entry, pool))
|
|
250
|
+
lints.push(`${surface} '${entry}' resolves to no live channel of ${poolName} pool`);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
for (const [shape, entry] of Object.entries(cfg.routing.map)) {
|
|
254
|
+
lintInertPrefer(`routing.map.${shape}.prefer`, entry.prefer, "worker", pools.worker);
|
|
255
|
+
}
|
|
256
|
+
lintInertPrefer("review.prefer", cfg.review.prefer, "review", pools.review);
|
|
257
|
+
lintInertPrefer("consult.prefer", cfg.consult.prefer, "consult", pools.consult);
|
|
258
|
+
const reviewProviders = new Set(pools.review.map((c) => modelProvider(c.model, c.vendor)));
|
|
259
|
+
if (pools.review.length > 0 && reviewProviders.size < 2) {
|
|
260
|
+
lints.push(`review pool concentration: ${pools.review.length} channel(s), ${reviewProviders.size} provider(s)`);
|
|
261
|
+
}
|
|
226
262
|
// The guard here was `channels.length && !fleetCanCrossVendorReview(channels)`, so the review lint went
|
|
227
263
|
// SILENT at zero channels — the first-run state, where review.required is set and nothing can route at all.
|
|
228
264
|
// Deleting that clause outright is worse than the silence: it fires "no cross-vendor reviewer pair in fleet"
|
|
@@ -233,7 +269,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
233
269
|
if (cfg.review.required && channels.length === 0) {
|
|
234
270
|
lints.push("review: review.required is set but no channel can route — the fleet is empty; install or authenticate an adapter");
|
|
235
271
|
}
|
|
236
|
-
else if (cfg.review.required && !fleetCanCrossVendorReview(
|
|
272
|
+
else if (cfg.review.required && !fleetCanCrossVendorReview(pools.review)) {
|
|
237
273
|
lints.push("review: no cross-vendor reviewer pair in fleet — set review.required: false to waive");
|
|
238
274
|
}
|
|
239
275
|
let cost = 0;
|
|
@@ -283,6 +319,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
283
319
|
lines.push(d);
|
|
284
320
|
lints.push(`${t.id}: unroutable — ${msg}`);
|
|
285
321
|
}
|
|
322
|
+
for (const advisory of oracleAdvisories.get(t.id) ?? [])
|
|
323
|
+
lines.push(` ! ${advisory}`);
|
|
286
324
|
}
|
|
287
325
|
const inputWarnings = inputFindings.filter((finding) => finding.severity === "warn");
|
|
288
326
|
if (inputWarnings.length) {
|
|
@@ -4,6 +4,7 @@ import { ttyVisual } from "../../adapters/model-lints.js";
|
|
|
4
4
|
import { addUsage } from "../../adapters/types.js";
|
|
5
5
|
import { dim, rule, title } from "../../brand.js";
|
|
6
6
|
import { loadConfig } from "../../config/config.js";
|
|
7
|
+
import { modelProvider } from "../../gates/review.js";
|
|
7
8
|
import { buildProofBundle, gateDeclined } from "../../report/bundle.js";
|
|
8
9
|
import { compareRuns } from "../../report/compare.js";
|
|
9
10
|
import { estimateCosts } from "../../report/cost.js";
|
|
@@ -35,6 +36,22 @@ const channelLabel = (data) => {
|
|
|
35
36
|
const { adapter, model } = a;
|
|
36
37
|
return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : EM;
|
|
37
38
|
};
|
|
39
|
+
const modelFromChannel = (channel) => channel.slice(channel.indexOf(":") + 1);
|
|
40
|
+
const reviewerIdentity = (data) => {
|
|
41
|
+
const details = typeof data.details === "string" ? data.details : "";
|
|
42
|
+
const fromDetails = /reviewer\s+([^\s;()]+:[^\s;()]+)/i.exec(details)?.[1];
|
|
43
|
+
const reviewer = typeof data.reviewer === "string" ? data.reviewer : fromDetails;
|
|
44
|
+
if (!reviewer || !reviewer.includes(":"))
|
|
45
|
+
return undefined;
|
|
46
|
+
const vendor = typeof data.vendor === "string"
|
|
47
|
+
? data.vendor
|
|
48
|
+
: /vendor:\s*([^;()]+)/i.exec(details)?.[1]?.trim()
|
|
49
|
+
?? /\(([^;:()]+)\):/.exec(details)?.[1]?.trim();
|
|
50
|
+
const provider = typeof data.provider === "string"
|
|
51
|
+
? data.provider
|
|
52
|
+
: modelProvider(modelFromChannel(reviewer), vendor ?? "unknown");
|
|
53
|
+
return { reviewer, provider, ...(vendor ? { vendor } : {}) };
|
|
54
|
+
};
|
|
38
55
|
const rateBasis = (row) => {
|
|
39
56
|
if (!row.rate)
|
|
40
57
|
return EM;
|
|
@@ -245,6 +262,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
245
262
|
const dispatches = events.filter((e) => e.taskId === taskId && e.event === "task-dispatch");
|
|
246
263
|
const channels = dispatches.map((e) => channelLabel(e.data));
|
|
247
264
|
const gates = events.filter((e) => e.taskId === taskId && e.event === "gate-result");
|
|
265
|
+
const leg2 = events.filter((e) => e.taskId === taskId && e.event === "review-leg2");
|
|
248
266
|
const consults = events.filter((e) => e.taskId === taskId && e.event === "consult-verdict");
|
|
249
267
|
const deviations = events.filter((e) => e.taskId === taskId && e.event === "route-deviation");
|
|
250
268
|
const merge = [...events].reverse().find((e) => e.taskId === taskId && e.event === "merge");
|
|
@@ -267,10 +285,28 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
|
267
285
|
if (gates.length) {
|
|
268
286
|
for (const g of gates) {
|
|
269
287
|
// T11: a declined gate never ran — report it as declined, never as a pass.
|
|
270
|
-
|
|
288
|
+
let pass = gateDeclined(g.data) ? "declined" : g.data.pass === true ? "pass" : g.data.pass === false ? "fail" : EM;
|
|
271
289
|
const gate = typeof g.data.gate === "string" ? g.data.gate : EM;
|
|
290
|
+
if (gate === "review" && pass === "pass") {
|
|
291
|
+
const reviewer = reviewerIdentity(g.data);
|
|
292
|
+
const before = events.slice(0, events.indexOf(g) + 1).reverse()
|
|
293
|
+
.find((e) => e.taskId === taskId && e.event === "task-dispatch");
|
|
294
|
+
const author = channelLabel(before?.data ?? {});
|
|
295
|
+
const authorProvider = author === EM ? "unknown" : modelProvider(modelFromChannel(author));
|
|
296
|
+
if (reviewer && authorProvider !== "unknown" && authorProvider === reviewer.provider) {
|
|
297
|
+
pass = "pass (same-provider — independence not established)";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
272
300
|
lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}`);
|
|
273
301
|
}
|
|
302
|
+
for (const row of leg2) {
|
|
303
|
+
const pass = row.data.pass === true ? "pass" : row.data.pass === false ? "fail" : EM;
|
|
304
|
+
const identity = reviewerIdentity(row.data);
|
|
305
|
+
const identityText = identity
|
|
306
|
+
? `reviewer ${identity.reviewer}${identity.vendor ? ` (vendor: ${identity.vendor}; provider: ${identity.provider})` : ` (provider: ${identity.provider})`}`
|
|
307
|
+
: firstLine(row.data.details);
|
|
308
|
+
lines.push(` - review-leg2: ${pass} (supersedes daemon review) — ${identityText}`);
|
|
309
|
+
}
|
|
274
310
|
}
|
|
275
311
|
else
|
|
276
312
|
lines.push(` - ${EM}`);
|
|
@@ -21,6 +21,7 @@ export declare const GATE_KEYS: {
|
|
|
21
21
|
readonly acceptance: "A";
|
|
22
22
|
readonly review: "R";
|
|
23
23
|
};
|
|
24
|
+
export declare const STATUS_HELP = "usage: tickmarkr status [<runId>] [--watch [--events]] [--oneline]\nWith --watch --events, stdout is one JSON document per decision event and stderr carries keepalive lines.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the JSON document stream.";
|
|
24
25
|
export declare const decisionEventsFromJournal: (events: JournalEvent[], runId: string, stateDir?: string) => DecisionEvent[];
|
|
25
26
|
export declare const taskBox: (status: TaskStatus) => string;
|
|
26
27
|
export declare const gateBox: (state: "open" | "pass" | "fail" | "skip", unicode: boolean) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { GLYPHS, LIVE } from "../../brand.js";
|
|
4
5
|
import { DEFAULT_CONFIG, loadConfig } from "../../config/config.js";
|
|
@@ -108,6 +109,11 @@ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
|
|
|
108
109
|
const ASCII_SPINNER = ["|", "/", "-", "\\"];
|
|
109
110
|
const SAVE_TERMINAL_TITLE = "\x1b[22;0t";
|
|
110
111
|
const RESTORE_TERMINAL_TITLE = "\x1b[23;0t";
|
|
112
|
+
const ENTER_ALTERNATE_SCREEN = "\x1b[?1049h";
|
|
113
|
+
const LEAVE_ALTERNATE_SCREEN = "\x1b[?1049l";
|
|
114
|
+
export const STATUS_HELP = `usage: tickmarkr status [<runId>] [--watch [--events]] [--oneline]
|
|
115
|
+
With --watch --events, stdout is one JSON document per decision event and stderr carries keepalive lines.
|
|
116
|
+
Do not merge stdout and stderr (for example with 2>&1): doing so corrupts the JSON document stream.`;
|
|
111
117
|
const decisionEvidence = (stateDir, runId, sequence) => `${stateDir}/runs/${runId}/journal.jsonl#L${sequence}`;
|
|
112
118
|
const DECISION_GATE_VERDICTS = {
|
|
113
119
|
passed: "passed",
|
|
@@ -1109,7 +1115,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1109
1115
|
const tally = tipPhase
|
|
1110
1116
|
? `${progressTone(`${progressGlyph} ${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
1111
1117
|
: done === total && total > 0 ? ok(`${done}/${total} done`) : information(`${done}/${total} done`);
|
|
1112
|
-
const live = liveness(events, daemon, now)
|
|
1118
|
+
const live = `${liveness(events, daemon, now)} · load1 ${os.loadavg()[0].toFixed(2)}`
|
|
1113
1119
|
.replace(/\bdead\b/u, fail("dead"))
|
|
1114
1120
|
.replace(/\bfinished\b/u, dim("finished"))
|
|
1115
1121
|
.replace(/\balive\b/u, running("alive"))
|
|
@@ -1324,6 +1330,8 @@ const oneLine = (cwd, namedRunId) => {
|
|
|
1324
1330
|
return sanitizeTaskText([record.runId, ...claims].join(" · "));
|
|
1325
1331
|
};
|
|
1326
1332
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
1333
|
+
if (argv.some((arg) => arg === "--help" || arg === "-h"))
|
|
1334
|
+
return STATUS_HELP;
|
|
1327
1335
|
const namedRunId = positionalRunId(argv);
|
|
1328
1336
|
if (argv.includes("--oneline"))
|
|
1329
1337
|
return oneLine(cwd, namedRunId);
|
|
@@ -1456,6 +1464,13 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1456
1464
|
// a dead board keeps writing and reads ARMED. That is the over-claiming direction, the one an operator
|
|
1457
1465
|
// acts on, and the one this instrument exists to close.
|
|
1458
1466
|
const armed = bounded ? undefined : armWatchSupervision(cwd, opts.supervisionBeatMs);
|
|
1467
|
+
let alternateScreen = false;
|
|
1468
|
+
const leaveAlternateScreen = () => {
|
|
1469
|
+
if (!alternateScreen)
|
|
1470
|
+
return;
|
|
1471
|
+
alternateScreen = false;
|
|
1472
|
+
process.stdout.write(LEAVE_ALTERNATE_SCREEN);
|
|
1473
|
+
};
|
|
1459
1474
|
let titleSaved = false;
|
|
1460
1475
|
const restoreTitle = () => {
|
|
1461
1476
|
if (!titleSaved)
|
|
@@ -1478,6 +1493,28 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1478
1493
|
}
|
|
1479
1494
|
process.stdout.write(`\x1b]0;⏳ ${hotPhase.taskId} ${phaseLabel(hotPhase)} ${fmtElapsed(nowMs - hotPhase.startedAt)}\x07`);
|
|
1480
1495
|
};
|
|
1496
|
+
const signalHandlers = [];
|
|
1497
|
+
const exitFromSignal = (code) => () => {
|
|
1498
|
+
if (titleSaved) {
|
|
1499
|
+
process.removeListener("exit", restoreTitle);
|
|
1500
|
+
restoreTitle();
|
|
1501
|
+
}
|
|
1502
|
+
if (alternateScreen) {
|
|
1503
|
+
process.removeListener("exit", leaveAlternateScreen);
|
|
1504
|
+
leaveAlternateScreen();
|
|
1505
|
+
}
|
|
1506
|
+
process.exit(code);
|
|
1507
|
+
};
|
|
1508
|
+
if (!bounded && !eventStream && tty) {
|
|
1509
|
+
process.stdout.write(ENTER_ALTERNATE_SCREEN);
|
|
1510
|
+
alternateScreen = true;
|
|
1511
|
+
process.once("exit", leaveAlternateScreen);
|
|
1512
|
+
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) {
|
|
1513
|
+
const handler = exitFromSignal(code);
|
|
1514
|
+
signalHandlers.push([signal, handler]);
|
|
1515
|
+
process.once(signal, handler);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1481
1518
|
try {
|
|
1482
1519
|
for (let i = 0; i < iterations; i++) {
|
|
1483
1520
|
const nowMs = now();
|
|
@@ -1490,6 +1527,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1490
1527
|
if (bounded)
|
|
1491
1528
|
eventLines.push(line);
|
|
1492
1529
|
}
|
|
1530
|
+
process.stderr.write("status: keepalive\n");
|
|
1493
1531
|
}
|
|
1494
1532
|
else {
|
|
1495
1533
|
frame = renderFrame(cwd, binaryVersion, nowMs, i, workerLiveness, true, namedRunId);
|
|
@@ -1525,6 +1563,12 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1525
1563
|
process.removeListener("exit", restoreTitle);
|
|
1526
1564
|
restoreTitle();
|
|
1527
1565
|
}
|
|
1566
|
+
for (const [signal, handler] of signalHandlers)
|
|
1567
|
+
process.removeListener(signal, handler);
|
|
1568
|
+
if (alternateScreen) {
|
|
1569
|
+
process.removeListener("exit", leaveAlternateScreen);
|
|
1570
|
+
leaveAlternateScreen();
|
|
1571
|
+
}
|
|
1528
1572
|
}
|
|
1529
1573
|
return bounded ? eventStream ? eventLines.join("\n") : frames.join(sep) : "";
|
|
1530
1574
|
}
|
|
@@ -4,6 +4,12 @@ export declare function parseCriteria(text: string): AcceptanceItem[];
|
|
|
4
4
|
export declare const HUMAN_CHANNEL: BillingChannel;
|
|
5
5
|
export declare const HUMAN_AUTHOR: Assignment;
|
|
6
6
|
export declare function verifyStateDir(cwd: string): string;
|
|
7
|
+
/** Linked worktrees share operational state with the repository that owns the common git dir. */
|
|
8
|
+
export declare function verifyStateRoot(cwd: string): Promise<string>;
|
|
9
|
+
/** Cache identity is repository content, never a checkout path. */
|
|
10
|
+
export declare function baselineCachePath(cwd: string, baseSha: string, commands: Record<string, string>): string;
|
|
11
|
+
export declare function excludeAuthorProvider(channels: BillingChannel[], author: BillingChannel): BillingChannel[];
|
|
12
|
+
export declare const VERIFY_HELP = "usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]\nThe final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.";
|
|
7
13
|
export declare function verify(argv: string[], cwd?: string): Promise<{
|
|
8
14
|
out: string;
|
|
9
15
|
code: number;
|