tickmarkr 2.2.1 → 2.3.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/types.d.ts +20 -1
- package/dist/adapters/types.js +42 -2
- package/dist/cli/commands/approve.js +5 -4
- package/dist/cli/commands/beat.js +7 -4
- package/dist/cli/commands/doctor.d.ts +5 -1
- package/dist/cli/commands/doctor.js +67 -7
- package/dist/cli/commands/init.js +36 -21
- package/dist/cli/commands/plan.js +16 -2
- package/dist/cli/commands/report.js +37 -1
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +140 -25
- package/dist/compile/collateral.js +15 -9
- package/dist/compile/native.js +3 -3
- package/dist/config/config.js +1 -1
- package/dist/drivers/index.d.ts +6 -0
- package/dist/drivers/index.js +19 -4
- package/dist/drivers/orca.d.ts +22 -1
- package/dist/drivers/orca.js +147 -6
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +16 -9
- package/dist/drivers/types.d.ts +2 -0
- package/dist/gates/baseline.d.ts +2 -0
- package/dist/gates/baseline.js +21 -4
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +25 -9
- package/dist/gates/review.d.ts +3 -1
- package/dist/gates/review.js +40 -12
- package/dist/gates/run-gates.js +17 -10
- package/dist/gates/verdict-cause.d.ts +6 -2
- package/dist/gates/verdict-cause.js +8 -4
- package/dist/run/consult.js +5 -1
- package/dist/run/daemon.d.ts +12 -0
- package/dist/run/daemon.js +218 -25
- package/dist/run/git.d.ts +1 -0
- package/dist/run/git.js +4 -0
- package/dist/run/journal.d.ts +15 -2
- package/dist/run/journal.js +70 -12
- package/dist/run/supervision.d.ts +6 -0
- package/dist/run/supervision.js +29 -1
- package/dist/tui/ink/init-app.js +4 -4
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +77 -18
- package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { allAdapters, probeAll, readDoctor, rolePools } from "../../adapters/registry.js";
|
|
7
7
|
import { channelKey } from "../../adapters/types.js";
|
|
8
8
|
import { loadConfig } from "../../config/config.js";
|
|
9
9
|
import { captureBaseline, detectGateCommands } from "../../gates/baseline.js";
|
|
10
|
+
import { modelProvider } from "../../gates/review.js";
|
|
10
11
|
import { runGates } from "../../gates/run-gates.js";
|
|
11
12
|
import { getTask, loadGraph } from "../../graph/graph.js";
|
|
12
13
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
13
14
|
import { linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.js";
|
|
15
|
+
import { Journal } from "../../run/journal.js";
|
|
14
16
|
/**
|
|
15
17
|
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
16
18
|
*
|
|
@@ -59,6 +61,70 @@ const DIRTY_WHY = `refusing to gate a dirty worktree: the shell gates run agains
|
|
|
59
61
|
export function verifyStateDir(cwd) {
|
|
60
62
|
return join(realpathSync(tmpdir()), "tickmarkr-verify", createHash("sha256").update(cwd).digest("hex").slice(0, 12));
|
|
61
63
|
}
|
|
64
|
+
const STATE_FILES = ["graph.json", "doctor.json", "config.yaml"];
|
|
65
|
+
const LOCKFILES = ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
66
|
+
/** Linked worktrees share operational state with the repository that owns the common git dir. */
|
|
67
|
+
export async function verifyStateRoot(cwd) {
|
|
68
|
+
const commonDir = resolve(cwd, (await shGitOk("git rev-parse --git-common-dir", cwd)).trim());
|
|
69
|
+
const commonRoot = realpathSync(dirname(commonDir));
|
|
70
|
+
if (commonRoot === realpathSync(cwd))
|
|
71
|
+
return cwd;
|
|
72
|
+
const localState = join(cwd, ".tickmarkr");
|
|
73
|
+
const commonState = join(commonRoot, ".tickmarkr");
|
|
74
|
+
return STATE_FILES.some((file) => !existsSync(join(localState, file)) && existsSync(join(commonState, file)))
|
|
75
|
+
? commonRoot
|
|
76
|
+
: cwd;
|
|
77
|
+
}
|
|
78
|
+
const hashParts = (parts) => {
|
|
79
|
+
const hash = createHash("sha256");
|
|
80
|
+
for (const part of parts)
|
|
81
|
+
hash.update(part);
|
|
82
|
+
return hash.digest("hex").slice(0, 12);
|
|
83
|
+
};
|
|
84
|
+
/** Cache identity is repository content, never a checkout path. */
|
|
85
|
+
export function baselineCachePath(cwd, baseSha, commands) {
|
|
86
|
+
const lockParts = [];
|
|
87
|
+
for (const file of LOCKFILES) {
|
|
88
|
+
const path = join(cwd, file);
|
|
89
|
+
lockParts.push(file, existsSync(path) ? readFileSync(path) : "<absent>");
|
|
90
|
+
}
|
|
91
|
+
const commandParts = Object.entries(commands).sort(([a], [b]) => a.localeCompare(b)).map(([name, command]) => `${name}\0${command}\0`);
|
|
92
|
+
return join(realpathSync(tmpdir()), "tickmarkr-verify", "cache", `baseline-${baseSha.slice(0, 12)}-${hashParts(lockParts)}-${hashParts(commandParts)}.json`);
|
|
93
|
+
}
|
|
94
|
+
const verdictlessCommands = (baseline, commands) => Object.keys(commands).filter((name) => baseline.commands[name]?.exitCode === undefined);
|
|
95
|
+
export function excludeAuthorProvider(channels, author) {
|
|
96
|
+
const provider = modelProvider(author.model, author.vendor);
|
|
97
|
+
return [author, ...channels.filter((candidate) => candidate !== author && modelProvider(candidate.model, candidate.vendor) !== provider)];
|
|
98
|
+
}
|
|
99
|
+
function recordedMerges(repoRoot) {
|
|
100
|
+
const runs = join(repoRoot, ".tickmarkr", "runs");
|
|
101
|
+
if (!existsSync(runs))
|
|
102
|
+
return [];
|
|
103
|
+
const rows = [];
|
|
104
|
+
for (const runId of readdirSync(runs).filter((name) => name.startsWith("run-")).sort().reverse()) {
|
|
105
|
+
const path = join(runs, runId, "journal.jsonl");
|
|
106
|
+
if (!existsSync(path))
|
|
107
|
+
continue;
|
|
108
|
+
for (const line of readFileSync(path, "utf8").split("\n").filter(Boolean)) {
|
|
109
|
+
try {
|
|
110
|
+
const row = JSON.parse(line);
|
|
111
|
+
if (row.event === "merge" && row.taskId && row.data?.commit)
|
|
112
|
+
rows.push({ runId, taskId: row.taskId, commit: row.data.commit });
|
|
113
|
+
}
|
|
114
|
+
catch { /* torn journal tail is not a merge record */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
118
|
+
}
|
|
119
|
+
async function warnWideTaskRange(cwd, stateRoot, taskId, mergeBase) {
|
|
120
|
+
const inRange = new Set((await shGitOk(`git rev-list --merges '${mergeBase}..HEAD'`, cwd)).trim().split("\n").filter(Boolean));
|
|
121
|
+
const merges = recordedMerges(stateRoot).filter((row) => inRange.has(row.commit));
|
|
122
|
+
const own = merges.find((row) => row.taskId === taskId);
|
|
123
|
+
const others = own ? merges.filter((row) => row.taskId !== taskId) : [];
|
|
124
|
+
if (own && others.length) {
|
|
125
|
+
console.error(`verify: WARNING --task ${taskId} range also carries merge commit(s) for ${others.map((row) => `${row.taskId} ${row.commit.slice(0, 12)}`).join(", ")}; ${taskId}'s own merge ${own.commit} is the intended HEAD`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
62
128
|
export async function verify(argv, cwd = process.cwd()) {
|
|
63
129
|
const { values } = parseArgs({
|
|
64
130
|
args: argv,
|
|
@@ -69,13 +135,18 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
69
135
|
files: { type: "string", multiple: true },
|
|
70
136
|
author: { type: "string" },
|
|
71
137
|
baseline: { type: "string" },
|
|
138
|
+
record: { type: "string" },
|
|
72
139
|
json: { type: "boolean", default: false },
|
|
73
140
|
"no-review": { type: "boolean", default: false },
|
|
74
141
|
"no-acceptance": { type: "boolean", default: false },
|
|
75
142
|
},
|
|
76
143
|
allowPositionals: false,
|
|
77
144
|
});
|
|
78
|
-
const
|
|
145
|
+
const stateRoot = await verifyStateRoot(cwd);
|
|
146
|
+
if (resolve(stateRoot) !== resolve(cwd)) {
|
|
147
|
+
console.error(`verify: state files graph.json, doctor.json and config.yaml resolved read-only from ${join(stateRoot, ".tickmarkr")} (linked worktree)`);
|
|
148
|
+
}
|
|
149
|
+
const cfg = loadConfig(stateRoot);
|
|
79
150
|
const head = (await shGitOk("git rev-parse HEAD", cwd)).trim();
|
|
80
151
|
const baseTip = (await shGitOk(`git rev-parse '${values.base}'`, cwd).catch(() => {
|
|
81
152
|
throw new Error(`--base ${values.base} is not a resolvable ref — pass --base <ref> naming the branch this diff targets`);
|
|
@@ -89,9 +160,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
89
160
|
let files = values.files ?? [];
|
|
90
161
|
let goal = `independent verification of the ${values.base}..HEAD diff`;
|
|
91
162
|
if (values.task) {
|
|
92
|
-
const t = getTask(loadGraph(
|
|
93
|
-
if (!t)
|
|
94
|
-
throw new Error(`--task ${values.task}: no such task in .tickmarkr/graph.json`);
|
|
163
|
+
const t = getTask(loadGraph(stateRoot), values.task);
|
|
95
164
|
acceptance = t.acceptance;
|
|
96
165
|
if (!files.length)
|
|
97
166
|
files = t.files;
|
|
@@ -102,6 +171,8 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
102
171
|
if (!acceptance.length)
|
|
103
172
|
throw new Error(`--criteria ${values.criteria}: no criteria found (bullets or command:/test:/judge: lines)`);
|
|
104
173
|
}
|
|
174
|
+
if (values.task)
|
|
175
|
+
await warnWideTaskRange(cwd, stateRoot, values.task, mergeBase);
|
|
105
176
|
const wantAcceptance = acceptance.length > 0 && !values["no-acceptance"];
|
|
106
177
|
const wantReview = !values["no-review"];
|
|
107
178
|
// A gate that enforced nothing must not print a green row. `files` has exactly three sources —
|
|
@@ -145,7 +216,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
145
216
|
let author = HUMAN_AUTHOR;
|
|
146
217
|
const adapters = allAdapters();
|
|
147
218
|
if (wantAcceptance || wantReview) {
|
|
148
|
-
const health = readDoctor(
|
|
219
|
+
const health = readDoctor(stateRoot) ?? (await probeAll(adapters));
|
|
149
220
|
const pools = rolePools(cfg, adapters, health);
|
|
150
221
|
judgeChannels = pools.judge;
|
|
151
222
|
channels = pools.review;
|
|
@@ -157,6 +228,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
157
228
|
throw new Error(`--author ${values.author} does not name a discoverable review channel — one of: ${channels.map(channelKey).join(", ") || "(none)"}`);
|
|
158
229
|
}
|
|
159
230
|
author = { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
231
|
+
channels = excludeAuthorProvider(channels, c);
|
|
160
232
|
}
|
|
161
233
|
else {
|
|
162
234
|
channels = [...channels, HUMAN_CHANNEL];
|
|
@@ -173,28 +245,56 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
173
245
|
const stateDir = verifyStateDir(cwd);
|
|
174
246
|
mkdirSync(stateDir, { recursive: true });
|
|
175
247
|
let baseline;
|
|
176
|
-
const cachePath =
|
|
248
|
+
const cachePath = baselineCachePath(cwd, mergeBase, commands);
|
|
249
|
+
const verdictlessMarker = `${cachePath}.verdictless`;
|
|
250
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
177
251
|
if (values.baseline) {
|
|
178
252
|
baseline = JSON.parse(readFileSync(values.baseline, "utf8"));
|
|
179
253
|
}
|
|
180
|
-
else if (existsSync(cachePath)) {
|
|
181
|
-
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
|
|
182
|
-
baseline = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
183
|
-
}
|
|
184
254
|
else {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
255
|
+
let cached;
|
|
256
|
+
if (existsSync(cachePath)) {
|
|
257
|
+
cached = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
258
|
+
const missing = verdictlessCommands(cached, commands);
|
|
259
|
+
if (missing.length) {
|
|
260
|
+
console.error(`verify: cached baseline recorded no verdict for ${missing.join(", ")}; it was not reusable and will be recaptured`);
|
|
261
|
+
rmSync(cachePath, { force: true });
|
|
262
|
+
cached = undefined;
|
|
263
|
+
}
|
|
193
264
|
}
|
|
194
|
-
|
|
195
|
-
|
|
265
|
+
if (cached) {
|
|
266
|
+
console.error(`verify: reusing cached baseline for ${mergeBase.slice(0, 12)} (${cachePath})`);
|
|
267
|
+
baseline = cached;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
if (existsSync(verdictlessMarker)) {
|
|
271
|
+
console.error(`verify: prior baseline recorded no verdict and was not cached; recapturing merge-base ${mergeBase.slice(0, 12)}`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.error(`verify: capturing baseline at merge-base ${mergeBase.slice(0, 12)} (cached by base, lockfile and command hashes at ${cachePath})`);
|
|
275
|
+
}
|
|
276
|
+
const baseDir = join(stateDir, `base-${mergeBase.slice(0, 12)}`);
|
|
277
|
+
await shGit(`git worktree remove --force '${baseDir}'`, cwd); // stale leftover from an interrupted run
|
|
278
|
+
await shGitOk(`git worktree add --detach '${baseDir}' '${mergeBase}'`, cwd);
|
|
279
|
+
try {
|
|
280
|
+
linkNodeModules(cwd, baseDir, { force: true });
|
|
281
|
+
baseline = await captureBaseline(baseDir, commands);
|
|
282
|
+
const missing = verdictlessCommands(baseline, commands);
|
|
283
|
+
if (missing.length) {
|
|
284
|
+
writeFileSync(verdictlessMarker, JSON.stringify({ base: mergeBase, commands: missing }) + "\n");
|
|
285
|
+
rmSync(cachePath, { force: true });
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
writeFileSync(cachePath, JSON.stringify(baseline, null, 2));
|
|
289
|
+
rmSync(verdictlessMarker, { force: true });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
await removeWorktree(cwd, baseDir);
|
|
294
|
+
}
|
|
196
295
|
}
|
|
197
296
|
}
|
|
297
|
+
const recordJournal = values.record ? Journal.open(stateRoot, values.record) : undefined;
|
|
198
298
|
const artifactDir = join(stateDir, new Date().toISOString().replace(/[:.]/g, "-"));
|
|
199
299
|
mkdirSync(artifactDir, { recursive: true });
|
|
200
300
|
const { results } = await runGates(task, {
|
|
@@ -212,12 +312,27 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
212
312
|
},
|
|
213
313
|
});
|
|
214
314
|
const green = results.length > 0 && results.every((r) => r.pass || r.meta?.skipped === true);
|
|
315
|
+
const reviewRows = results.filter((result) => result.gate === "review");
|
|
316
|
+
const reviewFindings = reviewRows.flatMap((result) => result.details.split("\n").flatMap((line) => {
|
|
317
|
+
const match = /^- \[([^\]]+)\] (.*)$/.exec(line);
|
|
318
|
+
return match ? [{ classification: match[1], note: match[2], reviewer: result.meta?.reviewer }] : [];
|
|
319
|
+
}));
|
|
320
|
+
const artifactPath = join(artifactDir, "verify-results.json");
|
|
321
|
+
writeFileSync(artifactPath, JSON.stringify({ base: baseTip, head, mergeBase, green, gateRows: results, reviewFindings }, null, 2) + "\n");
|
|
322
|
+
console.error(`verify: artifacts written to ${artifactPath}`);
|
|
323
|
+
const review = reviewRows.at(-1);
|
|
324
|
+
if (recordJournal && review) {
|
|
325
|
+
recordJournal.append("review-leg2", values.task ?? "VERIFY", {
|
|
326
|
+
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath,
|
|
327
|
+
...review,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
215
330
|
if (values.json) {
|
|
216
|
-
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, results }, null, 2), code: green ? 0 : 2 };
|
|
331
|
+
return { out: JSON.stringify({ base: baseTip, head, mergeBase, green, artifactPath, results }, null, 2), code: green ? 0 : 2 };
|
|
217
332
|
}
|
|
218
333
|
const lines = results.map((r) => `${r.pass ? "PASS" : "FAIL"} ${r.gate}\n${r.details.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
219
334
|
const verdict = green
|
|
220
|
-
? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision)`
|
|
221
|
-
: `verify RED — first failure decides; artifacts
|
|
335
|
+
? `verify GREEN — ${results.length} gate(s) passed on ${mergeBase.slice(0, 12)}..${head.slice(0, 12)} (merge is a human decision; artifacts: ${artifactPath})`
|
|
336
|
+
: `verify RED — first failure decides; artifacts: ${artifactPath}`;
|
|
222
337
|
return { out: [...lines, "", verdict].join("\n"), code: green ? 0 : 2 };
|
|
223
338
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { extname, join, relative } from "node:path";
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
@@ -15,6 +15,7 @@ const MAX_WALK_FILES = 400;
|
|
|
15
15
|
const MAX_HITS_PER_TASK = 20;
|
|
16
16
|
/** Skip giant fixtures / snapshots. */
|
|
17
17
|
const MAX_READ_BYTES = 512 * 1024;
|
|
18
|
+
const EXPORT_MANIFEST_TEST = "tests/repo/export-manifest.test.ts";
|
|
18
19
|
const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
19
20
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
20
21
|
function isSrcPath(p) {
|
|
@@ -112,15 +113,20 @@ export function collateralHits(tasks, repoRoot) {
|
|
|
112
113
|
const read = makeReader(repoRoot);
|
|
113
114
|
for (const t of tasks) {
|
|
114
115
|
// OBS-22: scopeGate accepts picomatch globs; advisory collateral warnings must agree.
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const files = t.files.map((f) => f.replace(/^\.\//, ""));
|
|
117
|
+
const scoped = filesGlob(files);
|
|
118
|
+
const srcFiles = files.filter(isSrcPath);
|
|
119
|
+
const hits = [];
|
|
120
|
+
// Exact-enumerated scripts are an export-set contract: a new path has no name or symbol for the
|
|
121
|
+
// ordinary sweep to find, but the manifest test will reject it unless the task owns that oracle.
|
|
122
|
+
if (testFiles.includes(EXPORT_MANIFEST_TEST)
|
|
123
|
+
&& !scoped(EXPORT_MANIFEST_TEST)
|
|
124
|
+
&& files.some((path) => path.startsWith("scripts/") && !/[?*{[]/.test(path) && !existsSync(join(repoRoot, path))))
|
|
125
|
+
hits.push(EXPORT_MANIFEST_TEST);
|
|
119
126
|
// needles unioned across all src files in this task
|
|
120
127
|
const needles = [...new Set(srcFiles.flatMap(needlesFor))];
|
|
121
|
-
const hits = [];
|
|
122
128
|
for (const tf of testFiles) {
|
|
123
|
-
if (scoped(tf))
|
|
129
|
+
if (scoped(tf) || hits.includes(tf))
|
|
124
130
|
continue;
|
|
125
131
|
const text = read(tf);
|
|
126
132
|
if (text === null)
|
|
@@ -128,9 +134,9 @@ export function collateralHits(tasks, repoRoot) {
|
|
|
128
134
|
if (mentions(text, needles))
|
|
129
135
|
hits.push(tf);
|
|
130
136
|
}
|
|
131
|
-
// deterministic: walk
|
|
137
|
+
// deterministic: the special hit and walk are stable; sort their union for one canonical map.
|
|
132
138
|
if (hits.length)
|
|
133
|
-
map.set(t.id, hits);
|
|
139
|
+
map.set(t.id, hits.sort());
|
|
134
140
|
}
|
|
135
141
|
return map;
|
|
136
142
|
}
|
package/dist/compile/native.js
CHANGED
|
@@ -805,9 +805,9 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
805
805
|
set closed — this milestone paid a halted run to learn that the two populations are not identical.
|
|
806
806
|
- SPIKE-THE-CONTRACT-THEN-SCOPE trigger question: COULD A TEST THIS TASK DOES NOT OWN BE ASSERTING THE
|
|
807
807
|
THING I AM CHANGING? "I'D HAVE TO GREP TO KNOW" IS YES. This applies to observable contracts:
|
|
808
|
-
execution order, event-stream order, diagnostics/output sets, CLI surface, serialised formats,
|
|
809
|
-
timing measurements. If yes, implement the change as a
|
|
810
|
-
reds, THEN scope files[].
|
|
808
|
+
execution order, event-stream order, diagnostics/output sets, CLI surface, serialised formats,
|
|
809
|
+
timing measurements, or adding or removing a shipped file. If yes, implement the change as a
|
|
810
|
+
throwaway spike, run the full suite, read the reds, THEN scope files[].
|
|
811
811
|
- Caveat: a spike measures ONE implementation. It converts unknown collateral into
|
|
812
812
|
measured-for-one-specimen collateral; it does NOT make its reds the closed blocker set for every
|
|
813
813
|
route. A worker taking a different route can still red on unowned collateral; that remains a PLAN
|
package/dist/config/config.js
CHANGED
|
@@ -713,7 +713,7 @@ export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
|
|
|
713
713
|
export function configTemplate(overlay) {
|
|
714
714
|
const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
|
|
715
715
|
# concurrency: 3
|
|
716
|
-
# driver: auto # auto
|
|
716
|
+
# driver: auto # auto: herdr when HERDR_ENV=1, then orca when TERM_PROGRAM=Orca + ORCA_TERMINAL_HANDLE, else subprocess; name orca explicitly outside an Orca terminal
|
|
717
717
|
# taskTimeoutMinutes: 30
|
|
718
718
|
# contextWarnTokens: 170000 # v1.23: journal+notify once per attempt when live worker context crosses this (status shows the sample)
|
|
719
719
|
# setup: npm ci --prefer-offline # run in each fresh task worktree before dispatch
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ import type { TickmarkrConfig } from "../config/config.js";
|
|
|
2
2
|
import type { ExecutorDriver } from "./types.js";
|
|
3
3
|
export declare const DRIVER_CHOICES: readonly ["auto", "herdr", "subprocess", "orca"];
|
|
4
4
|
export type DriverChoice = (typeof DRIVER_CHOICES)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Orca authors both markers on every terminal it creates. Requiring the pair avoids treating an
|
|
7
|
+
* unrelated TERM_PROGRAM value or a copied terminal handle as host identity. This is deliberately
|
|
8
|
+
* environment-only: selection must not execute a binary or contact the Orca runtime.
|
|
9
|
+
*/
|
|
10
|
+
export declare function orcaHostDetected(env?: NodeJS.ProcessEnv): boolean;
|
|
5
11
|
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
6
12
|
export declare function parseDriverOverride(override?: string): DriverChoice | undefined;
|
|
7
13
|
export declare function driverEvidence(cfg: TickmarkrConfig, driver: ExecutorDriver, override?: string): string;
|
package/dist/drivers/index.js
CHANGED
|
@@ -3,6 +3,14 @@ import { OrcaDriver } from "./orca.js";
|
|
|
3
3
|
import { SubprocessDriver } from "./subprocess.js";
|
|
4
4
|
export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
|
|
5
5
|
const overrideByDriver = new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Orca authors both markers on every terminal it creates. Requiring the pair avoids treating an
|
|
8
|
+
* unrelated TERM_PROGRAM value or a copied terminal handle as host identity. This is deliberately
|
|
9
|
+
* environment-only: selection must not execute a binary or contact the Orca runtime.
|
|
10
|
+
*/
|
|
11
|
+
export function orcaHostDetected(env = process.env) {
|
|
12
|
+
return env.TERM_PROGRAM === "Orca" && env.ORCA_TERMINAL_HANDLE !== undefined;
|
|
13
|
+
}
|
|
6
14
|
/** Validate argv at the CLI boundary rather than casting an arbitrary string into a driver choice. */
|
|
7
15
|
export function parseDriverOverride(override) {
|
|
8
16
|
if (override === undefined)
|
|
@@ -20,8 +28,13 @@ export function driverEvidence(cfg, driver, override) {
|
|
|
20
28
|
if (want !== "auto")
|
|
21
29
|
return `${driver.id} (config)`;
|
|
22
30
|
const herdrAvailable = process.env.HERDR_ENV === "1";
|
|
23
|
-
if (driver.id ===
|
|
24
|
-
return
|
|
31
|
+
if (herdrAvailable && driver.id === "herdr")
|
|
32
|
+
return "auto → herdr (HERDR_ENV=1)";
|
|
33
|
+
if (!herdrAvailable && orcaHostDetected() && driver.id === "orca") {
|
|
34
|
+
return "auto → orca (TERM_PROGRAM+ORCA_TERMINAL_HANDLE)";
|
|
35
|
+
}
|
|
36
|
+
if (!herdrAvailable && !orcaHostDetected() && driver.id === "subprocess") {
|
|
37
|
+
return "auto → subprocess (HERDR_ENV unset)";
|
|
25
38
|
}
|
|
26
39
|
return `auto → ${driver.id} (runtime)`;
|
|
27
40
|
}
|
|
@@ -34,9 +47,11 @@ export function pickDriver(cfg, override) {
|
|
|
34
47
|
const driver = want === "herdr" ? new HerdrDriver("herdr", cfg.visibility.workersPerTab)
|
|
35
48
|
: want === "subprocess" ? new SubprocessDriver()
|
|
36
49
|
// Orca is an operator-selected execution surface. Its runtime failure stays on Orca; selection
|
|
37
|
-
// must never substitute a hidden subprocess worker after
|
|
50
|
+
// must never substitute a hidden subprocess worker after an explicit or detected choice.
|
|
38
51
|
: want === "orca" ? new OrcaDriver()
|
|
39
|
-
: HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab)
|
|
52
|
+
: HerdrDriver.available() ? new HerdrDriver("herdr", cfg.visibility.workersPerTab)
|
|
53
|
+
: orcaHostDetected() ? new OrcaDriver()
|
|
54
|
+
: new SubprocessDriver();
|
|
40
55
|
if (selectedOverride !== undefined)
|
|
41
56
|
overrideByDriver.set(driver, selectedOverride);
|
|
42
57
|
return driver;
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ShResult } from "../run/git.js";
|
|
2
|
+
import { type JournalEvent } from "../run/journal.js";
|
|
2
3
|
import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
|
|
3
4
|
/** The response families the ONE shared envelope parser serves. There is no second JSON seam. */
|
|
4
|
-
export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close"];
|
|
5
|
+
export declare const ORCA_RESPONSE_FAMILIES: readonly ["status", "create", "list", "read", "send", "wait", "show", "close", "worktree-current", "hooks-status"];
|
|
5
6
|
export type OrcaFamily = (typeof ORCA_RESPONSE_FAMILIES)[number];
|
|
6
7
|
export declare const ORCA_FIXTURE_VERSION = "1.4.195";
|
|
7
8
|
export declare const ORCA_CLI_COMMAND_ENV = "ORCA_CLI_COMMAND";
|
|
@@ -13,6 +14,7 @@ export declare const NOT_WRITABLE_CODE = "terminal_not_writable";
|
|
|
13
14
|
/** The ONLY terminal status that licenses reading a terminal's bytes or its agent state. */
|
|
14
15
|
export declare const RUNNING_STATUS = "running";
|
|
15
16
|
export declare const STATUS_GOVERNED_METHODS: readonly ["read", "waitOutput", "status", "waitAgentStatus"];
|
|
17
|
+
export declare const WORKTREE_ADOPTION_TIMEOUT_MS = 60000;
|
|
16
18
|
export interface OrcaExec {
|
|
17
19
|
(args: string[], cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
18
20
|
}
|
|
@@ -113,6 +115,9 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
113
115
|
private pageLines;
|
|
114
116
|
private pollMs;
|
|
115
117
|
private probeStalenessMs;
|
|
118
|
+
private journalRoots;
|
|
119
|
+
private narrate?;
|
|
120
|
+
private hookCoverage?;
|
|
116
121
|
constructor(opts?: OrcaDriverOpts);
|
|
117
122
|
private call;
|
|
118
123
|
/** The live runtime's identity, or an explicit failure. A missing or unreachable runtime is a
|
|
@@ -128,6 +133,15 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
128
133
|
private assertAvailable;
|
|
129
134
|
run(slot: Slot, cmd: string): Promise<void>;
|
|
130
135
|
private create;
|
|
136
|
+
/**
|
|
137
|
+
* A freshly-created git checkout does not become a valid Orca selector atomically. Ask
|
|
138
|
+
* `worktree current` FROM that checkout until Orca itself resolves the exact filesystem identity;
|
|
139
|
+
* an enclosing checkout is still not adoption. Only selector_not_found is a retryable refusal —
|
|
140
|
+
* malformed envelopes and every other refusal remain explicit driver failures.
|
|
141
|
+
*/
|
|
142
|
+
private awaitWorktreeAdoption;
|
|
143
|
+
/** Same repo/run/narration path Herdr uses for its driver-owned dispatch-retry row. */
|
|
144
|
+
private appendAdoptionWait;
|
|
131
145
|
/**
|
|
132
146
|
* Every terminal-addressed call — read AND write — goes through here, and the runtime identity is
|
|
133
147
|
* established BEFORE the runtime-scoped handle goes on the wire. Discarding a lookalike's answer
|
|
@@ -149,6 +163,8 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
149
163
|
private liveShowTerm;
|
|
150
164
|
private tailText;
|
|
151
165
|
private readPage;
|
|
166
|
+
/** A rendered-frame liveness read. `--screen` and `--cursor` are mutually exclusive in Orca. */
|
|
167
|
+
private readScreen;
|
|
152
168
|
/** A single UNPAGED tail read — exactly what the caller asked for and nothing more. Markers split
|
|
153
169
|
* across cursor pages are not reassembled here; that is waitOutput's job. */
|
|
154
170
|
read(slot: Slot, lines: number): Promise<string>;
|
|
@@ -163,12 +179,17 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
163
179
|
regex?: boolean;
|
|
164
180
|
}): Promise<boolean>;
|
|
165
181
|
status(slot: Slot): Promise<string>;
|
|
182
|
+
private hookAgent;
|
|
183
|
+
/** true = hooked, false = definitively unhooked, undefined = agent absent from Orca's table. */
|
|
184
|
+
private agentHookAvailable;
|
|
185
|
+
private loadHookCoverage;
|
|
166
186
|
/** One `terminal wait` through the full identity machinery. The recorded 1.4.186 elapsed answer
|
|
167
187
|
* is rc 1 + ok:true + {handle, condition, satisfied:false, status:"running"}; it is "not yet"
|
|
168
188
|
* only after this method validates all four fields. Any malformed/refused wait remains explicit. */
|
|
169
189
|
private waitCondition;
|
|
170
190
|
waitAgentStatus(slot: Slot, status: string, timeoutMs: number): Promise<boolean>;
|
|
171
191
|
notify(msg: string, opts?: NotifyOpts): Promise<void>;
|
|
192
|
+
narrateWith(narrate: (event: JournalEvent) => void): void;
|
|
172
193
|
close(slot: Slot): Promise<void>;
|
|
173
194
|
/**
|
|
174
195
|
* The one destructive call in this driver, for a slot's own terminal AND for a reconcile candidate
|