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
package/dist/drivers/orca.js
CHANGED
|
@@ -2,8 +2,9 @@ import { realpathSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { createWorktree, sh } from "../run/git.js";
|
|
5
|
+
import { Journal } from "../run/journal.js";
|
|
5
6
|
import { MAX_BUF } from "./subprocess.js";
|
|
6
|
-
import { formatOwnedName, panesToClose } from "./types.js";
|
|
7
|
+
import { formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
|
|
7
8
|
// Orca (onorca.dev) as a third execution surface beside herdr and subprocess. tickmarkr keeps
|
|
8
9
|
// worktrees, routing, gates, journal and merges; orca supplies visible terminals only. Everything
|
|
9
10
|
// here is bound by the 1.4.186 conformance spike
|
|
@@ -31,7 +32,10 @@ import { formatOwnedName, panesToClose } from "./types.js";
|
|
|
31
32
|
// - `terminal list`'s `--worktree` is OPTIONAL (same help): the reconcile sweep omits it, because
|
|
32
33
|
// an older run's leftover sits in a checkout this run never knew (T2).
|
|
33
34
|
/** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
|
|
34
|
-
export const ORCA_RESPONSE_FAMILIES = [
|
|
35
|
+
export const ORCA_RESPONSE_FAMILIES = [
|
|
36
|
+
"status", "create", "list", "read", "send", "wait", "show", "close",
|
|
37
|
+
"worktree-current", "worktree-set", "hooks-status",
|
|
38
|
+
];
|
|
35
39
|
export const ORCA_FIXTURE_VERSION = "1.4.195";
|
|
36
40
|
export const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
37
41
|
export const STALE_HANDLE_CODE = "terminal_handle_stale";
|
|
@@ -50,6 +54,10 @@ const PAGE_LINES = 500; // per-page ask; orca caps server-side and reports `limi
|
|
|
50
54
|
const LIST_LIMIT = 10000; // well past orca's own row default; `truncated` still decides (listAll)
|
|
51
55
|
const MAX_PAGES = 400; // runaway guard: a cursor that stops advancing ends the sweep, never loops
|
|
52
56
|
const POLL_MS = 200;
|
|
57
|
+
export const WORKTREE_ADOPTION_TIMEOUT_MS = 60_000;
|
|
58
|
+
const WORKTREE_ADOPTION_POLL_MS = 1_000;
|
|
59
|
+
const WORKTREE_ADOPTION_JOURNAL_MS = 2_000;
|
|
60
|
+
const NUDGE_ECHO_TIMEOUT_MS = 2_000;
|
|
53
61
|
const SYSTEM_TIME = {
|
|
54
62
|
now: () => Date.now(),
|
|
55
63
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
@@ -252,6 +260,11 @@ export class OrcaDriver {
|
|
|
252
260
|
pageLines;
|
|
253
261
|
pollMs;
|
|
254
262
|
probeStalenessMs;
|
|
263
|
+
journalRoots = new Map();
|
|
264
|
+
narrate;
|
|
265
|
+
hookCoverage;
|
|
266
|
+
taskWorktrees = new Map();
|
|
267
|
+
pendingProjects = new Map();
|
|
255
268
|
constructor(opts = {}) {
|
|
256
269
|
this.bin = opts.bin ?? resolveOrcaCliBinary(process.cwd(), { env: opts.env, platform: opts.platform }) ?? "orca";
|
|
257
270
|
// Config values flow into a shell here: every argv element is quoted, always.
|
|
@@ -347,7 +360,16 @@ export class OrcaDriver {
|
|
|
347
360
|
// Canonical: git and Orca can spell one checkout two ways, and every later comparison — create
|
|
348
361
|
// receipt, relist, reconcile — is against THIS value.
|
|
349
362
|
const worktree = canonicalWorktreePath(cwd);
|
|
350
|
-
this.slots.set(id, { title, cwd: worktree, buf: "", recoveries: 0, recovering: false });
|
|
363
|
+
this.slots.set(id, { title, cwd: worktree, agent: opts?.agent, buf: "", recoveries: 0, recovering: false });
|
|
364
|
+
const owned = parseOwnedName(title);
|
|
365
|
+
if (owned?.role === "worker") {
|
|
366
|
+
this.taskWorktrees.set(owned.taskId, worktree);
|
|
367
|
+
const pending = this.pendingProjects.get(owned.taskId);
|
|
368
|
+
if (pending) {
|
|
369
|
+
await this.setWorkspaceStatus(worktree, pending);
|
|
370
|
+
this.pendingProjects.delete(owned.taskId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
351
373
|
return { id, name: title, cwd: worktree, group: opts?.group };
|
|
352
374
|
}
|
|
353
375
|
/** Where to invoke the CLI for this slot's calls (see OrcaSlotState.dir). */
|
|
@@ -368,6 +390,17 @@ export class OrcaDriver {
|
|
|
368
390
|
if (st.unavailable)
|
|
369
391
|
throw new OrcaUnavailableError(family, st.unavailable, "");
|
|
370
392
|
}
|
|
393
|
+
describe(slot) {
|
|
394
|
+
const st = this.state(slot);
|
|
395
|
+
// The daemon asks only after run(), but callers probing a lazy slot must see absence rather
|
|
396
|
+
// than an invented placement. The interface's object spread accepts this runtime absence.
|
|
397
|
+
if (!st.handle)
|
|
398
|
+
return undefined;
|
|
399
|
+
return {
|
|
400
|
+
...(st.surface === undefined ? {} : { surface: st.surface }),
|
|
401
|
+
...(st.hostPlatform === undefined ? {} : { hostPlatform: st.hostPlatform }),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
371
404
|
async run(slot, cmd) {
|
|
372
405
|
const st = this.state(slot);
|
|
373
406
|
if (!st.handle) {
|
|
@@ -377,25 +410,32 @@ export class OrcaDriver {
|
|
|
377
410
|
this.assertAvailable("send", st);
|
|
378
411
|
// Later deliveries go into the terminal this slot already owns — never a second create.
|
|
379
412
|
// terminalOp proves the runtime binding before the handle goes on the wire.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
413
|
+
await this.sendText(st, cmd);
|
|
414
|
+
}
|
|
415
|
+
async sendText(st, text) {
|
|
416
|
+
const env = await this.terminalOp("send", st, (h) => this.call("send", ["terminal", "send", "--terminal", h, "--text", text, "--enter"], this.cliCwd(st)), { mutating: true });
|
|
417
|
+
// Recorded 1.4.195 send receipt: result.send = {handle, accepted, bytesWritten}. `ok:true`
|
|
418
|
+
// alone is not delivery: the accepted receipt must name this handle and account for the bytes.
|
|
419
|
+
const receipt = this.sendReceipt(env, st);
|
|
420
|
+
const expectedBytes = Buffer.byteLength(text, "utf8") + 1;
|
|
421
|
+
if (receipt.accepted !== true || typeof receipt.bytesWritten !== "number" || receipt.bytesWritten !== expectedBytes) {
|
|
422
|
+
throw new OrcaError("send", `send receipt does not report expected byte delivery (accepted: ${JSON.stringify(receipt.accepted)}, bytesWritten: ${receipt.bytesWritten}, expected: ${expectedBytes})`, env.raw);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
sendReceipt(env, st) {
|
|
384
426
|
const receipt = env.result.send;
|
|
385
427
|
if (typeof receipt !== "object" || receipt === null || Array.isArray(receipt)) {
|
|
386
428
|
throw new OrcaError("send", "send response carries no send receipt", env.raw);
|
|
387
429
|
}
|
|
388
|
-
const
|
|
389
|
-
if (str(
|
|
390
|
-
throw new OrcaError("send", `send receipt names terminal ${str(
|
|
391
|
-
}
|
|
392
|
-
const expectedBytes = Buffer.byteLength(cmd, "utf8") + 1; // 1 for the --enter newline
|
|
393
|
-
if (s.accepted !== true || typeof s.bytesWritten !== "number" || s.bytesWritten !== expectedBytes) {
|
|
394
|
-
throw new OrcaError("send", `send receipt does not report expected byte delivery (accepted: ${JSON.stringify(s.accepted)}, bytesWritten: ${s.bytesWritten}, expected: ${expectedBytes})`, env.raw);
|
|
430
|
+
const parsed = receipt;
|
|
431
|
+
if (str(parsed.handle) !== st.handle) {
|
|
432
|
+
throw new OrcaError("send", `send receipt names terminal ${str(parsed.handle) ?? "none"}, not the addressed ${st.handle}`, env.raw);
|
|
395
433
|
}
|
|
434
|
+
return parsed;
|
|
396
435
|
}
|
|
397
436
|
async create(st, cmd) {
|
|
398
437
|
await this.probeRuntime(this.cliCwd(st));
|
|
438
|
+
await this.awaitWorktreeAdoption(st);
|
|
399
439
|
// The selector names THIS slot's checkout outright, and the CLI child is bound to it too, so
|
|
400
440
|
// neither the UI's active worktree (`active`/`current`) nor the daemon's cwd can place it.
|
|
401
441
|
const env = await this.call("create", [
|
|
@@ -424,11 +464,70 @@ export class OrcaDriver {
|
|
|
424
464
|
st.handle = handle;
|
|
425
465
|
// The handle is bound to the runtime identity that ANSWERED its create.
|
|
426
466
|
st.runtimeId = env.runtimeId;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
467
|
+
st.surface = str(term.surface);
|
|
468
|
+
st.hostPlatform = str(term.hostPlatform);
|
|
469
|
+
if (st.surface !== undefined && st.surface !== "visible") {
|
|
470
|
+
await this.notify(`tickmarkr orca terminal created on ${st.surface} surface`, { tier: "attention" });
|
|
430
471
|
}
|
|
431
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* A freshly-created git checkout does not become a valid Orca selector atomically. Ask
|
|
475
|
+
* `worktree current` FROM that checkout until Orca itself resolves the exact filesystem identity;
|
|
476
|
+
* an enclosing checkout is still not adoption. Only selector_not_found is a retryable refusal —
|
|
477
|
+
* malformed envelopes and every other refusal remain explicit driver failures.
|
|
478
|
+
*/
|
|
479
|
+
async awaitWorktreeAdoption(st) {
|
|
480
|
+
const started = this.time.now();
|
|
481
|
+
const deadline = started + WORKTREE_ADOPTION_TIMEOUT_MS;
|
|
482
|
+
let lastReported;
|
|
483
|
+
for (;;) {
|
|
484
|
+
const left = deadline - this.time.now();
|
|
485
|
+
if (left < 0)
|
|
486
|
+
break;
|
|
487
|
+
try {
|
|
488
|
+
const env = await this.call("worktree-current", ["worktree", "current"], st.cwd, Math.max(1, left));
|
|
489
|
+
const worktree = env.result.worktree;
|
|
490
|
+
if (typeof worktree !== "object" || worktree === null || Array.isArray(worktree)) {
|
|
491
|
+
throw new OrcaError("worktree-current", "response carries no worktree record", env.raw, { runtimeId: env.runtimeId });
|
|
492
|
+
}
|
|
493
|
+
const reported = terminalWorktree(worktree);
|
|
494
|
+
if (!reported) {
|
|
495
|
+
throw new OrcaError("worktree-current", "worktree record carries no path", env.raw, { runtimeId: env.runtimeId });
|
|
496
|
+
}
|
|
497
|
+
lastReported = canonicalWorktreePath(reported);
|
|
498
|
+
if (lastReported === st.cwd) {
|
|
499
|
+
const waitedMs = this.time.now() - started;
|
|
500
|
+
if (waitedMs > WORKTREE_ADOPTION_JOURNAL_MS)
|
|
501
|
+
this.appendAdoptionWait(st, waitedMs);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (!(error instanceof OrcaError) || error.code !== "selector_not_found")
|
|
507
|
+
throw error;
|
|
508
|
+
lastReported = undefined;
|
|
509
|
+
}
|
|
510
|
+
const remaining = deadline - this.time.now();
|
|
511
|
+
if (remaining <= 0)
|
|
512
|
+
break;
|
|
513
|
+
await this.time.sleep(Math.min(WORKTREE_ADOPTION_POLL_MS, remaining));
|
|
514
|
+
}
|
|
515
|
+
const waitedMs = this.time.now() - started;
|
|
516
|
+
throw new OrcaUnavailableError("worktree-current", `Orca did not adopt ${st.cwd} within ${WORKTREE_ADOPTION_TIMEOUT_MS}ms${lastReported ? ` (last answered ${lastReported})` : ""}`, `waitedMs=${waitedMs}`);
|
|
517
|
+
}
|
|
518
|
+
/** Same repo/run/narration path Herdr uses for its driver-owned dispatch-retry row. */
|
|
519
|
+
appendAdoptionWait(st, waitedMs) {
|
|
520
|
+
const owned = parseOwnedName(st.title);
|
|
521
|
+
if (!owned)
|
|
522
|
+
throw new Error(`cannot journal worktree-adoption-wait: slot ${st.title} carries no run identity`);
|
|
523
|
+
const repoRoot = this.journalRoots.get(st.cwd);
|
|
524
|
+
if (!repoRoot) {
|
|
525
|
+
throw new Error(`cannot journal worktree-adoption-wait: slot ${st.title} has no daemon repo binding for ${st.cwd}`);
|
|
526
|
+
}
|
|
527
|
+
Journal.open(repoRoot, owned.runId, this.narrate).append("worktree-adoption-wait", owned.taskId, {
|
|
528
|
+
milliseconds: waitedMs,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
432
531
|
// ---- handle identity and restart recovery ----------------------------------------------------
|
|
433
532
|
/**
|
|
434
533
|
* Every terminal-addressed call — read AND write — goes through here, and the runtime identity is
|
|
@@ -628,6 +727,18 @@ export class OrcaDriver {
|
|
|
628
727
|
], this.cliCwd(st)), { onRecovered: () => { recovered = true; } });
|
|
629
728
|
return { term: this.validated(family, st, env), raw: env.raw, recovered };
|
|
630
729
|
}
|
|
730
|
+
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
731
|
+
async readScreen(st) {
|
|
732
|
+
const env = await this.terminalOp("status", st, (h) => this.call("read", [
|
|
733
|
+
"terminal", "read", "--terminal", h, "--screen",
|
|
734
|
+
], this.cliCwd(st)));
|
|
735
|
+
const term = this.validated("status", st, env);
|
|
736
|
+
const source = str(term.source);
|
|
737
|
+
if (source !== "screen" && source !== "screen-unavailable") {
|
|
738
|
+
throw new OrcaError("read", `screen read reports source ${source ?? "absent"}, not screen or screen-unavailable`, env.raw);
|
|
739
|
+
}
|
|
740
|
+
return { term, source };
|
|
741
|
+
}
|
|
631
742
|
/** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
|
|
632
743
|
* across cursor pages are not reassembled here; that is waitOutput's job. */
|
|
633
744
|
async read(slot, lines) {
|
|
@@ -704,10 +815,14 @@ export class OrcaDriver {
|
|
|
704
815
|
// reporting even when the show record alone would still look connected (show carries no
|
|
705
816
|
// status field of its own, so a terminal can report "unknown"/"exited" on read while its
|
|
706
817
|
// show row still says connected — the read leg is the only place that catches that).
|
|
707
|
-
await this.
|
|
818
|
+
const screen = await this.readScreen(st);
|
|
708
819
|
if (`${st.runtimeId}:${st.handle}:${st.recoveries}` !== gen) {
|
|
709
820
|
continue;
|
|
710
821
|
}
|
|
822
|
+
// No rendered frame means no trustworthy TUI state. In particular, the stream fragments this
|
|
823
|
+
// call replaced cannot license an idle verdict or a wait probe.
|
|
824
|
+
if (screen.source === "screen-unavailable")
|
|
825
|
+
return "unknown";
|
|
711
826
|
const env = await this.terminalOp("show", st, (h) => this.call("show", ["terminal", "show", "--terminal", h], this.cliCwd(st)));
|
|
712
827
|
if (`${st.runtimeId}:${st.handle}:${st.recoveries}` !== gen) {
|
|
713
828
|
continue;
|
|
@@ -715,6 +830,11 @@ export class OrcaDriver {
|
|
|
715
830
|
const term = this.liveShowTerm("status", st, env);
|
|
716
831
|
if (term.agentWait === true)
|
|
717
832
|
return "blocked";
|
|
833
|
+
// Orca can only report tui-idle/agentWait for agents whose managed hook is installed. A
|
|
834
|
+
// definitively unhooked adapter is unknown; an agent absent from Orca's table keeps the
|
|
835
|
+
// legacy probe because absence is not proof that the CLI has no compatible status surface.
|
|
836
|
+
if (await this.agentHookAvailable(st) === false)
|
|
837
|
+
return "unknown";
|
|
718
838
|
// `idle` is proven only by orca's own tui-idle condition: a 1ms wait is a point-in-time probe —
|
|
719
839
|
// satisfied now → idle; elapsed (the recorded `timeout` refusal) → not idle.
|
|
720
840
|
const isIdle = await this.waitCondition(st, "tui-idle", 1);
|
|
@@ -724,6 +844,50 @@ export class OrcaDriver {
|
|
|
724
844
|
return mapAgentState(term, isIdle);
|
|
725
845
|
}
|
|
726
846
|
}
|
|
847
|
+
hookAgent(adapter) {
|
|
848
|
+
if (adapter === "claude-code")
|
|
849
|
+
return "claude";
|
|
850
|
+
if (adapter === "cursor-agent")
|
|
851
|
+
return "cursor";
|
|
852
|
+
return adapter;
|
|
853
|
+
}
|
|
854
|
+
/** true = hooked, false = definitively unhooked, undefined = agent absent from Orca's table. */
|
|
855
|
+
async agentHookAvailable(st) {
|
|
856
|
+
if (!st.agent)
|
|
857
|
+
return undefined;
|
|
858
|
+
this.hookCoverage ??= this.loadHookCoverage(st.cwd);
|
|
859
|
+
const coverage = await this.hookCoverage;
|
|
860
|
+
const state = coverage.states.get(this.hookAgent(st.agent));
|
|
861
|
+
// The table's omission is deliberately inconclusive even when managed hooks are disabled:
|
|
862
|
+
// Orca may not know this agent, so preserve the legacy probe exactly as an unlisted row does.
|
|
863
|
+
if (state === undefined)
|
|
864
|
+
return undefined;
|
|
865
|
+
return coverage.enabled && state === "installed";
|
|
866
|
+
}
|
|
867
|
+
async loadHookCoverage(cwd) {
|
|
868
|
+
const env = await this.call("hooks-status", ["agent", "hooks", "status"], cwd);
|
|
869
|
+
if (typeof env.result.enabled !== "boolean") {
|
|
870
|
+
throw new OrcaError("hooks-status", "response carries no boolean enabled", env.raw, { runtimeId: env.runtimeId });
|
|
871
|
+
}
|
|
872
|
+
const statuses = env.result.statuses;
|
|
873
|
+
if (!Array.isArray(statuses)) {
|
|
874
|
+
throw new OrcaError("hooks-status", "response carries no statuses array", env.raw, { runtimeId: env.runtimeId });
|
|
875
|
+
}
|
|
876
|
+
const states = new Map();
|
|
877
|
+
const allowed = new Set(["installed", "not_installed", "partial", "error"]);
|
|
878
|
+
for (const row of statuses) {
|
|
879
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
880
|
+
throw new OrcaError("hooks-status", "statuses carries a non-object row", env.raw, { runtimeId: env.runtimeId });
|
|
881
|
+
}
|
|
882
|
+
const agent = str(row.agent);
|
|
883
|
+
const state = str(row.state);
|
|
884
|
+
if (!agent || !state || !allowed.has(state)) {
|
|
885
|
+
throw new OrcaError("hooks-status", "status row carries no valid agent/state pair", env.raw, { runtimeId: env.runtimeId });
|
|
886
|
+
}
|
|
887
|
+
states.set(agent, state);
|
|
888
|
+
}
|
|
889
|
+
return { enabled: env.result.enabled, states };
|
|
890
|
+
}
|
|
727
891
|
/** One `terminal wait` through the full identity machinery. The recorded 1.4.186 elapsed answer
|
|
728
892
|
* is rc 1 + ok:true + {handle, condition, satisfied:false, status:"running"}; it is "not yet"
|
|
729
893
|
* only after this method validates all four fields. Any malformed/refused wait remains explicit. */
|
|
@@ -786,11 +950,83 @@ export class OrcaDriver {
|
|
|
786
950
|
await this.time.sleep(Math.min(this.pollMs, left2));
|
|
787
951
|
}
|
|
788
952
|
}
|
|
953
|
+
async sendKey(slot, key) {
|
|
954
|
+
const st = this.state(slot);
|
|
955
|
+
this.assertAvailable("send", st);
|
|
956
|
+
if (key === "enter") {
|
|
957
|
+
await this.sendText(st, "");
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (key === "ctrl+c") {
|
|
961
|
+
const env = await this.terminalOp("send", st, (h) => this.call("send", ["terminal", "send", "--terminal", h, "--interrupt"], this.cliCwd(st)), { mutating: true });
|
|
962
|
+
// UNRECORDED SHAPE: Orca 1.4.195 was not captured for `send --interrupt`. Until a live
|
|
963
|
+
// receipt exists, validate only the shared envelope and its handle binding; do not invent
|
|
964
|
+
// accepted/bytesWritten semantics for an interrupt.
|
|
965
|
+
this.sendReceipt(env, st);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
throw new OrcaError("send", `Orca has no terminal key verb for ${JSON.stringify(key)}`, "");
|
|
969
|
+
}
|
|
970
|
+
async nudge(slot, message) {
|
|
971
|
+
if (!message)
|
|
972
|
+
return false;
|
|
973
|
+
try {
|
|
974
|
+
const st = this.state(slot);
|
|
975
|
+
if (!await this.waitCondition(st, "tui-idle", 1))
|
|
976
|
+
return false;
|
|
977
|
+
// Drain to the current stream cursor before sending. A message already present makes the
|
|
978
|
+
// proof ambiguous, so decline rather than reporting delivery from old scrollback.
|
|
979
|
+
const before = await this.sweep(st);
|
|
980
|
+
if (before.includes(message) || joinWrapped(before).includes(message))
|
|
981
|
+
return false;
|
|
982
|
+
await this.sendText(st, message);
|
|
983
|
+
const deadline = this.time.now() + NUDGE_ECHO_TIMEOUT_MS;
|
|
984
|
+
for (;;) {
|
|
985
|
+
const after = await this.sweep(st);
|
|
986
|
+
if (after.includes(message) || joinWrapped(after).includes(message))
|
|
987
|
+
return true;
|
|
988
|
+
const left = deadline - this.time.now();
|
|
989
|
+
if (left <= 0)
|
|
990
|
+
return false;
|
|
991
|
+
await this.time.sleep(Math.min(this.pollMs, left));
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async narrator(cwd, command, runId) {
|
|
999
|
+
if (!runId)
|
|
1000
|
+
throw new OrcaError("create", "Orca narrator requires a run identity", "");
|
|
1001
|
+
const slot = await this.slot(cwd, formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }), { owned: { role: "watch", taskId: "run", attempt: 0, runId } });
|
|
1002
|
+
await this.run(slot, command);
|
|
1003
|
+
return slot;
|
|
1004
|
+
}
|
|
1005
|
+
async project(taskId, state) {
|
|
1006
|
+
const worktree = this.taskWorktrees.get(taskId);
|
|
1007
|
+
if (!worktree) {
|
|
1008
|
+
// The daemon projects in-progress immediately before it creates the task checkout/slot.
|
|
1009
|
+
// Hold only that latest state; slot() applies it once the task's own path is known.
|
|
1010
|
+
this.pendingProjects.set(taskId, state);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
await this.setWorkspaceStatus(worktree, state);
|
|
1014
|
+
}
|
|
1015
|
+
async setWorkspaceStatus(worktree, state) {
|
|
1016
|
+
// UNRECORDED SHAPE: no Orca 1.4.195 `worktree set` receipt was captured. The shared envelope
|
|
1017
|
+
// parser is the complete success proof here; no result payload is assumed or fabricated.
|
|
1018
|
+
await this.call("worktree-set", [
|
|
1019
|
+
"worktree", "set", "--worktree", `path:${worktree}`, "--workspace-status", state,
|
|
1020
|
+
], worktree);
|
|
1021
|
+
}
|
|
789
1022
|
async notify(msg, opts) {
|
|
790
1023
|
if (opts?.tier === "routine")
|
|
791
1024
|
return;
|
|
792
1025
|
console.log(`[tickmarkr] ${msg}`); // console fallback only — notification injection is out of scope
|
|
793
1026
|
}
|
|
1027
|
+
narrateWith(narrate) {
|
|
1028
|
+
this.narrate = narrate;
|
|
1029
|
+
}
|
|
794
1030
|
async close(slot) {
|
|
795
1031
|
const st = this.slots.get(slot.id);
|
|
796
1032
|
if (!st)
|
|
@@ -909,7 +1145,11 @@ export class OrcaDriver {
|
|
|
909
1145
|
catch { /* cosmetic — visibility hygiene never fails the run */ }
|
|
910
1146
|
}
|
|
911
1147
|
// tickmarkr's own createWorktree stays the sole checkout authority — orca never makes worktrees.
|
|
912
|
-
worktree(repo, branch, baseRef) {
|
|
913
|
-
|
|
1148
|
+
async worktree(repo, branch, baseRef) {
|
|
1149
|
+
const worktree = await createWorktree(repo, branch, baseRef);
|
|
1150
|
+
const repoRoot = canonicalWorktreePath(repo);
|
|
1151
|
+
this.journalRoots.set(repoRoot, repoRoot);
|
|
1152
|
+
this.journalRoots.set(canonicalWorktreePath(worktree), repoRoot);
|
|
1153
|
+
return worktree;
|
|
914
1154
|
}
|
|
915
1155
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { ExecutorDriver, NotifyOpts, Slot } from "./types.js";
|
|
2
2
|
export declare const MAX_BUF: number;
|
|
3
|
-
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH"];
|
|
3
|
+
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH", "ORCA_TERMINAL_HANDLE", "ORCA_PANE_KEY", "ORCA_TAB_ID"];
|
|
4
4
|
/**
|
|
5
|
-
* Copy of worker env with the fork cap applied and
|
|
5
|
+
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
6
6
|
* The cap is the one the enclosing run resolved (resolvedForkCap) — a worker's suites divide the
|
|
7
7
|
* same machine the gate shells do, so both seams have to read the same run-owned number rather
|
|
8
8
|
* than a flat constant. The operator's own export still wins.
|
|
9
9
|
*/
|
|
10
10
|
export declare function sealHerdrEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
|
-
/** Pane/login-shell form of the same worker env seal (herdr seed + daemon setup). */
|
|
11
|
+
/** Pane/login-shell form of the same host-neutral worker env seal (herdr seed + daemon setup). */
|
|
12
12
|
export declare function herdrSealShellPrefix(env?: NodeJS.ProcessEnv): string;
|
|
13
13
|
export declare class SubprocessDriver implements ExecutorDriver {
|
|
14
14
|
id: string;
|
|
@@ -8,13 +8,20 @@ import { createWorktree, FORK_CAP_ENV, resolvedForkCap } from "../run/git.js";
|
|
|
8
8
|
// waitOutput poll (~10MB/s sustained, far beyond agent-CLI rates). Tail-truncate, never head:
|
|
9
9
|
// consumers only ever tail-read.
|
|
10
10
|
export const MAX_BUF = 2 * 1024 * 1024;
|
|
11
|
-
// OBS-17 / v1.22 T3: control-plane vars that let a process
|
|
12
|
-
// Workers, judges, reviewers, and consults must never inherit them — only the
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
11
|
+
// OBS-17 / v1.22 T3 and OBS-843: host control-plane vars that let a process address the operator's
|
|
12
|
+
// herdr or Orca UI. Workers, judges, reviewers, and consults must never inherit them — only the
|
|
13
|
+
// daemon process and its driver calls keep the live session. TERM_PROGRAM is host description, not
|
|
14
|
+
// an addressing capability, and ORCA_AGENT_HOOK_* belongs to agent status transport, so both stay.
|
|
15
|
+
// Keep the exported name for compatibility even though the list is now host-neutral.
|
|
16
|
+
export const HERDR_CONTROL_VARS = [
|
|
17
|
+
"HERDR_ENV",
|
|
18
|
+
"HERDR_SOCKET_PATH",
|
|
19
|
+
"ORCA_TERMINAL_HANDLE",
|
|
20
|
+
"ORCA_PANE_KEY",
|
|
21
|
+
"ORCA_TAB_ID",
|
|
22
|
+
];
|
|
16
23
|
/**
|
|
17
|
-
* Copy of worker env with the fork cap applied and
|
|
24
|
+
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
18
25
|
* The cap is the one the enclosing run resolved (resolvedForkCap) — a worker's suites divide the
|
|
19
26
|
* same machine the gate shells do, so both seams have to read the same run-owned number rather
|
|
20
27
|
* than a flat constant. The operator's own export still wins.
|
|
@@ -27,7 +34,7 @@ export function sealHerdrEnv(env = process.env) {
|
|
|
27
34
|
delete out[k];
|
|
28
35
|
return out;
|
|
29
36
|
}
|
|
30
|
-
/** Pane/login-shell form of the same worker env seal (herdr seed + daemon setup). */
|
|
37
|
+
/** Pane/login-shell form of the same host-neutral worker env seal (herdr seed + daemon setup). */
|
|
31
38
|
export function herdrSealShellPrefix(env = process.env) {
|
|
32
39
|
const forkCap = sealHerdrEnv(env)[FORK_CAP_ENV] ?? resolvedForkCap();
|
|
33
40
|
return `export ${FORK_CAP_ENV}=${shq(forkCap)}; ` +
|
|
@@ -56,8 +63,8 @@ export class SubprocessDriver {
|
|
|
56
63
|
// HARD-05: interactive=false — no operator, so an open stdin pipe is a promise tickmarkr can never
|
|
57
64
|
// keep; codex exec appends a piped stdin as a <stdin> block (`codex exec --help`) and blocks on a
|
|
58
65
|
// read that never EOFs. One spawn site covers every adapter (D-06).
|
|
59
|
-
// v1.22 T3: seal
|
|
60
|
-
// operator's herdr
|
|
66
|
+
// v1.22 T3 / OBS-843: seal host control vars so worker/judge/review/consult children cannot
|
|
67
|
+
// address the operator's herdr or Orca UI. process.env of the daemon is untouched.
|
|
61
68
|
const p = spawn("bash", ["-lc", cmd], {
|
|
62
69
|
cwd: slot.cwd,
|
|
63
70
|
stdio: ["ignore", "pipe", "pipe"],
|
package/dist/drivers/types.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface SlotOpts {
|
|
|
36
36
|
group?: string;
|
|
37
37
|
label?: string;
|
|
38
38
|
owned?: OwnedName;
|
|
39
|
+
/** Adapter id for execution surfaces whose liveness support is agent-specific. */
|
|
40
|
+
agent?: string;
|
|
39
41
|
}
|
|
40
42
|
export declare const OWNED_ROLES: readonly ["worker", "judge", "review", "consult", "watch", "other"];
|
|
41
43
|
export type OwnedRole = (typeof OWNED_ROLES)[number];
|
|
@@ -64,9 +66,17 @@ export declare function panesToClose(agents: FleetAgent[], desired: Set<string>,
|
|
|
64
66
|
tabId?: string;
|
|
65
67
|
}[];
|
|
66
68
|
export declare function canonicalizeLegacyName(name: string, runId: string): OwnedName;
|
|
69
|
+
export interface SlotPlacement {
|
|
70
|
+
surface?: string;
|
|
71
|
+
hostPlatform?: string;
|
|
72
|
+
}
|
|
67
73
|
export interface ExecutorDriver {
|
|
68
74
|
id: string;
|
|
69
75
|
interactive: boolean;
|
|
76
|
+
/** The exact terminal read surface used for liveness evidence. */
|
|
77
|
+
readSource?: string;
|
|
78
|
+
/** Placement facts returned by drivers whose terminal host exposes them. */
|
|
79
|
+
describe?(slot: Slot): SlotPlacement | Promise<SlotPlacement>;
|
|
70
80
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
71
81
|
run(slot: Slot, cmd: string): Promise<void>;
|
|
72
82
|
waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
|
|
@@ -82,5 +92,7 @@ export interface ExecutorDriver {
|
|
|
82
92
|
narrateWith?(narrate: (event: JournalEvent) => void): void;
|
|
83
93
|
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
84
94
|
narrator?: (cwd: string, command: string, runId?: string) => Promise<Slot>;
|
|
95
|
+
/** Best-effort projection of a task's lifecycle onto the execution host. */
|
|
96
|
+
project?: (taskId: string, state: "in-progress" | "in-review" | "completed") => Promise<void>;
|
|
85
97
|
reconcile?: (desired: Set<string>, runId: string, opts?: PanesToCloseOpts) => Promise<void>;
|
|
86
98
|
}
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface BaselineCommand {
|
|
|
23
23
|
exitCode?: number;
|
|
24
24
|
fingerprints: string[];
|
|
25
25
|
missingCommand?: boolean;
|
|
26
|
+
/** Why a capture returned no verdict. */
|
|
27
|
+
invalidCause?: "ceiling-kill" | "resource-exhaustion";
|
|
26
28
|
/** What this command actually took at capture, on a pristine tree. Absent in pre-v1.90 baselines. */
|
|
27
29
|
durationMs?: number;
|
|
28
30
|
/** Sum of the per-file durations named by the runner; null when its output names none. */
|