tickmarkr 2.5.4 → 2.5.5
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/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +54 -6
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +195 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +1 -1
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +14 -8
- package/dist/config/config.js +29 -26
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +68 -11
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +38 -10
- package/dist/drivers/orca.d.ts +99 -10
- package/dist/drivers/orca.js +586 -97
- package/dist/gates/baseline.d.ts +3 -0
- package/dist/gates/baseline.js +2 -1
- package/dist/gates/cache.d.ts +100 -0
- package/dist/gates/cache.js +389 -0
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +117 -10
- package/dist/gates/test-manifest.d.ts +99 -0
- package/dist/gates/test-manifest.js +389 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +49 -0
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +160 -70
- package/dist/run/git.d.ts +10 -1
- package/dist/run/git.js +43 -9
- package/dist/run/journal.d.ts +14 -2
- package/dist/run/journal.js +78 -10
- package/dist/run/lease.d.ts +14 -0
- package/dist/run/lease.js +87 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +41 -27
- package/dist/tui/ink/fleet-app.js +204 -31
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
2
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
@@ -9,10 +9,11 @@ import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReus
|
|
|
9
9
|
import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
|
|
10
10
|
import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
11
11
|
import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
12
|
-
import { tickmarkrDir } from "../../graph/graph.js";
|
|
12
|
+
import { stateDirName, tickmarkrDir } from "../../graph/graph.js";
|
|
13
13
|
import { orcaHostDetected } from "../../drivers/index.js";
|
|
14
|
+
import { WORKTREES_DIR } from "../../run/git.js";
|
|
14
15
|
import { Journal } from "../../run/journal.js";
|
|
15
|
-
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
16
|
+
import { runLockOwner, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
16
17
|
import { doctor } from "./doctor.js";
|
|
17
18
|
import { assembleFleetEditor } from "./fleet.js";
|
|
18
19
|
const SCAFFOLD_SPEC = "tickmarkr.spec.md";
|
|
@@ -340,6 +341,191 @@ async function runInitWizard(cwd, input, output) {
|
|
|
340
341
|
return null;
|
|
341
342
|
return { overlay: result.overlay, installSkills: result.installSkills, installDocs: result.installDocs };
|
|
342
343
|
}
|
|
344
|
+
function realpathSafe(p) {
|
|
345
|
+
try {
|
|
346
|
+
return realpathSync(p);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
const parent = dirname(p);
|
|
350
|
+
if (parent === p)
|
|
351
|
+
return p;
|
|
352
|
+
return join(realpathSafe(parent), basename(p));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function isPathInside(parentDir, candidatePath) {
|
|
356
|
+
const resolvedParent = realpathSafe(resolve(parentDir));
|
|
357
|
+
const resolvedCandidate = realpathSafe(resolve(candidatePath));
|
|
358
|
+
const rel = relative(resolvedParent, resolvedCandidate);
|
|
359
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The ONLY way the --remove path runs git: GIT_DIR, GIT_WORK_TREE and GIT_COMMON_DIR are stripped so the repository acted on
|
|
363
|
+
* is always cwd's own, and LC_ALL=C keeps stderr parseable. Throws execFileSync's error (status, stderr) unchanged.
|
|
364
|
+
*/
|
|
365
|
+
function removeGit(cwd, args) {
|
|
366
|
+
const { GIT_DIR: _dir, GIT_WORK_TREE: _workTree, GIT_COMMON_DIR: _commonDir, ...env } = process.env;
|
|
367
|
+
return execFileSync("git", args, { cwd, env: { ...env, LC_ALL: "C" }, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
368
|
+
}
|
|
369
|
+
function isLinkedWorktree(cwd) {
|
|
370
|
+
try {
|
|
371
|
+
const gitDir = removeGit(cwd, ["rev-parse", "--git-dir"]).trim();
|
|
372
|
+
const gitCommonDir = removeGit(cwd, ["rev-parse", "--git-common-dir"]).trim();
|
|
373
|
+
return resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function checkLiveLock(cwd) {
|
|
380
|
+
const localOwner = runLockOwner(cwd);
|
|
381
|
+
if (localOwner?.live)
|
|
382
|
+
return localOwner;
|
|
383
|
+
try {
|
|
384
|
+
const gitDir = removeGit(cwd, ["rev-parse", "--git-dir"]).trim();
|
|
385
|
+
const gitCommonDir = removeGit(cwd, ["rev-parse", "--git-common-dir"]).trim();
|
|
386
|
+
if (resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir)) {
|
|
387
|
+
const mainRepo = dirname(resolve(cwd, gitCommonDir));
|
|
388
|
+
const mainOwner = runLockOwner(mainRepo);
|
|
389
|
+
if (mainOwner?.live)
|
|
390
|
+
return mainOwner;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// not a git repo
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Only git's discovery answer — exit 128, stderr `fatal: not a git repository (or any of the parent directories)` — means false.
|
|
400
|
+
* Every other failure (explicit bad GIT_DIR, invalid gitfile, permission, missing binary) refuses: unregistration could not be verified.
|
|
401
|
+
*/
|
|
402
|
+
function isGitRepo(cwd) {
|
|
403
|
+
try {
|
|
404
|
+
removeGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
const e = err;
|
|
409
|
+
const stderr = String(e.stderr ?? "").trim();
|
|
410
|
+
if (e.status === 128 && stderr.startsWith("fatal: not a git repository (or any of the parent directories)"))
|
|
411
|
+
return false;
|
|
412
|
+
throw new Error(`cannot remove: git repository probe failed: ${stderr || e.message || "unknown error"}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** Runs git and throws `message` plus git's stderr on failure — a remove never proceeds past a failed step. */
|
|
416
|
+
function git(cwd, args, message) {
|
|
417
|
+
try {
|
|
418
|
+
return removeGit(cwd, args);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
const stderr = String(err.stderr ?? "").trim();
|
|
422
|
+
throw new Error(stderr ? `${message}: ${stderr}` : message);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function registeredWorktrees(cwd, worktreesDir) {
|
|
426
|
+
const out = git(cwd, ["worktree", "list", "--porcelain"], "cannot remove: git worktree list failed");
|
|
427
|
+
return out
|
|
428
|
+
.split("\n")
|
|
429
|
+
.filter((line) => line.startsWith("worktree "))
|
|
430
|
+
.map((line) => line.slice("worktree ".length).trim())
|
|
431
|
+
.filter((wt) => isPathInside(worktreesDir, wt));
|
|
432
|
+
}
|
|
433
|
+
function handleRemove(cwd) {
|
|
434
|
+
const owner = checkLiveLock(cwd);
|
|
435
|
+
if (owner?.live) {
|
|
436
|
+
const holder = typeof owner.pid === "number" ? `live pid ${owner.pid}` : (owner.pid ?? "unreadable payload");
|
|
437
|
+
throw new Error(`cannot remove: graph.lock held by ${holder}`);
|
|
438
|
+
}
|
|
439
|
+
if (isLinkedWorktree(cwd)) {
|
|
440
|
+
throw new Error("cannot remove from inside a linked worktree");
|
|
441
|
+
}
|
|
442
|
+
const notes = [];
|
|
443
|
+
const stateDir = join(cwd, stateDirName(cwd));
|
|
444
|
+
const worktreesDir = join(stateDir, WORKTREES_DIR);
|
|
445
|
+
// 1. Unregister every git worktree under stateDir/WORKTREES_DIR, and prove it, before deleting anything
|
|
446
|
+
if (isGitRepo(cwd)) {
|
|
447
|
+
for (const wt of registeredWorktrees(cwd, worktreesDir)) {
|
|
448
|
+
// double --force: git refuses a locked worktree (present or already deleted) with a single one
|
|
449
|
+
git(cwd, ["worktree", "remove", "--force", "--force", wt], `cannot remove: failed to unregister worktree ${wt}`);
|
|
450
|
+
}
|
|
451
|
+
git(cwd, ["worktree", "prune"], "cannot remove: git worktree prune failed");
|
|
452
|
+
const left = registeredWorktrees(cwd, worktreesDir);
|
|
453
|
+
if (left.length > 0) {
|
|
454
|
+
throw new Error(`cannot remove: worktrees still registered: ${left.join(", ")}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// 2. Delete .tickmarkr state directory
|
|
458
|
+
if (existsSync(stateDir)) {
|
|
459
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
460
|
+
notes.push(`deleted state directory ${stateDir}`);
|
|
461
|
+
}
|
|
462
|
+
// 3. Delete shipped skill directories at every host location
|
|
463
|
+
const targetSkillDirs = [
|
|
464
|
+
join(cwd, ".agents", "skills"),
|
|
465
|
+
join(cwd, ".claude", "skills"),
|
|
466
|
+
];
|
|
467
|
+
for (const dir of targetSkillDirs) {
|
|
468
|
+
for (const skill of AGENT_SKILLS) {
|
|
469
|
+
const skillPath = join(dir, skill);
|
|
470
|
+
if (existsSync(skillPath)) {
|
|
471
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
472
|
+
notes.push(`deleted skill ${skillPath}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// 4. Delete marker-bounded guidance block in AGENTS.md and CLAUDE.md leaving every byte outside identical
|
|
477
|
+
const marked = new RegExp(`<!-- (?:tickmarkr|${LEGACY_PREFIX}):agent-docs begin -->[\\s\\S]*?`
|
|
478
|
+
+ `<!-- (?:tickmarkr|${LEGACY_PREFIX}):agent-docs end -->`);
|
|
479
|
+
for (const doc of ["AGENTS.md", "CLAUDE.md"]) {
|
|
480
|
+
const docPath = join(cwd, doc);
|
|
481
|
+
if (existsSync(docPath)) {
|
|
482
|
+
const current = readFileSync(docPath, "utf8");
|
|
483
|
+
const m = current.match(marked);
|
|
484
|
+
if (m && m.index !== undefined) {
|
|
485
|
+
const before = current.slice(0, m.index);
|
|
486
|
+
const after = current.slice(m.index + m[0].length);
|
|
487
|
+
writeFileSync(docPath, before + after);
|
|
488
|
+
notes.push(`removed guidance block from ${docPath}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// 5. Delete scaffold spec only while it still equals the shipped template
|
|
493
|
+
const specPath = join(cwd, SCAFFOLD_SPEC);
|
|
494
|
+
if (existsSync(specPath)) {
|
|
495
|
+
const currentSpec = readFileSync(specPath, "utf8");
|
|
496
|
+
if (currentSpec === specTemplate()) {
|
|
497
|
+
rmSync(specPath, { force: true });
|
|
498
|
+
notes.push(`deleted scaffold spec ${specPath}`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
notes.push(`kept edited spec ${specPath}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// 6. Query and print remaining branches and preserved refs
|
|
505
|
+
let branches = [];
|
|
506
|
+
try {
|
|
507
|
+
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/tickmarkr/"]);
|
|
508
|
+
branches = out.split("\n").map((b) => b.trim()).filter(Boolean);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// ignore
|
|
512
|
+
}
|
|
513
|
+
let preservedRefs = [];
|
|
514
|
+
try {
|
|
515
|
+
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname)", "refs/tickmarkr/preserved/"]);
|
|
516
|
+
preservedRefs = out.split("\n").map((r) => r.trim()).filter(Boolean);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// ignore
|
|
520
|
+
}
|
|
521
|
+
for (const b of branches) {
|
|
522
|
+
notes.push(`left branch: ${b}`);
|
|
523
|
+
}
|
|
524
|
+
for (const r of preservedRefs) {
|
|
525
|
+
notes.push(`left ref: ${r}`);
|
|
526
|
+
}
|
|
527
|
+
return notes.join("\n");
|
|
528
|
+
}
|
|
343
529
|
export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
344
530
|
let bannerEmitted = false;
|
|
345
531
|
const emitBanner = () => {
|
|
@@ -348,6 +534,8 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
348
534
|
process.stdout.write(BANNER);
|
|
349
535
|
}
|
|
350
536
|
};
|
|
537
|
+
// banner at START on the visual surface — every init path, not just the wizard, and never trailing the probe (operator report 2026-07-17)
|
|
538
|
+
emitBanner();
|
|
351
539
|
const { values } = parseArgs({
|
|
352
540
|
args: argv,
|
|
353
541
|
options: {
|
|
@@ -357,13 +545,15 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
|
|
|
357
545
|
docs: { type: "boolean" },
|
|
358
546
|
fresh: { type: "boolean" },
|
|
359
547
|
yes: { type: "boolean" },
|
|
548
|
+
remove: { type: "boolean" },
|
|
360
549
|
},
|
|
361
550
|
});
|
|
362
|
-
// banner at START on the visual surface — every init path, not just the wizard, and never trailing the probe (operator report 2026-07-17)
|
|
363
|
-
emitBanner();
|
|
364
551
|
if (packageName(cwd) === "tickmarkr") {
|
|
365
552
|
throw new Error("OBS-584: tickmarkr init refuses to run inside the tickmarkr source repository");
|
|
366
553
|
}
|
|
554
|
+
if (values.remove) {
|
|
555
|
+
return handleRemove(cwd);
|
|
556
|
+
}
|
|
367
557
|
const gdir = values["global-dir"] ?? globalConfigDir();
|
|
368
558
|
mkdirSync(gdir, { recursive: true });
|
|
369
559
|
const notes = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { loadConfig } from "../../config/config.js";
|
|
3
|
-
import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
|
|
3
|
+
import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } from "../../drivers/index.js";
|
|
4
4
|
import { loadGraph } from "../../graph/graph.js";
|
|
5
5
|
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
6
6
|
import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
|
|
@@ -38,6 +38,8 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
38
38
|
throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
|
|
39
39
|
}
|
|
40
40
|
await assertRefsWritable(cwd, "resume");
|
|
41
|
+
const host = classifyHost();
|
|
42
|
+
preflightHostDriver(cfg, driverOverride, host);
|
|
41
43
|
const narrate = narrationSink(runId);
|
|
42
44
|
const s = await runDaemon(cwd, {
|
|
43
45
|
runId,
|
|
@@ -45,7 +47,7 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
45
47
|
graphChanged,
|
|
46
48
|
retryFailed,
|
|
47
49
|
// bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
|
|
48
|
-
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
50
|
+
driver: bindNarration(pickDriver(cfg, driverOverride, host), narrate),
|
|
49
51
|
// v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
|
|
50
52
|
// pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
|
|
51
53
|
// would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { allAdapters, discoverChannels, probeAll, readDoctor } from "../../adapters/registry.js";
|
|
3
3
|
import { ROUTING_MODES } from "../../config/config.js";
|
|
4
|
-
import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
|
|
4
|
+
import { classifyHost, parseDriverOverride, pickDriver, preflightHostDriver } from "../../drivers/index.js";
|
|
5
5
|
import { loadGraph, onDiskSpecHash, readCompileRefusal } from "../../graph/graph.js";
|
|
6
6
|
import { formatSummary, resolveRunMode, runDaemon } from "../../run/daemon.js";
|
|
7
7
|
import { isRunLockLive } from "../../run/lock.js";
|
|
@@ -106,6 +106,7 @@ export const RAIL_ROWS = {
|
|
|
106
106
|
"gate-fingerprint-cap": { label: "repeat capped", tone: "attention" },
|
|
107
107
|
// a scope red every collateral prediction already named: the daemon parks the task WITHOUT
|
|
108
108
|
// charging an attempt, which is the most consequential unchargeable decision it makes
|
|
109
|
+
"scope-request": { label: "scope requested", tone: "attention" },
|
|
109
110
|
"scope-authoring": { label: "authoring defect", tone: "attention" },
|
|
110
111
|
"session-reset": { label: "session reset", tone: "attention" },
|
|
111
112
|
"worker-mode-fallback": { label: "dispatch mode", tone: "attention" },
|
|
@@ -131,6 +132,9 @@ export const RAIL_ROWS = {
|
|
|
131
132
|
"phase-start": { label: "gate start", tone: "active" },
|
|
132
133
|
"gate-result": { label: "gate", tone: "pass" },
|
|
133
134
|
"baseline-wait": { label: "baseline wait", tone: "active" },
|
|
135
|
+
"suite-wait": { label: "suite wait", tone: "active" },
|
|
136
|
+
"suite-wait-ceiling": { label: "suite wait ceiling", tone: "attention" },
|
|
137
|
+
"worker-reaped-before-harvest": { label: "worker reaped", tone: "attention" },
|
|
134
138
|
"suite-budget": { label: "suite budget", tone: "attention" },
|
|
135
139
|
"gate-replayed": { label: "gate replayed", tone: "attention" },
|
|
136
140
|
"gate-reused": { label: "gate reused", tone: "neutral" },
|
|
@@ -320,6 +324,7 @@ const RAIL_PROJECTION = {
|
|
|
320
324
|
? `to ${d.chosen}${typeof d.static === "string" ? ` over ${d.static}` : ""}`
|
|
321
325
|
: undefined,
|
|
322
326
|
// both rehash hashes: the ladder reaches `from` (null on an unbound journal) and never `to`
|
|
327
|
+
"scope-request": (d) => Array.isArray(d.paths) ? `files[]: ${d.paths.join(", ")}` : undefined,
|
|
323
328
|
"graph-rehash": (d) => (typeof d.to === "string" ? `to ${d.to.slice(0, 12)}` : undefined),
|
|
324
329
|
"repair-dispatch": (d) => typeof d.diffBytes === "number" ? `diff ${d.diffBytes}B${d.capped === true ? ", capped" : ""}` : undefined,
|
|
325
330
|
"tip-moved": (d) => typeof d.gatedCommit === "string" && typeof d.branchTip === "string"
|
|
@@ -328,6 +333,8 @@ const RAIL_PROJECTION = {
|
|
|
328
333
|
"trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
|
|
329
334
|
// SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
|
|
330
335
|
// would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
|
|
336
|
+
"suite-wait": (d) => typeof d.count === "number" ? `waiting for ${d.count} command(s)` : undefined,
|
|
337
|
+
"suite-wait-ceiling": (d) => typeof d.count === "number" ? `${d.count} still live after ${d.waitedMs}ms` : undefined,
|
|
331
338
|
"suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
|
|
332
339
|
? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
|
|
333
340
|
: undefined,
|
|
@@ -503,6 +510,8 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
503
510
|
throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
|
|
504
511
|
}
|
|
505
512
|
await assertRefsWritable(cwd, "run");
|
|
513
|
+
const host = classifyHost();
|
|
514
|
+
preflightHostDriver(cfg, driverOverride, host);
|
|
506
515
|
// The run id is minted HERE rather than inside the daemon, because the narration sink has to know
|
|
507
516
|
// which run it is narrating before the first event arrives (the daemon's `narrate` callback is
|
|
508
517
|
// handed an event and nothing else, and `run-start` carries no run id). `runDaemon` uses the id
|
|
@@ -512,7 +521,7 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
512
521
|
const s = await runDaemon(cwd, {
|
|
513
522
|
runId,
|
|
514
523
|
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
515
|
-
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
524
|
+
driver: bindNarration(pickDriver(cfg, driverOverride, host), narrate),
|
|
516
525
|
mode: flagMode,
|
|
517
526
|
supersedes: values.supersedes,
|
|
518
527
|
narrate,
|
|
@@ -306,7 +306,7 @@ export async function verify(argv, cwd = process.cwd()) {
|
|
|
306
306
|
worktree: cwd, baseRef: mergeBase,
|
|
307
307
|
result: { ok: true, summary: "standalone verify — no worker claims to trust", deviations: [], raw: "" },
|
|
308
308
|
author, commands, baseline, channels, ...(judgeChannels ? { judgeChannels } : {}), adapters, cfg,
|
|
309
|
-
pipeline: "v185", artifactDir,
|
|
309
|
+
pipeline: "v185", verificationScope: "standalone", artifactDir, stateDir: join(stateRoot, ".tickmarkr"),
|
|
310
310
|
onGate: (e) => {
|
|
311
311
|
if (e.phase === "start")
|
|
312
312
|
console.error(`verify: → ${e.gate} (${e.index}/${e.total})`);
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const COMMAND_HELP: {
|
|
|
15
15
|
"--docs": string;
|
|
16
16
|
"--fresh": string;
|
|
17
17
|
"--yes": string;
|
|
18
|
+
"--remove": string;
|
|
18
19
|
};
|
|
19
20
|
examples: string[];
|
|
20
21
|
};
|
|
@@ -160,6 +161,7 @@ export declare const COMMAND_HELP: {
|
|
|
160
161
|
usage: string;
|
|
161
162
|
description: string;
|
|
162
163
|
options: {
|
|
164
|
+
"--files <glob,\u2026>": string;
|
|
163
165
|
"--by <name>": string;
|
|
164
166
|
"--reason <text>": string;
|
|
165
167
|
"--waive": string;
|
package/dist/cli/help.js
CHANGED
|
@@ -10,7 +10,8 @@ export const COMMAND_HELP = {
|
|
|
10
10
|
"--docs": "Include agent guidance documents with --agent.",
|
|
11
11
|
"--fresh": "Force fresh adapter probes instead of reusing recent health.",
|
|
12
12
|
"--yes": "Skip the interactive setup wizard and use defaults.",
|
|
13
|
-
|
|
13
|
+
"--remove": "Reverse init without scaffolding: delete state, skills, docs block, unedited spec, and prune worktrees.",
|
|
14
|
+
}, examples: ["init --yes", "init --agent --force --docs", "init --remove"],
|
|
14
15
|
},
|
|
15
16
|
doctor: {
|
|
16
17
|
usage: "doctor [options]", description: "Probe configured adapters and models and print diagnostics. Default probing may make model calls and refresh a stale catalog.",
|
|
@@ -128,6 +129,7 @@ export const COMMAND_HELP = {
|
|
|
128
129
|
approve: {
|
|
129
130
|
usage: "approve <run-id> <task-id> [options]", description: "Append a validated park decision. A running owner may enact it; a closed run requires a separate resume. Decisions cannot be undone.",
|
|
130
131
|
options: {
|
|
132
|
+
"--files <glob,…>": "Extend files[] for a scope-request park with comma-separated repository-relative globs; required for scope approval.",
|
|
131
133
|
"--by <name>": "Name the actor (default: current OS user).",
|
|
132
134
|
"--reason <text>": "Record the decision reason.",
|
|
133
135
|
"--waive": "Waive only the identified failed gate.",
|
package/dist/config/config.d.ts
CHANGED
|
@@ -364,24 +364,30 @@ export type FleetTierAssignment = {
|
|
|
364
364
|
export type FleetEditable = {
|
|
365
365
|
denyAdapters: string[];
|
|
366
366
|
denyModels: string[];
|
|
367
|
+
denyWorkersAdapters?: string[];
|
|
368
|
+
denyWorkersModels?: string[];
|
|
367
369
|
tiers: Record<string, Record<string, FleetTierAssignment | null>>;
|
|
368
370
|
map: Record<string, MapEntry>;
|
|
369
371
|
floors: Record<string, Tier>;
|
|
370
372
|
};
|
|
371
373
|
export declare function repoOverlayPath(repoRoot: string): string;
|
|
372
374
|
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
375
|
+
/** A discovered fleet universe row: the served models of one adapter and, per model, the resolved
|
|
376
|
+
* identity doctor recorded for a floating alias (LEG2-T3 round 2 finding 3). */
|
|
377
|
+
export type FleetUniverseRow = {
|
|
378
|
+
adapter: string;
|
|
379
|
+
models: string[];
|
|
380
|
+
identities?: Record<string, string>;
|
|
381
|
+
};
|
|
382
|
+
/** Does a deny/allow entry name this universe channel? Grammar A1 (adapter id, bare model id,
|
|
383
|
+
* adapter:model) plus the recorded identity (bare or adapter:identity) — the collector's authority. */
|
|
384
|
+
export declare function universeEntryMatches(row: FleetUniverseRow, model: string, entry: string): boolean;
|
|
373
385
|
/** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
|
|
374
386
|
* (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
|
|
375
387
|
* did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
|
|
376
388
|
* membership state and must survive a membership write verbatim, never be re-derived away. */
|
|
377
|
-
export declare function universeCovers(universe:
|
|
378
|
-
|
|
379
|
-
models: string[];
|
|
380
|
-
}[], entry: string): boolean;
|
|
381
|
-
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: {
|
|
382
|
-
adapter: string;
|
|
383
|
-
models: string[];
|
|
384
|
-
}[]): FleetEditable;
|
|
389
|
+
export declare function universeCovers(universe: FleetUniverseRow[], entry: string): boolean;
|
|
390
|
+
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?: FleetUniverseRow[]): FleetEditable;
|
|
385
391
|
/** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
|
|
386
392
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
387
393
|
globalDir?: string;
|
package/dist/config/config.js
CHANGED
|
@@ -887,6 +887,14 @@ export function readOverlayFile(path) {
|
|
|
887
887
|
throw new ConfigError(`invalid overlay at ${path}`);
|
|
888
888
|
return raw;
|
|
889
889
|
}
|
|
890
|
+
/** Does a deny/allow entry name this universe channel? Grammar A1 (adapter id, bare model id,
|
|
891
|
+
* adapter:model) plus the recorded identity (bare or adapter:identity) — the collector's authority. */
|
|
892
|
+
export function universeEntryMatches(row, model, entry) {
|
|
893
|
+
if (entry === row.adapter || entry === model || entry === `${row.adapter}:${model}`)
|
|
894
|
+
return true;
|
|
895
|
+
const identity = row.identities?.[model];
|
|
896
|
+
return identity !== undefined && (entry === identity || entry === `${row.adapter}:${identity}`);
|
|
897
|
+
}
|
|
890
898
|
/** Grammar A1 membership test: does any discovered universe row cover this deny/allow entry
|
|
891
899
|
* (adapter id, bare model id, or adapter:model)? Uncovered entries name channels the probe
|
|
892
900
|
* did not serve this session (failed auth, rate limit, retired sku) — they are NOT editable
|
|
@@ -895,9 +903,11 @@ export function universeCovers(universe, entry) {
|
|
|
895
903
|
return universe.some((row) => entry === row.adapter || row.models.some((m) => entry === m || entry === `${row.adapter}:${m}`));
|
|
896
904
|
}
|
|
897
905
|
// v1.92 fleet membership: with a discovered `universe` (classified models only) the deny sets become
|
|
898
|
-
// the EFFECTIVE out-of-fleet
|
|
899
|
-
// routing.deny adapters/models
|
|
900
|
-
//
|
|
906
|
+
// the EFFECTIVE out-of-fleet set — the routing.allow complement (fail-closed) as adapter ids and
|
|
907
|
+
// adapter:model keys, UNION every authored routing.deny adapters/models entry VERBATIM in its own list
|
|
908
|
+
// (never workers). LEG2-T3 round 2 finding 2: authored entries are never collapsed into a membership
|
|
909
|
+
// key, so one Space press clears one reason; finding 3: allow admission honours recorded identity.
|
|
910
|
+
// Absent universe ⇒ deny arrays verbatim, so non-UI callers stay byte-identical.
|
|
901
911
|
export function fleetEditableFromConfig(cfg, universe) {
|
|
902
912
|
const tiers = {};
|
|
903
913
|
for (const [adapter, entry] of Object.entries(cfg.tiers)) {
|
|
@@ -909,43 +919,32 @@ export function fleetEditableFromConfig(cfg, universe) {
|
|
|
909
919
|
let denyAdapters = [...(cfg.routing.deny?.adapters ?? [])].sort();
|
|
910
920
|
let denyModels = [...(cfg.routing.deny?.models ?? [])].sort();
|
|
911
921
|
if (universe !== undefined) {
|
|
912
|
-
const { allow
|
|
913
|
-
// Grammar A1 (disallowedBy precedent): entries match adapter id, bare model id, or adapter:model.
|
|
914
|
-
// Inlined rather than imported — route/preference.ts imports this module.
|
|
915
|
-
const denyEntries = [...(deny?.adapters ?? []), ...(deny?.models ?? [])];
|
|
922
|
+
const { allow } = cfg.routing;
|
|
916
923
|
const allowEntries = allow ? [...(allow.adapters ?? []), ...(allow.models ?? [])] : null;
|
|
917
|
-
const
|
|
918
|
-
const matches = (e) => e === adapter || e === model || e === `${adapter}:${model}`;
|
|
919
|
-
if (denyEntries.some(matches))
|
|
920
|
-
return true;
|
|
921
|
-
return allowEntries !== null && !allowEntries.some(matches);
|
|
922
|
-
};
|
|
924
|
+
const notAdmitted = (row, model) => allowEntries !== null && !allowEntries.some((entry) => universeEntryMatches(row, model, entry));
|
|
923
925
|
const adaptersOut = [];
|
|
924
926
|
const modelsOut = [];
|
|
925
927
|
for (const row of universe) {
|
|
926
|
-
const out = row.models.filter((m) =>
|
|
927
|
-
if (row.models.length ? out.length === row.models.length :
|
|
928
|
+
const out = row.models.filter((m) => notAdmitted(row, m));
|
|
929
|
+
if (row.models.length ? out.length === row.models.length : notAdmitted(row, "")) {
|
|
928
930
|
adaptersOut.push(row.adapter);
|
|
929
931
|
}
|
|
930
932
|
else {
|
|
931
933
|
modelsOut.push(...out.map((m) => `${row.adapter}:${m}`));
|
|
932
934
|
}
|
|
933
935
|
}
|
|
934
|
-
// OBS-517: deny entries the probe universe
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
...adaptersOut,
|
|
939
|
-
...(deny?.adapters ?? []).filter((e) => !universeCovers(universe, e)),
|
|
940
|
-
])].sort();
|
|
941
|
-
denyModels = [...new Set([
|
|
942
|
-
...modelsOut,
|
|
943
|
-
...(deny?.models ?? []).filter((e) => !universeCovers(universe, e)),
|
|
944
|
-
])].sort();
|
|
936
|
+
// OBS-517: authored deny entries — covered by the probe universe or not — ride verbatim, so a
|
|
937
|
+
// transient probe failure can never un-deny one on the next write (fail-open on the ban list).
|
|
938
|
+
denyAdapters = [...new Set([...adaptersOut, ...denyAdapters])].sort();
|
|
939
|
+
denyModels = [...new Set([...modelsOut, ...denyModels])].sort();
|
|
945
940
|
}
|
|
946
941
|
return {
|
|
947
942
|
denyAdapters,
|
|
948
943
|
denyModels,
|
|
944
|
+
// OBS-994: workers-only deny is never folded into the all-seats complement above — it is
|
|
945
|
+
// returned verbatim beside the flat scopes so the fleet browser can show and toggle it.
|
|
946
|
+
denyWorkersAdapters: [...(cfg.routing.deny?.workers?.adapters ?? [])].sort(),
|
|
947
|
+
denyWorkersModels: [...(cfg.routing.deny?.workers?.models ?? [])].sort(),
|
|
949
948
|
tiers,
|
|
950
949
|
map: structuredClone(cfg.routing.map),
|
|
951
950
|
floors: { ...cfg.routing.floors },
|
|
@@ -988,6 +987,10 @@ export function formatFleetPrint(repoRoot, opts = {}) {
|
|
|
988
987
|
annotate(["routing", "deny", "adapters"]);
|
|
989
988
|
if (effective.routing.deny?.models !== undefined)
|
|
990
989
|
annotate(["routing", "deny", "models"]);
|
|
990
|
+
if (effective.routing.deny?.workers?.adapters !== undefined)
|
|
991
|
+
annotate(["routing", "deny", "workers", "adapters"]);
|
|
992
|
+
if (effective.routing.deny?.workers?.models !== undefined)
|
|
993
|
+
annotate(["routing", "deny", "workers", "models"]);
|
|
991
994
|
for (const shape of Object.keys(effective.routing.map))
|
|
992
995
|
annotate(["routing", "map", shape]);
|
|
993
996
|
annotate(["routing", "floors"]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FleetEditable, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type RoutingMode } from "./config.js";
|
|
2
2
|
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
3
3
|
export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
|
|
4
4
|
export type FleetOverlayWrite = {
|
|
@@ -19,10 +19,7 @@ export type FleetOverlayWrite = {
|
|
|
19
19
|
consult?: string[];
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
-
universe?:
|
|
23
|
-
adapter: string;
|
|
24
|
-
models: string[];
|
|
25
|
-
}[];
|
|
22
|
+
universe?: FleetUniverseRow[];
|
|
26
23
|
};
|
|
27
24
|
export type FleetFirstTouch = {
|
|
28
25
|
vendor: string;
|
|
@@ -33,10 +30,7 @@ export declare function fleetFirstTouchProvenance(note: string, firstTouch: Flee
|
|
|
33
30
|
* keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
|
|
34
31
|
export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
|
|
35
32
|
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
36
|
-
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?:
|
|
37
|
-
adapter: string;
|
|
38
|
-
models: string[];
|
|
39
|
-
}[]): Record<string, unknown>;
|
|
33
|
+
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>, universe?: FleetUniverseRow[]): Record<string, unknown>;
|
|
40
34
|
export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
|
|
41
35
|
export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
|
|
42
36
|
export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
|