tickmarkr 2.0.0 → 2.1.1
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 +19 -1
- package/dist/cli/commands/doctor.d.ts +19 -0
- package/dist/cli/commands/doctor.js +59 -0
- package/dist/cli/commands/init.js +5 -2
- package/dist/cli/commands/resume.js +17 -6
- package/dist/cli/commands/run.js +5 -2
- package/dist/cli/commands/status.js +30 -6
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +2 -2
- package/dist/drivers/herdr.d.ts +8 -8
- package/dist/drivers/herdr.js +29 -32
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/index.js +16 -1
- package/dist/drivers/orca.d.ts +189 -0
- package/dist/drivers/orca.js +879 -0
- package/dist/gates/baseline.d.ts +19 -0
- package/dist/gates/baseline.js +28 -1
- package/dist/run/git.d.ts +24 -5
- package/dist/run/git.js +25 -6
- package/dist/tui/ink/init-app.js +14 -4
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +85 -6
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +26 -4
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +9 -5
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ tickmarkr is a spec-driven orchestration harness for AI coding agent CLIs. You w
|
|
|
13
13
|
acceptance criteria; the engine routes tasks to the best installed agent CLI (claude-code, codex,
|
|
14
14
|
cursor-agent, opencode, grok, pi, kimi) by cost and capability, dispatches work in git worktrees for
|
|
15
15
|
change isolation — as interactive TUIs when running under [herdr](https://herdr.dev), headless
|
|
16
|
-
subprocesses otherwise
|
|
16
|
+
subprocesses otherwise, or in [Orca](https://onorca.dev) terminals when you name that driver
|
|
17
|
+
yourself — and independently verifies each committed result by checking for no new
|
|
17
18
|
baseline failures per task, then strictly verifying the integration tip. Green tasks consolidate onto a
|
|
18
19
|
`tickmarkr/<runId>` branch; merging to your mainline is always your call, never automated. Engage
|
|
19
20
|
with full visibility into routing decisions, worker progress, and gate verdicts — or run headless
|
|
@@ -250,6 +251,23 @@ and first-attempt success rate. Cost reporting follows strict honesty rules and
|
|
|
250
251
|
When running under [herdr](https://herdr.dev), tickmarkr creates a labeled pane-and-tab workspace
|
|
251
252
|
for real-time visibility (optional — omit `--driver herdr` or run headless if preferred).
|
|
252
253
|
|
|
254
|
+
### Orca: an explicit-selection execution surface
|
|
255
|
+
|
|
256
|
+
[Orca](https://onorca.dev) is the third execution surface, and the only one you must ask for by
|
|
257
|
+
name: `--driver orca` or `driver: orca` in config. `--driver auto` never selects it — auto picks
|
|
258
|
+
herdr when a herdr session is live and subprocess otherwise — so Orca is never inherited from an
|
|
259
|
+
ambient environment variable, and an Orca that is installed but unreachable is not silently
|
|
260
|
+
downgraded to a hidden subprocess worker either. Naming it is the whole gate; its runtime failures
|
|
261
|
+
stay Orca's, reported as failures.
|
|
262
|
+
|
|
263
|
+
What Orca supplies is terminals. What tickmarkr keeps is everything that decides whether work
|
|
264
|
+
ships: **it creates and owns the git worktree** for every task (Orca is told which checkout to bind
|
|
265
|
+
its terminal to, and never makes one), **it runs the full gate battery** — build, test, lint,
|
|
266
|
+
evidence, scope, acceptance, review — against the commits that land there, and **it holds merge
|
|
267
|
+
authority**, consolidating only green tasks onto the run's `tickmarkr/<runId>` integration branch.
|
|
268
|
+
Orca is given no say over any of the three. Merging that branch to your mainline remains your call,
|
|
269
|
+
exactly as with every other driver.
|
|
270
|
+
|
|
253
271
|
tickmarkr borrows audit-firm vocabulary for its roles: **you** are the *Partner* (final sign-off),
|
|
254
272
|
workers are the *field team*, the acceptance judge is the *EQR* (engagement quality reviewer), and
|
|
255
273
|
the frontier-model consult is the *National Office*. The terms below use that vocabulary:
|
|
@@ -2,6 +2,7 @@ import { type ClaudeAlias } from "../../adapters/claude-code.js";
|
|
|
2
2
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
3
3
|
import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
|
|
4
4
|
import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
|
|
5
|
+
import { type ShResult } from "../../run/git.js";
|
|
5
6
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
6
7
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
7
8
|
export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -15,7 +16,24 @@ export type DoctorOpts = {
|
|
|
15
16
|
/** init's between-acts surface: status rows only — the model matrix and inline drift stay
|
|
16
17
|
* behind `tickmarkr doctor` (files are still written; only the RETURNED string shrinks). */
|
|
17
18
|
compact?: boolean;
|
|
19
|
+
/** Test seam for the same `orca status --json` transport production invokes. */
|
|
20
|
+
orcaStatusProbe?: (cwd: string, binary: string) => Promise<ShResult>;
|
|
21
|
+
/** Test seam for shell-path discovery; absence remains a normal doctor row, never an exception. */
|
|
22
|
+
resolveOrcaBinary?: (cwd: string) => string | undefined;
|
|
18
23
|
};
|
|
24
|
+
type OrcaCapability = {
|
|
25
|
+
verdict: "pass" | "fail";
|
|
26
|
+
detail: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Orca's status body is deliberately interpreted by T1's one shared envelope parser. Doctor owns
|
|
30
|
+
* only capability presentation: it may classify an absent executable, but it never invents a second
|
|
31
|
+
* permissive JSON reader for a malformed or refused status response.
|
|
32
|
+
*
|
|
33
|
+
* Capability-row `detail` stays hermetic — never `OrcaError.message`, which embeds volatile CLI
|
|
34
|
+
* stderr (Electron timestamps), so the row is byte-stable across runs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function probeOrcaCapability(cwd: string, opts?: Pick<DoctorOpts, "orcaStatusProbe" | "resolveOrcaBinary">): Promise<OrcaCapability>;
|
|
19
37
|
export declare function runnerIgnoreFinding(cwd: string): {
|
|
20
38
|
verdict: "pass" | "warn";
|
|
21
39
|
detail: string;
|
|
@@ -71,3 +89,4 @@ export declare function selfShadowFinding(ownVersion: string, cwd?: string, reso
|
|
|
71
89
|
*/
|
|
72
90
|
export declare function liveBenchStalenessFinding(now: Date): string | undefined;
|
|
73
91
|
export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
|
|
92
|
+
export {};
|
|
@@ -6,14 +6,17 @@ import { detectPackageManager, turboContinueFindings } from "../../gates/baselin
|
|
|
6
6
|
import { version } from "./version.js";
|
|
7
7
|
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, resolveShellBinary, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
8
8
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
9
|
+
import { shq } from "../../adapters/types.js";
|
|
9
10
|
import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
10
11
|
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
11
12
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
12
13
|
import { loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
13
14
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
15
|
+
import { parseEnvelope } from "../../drivers/orca.js";
|
|
14
16
|
import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
15
17
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
16
18
|
import { LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
19
|
+
import { sh } from "../../run/git.js";
|
|
17
20
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
18
21
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
19
22
|
export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -21,6 +24,57 @@ export const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
|
|
|
21
24
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
22
25
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
23
26
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
27
|
+
/**
|
|
28
|
+
* Orca's status body is deliberately interpreted by T1's one shared envelope parser. Doctor owns
|
|
29
|
+
* only capability presentation: it may classify an absent executable, but it never invents a second
|
|
30
|
+
* permissive JSON reader for a malformed or refused status response.
|
|
31
|
+
*
|
|
32
|
+
* Capability-row `detail` stays hermetic — never `OrcaError.message`, which embeds volatile CLI
|
|
33
|
+
* stderr (Electron timestamps), so the row is byte-stable across runs.
|
|
34
|
+
*/
|
|
35
|
+
export async function probeOrcaCapability(cwd, opts = {}) {
|
|
36
|
+
const binary = opts.resolveOrcaBinary ? opts.resolveOrcaBinary(cwd) : resolveShellBinary("orca", cwd).resolved;
|
|
37
|
+
if (!binary)
|
|
38
|
+
return { verdict: "fail", detail: "CLI not installed" };
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await (opts.orcaStatusProbe ?? ((probeCwd, executable) => sh(`${shq(executable)} status --json`, probeCwd, 10_000)))(cwd, binary);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// The seam (or the shell) never reaching a verdict is a failed probe, never an exception out of
|
|
45
|
+
// doctor: an absent or sick runtime must still render its row.
|
|
46
|
+
return { verdict: "fail", detail: "CLI installed but runtime probe failed" };
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Do this before accepting the process result: Orca's documented refused transport is rc=1
|
|
50
|
+
// with an ok:false envelope on stdout, and parseEnvelope preserves that refusal fail-closed.
|
|
51
|
+
const envelope = parseEnvelope("status", response.stdout);
|
|
52
|
+
if (response.code !== 0 || response.timedOut) {
|
|
53
|
+
return {
|
|
54
|
+
verdict: "fail",
|
|
55
|
+
detail: `CLI installed but runtime probe failed — orca status exited ${response.code}${response.timedOut ? " after timeout" : ""}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const runtime = envelope.result.runtime;
|
|
59
|
+
const reachable = typeof runtime === "object" && runtime !== null && !Array.isArray(runtime)
|
|
60
|
+
? runtime.reachable
|
|
61
|
+
: undefined;
|
|
62
|
+
if (reachable === true)
|
|
63
|
+
return { verdict: "pass", detail: `runtime reachable (${envelope.runtimeId})` };
|
|
64
|
+
if (reachable === false)
|
|
65
|
+
return { verdict: "fail", detail: "CLI installed but runtime unreachable" };
|
|
66
|
+
return { verdict: "fail", detail: "CLI installed but runtime probe failed — status carries no reachability proof" };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Only the shared parser reads this body. Every shape it rejects — unparseable, non-object,
|
|
70
|
+
// ok:false refusal, no result, or absent/`none` `_meta.runtimeId` — is a FAILED probe, never
|
|
71
|
+
// an unreachable-runtime claim: a doctor that re-read those bytes with its own permissive
|
|
72
|
+
// reader would call malformed metadata "installed but unreachable". Unreachable is proven only
|
|
73
|
+
// by a well-formed envelope that says `reachable:false` (the shape a live orca CLI emits with
|
|
74
|
+
// its runtime down — tests/helpers/fake-orca.ts).
|
|
75
|
+
return { verdict: "fail", detail: "CLI installed but runtime probe failed" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
24
78
|
function detectRunner(cwd) {
|
|
25
79
|
const readIf = (p) => (existsSync(join(cwd, p)) ? readFileSync(join(cwd, p), "utf8") : "");
|
|
26
80
|
let pkg = {};
|
|
@@ -373,6 +427,11 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
373
427
|
}
|
|
374
428
|
if (trustNa.length)
|
|
375
429
|
rows.push(` ${dim("=")} ${dim(`n/a (${trustNa.length}): ${trustNa.join(", ")}`)}`);
|
|
430
|
+
// Orca is an explicit choice, so its health is capability information rather than an auto-routing
|
|
431
|
+
// input. A failed probe never changes pickDriver's auto ordering or substitutes subprocess.
|
|
432
|
+
const orca = await probeOrcaCapability(cwd, opts);
|
|
433
|
+
rows.push(legend("execution runtime:"));
|
|
434
|
+
rows.push(alignedStatusRow(orca.verdict, "orca", orca.detail));
|
|
376
435
|
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
377
436
|
if (!health[sel.adapter]?.installed) {
|
|
378
437
|
rows.push(attentionRow(`${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`));
|
|
@@ -12,11 +12,14 @@ import { Journal } from "../../run/journal.js";
|
|
|
12
12
|
import { doctor } from "./doctor.js";
|
|
13
13
|
import { assembleFleetEditor } from "./fleet.js";
|
|
14
14
|
const SCAFFOLD_SPEC = "tickmarkr.spec.md";
|
|
15
|
-
// Operator-approved (2026-07-17) environments footer —
|
|
16
|
-
//
|
|
15
|
+
// 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). The orca row joins it with the v2.1
|
|
17
|
+
// driver: it is a THIRD execution surface an operator selects outright — `auto` still resolves
|
|
18
|
+
// herdr-else-subprocess and never picks it, so the footer names it beside the other two choices.
|
|
17
19
|
const ENVIRONMENTS_FOOTER = [
|
|
18
20
|
"environments:",
|
|
19
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 explicit driver choice: set driver: orca (auto never picks it) · https://onorca.dev",
|
|
20
23
|
" claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively",
|
|
21
24
|
" anywhere — no herdr? same fail-closed gates, headless subprocess driver",
|
|
22
25
|
].join("\n");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
1
2
|
import { loadConfig } from "../../config/config.js";
|
|
2
|
-
import { pickDriver } from "../../drivers/index.js";
|
|
3
|
+
import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
|
|
3
4
|
import { loadGraph } from "../../graph/graph.js";
|
|
4
5
|
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
5
6
|
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
@@ -7,14 +8,24 @@ import { narrationSink, bindNarration } from "./run.js";
|
|
|
7
8
|
const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
8
9
|
&& s.tipVerify !== "failed";
|
|
9
10
|
export async function resume(argv, cwd = process.cwd()) {
|
|
10
|
-
const
|
|
11
|
+
const { values, positionals } = parseArgs({
|
|
12
|
+
args: argv,
|
|
13
|
+
options: {
|
|
14
|
+
"graph-changed": { type: "boolean" },
|
|
15
|
+
"retry-failed": { type: "boolean" },
|
|
16
|
+
driver: { type: "string" },
|
|
17
|
+
},
|
|
18
|
+
allowPositionals: true,
|
|
19
|
+
});
|
|
20
|
+
const runId = positionals[0];
|
|
11
21
|
if (!runId)
|
|
12
|
-
throw new Error("usage: tickmarkr resume <run-id> [--graph-changed] [--retry-failed]");
|
|
22
|
+
throw new Error("usage: tickmarkr resume <run-id> [--graph-changed] [--retry-failed] [--driver <auto|herdr|subprocess|orca>]");
|
|
23
|
+
const driverOverride = parseDriverOverride(values.driver);
|
|
13
24
|
// T3: --graph-changed is the operator's audited release of the engagement-identity guard (Sol #2 /
|
|
14
25
|
// Fable F2) — the daemon refuses a mismatched/unbound journal unless this is set, then journals a
|
|
15
26
|
// graph-rehash event naming both hashes. Strip the flag before runId resolution so a bare id still wins.
|
|
16
|
-
const graphChanged =
|
|
17
|
-
const retryFailed =
|
|
27
|
+
const graphChanged = values["graph-changed"] ?? false;
|
|
28
|
+
const retryFailed = values["retry-failed"] ?? false;
|
|
18
29
|
const cfg = loadConfig(cwd);
|
|
19
30
|
// v1.87 T3 (OBS-162, twice-carried workaround): the preflight runs AFTER the graph is read and
|
|
20
31
|
// sees only the shapes the resumed graph carries. A deny∩prefer collision on a shape no resumed
|
|
@@ -32,7 +43,7 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
32
43
|
graphChanged,
|
|
33
44
|
retryFailed,
|
|
34
45
|
// bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
|
|
35
|
-
driver: bindNarration(pickDriver(cfg), narrate),
|
|
46
|
+
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
36
47
|
// v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
|
|
37
48
|
// pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
|
|
38
49
|
// would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { allAdapters, discoverChannels, probeAll, readDoctor } from "../../adapters/registry.js";
|
|
3
3
|
import { ROUTING_MODES } from "../../config/config.js";
|
|
4
|
-
import { pickDriver } from "../../drivers/index.js";
|
|
4
|
+
import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
|
|
5
5
|
import { loadGraph } from "../../graph/graph.js";
|
|
6
6
|
import { formatSummary, resolveRunMode, runDaemon } from "../../run/daemon.js";
|
|
7
7
|
import { isRunLockLive } from "../../run/lock.js";
|
|
@@ -393,6 +393,9 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
393
393
|
if (!Number.isInteger(n) || n <= 0)
|
|
394
394
|
throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
|
|
395
395
|
}
|
|
396
|
+
// Keep invalid input at the argv boundary. In particular, do not cast a string into the closed
|
|
397
|
+
// driver union and accidentally turn an unknown explicit choice into auto-selection.
|
|
398
|
+
const driverOverride = parseDriverOverride(values.driver);
|
|
396
399
|
if (values.quality && values.mode !== undefined) {
|
|
397
400
|
throw new Error("--quality is a compatibility alias for --mode partner-led and cannot be combined with an explicit --mode — pass one or the other");
|
|
398
401
|
}
|
|
@@ -443,7 +446,7 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
443
446
|
const s = await runDaemon(cwd, {
|
|
444
447
|
runId,
|
|
445
448
|
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
446
|
-
driver: bindNarration(pickDriver(cfg,
|
|
449
|
+
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
447
450
|
mode: flagMode,
|
|
448
451
|
supersedes: values.supersedes,
|
|
449
452
|
narrate,
|
|
@@ -527,10 +527,34 @@ const foldTaskEffort = (tasks, events) => {
|
|
|
527
527
|
return [...effort.values()].sort((left, right) => right.total - left.total
|
|
528
528
|
|| order.get(left.taskId) - order.get(right.taskId));
|
|
529
529
|
};
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
530
|
+
// ONE glyph for every segment, distinction by COLOUR. This was shade-based — full block for dispatch,
|
|
531
|
+
// light shade for a review round, dark shade for a human park — because attention and failure share the
|
|
532
|
+
// ONE amethyst and shape was their only discriminator. OPERATOR 2026-08-25, with a screenshot: the rows
|
|
533
|
+
// rendered at DIFFERENT HEIGHTS. A row carrying the shade glyphs pulls a fallback font whose cell box is
|
|
534
|
+
// taller, so the whole line grows and its full blocks stretch with it — T1 (dispatch+review+park) stood
|
|
535
|
+
// visibly taller than T3 (dispatch only). Same display WIDTH was verified and preserved; nobody checked
|
|
536
|
+
// height, and the fitted-column arithmetic cannot see it.
|
|
537
|
+
//
|
|
538
|
+
// So review moves off amethyst onto `information` (cornflower) — an EXISTING token. Deliberately not a
|
|
539
|
+
// new amber: brand.ts calls the five live colours operator-approved and says "no token introduces a
|
|
540
|
+
// sixth colour", and a bar chart is not the place to spend that.
|
|
541
|
+
//
|
|
542
|
+
// The shape scheme existed so a reader WITHOUT colour could still separate a review round from a human
|
|
543
|
+
// park. That reader does not exist on this panel. `renderFrame` computes `const unicode = visual()`
|
|
544
|
+
// (`visual()` = `isTTY === true && NO_COLOR === undefined`) and, when it is false, EARLY-RETURNS the
|
|
545
|
+
// ASCII machine/CI surface at the `if (!unicode)` branch — which sits ABOVE the `effortPanel(...)` call
|
|
546
|
+
// below, so the panel is never built on that path. Measured before it was explained: a frame rendered
|
|
547
|
+
// with isTTY=true and NO_COLOR=1 contains no "WHERE THE EFFORT WENT" and no block glyph at all.
|
|
548
|
+
// (The `--watch` loop has its own `visual()` gate; that one is NOT what suppresses this panel, and the
|
|
549
|
+
// non-watch path reaches renderFrame unconditionally.) So the shades were paying an accessibility cost
|
|
550
|
+
// on a surface that cannot render colourless, and a conditional to restore them under NO_COLOR would
|
|
551
|
+
// have been an unreachable branch (the v1.80 injected-clock lesson: the fix for that is deletion).
|
|
552
|
+
//
|
|
553
|
+
// One glyph, three colours. Review moves off amethyst onto `information` (cornflower) — an EXISTING
|
|
554
|
+
// token, deliberately not a new amber: brand.ts calls the five live colours operator-approved and says
|
|
555
|
+
// "no token introduces a sixth colour". Every row now draws the same block, which is what makes the
|
|
556
|
+
// rows equal height.
|
|
557
|
+
const EFFORT_GLYPH = { dispatch: "\u2588", review: "\u2588", park: "\u2588" };
|
|
534
558
|
/**
|
|
535
559
|
* Prototype panel, fitted by the cockpit's display-cell authority before board-wide wrapping.
|
|
536
560
|
*
|
|
@@ -557,7 +581,7 @@ const effortPanel = (tasks, events, columns) => {
|
|
|
557
581
|
const reviewEnd = Math.round(((task.dispatches + task.reviews) / maxTotal) * barColumns);
|
|
558
582
|
const parkEnd = Math.round((task.total / maxTotal) * barColumns);
|
|
559
583
|
const stack = ok(EFFORT_GLYPH.dispatch.repeat(dispatchEnd))
|
|
560
|
-
+
|
|
584
|
+
+ information(EFFORT_GLYPH.review.repeat(Math.max(0, reviewEnd - dispatchEnd)))
|
|
561
585
|
+ fail(EFFORT_GLYPH.park.repeat(Math.max(0, parkEnd - reviewEnd)));
|
|
562
586
|
return `${prefix}${fitCells(stack, barColumns)} ${counts}`;
|
|
563
587
|
});
|
|
@@ -567,7 +591,7 @@ const effortPanel = (tasks, events, columns) => {
|
|
|
567
591
|
"",
|
|
568
592
|
...rows,
|
|
569
593
|
"",
|
|
570
|
-
` ${ok(EFFORT_GLYPH.dispatch)} ${dim("dispatch")} ${
|
|
594
|
+
` ${ok(EFFORT_GLYPH.dispatch)} ${dim("dispatch")} ${information(EFFORT_GLYPH.review)} ${dim("review round")} ${fail(EFFORT_GLYPH.park)} ${dim("human park")}`,
|
|
571
595
|
];
|
|
572
596
|
};
|
|
573
597
|
// VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
|
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 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 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|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";
|
|
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
|
@@ -35,7 +35,7 @@ usage: tickmarkr <command>
|
|
|
35
35
|
scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
|
|
36
36
|
plan dry-run routing table + cost estimate + floor lints
|
|
37
37
|
eval run checked-in fixtures against every channel in isolated temp repos
|
|
38
|
-
run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)
|
|
38
|
+
run execute the graph (--concurrency N --driver auto|herdr|subprocess|orca --route-strict; orca runs only when named)
|
|
39
39
|
status live run state
|
|
40
40
|
verify run the gate battery standalone against merge-base(--base, HEAD)..HEAD — no daemon, one verdict (--base main --criteria <file> | --task <id> [--files <glob>] [--author adapter:model] [--no-review] [--json])
|
|
41
41
|
resume <id> continue a run from its journal
|
package/dist/config/config.d.ts
CHANGED
package/dist/config/config.js
CHANGED
|
@@ -266,7 +266,7 @@ const ShapeGateParticipationSchema = z
|
|
|
266
266
|
});
|
|
267
267
|
export const TickmarkrConfigSchema = z.object({
|
|
268
268
|
concurrency: z.number().int().positive(),
|
|
269
|
-
driver: z.enum(["auto", "herdr", "subprocess"]),
|
|
269
|
+
driver: z.enum(["auto", "herdr", "subprocess", "orca"]),
|
|
270
270
|
integrationBranchPrefix: z
|
|
271
271
|
.string()
|
|
272
272
|
.regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "must be branch-safe (letters/digits/._/-, no spaces or shell metacharacters)")
|
|
@@ -713,7 +713,7 @@ export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
|
|
|
713
713
|
export function configTemplate(overlay) {
|
|
714
714
|
const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
|
|
715
715
|
# concurrency: 3
|
|
716
|
-
# driver: auto # auto | herdr | subprocess
|
|
716
|
+
# driver: auto # auto | herdr | subprocess | orca
|
|
717
717
|
# taskTimeoutMinutes: 30
|
|
718
718
|
# contextWarnTokens: 170000 # v1.23: journal+notify once per attempt when live worker context crosses this (status shows the sample)
|
|
719
719
|
# setup: npm ci --prefer-offline # run in each fresh task worktree before dispatch
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -23,17 +23,17 @@ export declare class DeliveryCorruptedError extends Error {
|
|
|
23
23
|
export type DriverJournal = (event: string, slotName: string, data: Record<string, unknown>) => void;
|
|
24
24
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
25
25
|
export declare function workerSplitDirection(paneCols: number | null, safeFloor?: number, margin?: number): "right" | "down";
|
|
26
|
-
export declare const
|
|
26
|
+
export declare const BOARD_WIDTH_SHARE = 0.5;
|
|
27
27
|
export interface BoardPlacement {
|
|
28
|
-
/** Always
|
|
29
|
-
direction: "
|
|
30
|
-
/** herdr's split ratio is the FIRST child's share, and a
|
|
31
|
-
* region — the
|
|
28
|
+
/** Always right: the board sits BESIDE the caller, so the narration keeps a column of its own. */
|
|
29
|
+
direction: "right";
|
|
30
|
+
/** herdr's split ratio is the FIRST child's share, and a right split's first child is the LEFT
|
|
31
|
+
* region — the caller's, i.e. the narration. The board takes the remainder, on the right.
|
|
32
|
+
* Measured against a live herdr 2026-08-25: a 256-col caller split right at 0.5 leaves the caller
|
|
33
|
+
* at x=36 w=128 and puts the NEW pane at x=164 w=128. */
|
|
32
34
|
ratio: number;
|
|
33
|
-
/** The new pane is swapped ABOVE the caller; the split alone would leave the board underneath. */
|
|
34
|
-
swap: "above";
|
|
35
35
|
}
|
|
36
|
-
/** The single approved
|
|
36
|
+
/** The single approved side-by-side record. The caller's columns are accepted and deliberately
|
|
37
37
|
* ignored: this signature is where width used to decide the arrangement, and the parameter stays
|
|
38
38
|
* so that "the plan does not depend on it" is a property a caller (and a test) can exercise. */
|
|
39
39
|
export declare function boardSplitPlan(_callerCols?: number | null): BoardPlacement;
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -73,15 +73,27 @@ export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_CO
|
|
|
73
73
|
// placement any more. Every width-derived variant of this placement has been wrong in the operator's
|
|
74
74
|
// tab: the halving floor sent a 189-column board below the seat (2026-08-18), and the width-first
|
|
75
75
|
// side split that replaced it puts the board and the narration shoulder to shoulder when the board is
|
|
76
|
-
// the surface the operator reads and the narration is the rail beneath it. The placement is
|
|
77
|
-
// record
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
// the surface the operator reads and the narration is the rail beneath it. The placement is ONE
|
|
77
|
+
// record and it is invariant: no terminal width, measured or unmeasurable, can select a different
|
|
78
|
+
// arrangement. THAT invariance is the hard-won part and it is unchanged here.
|
|
79
|
+
//
|
|
80
|
+
// What the record SAYS changed on operator instruction (2026-08-25): the board sits to the RIGHT of
|
|
81
|
+
// the caller, not stacked above it. The vertical stack gave the board 72% of the HEIGHT at full
|
|
82
|
+
// width, and in the operator's own tab that was mostly empty board over a squeezed narration rail —
|
|
83
|
+
// a task table is a handful of rows, so height was the axis it did not need and the narration did.
|
|
84
|
+
// Side by side spends the axis the board actually uses.
|
|
85
|
+
//
|
|
86
|
+
// ⚠ This is NOT a return to the width-derived placement that was wrong twice. Those variants let the
|
|
87
|
+
// MEASURED width choose the arrangement, so the same run rendered differently in different terminals
|
|
88
|
+
// and neither operator nor test could name one expected geometry. This record is constant: every
|
|
89
|
+
// caller width gets `right`, and `boardSplitPlan` still ignores the columns it is handed. The
|
|
90
|
+
// invariance test's width-sensitive control still fails, which is the property that mattered.
|
|
91
|
+
export const BOARD_WIDTH_SHARE = 0.5; // narration 50 / board 50, side by side
|
|
92
|
+
/** The single approved side-by-side record. The caller's columns are accepted and deliberately
|
|
81
93
|
* ignored: this signature is where width used to decide the arrangement, and the parameter stays
|
|
82
94
|
* so that "the plan does not depend on it" is a property a caller (and a test) can exercise. */
|
|
83
95
|
export function boardSplitPlan(_callerCols) {
|
|
84
|
-
return { direction: "
|
|
96
|
+
return { direction: "right", ratio: BOARD_WIDTH_SHARE };
|
|
85
97
|
}
|
|
86
98
|
/** The tab a slot belongs to: its TASK — worker, judge, review and consult panes for one task share it.
|
|
87
99
|
* Returns undefined for everything else, which keeps those on the dedicated-tab path.
|
|
@@ -1081,16 +1093,17 @@ export class HerdrDriver {
|
|
|
1081
1093
|
return p.workspace_id === this.ws && typeof p.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run";
|
|
1082
1094
|
}).map((p) => p.pane_id);
|
|
1083
1095
|
}
|
|
1084
|
-
// T2: the watch is a sibling of the daemon's own pane, never a separate tab —
|
|
1085
|
-
//
|
|
1096
|
+
// T2: the watch is a sibling of the daemon's own pane, never a separate tab — placed to the RIGHT
|
|
1097
|
+
// of it, always, whatever the terminal measures. Its durable owned name is how a
|
|
1086
1098
|
// later daemon RECOGNIZES the board it must retire, so a run never stacks a second one.
|
|
1087
1099
|
async watchSlot(cwd, name) {
|
|
1088
1100
|
if (!this.ws)
|
|
1089
1101
|
throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
|
|
1090
1102
|
if (!this.callerPane)
|
|
1091
1103
|
throw new Error("herdr watch placement requires HERDR_PANE_ID — refusing untargeted split");
|
|
1092
|
-
// One invariant placement (boardSplitPlan): split the caller
|
|
1093
|
-
//
|
|
1104
|
+
// One invariant placement (boardSplitPlan): split the caller RIGHT. No layout read decides this —
|
|
1105
|
+
// width chose the arrangement twice and was wrong twice. No swap: a right split already lands the
|
|
1106
|
+
// new pane — the board — beside the caller, so there is no second operation to verify.
|
|
1094
1107
|
const plan = boardSplitPlan();
|
|
1095
1108
|
const sp = await this.herdr(`pane split ${shq(this.callerPane)} --direction ${plan.direction} --ratio ${plan.ratio} --no-focus`);
|
|
1096
1109
|
if (sp.code !== 0)
|
|
@@ -1104,28 +1117,12 @@ export class HerdrDriver {
|
|
|
1104
1117
|
}
|
|
1105
1118
|
if (typeof pane !== "string" || !pane)
|
|
1106
1119
|
throw new Error(`herdr watch split returned no pane id: ${sp.stdout}`);
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1113
|
-
// that is exactly the path that would leave the board below the narration while the daemon
|
|
1114
|
-
// reported the stack. The documented `changed` flag is the verification; anything else — a
|
|
1115
|
-
// nonzero exit, `changed:false`, an unparseable result — fails closed.
|
|
1116
|
-
const swapped = await this.herdr(`pane swap --source-pane ${shq(pane)} --target-pane ${shq(this.callerPane)}`);
|
|
1117
|
-
let swapChanged;
|
|
1118
|
-
try {
|
|
1119
|
-
swapChanged = JSON.parse(swapped.stdout).result?.changed;
|
|
1120
|
-
}
|
|
1121
|
-
catch {
|
|
1122
|
-
/* fail closed below */
|
|
1123
|
-
}
|
|
1124
|
-
if (swapped.code !== 0 || swapChanged !== true) {
|
|
1125
|
-
await this.discardSplit(pane, `herdr watch swap ${plan.swap} failed: ${swapped.code !== 0
|
|
1126
|
-
? swapped.stderr || swapped.stdout
|
|
1127
|
-
: `herdr reported no swap took place: ${swapped.stdout || swapped.stderr}`}`);
|
|
1128
|
-
}
|
|
1120
|
+
// No swap step to verify any more. The stack needed one — the split landed the board UNDER the
|
|
1121
|
+
// caller and only the swap made the geometry the requested one, so a `pane swap` that no-opped
|
|
1122
|
+
// with a ZERO exit could leave a board below the narration while the daemon reported the stack;
|
|
1123
|
+
// the `changed` flag existed to catch exactly that. A right split places the board where it
|
|
1124
|
+
// belongs in ONE operation that either returns a pane id or throws above, so there is no
|
|
1125
|
+
// second-operation gap left to fail closed on.
|
|
1129
1126
|
const renamed = await this.herdr(`pane rename ${shq(pane)} ${shq(name)}`);
|
|
1130
1127
|
if (renamed.code !== 0 || await this.namedPaneId(name) !== pane) {
|
|
1131
1128
|
await this.discardSplit(pane, `herdr watch rename failed: ${renamed.stderr || renamed.stdout}`);
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { ExecutorDriver } from "./types.js";
|
|
3
|
-
export declare
|
|
3
|
+
export declare const DRIVER_CHOICES: readonly ["auto", "herdr", "subprocess", "orca"];
|
|
4
|
+
export type DriverChoice = (typeof DRIVER_CHOICES)[number];
|
|
5
|
+
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
6
|
+
export declare function parseDriverOverride(override?: string): DriverChoice | undefined;
|
|
7
|
+
export declare function pickDriver(cfg: TickmarkrConfig, override?: string): ExecutorDriver;
|
package/dist/drivers/index.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { HerdrDriver } from "./herdr.js";
|
|
2
|
+
import { OrcaDriver } from "./orca.js";
|
|
2
3
|
import { SubprocessDriver } from "./subprocess.js";
|
|
4
|
+
export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
|
|
5
|
+
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
6
|
+
export function parseDriverOverride(override) {
|
|
7
|
+
if (override === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
for (const choice of DRIVER_CHOICES)
|
|
10
|
+
if (override === choice)
|
|
11
|
+
return choice;
|
|
12
|
+
throw new Error(`usage: --driver must be one of ${DRIVER_CHOICES.join(" | ")} (got ${override})`);
|
|
13
|
+
}
|
|
3
14
|
export function pickDriver(cfg, override) {
|
|
4
|
-
const want = override ?? cfg.driver;
|
|
15
|
+
const want = parseDriverOverride(override) ?? cfg.driver;
|
|
5
16
|
// VIS-09 item 2: plumb the per-tab cap into the HerdrDriver — the driver takes it as a constructor
|
|
6
17
|
// param and never imports config (cfg is the only seam). Guaranteed present: DEFAULT_CONFIG seeds
|
|
7
18
|
// workersPerTab:3 and deepMerge overlays on top, so a missing overlay key still resolves.
|
|
@@ -9,5 +20,9 @@ export function pickDriver(cfg, override) {
|
|
|
9
20
|
return new HerdrDriver("herdr", cfg.visibility.workersPerTab);
|
|
10
21
|
if (want === "subprocess")
|
|
11
22
|
return new SubprocessDriver();
|
|
23
|
+
// Orca is an operator-selected execution surface. Its runtime failure stays on Orca; selection
|
|
24
|
+
// must never substitute a hidden subprocess worker after this explicit choice.
|
|
25
|
+
if (want === "orca")
|
|
26
|
+
return new OrcaDriver();
|
|
12
27
|
return HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab) : new SubprocessDriver();
|
|
13
28
|
}
|