tickmarkr 1.97.0 → 2.1.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 +19 -1
- package/dist/brand.d.ts +28 -0
- package/dist/brand.js +41 -0
- package/dist/cli/commands/compile.js +32 -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 +25 -8
- package/dist/cli/commands/run.d.ts +61 -1
- package/dist/cli/commands/run.js +372 -18
- package/dist/cli/commands/status.js +145 -28
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +25 -0
- package/dist/compile/collateral.js +46 -11
- package/dist/compile/native.js +10 -0
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +2 -2
- package/dist/drivers/herdr.d.ts +19 -13
- package/dist/drivers/herdr.js +90 -26
- 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/drivers/types.d.ts +2 -0
- package/dist/gates/acceptance.js +17 -7
- package/dist/gates/llm.d.ts +19 -0
- package/dist/gates/llm.js +104 -6
- package/dist/gates/run-gates.d.ts +18 -0
- package/dist/gates/run-gates.js +195 -29
- package/dist/gates/scope.d.ts +9 -1
- package/dist/gates/scope.js +22 -2
- package/dist/graph/graph.d.ts +1 -0
- package/dist/graph/graph.js +19 -2
- package/dist/report/compare.js +17 -2
- package/dist/run/daemon.d.ts +1 -8
- package/dist/run/daemon.js +231 -246
- package/dist/run/environment.d.ts +18 -1
- package/dist/run/environment.js +19 -2
- package/dist/run/journal.d.ts +50 -3
- package/dist/run/journal.js +181 -5
- package/dist/run/protocol.d.ts +4 -4
- package/dist/run/stall.d.ts +30 -0
- package/dist/run/stall.js +173 -0
- package/dist/tui/ink/init-app.js +14 -4
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +27 -15
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:
|
package/dist/brand.d.ts
CHANGED
|
@@ -48,6 +48,34 @@ export declare const TOKENS: {
|
|
|
48
48
|
readonly dim: (s: string) => string;
|
|
49
49
|
readonly bold: (s: string) => string;
|
|
50
50
|
};
|
|
51
|
+
/** Exact operator-approved live colours. Values stay hex so the authority is inspectable. */
|
|
52
|
+
export declare const LIVE_PALETTE: {
|
|
53
|
+
/** muted teal — brand identity and pass */
|
|
54
|
+
readonly brand: "#90C4A4";
|
|
55
|
+
/** cornflower blue — running state and information */
|
|
56
|
+
readonly running: "#5A76AE";
|
|
57
|
+
/** ice — primary text */
|
|
58
|
+
readonly text: "#E6FDFF";
|
|
59
|
+
/** cloud — chrome and secondary text */
|
|
60
|
+
readonly chrome: "#D9D7DD";
|
|
61
|
+
/** amethyst — attention and failure */
|
|
62
|
+
readonly attention: "#B07BAC";
|
|
63
|
+
};
|
|
64
|
+
export type LiveRole = keyof typeof LIVE_PALETTE;
|
|
65
|
+
/** Semantic live tokens. Aliases are intentional; no token introduces a sixth colour. */
|
|
66
|
+
export declare const LIVE: {
|
|
67
|
+
readonly brand: (s: string) => string;
|
|
68
|
+
readonly pass: (s: string) => string;
|
|
69
|
+
readonly running: (s: string) => string;
|
|
70
|
+
readonly information: (s: string) => string;
|
|
71
|
+
readonly text: (s: string) => string;
|
|
72
|
+
readonly primaryText: (s: string) => string;
|
|
73
|
+
readonly chrome: (s: string) => string;
|
|
74
|
+
readonly secondaryText: (s: string) => string;
|
|
75
|
+
readonly attention: (s: string) => string;
|
|
76
|
+
readonly failure: (s: string) => string;
|
|
77
|
+
readonly chip: (s: string) => string;
|
|
78
|
+
};
|
|
51
79
|
/**
|
|
52
80
|
* The glyph vocabulary — plain characters only; color layers on via tokens so
|
|
53
81
|
* shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
|
package/dist/brand.js
CHANGED
|
@@ -122,6 +122,47 @@ export const dim = sgr("2");
|
|
|
122
122
|
export const bold = sgr("1");
|
|
123
123
|
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
124
124
|
export const TOKENS = { brand, brandChip, ok, fail, warn, dim, bold };
|
|
125
|
+
// ── operator live-surface palette (v1.99) ───────────────────────────────────
|
|
126
|
+
// The tokens above remain the global design system for one-shot surfaces. LIVE is the closed
|
|
127
|
+
// five-colour system for long-running operator surfaces. Semantic aliases below deliberately share
|
|
128
|
+
// renderers: glyphs and words retain the distinction between pass/brand, running/information,
|
|
129
|
+
// chrome/secondary text, and attention/failure when colour is unavailable.
|
|
130
|
+
/** Exact operator-approved live colours. Values stay hex so the authority is inspectable. */
|
|
131
|
+
export const LIVE_PALETTE = {
|
|
132
|
+
/** muted teal — brand identity and pass */
|
|
133
|
+
brand: "#90C4A4",
|
|
134
|
+
/** cornflower blue — running state and information */
|
|
135
|
+
running: "#5A76AE",
|
|
136
|
+
/** ice — primary text */
|
|
137
|
+
text: "#E6FDFF",
|
|
138
|
+
/** cloud — chrome and secondary text */
|
|
139
|
+
chrome: "#D9D7DD",
|
|
140
|
+
/** amethyst — attention and failure */
|
|
141
|
+
attention: "#B07BAC",
|
|
142
|
+
};
|
|
143
|
+
const liveSgr = (hex) => {
|
|
144
|
+
const channels = hex.slice(1).match(/.{2}/gu).map((channel) => Number.parseInt(channel, 16));
|
|
145
|
+
return sgr(`38;2;${channels.join(";")}`);
|
|
146
|
+
};
|
|
147
|
+
const liveBrand = liveSgr(LIVE_PALETTE.brand);
|
|
148
|
+
const liveRunning = liveSgr(LIVE_PALETTE.running);
|
|
149
|
+
const liveText = liveSgr(LIVE_PALETTE.text);
|
|
150
|
+
const liveChrome = liveSgr(LIVE_PALETTE.chrome);
|
|
151
|
+
const liveAttention = liveSgr(LIVE_PALETTE.attention);
|
|
152
|
+
/** Semantic live tokens. Aliases are intentional; no token introduces a sixth colour. */
|
|
153
|
+
export const LIVE = {
|
|
154
|
+
brand: liveBrand,
|
|
155
|
+
pass: liveBrand,
|
|
156
|
+
running: liveRunning,
|
|
157
|
+
information: liveRunning,
|
|
158
|
+
text: liveText,
|
|
159
|
+
primaryText: liveText,
|
|
160
|
+
chrome: liveChrome,
|
|
161
|
+
secondaryText: liveChrome,
|
|
162
|
+
attention: liveAttention,
|
|
163
|
+
failure: liveAttention,
|
|
164
|
+
chip: liveBrand,
|
|
165
|
+
};
|
|
125
166
|
/**
|
|
126
167
|
* The glyph vocabulary — plain characters only; color layers on via tokens so
|
|
127
168
|
* shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
|
|
@@ -3,8 +3,28 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
4
4
|
import { compileSource } from "../../compile/index.js";
|
|
5
5
|
import { saveGraph, stateDirName } from "../../graph/graph.js";
|
|
6
|
+
import { formatPriorFindingEvidence, readPriorRunEvidence } from "../../run/journal.js";
|
|
7
|
+
import { shGit } from "../../run/git.js";
|
|
6
8
|
import { acquireRunLock, releaseRunLock } from "../../run/lock.js";
|
|
7
9
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
10
|
+
async function mergedPendingDiagnostics(cwd, pending, merges) {
|
|
11
|
+
const head = await shGit("git rev-parse HEAD", cwd);
|
|
12
|
+
const base = head.code === 0 ? head.stdout.trim() : "";
|
|
13
|
+
if (!/^[0-9a-f]{40}$/i.test(base))
|
|
14
|
+
return [];
|
|
15
|
+
const lines = [];
|
|
16
|
+
for (const taskId of pending) {
|
|
17
|
+
const candidates = merges.filter((merge) => merge.taskId === taskId).reverse();
|
|
18
|
+
for (const merge of candidates) {
|
|
19
|
+
const ancestor = await shGit(`git merge-base --is-ancestor ${merge.commit} ${base}`, cwd);
|
|
20
|
+
if (ancestor.code !== 0)
|
|
21
|
+
continue;
|
|
22
|
+
lines.push(`${taskId}: merged in run ${merge.runId}; compiles as pending (plan not marked done) — this dispatch rebuilds it`);
|
|
23
|
+
break; // one pure-information line per pending task, newest reachable merge wins
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
8
28
|
// v1.89 T4: harnessFrom is the resolver's INPUT (see plan.ts); the default is the INVOKED entrypoint
|
|
9
29
|
// (`process.argv[1]`, the bin symlink), never this module's own url — that names an internal module.
|
|
10
30
|
export async function compile(argv, cwd = process.cwd(), harnessFrom = process.argv[1]) {
|
|
@@ -22,6 +42,11 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
22
42
|
// resolve against the target repo, not the process cwd (the CLI test passes a tmp repo)
|
|
23
43
|
// Both modes reach the same pure compiler; --dry-run only removes the lock/write side effect below.
|
|
24
44
|
const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd);
|
|
45
|
+
// One bounded read supplies both cross-run surfaces: unresolved findings below and merge facts for
|
|
46
|
+
// the ancestry check. Neither fact mutates the compiled graph; status and every readiness predicate
|
|
47
|
+
// remain the source compiler's answer.
|
|
48
|
+
const prior = readPriorRunEvidence(cwd, g.tasks);
|
|
49
|
+
const mergedPending = await mergedPendingDiagnostics(cwd, new Set(g.tasks.filter((task) => task.status === "pending").map((task) => task.id)), prior.merges);
|
|
25
50
|
const stateDir = stateDirName(cwd);
|
|
26
51
|
if (!values["dry-run"]) {
|
|
27
52
|
// HARD-01 / Sol #3: hold the same link(2) run lock as the daemon around saveGraph so compile
|
|
@@ -41,5 +66,11 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
41
66
|
const diagnostics = scopeLints.length
|
|
42
67
|
? `\nscope lints:\n${scopeLints.map((lint) => ` ! ${lint}`).join("\n")}`
|
|
43
68
|
: "";
|
|
44
|
-
|
|
69
|
+
const priorFindings = prior.findings.length
|
|
70
|
+
? `\nprior-run evidence:\n${prior.findings.map((finding) => ` ${formatPriorFindingEvidence(finding)}`).join("\n")}`
|
|
71
|
+
: "";
|
|
72
|
+
const mergeHistory = mergedPending.length
|
|
73
|
+
? `\nmerge history:\n${mergedPending.map((line) => ` ${line}`).join("\n")}`
|
|
74
|
+
: "";
|
|
75
|
+
return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}${priorFindings}${mergeHistory}`;
|
|
45
76
|
}
|
|
@@ -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,20 +1,31 @@
|
|
|
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
|
-
import { formatJournalNarration } from "../../run/journal.js";
|
|
6
6
|
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
7
|
+
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
|
|
@@ -25,13 +36,19 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
25
36
|
if (collisions.length) {
|
|
26
37
|
throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
|
|
27
38
|
}
|
|
39
|
+
const narrate = narrationSink(runId);
|
|
28
40
|
const s = await runDaemon(cwd, {
|
|
29
41
|
runId,
|
|
30
42
|
resume: true,
|
|
31
43
|
graphChanged,
|
|
32
44
|
retryFailed,
|
|
33
|
-
driver
|
|
34
|
-
|
|
45
|
+
// bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
|
|
46
|
+
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
47
|
+
// v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
|
|
48
|
+
// pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
|
|
49
|
+
// would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
|
|
50
|
+
// the operator named, so a resumed run's lifecycle rows name THIS run and not a generic word.
|
|
51
|
+
narrate,
|
|
35
52
|
});
|
|
36
53
|
const out = `resumed ${s.runId} — ${formatSummary(s)}`;
|
|
37
54
|
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
@@ -1,6 +1,66 @@
|
|
|
1
|
+
import type { ExecutorDriver } from "../../drivers/types.js";
|
|
1
2
|
import { type JournalEvent } from "../../run/journal.js";
|
|
2
|
-
|
|
3
|
+
/** Closed repetitive set the rail suppresses on a TTY. An ungated `phase-start` joins them below —
|
|
4
|
+
* it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
|
|
5
|
+
export declare const TTY_NOISE_EVENTS: readonly ["worker-contact", "worker-status"];
|
|
6
|
+
type RailTone = "pass" | "fail" | "attention" | "active" | "neutral";
|
|
7
|
+
/** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
|
|
8
|
+
* own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
|
|
9
|
+
* or `ok` datum on the event overrides the default tone, so one gate row can read either way.
|
|
10
|
+
*
|
|
11
|
+
* MEMBERSHIP RULE — the daemon journals far more than this, and an allowlist built from whatever
|
|
12
|
+
* the tests happened to cover masks real events. An event earns a row when it changes what the run
|
|
13
|
+
* will DO next or reports an OUTCOME of it: a routing decision, a worker result, a gate start or
|
|
14
|
+
* verdict, a repair, an escalation, a merge, a run lifecycle step. Everything else — how the daemon
|
|
15
|
+
* got there (worktree setup, launch mechanics, baseline and routing lints) and every poll-time
|
|
16
|
+
* observation (contact reads, quota banners, held dead-verdicts, context samples) — stays off the
|
|
17
|
+
* rail and on the pipe, where it is byte-identical to the raw journal.
|
|
18
|
+
*
|
|
19
|
+
* Applying that rule is what put `graph-rehash` and the worker-nudge family here: a rehash is the
|
|
20
|
+
* operator's audited `--graph-changed` release, journaled by the resumed run through THIS sink, and
|
|
21
|
+
* a nudge is a decision the daemon takes on the operator's behalf (it contacts the worker and arms a
|
|
22
|
+
* grace deadline that force-concludes the wait), whose answered/failed/expired rows are that
|
|
23
|
+
* decision's outcome. Neither is mechanics, and both reach this process's narrate callback. */
|
|
24
|
+
export declare const RAIL_ROWS: Record<string, {
|
|
25
|
+
label: string;
|
|
26
|
+
tone: RailTone;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* One rail row, or null when the TTY rail suppresses this event. Never wider than `columns`: the
|
|
30
|
+
* identity and the label are clipped SEPARATELY and the detail takes only what they leave, so a row
|
|
31
|
+
* can never wrap into a second line and can never lose its meaning to a long identity.
|
|
32
|
+
*/
|
|
33
|
+
export declare function narrationRow(event: JournalEvent, runId: string, columns?: number): string | null;
|
|
34
|
+
/** The narration line for one event of the run named by `runId`: the raw journal formatter on a pipe
|
|
35
|
+
* (byte-identical, every event), the quiet rail on a TTY. Null means the rail suppressed it — the
|
|
36
|
+
* caller prints nothing. */
|
|
37
|
+
export declare const narrationLine: (event: JournalEvent, runId: string) => string | null;
|
|
38
|
+
/**
|
|
39
|
+
* The daemon's narration sink, BOUND TO THE RUN IT NARRATES: the rail on a TTY, the raw journal
|
|
40
|
+
* formatter on a pipe, and nothing at all for an event the rail suppressed. EVERY command that drives
|
|
41
|
+
* a daemon owes its narration to this sink - a second call site that prints `formatJournalNarration`
|
|
42
|
+
* itself is a surface where the rail does not exist, and the operator meets the old unfiltered dump
|
|
43
|
+
* under the newly stacked board.
|
|
44
|
+
*
|
|
45
|
+
* The binding is what puts a real identity on the run-scoped rows: the daemon's `narrate` callback is
|
|
46
|
+
* handed one event and nothing else, and the lifecycle events carry no run id of their own, so the
|
|
47
|
+
* run id can only come from the command that owns the run. Both daemon-driving commands supply it:
|
|
48
|
+
* `run` below mints the id it then passes to the daemon, and `resume` (src/cli/commands/resume.ts)
|
|
49
|
+
* carries the id the operator named.
|
|
50
|
+
*/
|
|
51
|
+
export declare const narrationSink: (runId: string) => (event: JournalEvent) => void;
|
|
52
|
+
/**
|
|
53
|
+
* The run's driver, BOUND to the run's narration sink.
|
|
54
|
+
*
|
|
55
|
+
* A driver journals events of its own that the daemon never sees: `dispatch-retry` is appended by
|
|
56
|
+
* HerdrDriver from inside a pane recovery, through a Journal it opens itself (src/drivers/herdr.ts).
|
|
57
|
+
* Unbound, that event lands in the file and on the pipe while the rail — the operator's only live
|
|
58
|
+
* surface — stays silent about a redispatch that already happened. Both daemon-driving commands
|
|
59
|
+
* wrap their driver here so neither can forget the binding.
|
|
60
|
+
*/
|
|
61
|
+
export declare function bindNarration<D extends ExecutorDriver>(driver: D, narrate: (event: JournalEvent) => void): D;
|
|
3
62
|
export declare function run(argv: string[], cwd?: string): Promise<{
|
|
4
63
|
out: string;
|
|
5
64
|
code: number;
|
|
6
65
|
}>;
|
|
66
|
+
export {};
|