tickmarkr 2.5.9 → 2.6.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/dist/adapters/claude-code.js +19 -6
- package/dist/adapters/prompt.js +1 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/report.js +11 -1
- package/dist/cli/commands/verify.js +2 -0
- package/dist/compile/native.js +39 -4
- package/dist/drivers/orca.d.ts +1 -1
- package/dist/drivers/orca.js +21 -4
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +9 -1
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +26 -13
- package/dist/gates/review.d.ts +2 -3
- package/dist/gates/review.js +21 -24
- package/dist/gates/run-gates.js +11 -1
- package/dist/gates/test-manifest.d.ts +5 -0
- package/dist/gates/test-manifest.js +87 -7
- package/dist/gates/test-reporter.js +4 -0
- package/dist/graph/graph.d.ts +1 -1
- package/dist/graph/graph.js +6 -1
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +10 -39
- package/dist/run/daemon.d.ts +7 -1
- package/dist/run/daemon.js +160 -19
- package/dist/run/git.d.ts +10 -0
- package/dist/run/git.js +49 -1
- package/dist/run/journal.d.ts +1 -1
- package/dist/run/journal.js +42 -6
- package/dist/run/merge.d.ts +1 -1
- package/dist/run/merge.js +30 -6
- package/dist/tui/cockpit/live-runtime.d.ts +4 -0
- package/dist/tui/cockpit/live-runtime.js +37 -3
- package/dist/tui/cockpit/live-store.d.ts +2 -0
- package/package.json +1 -1
- package/schema/rungraph.schema.json +7 -0
- package/skills/tickmarkr-overseer/SKILL.md +33 -8
package/dist/run/git.d.ts
CHANGED
|
@@ -190,6 +190,16 @@ export declare function resolveIntegrationBranch(_repo: string, branch: string):
|
|
|
190
190
|
*/
|
|
191
191
|
export declare function preserveWorktree(cwd: string): Promise<string | undefined>;
|
|
192
192
|
export declare function createWorktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
193
|
+
export interface DependencyLink {
|
|
194
|
+
/** Package link, relative to node_modules (including @scope/name). */
|
|
195
|
+
link: string;
|
|
196
|
+
target: string;
|
|
197
|
+
classification: "worktree" | "node_modules" | "outside";
|
|
198
|
+
}
|
|
199
|
+
/** Inventory only package-level links; never descend into installed packages. */
|
|
200
|
+
export declare function inventoryDependencyLinks(worktree: string): DependencyLink[];
|
|
201
|
+
/** Shared pre-command refusal; unreadable inventories cannot establish isolation either. */
|
|
202
|
+
export declare function dependencyLinkRefusal(worktree: string): string | undefined;
|
|
193
203
|
export declare function linkNodeModules(repo: string, dir: string, { force }?: {
|
|
194
204
|
force?: boolean | undefined;
|
|
195
205
|
}): boolean;
|
package/dist/run/git.js
CHANGED
|
@@ -4,7 +4,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
4
4
|
import { execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
7
|
-
import { join, resolve } from "node:path";
|
|
7
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { StringDecoder } from "node:string_decoder";
|
|
9
9
|
import { shq } from "../adapters/types.js";
|
|
10
10
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
@@ -624,6 +624,54 @@ function excludeNodeModules(dir) {
|
|
|
624
624
|
}
|
|
625
625
|
catch { /* not a git checkout (bare tmpdir in tests) — never fail provisioning over the exclude */ }
|
|
626
626
|
}
|
|
627
|
+
/** Inventory only package-level links; never descend into installed packages. */
|
|
628
|
+
export function inventoryDependencyLinks(worktree) {
|
|
629
|
+
const dependencyPath = join(worktree, "node_modules");
|
|
630
|
+
try {
|
|
631
|
+
lstatSync(dependencyPath);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
if (error.code === "ENOENT")
|
|
635
|
+
return [];
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
const root = realpathSync(worktree);
|
|
639
|
+
const dependencies = realpathSync(dependencyPath);
|
|
640
|
+
const inside = (parent, target) => {
|
|
641
|
+
const rel = relative(parent, target);
|
|
642
|
+
return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`));
|
|
643
|
+
};
|
|
644
|
+
const links = [];
|
|
645
|
+
const inspect = (link) => {
|
|
646
|
+
const path = join(dependencies, link);
|
|
647
|
+
if (!lstatSync(path).isSymbolicLink())
|
|
648
|
+
return;
|
|
649
|
+
const target = realpathSync(path);
|
|
650
|
+
links.push({ link, target, classification: inside(root, target) ? "worktree"
|
|
651
|
+
: inside(dependencies, target) ? "node_modules" : "outside" });
|
|
652
|
+
};
|
|
653
|
+
for (const entry of readdirSync(dependencies).sort()) {
|
|
654
|
+
inspect(entry);
|
|
655
|
+
if (entry.startsWith("@") && statSync(join(dependencies, entry)).isDirectory()) {
|
|
656
|
+
for (const child of readdirSync(join(dependencies, entry)).sort())
|
|
657
|
+
inspect(join(entry, child));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return links;
|
|
661
|
+
}
|
|
662
|
+
/** Shared pre-command refusal; unreadable inventories cannot establish isolation either. */
|
|
663
|
+
export function dependencyLinkRefusal(worktree) {
|
|
664
|
+
try {
|
|
665
|
+
const outside = inventoryDependencyLinks(worktree).filter(link => link.classification === "outside");
|
|
666
|
+
if (!outside.length)
|
|
667
|
+
return undefined;
|
|
668
|
+
return "infra: OBS-1118 refusing verification: dependency links resolve outside the worktree and real node_modules:\n"
|
|
669
|
+
+ outside.map(({ link, target }) => `node_modules/${link} -> ${target}`).join("\n");
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
return `infra: OBS-1118 refusing verification: cannot inventory dependency links in ${worktree}: ${String(error)}`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
627
675
|
export function linkNodeModules(repo, dir, { force = false } = {}) {
|
|
628
676
|
const src = join(repo, "node_modules");
|
|
629
677
|
const dest = join(dir, "node_modules");
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -437,7 +437,7 @@ export declare class Journal {
|
|
|
437
437
|
assignment?: Assignment;
|
|
438
438
|
} | null;
|
|
439
439
|
replayResumeState(): Map<string, ResumeState>;
|
|
440
|
-
replaySatisfiedGates(): Map<string, GateName>;
|
|
440
|
+
replaySatisfiedGates(currentSubjects?: ReadonlyMap<string, string>): Map<string, GateName>;
|
|
441
441
|
replayCurrentAttemptGateResults(): Map<string, CurrentAttemptGateReplay>;
|
|
442
442
|
replayExcludedChannels(): Set<string>;
|
|
443
443
|
telemetry(row: TelemetryRow): void;
|
package/dist/run/journal.js
CHANGED
|
@@ -1734,26 +1734,62 @@ export class Journal {
|
|
|
1734
1734
|
}
|
|
1735
1735
|
return m;
|
|
1736
1736
|
}
|
|
1737
|
-
// OBS-130/OBS-571:
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
// before
|
|
1741
|
-
|
|
1742
|
-
replaySatisfiedGates() {
|
|
1737
|
+
// OBS-130/OBS-571: only an explicit approval grants gate authority; recreation consumes
|
|
1738
|
+
// its immediate enactment. OBS-1133: retain a review waiver's subject separately so a later
|
|
1739
|
+
// recheck may carry it. Tool waivers never survive recheck. The daemon supplies the recreated
|
|
1740
|
+
// subject before using a carried waiver; absent that, replay can only compare journaled subjects.
|
|
1741
|
+
replaySatisfiedGates(currentSubjects) {
|
|
1743
1742
|
const satisfied = new Map();
|
|
1743
|
+
const reviewSubjects = new Map();
|
|
1744
|
+
const subjects = new Map();
|
|
1745
|
+
const reviewWaivers = new Map();
|
|
1744
1746
|
for (const e of this.read()) {
|
|
1745
1747
|
if (!e.taskId)
|
|
1746
1748
|
continue;
|
|
1749
|
+
if (e.event === "task-dispatch") {
|
|
1750
|
+
reviewSubjects.delete(e.taskId);
|
|
1751
|
+
subjects.delete(e.taskId);
|
|
1752
|
+
reviewWaivers.delete(e.taskId);
|
|
1753
|
+
satisfied.delete(e.taskId);
|
|
1754
|
+
}
|
|
1755
|
+
if (e.event === "gate-result") {
|
|
1756
|
+
const commit = typeof e.data.commit === "string" && e.data.commit ? e.data.commit : undefined;
|
|
1757
|
+
if (commit)
|
|
1758
|
+
subjects.set(e.taskId, commit);
|
|
1759
|
+
else
|
|
1760
|
+
subjects.delete(e.taskId);
|
|
1761
|
+
if (reviewWaivers.has(e.taskId) && reviewWaivers.get(e.taskId) !== commit) {
|
|
1762
|
+
reviewWaivers.delete(e.taskId);
|
|
1763
|
+
}
|
|
1764
|
+
if (e.data.gate === "review") {
|
|
1765
|
+
if (commit && e.data.pass === false)
|
|
1766
|
+
reviewSubjects.set(e.taskId, commit);
|
|
1767
|
+
else
|
|
1768
|
+
reviewSubjects.delete(e.taskId);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1747
1771
|
if (e.event === "worktree-recreation") {
|
|
1748
1772
|
satisfied.delete(e.taskId);
|
|
1749
1773
|
continue;
|
|
1750
1774
|
}
|
|
1751
1775
|
if (e.event === "task-approved") {
|
|
1752
1776
|
satisfied.delete(e.taskId);
|
|
1777
|
+
if (e.data.release === RECHECK_RELEASE) {
|
|
1778
|
+
const waivedSubject = reviewWaivers.get(e.taskId);
|
|
1779
|
+
const subject = currentSubjects?.get(e.taskId) ?? subjects.get(e.taskId);
|
|
1780
|
+
if (waivedSubject && waivedSubject === subject)
|
|
1781
|
+
satisfied.set(e.taskId, "review");
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
reviewWaivers.delete(e.taskId);
|
|
1753
1785
|
if (e.data.release === GATE_SATISFIED_RELEASE
|
|
1754
1786
|
&& typeof e.data.gate === "string"
|
|
1755
1787
|
&& GATE_NAMES.includes(e.data.gate)) {
|
|
1756
1788
|
satisfied.set(e.taskId, e.data.gate);
|
|
1789
|
+
const subject = reviewSubjects.get(e.taskId);
|
|
1790
|
+
if (e.data.gate === "review" && subject && subject === subjects.get(e.taskId)) {
|
|
1791
|
+
reviewWaivers.set(e.taskId, subject);
|
|
1792
|
+
}
|
|
1757
1793
|
}
|
|
1758
1794
|
}
|
|
1759
1795
|
}
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface TipVerifyResult {
|
|
|
5
5
|
evidenceReceipt?: GateEvidenceReceipt;
|
|
6
6
|
evidenceReceipts?: GateEvidenceReceipt[];
|
|
7
7
|
evidenceAbsence?: "provenance-refused" | "not-started" | "historical-cache";
|
|
8
|
-
/**
|
|
8
|
+
/** Directory the evidence setup returned; receipt artifact paths resolve against it. */
|
|
9
9
|
originRunRoot?: string;
|
|
10
10
|
nonce?: string;
|
|
11
11
|
stdoutPath?: string;
|
package/dist/run/merge.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { shq } from "../adapters/types.js";
|
|
4
4
|
import { beginGateEvidence, ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, fingerprint, freshFailures, } from "../gates/baseline.js";
|
|
5
5
|
import { evaluateManifestedTest, isVitestTestCommand } from "../gates/test-manifest.js";
|
|
6
6
|
import { computeVerificationIdentity, formatReusedDetails, getVerdictStore, getWorktreeTree, resolveStateDir, isInfraResult, } from "../gates/cache.js";
|
|
7
7
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
8
|
-
import { describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
8
|
+
import { dependencyLinkRefusal, describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
|
|
9
9
|
import { executionSignal } from "./execution-budget.js";
|
|
10
10
|
// Diagnostic convenience logs are observational, just like receipt persistence.
|
|
11
11
|
function writeTipLog(path, text) {
|
|
@@ -128,6 +128,16 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
128
128
|
if (!commands.test && commands.tipTest) {
|
|
129
129
|
gatesToRun.push(["test", commands.tipTest]);
|
|
130
130
|
}
|
|
131
|
+
const dependencyRefusal = dependencyLinkRefusal(intWt);
|
|
132
|
+
if (dependencyRefusal) {
|
|
133
|
+
for (const [gate, cmd] of gatesToRun.length ? gatesToRun : [["build", ""]]) {
|
|
134
|
+
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
135
|
+
writeTipLog(artifact, dependencyRefusal + "\n");
|
|
136
|
+
results.push({ gate, cmd, pass: false, exitCode: 1, fingerprints: [],
|
|
137
|
+
cause: "infra", details: dependencyRefusal, evidenceAbsence: "not-started", artifact });
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
131
141
|
if (journalFound && hasRunEvidenceOrMalformed && runStartCommands === undefined) {
|
|
132
142
|
for (const [gate, cmd] of gatesToRun) {
|
|
133
143
|
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
@@ -151,6 +161,8 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
151
161
|
// Publish only after the entire verification completes. The daemon kills a cancelled
|
|
152
162
|
// verifier process, so completed early gates must remain in memory until then.
|
|
153
163
|
const pending = new Map();
|
|
164
|
+
// D-267 (1): each fresh receipt's origin is the root its own beginGateEvidence returned.
|
|
165
|
+
const evidenceRoots = new Map();
|
|
154
166
|
for (const [gate, cmd] of gatesToRun) {
|
|
155
167
|
if (runStartCommands !== undefined) {
|
|
156
168
|
const expectedCmd = gate === "test"
|
|
@@ -226,13 +238,20 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
226
238
|
// a detected vitest test command never reaches the stdout-count/file-count path below (this
|
|
227
239
|
// gate's own manifest is always the FULL suite; verify has no selected screen to hold). A
|
|
228
240
|
// scripted test command that is not the detected runner falls through unchanged.
|
|
241
|
+
const evidenceSetup = { artifactDir: runDir, runId: runDir, ...evidenceOptions };
|
|
229
242
|
if (gate === "test" && isVitestTestCommand(cmd, intWt)) {
|
|
243
|
+
// D-267 (1): stamp the root the evidence setup returned, then give that exact root to every
|
|
244
|
+
// manifested invocation. Do not reconstruct it from the caller's options: the receipt's
|
|
245
|
+
// artifact paths are relative to the root returned by beginGateEvidence.
|
|
246
|
+
const evidence = beginGateEvidence(intWt, gate, cmd, evidenceSetup);
|
|
247
|
+
const manifestedEvidence = { ...evidenceSetup, artifactDir: evidence.root };
|
|
248
|
+
evidenceRoots.set(gate, evidence.root);
|
|
230
249
|
const outcome = await evaluateManifestedTest(cmd, intWt, {
|
|
231
250
|
baselineDurations: entry?.fileDurations,
|
|
232
251
|
longestFile: entry?.longestFile,
|
|
233
252
|
overallCeilingMs: effectiveCeilingMs(entry),
|
|
234
253
|
artifactDir: runDir,
|
|
235
|
-
evidence:
|
|
254
|
+
evidence: manifestedEvidence,
|
|
236
255
|
});
|
|
237
256
|
const artifact = join(runDir, `tip-verify-${gate}.log`);
|
|
238
257
|
if (!outcome.pass)
|
|
@@ -264,7 +283,8 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
264
283
|
const ceilingMs = effectiveCeilingMs(entry);
|
|
265
284
|
const evidenceReceipts = [];
|
|
266
285
|
const execute = async () => {
|
|
267
|
-
const evidence = beginGateEvidence(intWt, gate, cmd,
|
|
286
|
+
const evidence = beginGateEvidence(intWt, gate, cmd, evidenceSetup);
|
|
287
|
+
evidenceRoots.set(gate, evidence.root);
|
|
268
288
|
try {
|
|
269
289
|
const result = await sh(cmd, intWt, ceilingMs, { env: evidenceOptions.env, onReceipt: receipt => evidence.observe(receipt) });
|
|
270
290
|
evidenceReceipts.push(...evidence.history, evidence.finish(result.stdout, result.stderr));
|
|
@@ -372,8 +392,12 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
|
|
|
372
392
|
// spawns in the daemon child, and a command that changed the tree invalidates reuse.
|
|
373
393
|
const completedTree = pending.size ? await getWorktreeTree(intWt) : undefined;
|
|
374
394
|
for (const result of results) {
|
|
375
|
-
if (!result.reused && result.evidenceReceipt)
|
|
376
|
-
|
|
395
|
+
if (!result.reused && result.evidenceReceipt) {
|
|
396
|
+
const evidenceRoot = evidenceRoots.get(result.gate);
|
|
397
|
+
if (evidenceRoot === undefined)
|
|
398
|
+
throw new Error(`tip verify receipt for ${result.gate} has no evidence root from its evidence setup`);
|
|
399
|
+
result.originRunRoot = evidenceRoot;
|
|
400
|
+
}
|
|
377
401
|
const id = pending.get(result.gate);
|
|
378
402
|
if (id && id.tree === completedTree && !isInfraResult(result)) {
|
|
379
403
|
store.set(id, { ...result, meta: {
|
|
@@ -22,6 +22,10 @@ export interface ShellDelivery {
|
|
|
22
22
|
diagnostics: LiveStore["diagnostics"];
|
|
23
23
|
stage: (edits: readonly string[]) => void;
|
|
24
24
|
geometry: () => ShellCommit | undefined;
|
|
25
|
+
/** Application render commits since mount (OBS-1132: an idle board adds none). */
|
|
26
|
+
frames: () => number;
|
|
27
|
+
/** Model derivations (the App body) since mount. */
|
|
28
|
+
derivations: () => number;
|
|
25
29
|
}
|
|
26
30
|
export interface ConsolidatedOptions {
|
|
27
31
|
input: NodeJS.ReadStream;
|
|
@@ -132,10 +132,42 @@ export async function runConsolidatedCockpit(options) {
|
|
|
132
132
|
let committedTargets = [];
|
|
133
133
|
let revision = 0;
|
|
134
134
|
const listeners = new Set();
|
|
135
|
-
|
|
135
|
+
// Every publish resyncs the key so a view switch never earns a second frame on the next tick.
|
|
136
|
+
const publish = () => { revision++; observed = observationKey(source.snapshot()); for (const listener of listeners)
|
|
136
137
|
listener(); };
|
|
137
|
-
|
|
138
|
+
let observed = "";
|
|
138
139
|
const geometry = () => planShell(output.columns ?? 80, output.rows ?? 24, state.shortcutColumns);
|
|
140
|
+
// OBS-1132: the store publishes on every observation tick; the board only re-derives and
|
|
141
|
+
// re-renders when what it would draw changed. Lock and supervision observation continue
|
|
142
|
+
// regardless. Excluded on purpose: sequence, observedAt, metrics and beat ages, which move
|
|
143
|
+
// every tick without changing a visible cell.
|
|
144
|
+
// ponytail: mirrors board.ts `ago` buckets and its ≥118-cell "wide" header — the only clock reader,
|
|
145
|
+
// and only the Run view draws the board, so Home and Evidence never invalidate on the age.
|
|
146
|
+
// The lock is compared by what it says (owner, state, liveness), never by its file stamp: the
|
|
147
|
+
// daemon heartbeat re-touches graph.lock every 10 s without changing a visible cell. Its clock-derived
|
|
148
|
+
// `expired` flag is not drawn by any view either, so a lock ageing past STALE_MS earns no frame.
|
|
149
|
+
const visibleAge = (ms) => ms < 6e4 ? "just now" : ms < 3.6e6 ? `${Math.round(ms / 6e4)}m` : `${Math.round(ms / 3.6e6)}h`;
|
|
150
|
+
const observationKey = (snap) => {
|
|
151
|
+
const last = snap.operator.lastEventAt;
|
|
152
|
+
const clock = state.view === "run" && geometry().bodyColumns >= 118 && last ? visibleAge((options.now ?? Date.now)() - Date.parse(last)) : "";
|
|
153
|
+
return JSON.stringify([
|
|
154
|
+
snap.freshness, snap.actionsEnabled, snap.errors, snap.viewport, snap.inputSequence, clock,
|
|
155
|
+
snap.journal.generation, snap.journal.offset, snap.journal.status, snap.journal.malformedCount, snap.journal.backlogBytes, snap.journal.pending, snap.journal.error,
|
|
156
|
+
snap.graph.identity, snap.graph.status, snap.config.identity, snap.config.status, snap.cache.identity, snap.cache.status,
|
|
157
|
+
snap.lock.value, snap.lock.status, snap.lock.state, snap.lock.alive,
|
|
158
|
+
// D-383: supervision tier state is not drawn by any view; an ARMED→STALE transition earns no
|
|
159
|
+
// frame. Its UNREADABLE case already surfaces through snap.errors above.
|
|
160
|
+
]);
|
|
161
|
+
};
|
|
162
|
+
observed = observationKey(source.snapshot());
|
|
163
|
+
const unsubscribe = source.subscribe(() => {
|
|
164
|
+
const next = observationKey(source.snapshot());
|
|
165
|
+
if (next !== observed) {
|
|
166
|
+
observed = next;
|
|
167
|
+
publish();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
let frames = 0, derivations = 0;
|
|
139
171
|
const openEvidence = (identity) => {
|
|
140
172
|
evidenceNavigation++;
|
|
141
173
|
selectedEvidence = identity;
|
|
@@ -484,7 +516,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
484
516
|
stop(e);
|
|
485
517
|
return false;
|
|
486
518
|
}
|
|
487
|
-
}, diagnostics: source.diagnostics, stage: edits => { state = { ...state, staged: [...edits] }; publish(); } };
|
|
519
|
+
}, diagnostics: source.diagnostics, stage: edits => { state = { ...state, staged: [...edits] }; publish(); }, frames: () => frames, derivations: () => derivations };
|
|
488
520
|
options.onDelivery?.(delivery);
|
|
489
521
|
options.onShellDelivery?.(delivery);
|
|
490
522
|
resize = () => {
|
|
@@ -530,6 +562,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
530
562
|
function App() {
|
|
531
563
|
useSyncExternalStore(listener => { listeners.add(listener); return () => { listeners.delete(listener); }; }, () => revision);
|
|
532
564
|
const stdinContext = useStdin();
|
|
565
|
+
useLayoutEffect(() => { frames++; });
|
|
533
566
|
useLayoutEffect(() => {
|
|
534
567
|
const emitter = stdinContext.internal_eventEmitter;
|
|
535
568
|
leafInput = emitter;
|
|
@@ -565,6 +598,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
565
598
|
stop(error);
|
|
566
599
|
}
|
|
567
600
|
});
|
|
601
|
+
derivations++;
|
|
568
602
|
const snap = source.snapshot();
|
|
569
603
|
const p = geometry();
|
|
570
604
|
const nextDecisionsKey = `${snap.journal.generation}:${snap.journal.offset}:${snap.graph.identity}`;
|
|
@@ -147,6 +147,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
147
147
|
kind: "fixture";
|
|
148
148
|
paths: string[];
|
|
149
149
|
})[] | undefined;
|
|
150
|
+
outOfScope?: string[] | undefined;
|
|
150
151
|
routingHints?: {
|
|
151
152
|
pin?: {
|
|
152
153
|
via: string;
|
|
@@ -248,6 +249,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
248
249
|
kind: "fixture";
|
|
249
250
|
paths: string[];
|
|
250
251
|
})[] | undefined;
|
|
252
|
+
outOfScope?: string[] | undefined;
|
|
251
253
|
routingHints?: {
|
|
252
254
|
pin?: {
|
|
253
255
|
via: string;
|
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@ through brief lineage. **An executor choice nobody made is still an executor cho
|
|
|
62
62
|
- **On herdr (`HERDR_ENV=1`)**: Every bundled `watch-*.sh` arm, including `watch-artifacts.sh`, sets `TKR_ARMING_SEAT=<seat>` and writes its own pid under
|
|
63
63
|
`<state-dir>/overseer/pids/<arming-seat>-<script>-<pid>.pid`. A seat retires only watchers it armed,
|
|
64
64
|
by reading those files and killing the exact recorded pids; it never uses `pkill -f`, `pgrep -f`, or
|
|
65
|
-
any argv/path pattern. A journal path is shared by partner tiers and therefore cannot prove ownership.
|
|
65
|
+
any argv/path pattern. The pre-arm beat probe lists unowned writers and is reconciled in the beat section; retiring a watcher this seat armed stays kill-by-recorded-pid and never that probe. A journal path is shared by partner tiers and therefore cannot prove ownership.
|
|
66
66
|
Verify each executing pid in two process-table reads before acting, kill-by-pid, arm the replacement,
|
|
67
67
|
then verify its new pid in two process-table reads. A stand-down order inventories both sets: the
|
|
68
68
|
ordering seat's recorded pids to retire, and the partner's watchers armed on the ordering seat that
|
|
@@ -72,7 +72,7 @@ through brief lineage. **An executor choice nobody made is still an executor cho
|
|
|
72
72
|
and nothing had been watching either file.
|
|
73
73
|
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: File and journal watchers record
|
|
74
74
|
owner pid and arm id beside the evidence files; a seat retires only watchers it armed, by those
|
|
75
|
-
recorded pids. It never uses `pkill -f`, `pgrep -f`, or any argv/path pattern. Verify each executing
|
|
75
|
+
recorded pids. It never uses `pkill -f`, `pgrep -f`, or any argv/path pattern. The pre-arm beat probe lists unowned writers and is reconciled in the beat section; retiring a watcher this seat armed stays kill-by-recorded-pid and never that probe. Verify each executing
|
|
76
76
|
pid in two process-table reads before acting. A stand-down inventories this seat's recorded pids
|
|
77
77
|
and the partner's watchers that must survive it.
|
|
78
78
|
**An adopted seat ANNOUNCES itself, in the same act as re-arming:** tell the adopted orchestrator the
|
|
@@ -806,6 +806,12 @@ they are left implicit:
|
|
|
806
806
|
report INSIDE the worktree and to commit nothing — the overseer commits from the main checkout — or give the work to a
|
|
807
807
|
claude seat, or to a throwaway CLONE (a real `.git` directory). A brief that tells a codex-in-worktree seat to commit
|
|
808
808
|
buys a stall, not a commit.
|
|
809
|
+
|
|
810
|
+
**Every one-shot `codex exec` from an agent shell closes stdin (D-307-pre).** This skill has no
|
|
811
|
+
`codex exec` recipe — cross-vendor seats are spawned interactive — and the seat's ad-hoc one-shot
|
|
812
|
+
did not close stdin on that command. The agent shell left stdin open, and `codex exec` then
|
|
813
|
+
blocks on reading additional input from stdin (`Reading additional input from stdin…`) until the
|
|
814
|
+
pipe ends (42 minutes, measured). Run it as `codex exec … < /dev/null`.
|
|
809
815
|
4. **Gate every exec lane with the shipped battery, not hand-rolled greps.**
|
|
810
816
|
`tickmarkr verify --base <ref> --criteria <file>` is the standalone form of the engine's own gates —
|
|
811
817
|
build/test/lint diffed against a recorded baseline, evidence, scope, plus the semantic judges — one
|
|
@@ -826,8 +832,13 @@ they are left implicit:
|
|
|
826
832
|
- **Verified send protocol**:
|
|
827
833
|
- **On herdr (`HERDR_ENV=1`)**: `herdr agent send` writes WITHOUT Enter, and `pane run`'s Enter can be swallowed
|
|
828
834
|
by bracketed-paste on long payloads. Robust sequence: read the pane (bare prompt required) → send-text →
|
|
829
|
-
sleep 2–3s → send-keys Enter
|
|
830
|
-
|
|
835
|
+
sleep 2–3s → read back. Send `send-keys Enter` only when that read-back shows the staged text on the
|
|
836
|
+
composer and no permission prompt or numbered choice holds focus — an Enter sent onto an active prompt
|
|
837
|
+
approves it or picks a choice instead of submitting the brief (OBS-1119). When a prompt or choice holds
|
|
838
|
+
focus instead, resolve it first, then re-read before retrying. After a submitted Enter, read back once
|
|
839
|
+
more and confirm the composer is empty or the agent shows `working` — the pre-Enter read only proved
|
|
840
|
+
Enter was safe to send, not that the brief was submitted. Never report "briefed" without
|
|
841
|
+
both read-backs. Long content goes in a brief file, never pane text. `scripts/seat-send.sh` encodes
|
|
831
842
|
this whole path — size guard, atomic prompt, prompt-line read-back, optional interrupt — and never
|
|
832
843
|
auto-resends. Each adapter declares its prompt glyph beside its input-box matchers; `seat-send.sh` reads
|
|
833
844
|
that declaration rather than assuming Claude's `❯`.
|
|
@@ -1009,7 +1020,7 @@ the repo root as its own `run_in_background` Bash call:
|
|
|
1009
1020
|
|
|
1010
1021
|
```bash
|
|
1011
1022
|
cd <repo> && tickmarkr beat overseer --seat <overseer-agent-or-pane> --loop
|
|
1012
|
-
tickmarkr beat overseer --seat <overseer-agent-or-pane> --stand-down # deliberately hand off; --loop exits
|
|
1023
|
+
cd <repo> && tickmarkr beat overseer --seat <overseer-agent-or-pane> --stand-down # deliberately hand off; --loop exits
|
|
1013
1024
|
```
|
|
1014
1025
|
|
|
1015
1026
|
**The legacy wrapper loop, `while :; do tickmarkr beat overseer --seat <pane>; sleep 10; done`, is the
|
|
@@ -1043,9 +1054,10 @@ had three beat writers, one owned by an unrelated session. The shipped `--loop`
|
|
|
1043
1054
|
but its process ownership still needs a live check. So:
|
|
1044
1055
|
|
|
1045
1056
|
- **Split the liveness reads.** A tier's liveness is read from beat freshness in the repository status
|
|
1046
|
-
path
|
|
1047
|
-
--loop` in this repo)
|
|
1048
|
-
|
|
1057
|
+
path. The loop's liveness is read from the live process payload of both arm forms: the loop arm
|
|
1058
|
+
(`tickmarkr beat <tier> --seat <seat> --loop` in this repo) and the legacy wrapper
|
|
1059
|
+
(`while :; do tickmarkr beat <tier> --seat <seat>; sleep 10; done`). Neither liveness claim is read from a recorded pid:
|
|
1060
|
+
a pid recorded earlier can be stale, reused, or detached from the beat now holding the tier green.
|
|
1049
1061
|
- **At every adopt, clear, or re-brief, sweep for pre-existing beat writers on YOUR tier before
|
|
1050
1062
|
arming one** (`pgrep -f "tickmarkr beat <tier>"`, **read twice and intersected** — this exact probe
|
|
1051
1063
|
returned its own shell as pid 14680 on 2026-08-31). **The PRIMARY target is the legacy
|
|
@@ -1053,6 +1065,11 @@ but its process ownership still needs a live check. So:
|
|
|
1053
1065
|
a `--loop` exits by itself once your new arm replaces its own, but the wrapper's bare tick beats
|
|
1054
1066
|
whatever arm is on disk, so it survives your re-arm and never exits on its own. Trace each survivor
|
|
1055
1067
|
to its parent session. Stop an unowned writer only — never its parent — then verify the parent survived.
|
|
1068
|
+
**Reconcile this probe with the recorded-pid ownership rule.** That rule retires watchers this seat
|
|
1069
|
+
armed by the exact recorded pid, and it never uses `pkill -f`, `pgrep -f`, or an argv pattern. This
|
|
1070
|
+
probe is the listing of unowned legacy writers that have no pid file; the pattern stays
|
|
1071
|
+
`pgrep -f "tickmarkr beat <tier>"` with no `--loop` qualifier. A stop is kill-by-pid of an unowned
|
|
1072
|
+
survivor after the two reads. The probe does not retire a recorded pid.
|
|
1056
1073
|
- **`ARMED (<seat>)` is an attributable claim, not proof that the named seat is still alive.** Before
|
|
1057
1074
|
trusting it, ask whose session owns the beater; a still-running `--loop` can keep naming a departed
|
|
1058
1075
|
seat (rule 11's outliving-its-trigger failure, in beat form).
|
|
@@ -1547,6 +1564,14 @@ twice.** They are mission-independent on purpose: nothing here names a task, a l
|
|
|
1547
1564
|
the same root read the other way: a DETACHED loop outlives its seat and holds a tier `ARMED` with
|
|
1548
1565
|
nobody home (OBS-583). Neither direction may be assumed; the lifetime is a property of how the watcher
|
|
1549
1566
|
was launched, and it belongs in writing next to every claim that one is armed.
|
|
1567
|
+
**A seat's background tasks are bound to the seat's life (D-302 add.3).** An ad-hoc background task —
|
|
1568
|
+
an analysis heredoc, a one-shot shell — dies with the seat that launched it. Record its pid and kill
|
|
1569
|
+
that pid at stand-down, the same recorded-pid act as any other task this seat owns. Leave it attached
|
|
1570
|
+
to the seat so it is not reparented to pid 1 and left running after the seat is gone. A watcher this
|
|
1571
|
+
skill explicitly labels detached, with its heartbeat file, is the process that may outlive the seat.
|
|
1572
|
+
**Forbid the home-wide recursive glob by name:** `glob.glob('~/**/.tickmarkr/runs/*/journal.jsonl', recursive=True)`.
|
|
1573
|
+
That heredoc walked the whole home (`~/**`, every worktree, `node_modules`, Library) and ran orphaned
|
|
1574
|
+
for 30 hours at 100% CPU after its seat was gone. No seat glob starts at `~` or `$HOME`.
|
|
1550
1575
|
**And EVERY process-table probe has an idiom that defeats it, so the rule above needs the one test
|
|
1551
1576
|
that survives all of them. Lead with this; it is not the last resort, it is the first move:**
|
|
1552
1577
|
|