tickmarkr 2.5.3 → 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.d.ts +4 -0
- 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/plan.js +144 -3
- package/dist/cli/commands/resume.js +6 -2
- package/dist/cli/commands/run.js +21 -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/compile/collateral.d.ts +2 -0
- package/dist/compile/collateral.js +50 -9
- package/dist/compile/ownership.d.ts +7 -0
- package/dist/compile/ownership.js +59 -22
- package/dist/config/config.d.ts +23 -8
- package/dist/config/config.js +42 -28
- 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/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +14 -3
- 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/drivers/types.d.ts +1 -0
- 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/review.d.ts +35 -2
- package/dist/gates/review.js +64 -16
- package/dist/gates/run-gates.d.ts +5 -0
- package/dist/gates/run-gates.js +134 -18
- 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.d.ts +13 -0
- package/dist/route/router.js +95 -17
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +398 -96
- package/dist/run/git.d.ts +35 -1
- package/dist/run/git.js +111 -10
- 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 +83 -31
- 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 +180 -99
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
@@ -11,11 +11,11 @@ import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.
|
|
|
11
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
12
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
13
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
14
|
-
import { staffLedEvidence } from "../../route/profile.js";
|
|
14
|
+
import { decayWeight, HALF_LIFE_RUNS, staffLedEvidence } from "../../route/profile.js";
|
|
15
15
|
import { route, RoutingError } from "../../route/router.js";
|
|
16
16
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
17
17
|
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
18
|
-
import { Journal, loadRoutingProfile, recordedGraphDefinitionHash } from "../../run/journal.js";
|
|
18
|
+
import { Journal, loadRoutingProfile, readProfileCursor, recordedGraphDefinitionHash, RUNS_WINDOW } from "../../run/journal.js";
|
|
19
19
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
20
20
|
import { channelKey, shq } from "../../adapters/types.js";
|
|
21
21
|
import { shGit } from "../../run/git.js";
|
|
@@ -292,6 +292,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
|
+
lints.push(...tierEscalationAdvisories(cwd, cfg, g.tasks));
|
|
295
296
|
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
296
297
|
if (!health[sel.adapter]?.installed)
|
|
297
298
|
lints.push(`${role}: ${sel.adapter}:${sel.model} not installed — that gate/consult will fail closed`);
|
|
@@ -437,3 +438,143 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
437
438
|
lines.push("", "scope lints:", ...scopeLints.map((l) => ` ! ${l}`));
|
|
438
439
|
return stylizePlan(lines.join("\n"));
|
|
439
440
|
}
|
|
441
|
+
function extractClimb(event, taskShapeMap) {
|
|
442
|
+
if (event.event !== "tier-escalated")
|
|
443
|
+
return undefined;
|
|
444
|
+
const d = (event.data && typeof event.data === "object") ? event.data : {};
|
|
445
|
+
const shape = (typeof d.shape === "string" && d.shape)
|
|
446
|
+
|| (typeof event.shape === "string" && event.shape)
|
|
447
|
+
|| (typeof event.taskId === "string" ? taskShapeMap.get(event.taskId) : undefined)
|
|
448
|
+
|| (typeof d.taskId === "string" ? taskShapeMap.get(d.taskId) : undefined);
|
|
449
|
+
if (!shape)
|
|
450
|
+
return undefined;
|
|
451
|
+
const beforeAssignment = (d.beforeAssignment && typeof d.beforeAssignment === "object") ? d.beforeAssignment : undefined;
|
|
452
|
+
const afterAssignment = (d.afterAssignment && typeof d.afterAssignment === "object") ? d.afterAssignment : undefined;
|
|
453
|
+
let fromTier = (typeof d.fromTier === "string" && d.fromTier)
|
|
454
|
+
|| (typeof d.from === "string" && d.from)
|
|
455
|
+
|| (typeof d.tierBefore === "string" && d.tierBefore)
|
|
456
|
+
|| (typeof d.before === "string" && d.before)
|
|
457
|
+
|| (typeof d.previousTier === "string" && d.previousTier)
|
|
458
|
+
|| (typeof event.fromTier === "string" && event.fromTier)
|
|
459
|
+
|| (typeof event.from === "string" && event.from)
|
|
460
|
+
|| (typeof beforeAssignment?.tier === "string" && beforeAssignment.tier);
|
|
461
|
+
let toTier = (typeof d.toTier === "string" && d.toTier)
|
|
462
|
+
|| (typeof d.to === "string" && d.to)
|
|
463
|
+
|| (typeof d.tierAfter === "string" && d.tierAfter)
|
|
464
|
+
|| (typeof d.after === "string" && d.after)
|
|
465
|
+
|| (typeof d.nextTier === "string" && d.nextTier)
|
|
466
|
+
|| (typeof d.tier === "string" && d.tier)
|
|
467
|
+
|| (typeof event.toTier === "string" && event.toTier)
|
|
468
|
+
|| (typeof event.to === "string" && event.to)
|
|
469
|
+
|| (typeof afterAssignment?.tier === "string" && afterAssignment.tier);
|
|
470
|
+
if (!fromTier || !toTier) {
|
|
471
|
+
const text = [d.details, d.message, d.reason, d.summary, d.provenance].filter((s) => typeof s === "string").join(" ");
|
|
472
|
+
const m = /\b(cheap|mid|frontier)\s+to\s+(cheap|mid|frontier)\b/i.exec(text);
|
|
473
|
+
if (m) {
|
|
474
|
+
fromTier = fromTier || m[1].toLowerCase();
|
|
475
|
+
toTier = toTier || m[2].toLowerCase();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!fromTier || !toTier)
|
|
479
|
+
return undefined;
|
|
480
|
+
return { shape, fromTier, toTier };
|
|
481
|
+
}
|
|
482
|
+
function tierEscalationAdvisories(cwd, cfg, graphTasks) {
|
|
483
|
+
const runsRoot = join(cwd, stateDirName(cwd), "runs");
|
|
484
|
+
if (!existsSync(runsRoot))
|
|
485
|
+
return [];
|
|
486
|
+
let candidateRunIds;
|
|
487
|
+
try {
|
|
488
|
+
candidateRunIds = readdirSync(runsRoot)
|
|
489
|
+
.filter((d) => d.startsWith("run-") && existsSync(join(runsRoot, d, "journal.jsonl")))
|
|
490
|
+
.sort();
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const cursor = readProfileCursor(cwd);
|
|
496
|
+
if (cursor)
|
|
497
|
+
candidateRunIds = candidateRunIds.filter((id) => id > cursor);
|
|
498
|
+
const windowRunIds = candidateRunIds.slice(-RUNS_WINDOW);
|
|
499
|
+
if (!windowRunIds.length)
|
|
500
|
+
return [];
|
|
501
|
+
// Leg-2 T5 (OBS-971): task ids restart at T1 every spec, so a shape lookup is only valid against the
|
|
502
|
+
// graph the run actually executed. Fallback to the current graph only when the run left no snapshot.
|
|
503
|
+
const currentShapeMap = new Map(graphTasks.map((t) => [t.id, t.shape]));
|
|
504
|
+
const numDistinct = windowRunIds.length;
|
|
505
|
+
const ascIndex = new Map(windowRunIds.map((id, i) => [id, i]));
|
|
506
|
+
const halfLife = cfg.routing.learnedTuning?.halfLifeRuns ?? HALF_LIFE_RUNS;
|
|
507
|
+
const shapeClimbs = new Map();
|
|
508
|
+
for (const runId of windowRunIds) {
|
|
509
|
+
const journalPath = join(runsRoot, runId, "journal.jsonl");
|
|
510
|
+
let lines;
|
|
511
|
+
try {
|
|
512
|
+
lines = readFileSync(journalPath, "utf8").split("\n");
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
let taskShapeMap = currentShapeMap;
|
|
518
|
+
const runGraphPath = join(runsRoot, runId, "graph.json");
|
|
519
|
+
if (existsSync(runGraphPath)) {
|
|
520
|
+
try {
|
|
521
|
+
const runGraph = JSON.parse(readFileSync(runGraphPath, "utf8"));
|
|
522
|
+
if (Array.isArray(runGraph.tasks)) {
|
|
523
|
+
taskShapeMap = new Map();
|
|
524
|
+
for (const rt of runGraph.tasks) {
|
|
525
|
+
if (typeof rt?.id === "string" && typeof rt?.shape === "string")
|
|
526
|
+
taskShapeMap.set(rt.id, rt.shape);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// unreadable snapshot: fall back to the current graph
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const runClimbedShapes = new Map();
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
if (!line.trim())
|
|
537
|
+
continue;
|
|
538
|
+
let parsed;
|
|
539
|
+
try {
|
|
540
|
+
parsed = JSON.parse(line);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!parsed || typeof parsed !== "object")
|
|
546
|
+
continue;
|
|
547
|
+
const event = parsed;
|
|
548
|
+
if (event.event !== "tier-escalated")
|
|
549
|
+
continue;
|
|
550
|
+
const climb = extractClimb(event, taskShapeMap);
|
|
551
|
+
if (climb) {
|
|
552
|
+
runClimbedShapes.set(climb.shape, { fromTier: climb.fromTier, toTier: climb.toTier });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const age = numDistinct - 1 - ascIndex.get(runId);
|
|
556
|
+
const w = decayWeight(age, halfLife);
|
|
557
|
+
for (const [shape, { fromTier, toTier }] of runClimbedShapes) {
|
|
558
|
+
let entry = shapeClimbs.get(shape);
|
|
559
|
+
if (!entry) {
|
|
560
|
+
entry = { fromTier, toTier, runs: new Set(), weighted: 0 };
|
|
561
|
+
shapeClimbs.set(shape, entry);
|
|
562
|
+
}
|
|
563
|
+
const tierRank = (t) => (t in TIER_RANK ? TIER_RANK[t] : 0);
|
|
564
|
+
if (tierRank(toTier) > tierRank(entry.toTier)) {
|
|
565
|
+
entry.toTier = toTier;
|
|
566
|
+
}
|
|
567
|
+
entry.runs.add(runId);
|
|
568
|
+
entry.weighted += w;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const advisories = [];
|
|
572
|
+
for (const shape of [...shapeClimbs.keys()].sort()) {
|
|
573
|
+
const info = shapeClimbs.get(shape);
|
|
574
|
+
const climbedRuns = info.runs.size;
|
|
575
|
+
const totalRuns = numDistinct;
|
|
576
|
+
const weightVal = Number.isInteger(info.weighted) ? info.weighted : Number(info.weighted.toFixed(2));
|
|
577
|
+
advisories.push(`${shape}: climbed from ${info.fromTier} to ${info.toTier} in ${climbedRuns} of ${totalRuns} runs (weighted ${weightVal}) — consider routing.floors.${shape}: ${info.toTier}`);
|
|
578
|
+
}
|
|
579
|
+
return advisories;
|
|
580
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
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";
|
|
7
7
|
import { narrationSink, bindNarration } from "./run.js";
|
|
8
|
+
import { assertRefsWritable } from "../../run/git.js";
|
|
8
9
|
const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
9
10
|
&& s.tipVerify !== "failed";
|
|
10
11
|
export async function resume(argv, cwd = process.cwd()) {
|
|
@@ -36,6 +37,9 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
36
37
|
if (collisions.length) {
|
|
37
38
|
throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
|
|
38
39
|
}
|
|
40
|
+
await assertRefsWritable(cwd, "resume");
|
|
41
|
+
const host = classifyHost();
|
|
42
|
+
preflightHostDriver(cfg, driverOverride, host);
|
|
39
43
|
const narrate = narrationSink(runId);
|
|
40
44
|
const s = await runDaemon(cwd, {
|
|
41
45
|
runId,
|
|
@@ -43,7 +47,7 @@ export async function resume(argv, cwd = process.cwd()) {
|
|
|
43
47
|
graphChanged,
|
|
44
48
|
retryFailed,
|
|
45
49
|
// bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
|
|
46
|
-
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
50
|
+
driver: bindNarration(pickDriver(cfg, driverOverride, host), narrate),
|
|
47
51
|
// v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
|
|
48
52
|
// pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
|
|
49
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";
|
|
@@ -10,6 +10,7 @@ import { formatJournalNarration, loadRoutingProfile, newRunId } from "../../run/
|
|
|
10
10
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
11
11
|
import { GLYPHS, LIVE } from "../../brand.js";
|
|
12
12
|
import { cellWidth, fitCells } from "../../tui/cockpit/width.js";
|
|
13
|
+
import { assertRefsWritable } from "../../run/git.js";
|
|
13
14
|
// ── the operator event rail (v1.99 T2) ──────────────────────────────────────────────────────────
|
|
14
15
|
// A run's TTY narration is an operator EVENT RAIL, not the journal dump it used to echo. The
|
|
15
16
|
// repetitive worker-contact / worker-status polls and the ungated phase-start rows are the bulk of
|
|
@@ -61,10 +62,16 @@ export const RAIL_ROWS = {
|
|
|
61
62
|
// run lifecycle
|
|
62
63
|
"run-start": { label: "started", tone: "active" },
|
|
63
64
|
"run-resume": { label: "resumed", tone: "active" },
|
|
65
|
+
"restore-rerouted": { label: "restore rerouted", tone: "attention" },
|
|
66
|
+
"tier-escalated": { label: "tier climbed", tone: "attention" },
|
|
64
67
|
"resume-restore": { label: "restored", tone: "neutral" },
|
|
65
68
|
// the operator's audited --graph-changed release: the resumed run journals it through this sink
|
|
66
69
|
"graph-rehash": { label: "graph rehashed", tone: "attention" },
|
|
67
70
|
"lock-reclaimed": { label: "lock reclaimed", tone: "neutral" },
|
|
71
|
+
// WB-1 (OBS-988): the daemon noticed its own cockpit die and what it did about it
|
|
72
|
+
"watch-board-lost": { label: "board lost", tone: "attention" },
|
|
73
|
+
"watch-board-reopened": { label: "board reopened", tone: "pass" },
|
|
74
|
+
"watch-board-reopen-failed": { label: "board reopen failed", tone: "fail" },
|
|
68
75
|
"run-end": { label: "finished", tone: "neutral" },
|
|
69
76
|
"tip-verify": { label: "tip verify", tone: "pass" },
|
|
70
77
|
"tip-verify-failed": { label: "tip verify", tone: "fail" },
|
|
@@ -99,6 +106,7 @@ export const RAIL_ROWS = {
|
|
|
99
106
|
"gate-fingerprint-cap": { label: "repeat capped", tone: "attention" },
|
|
100
107
|
// a scope red every collateral prediction already named: the daemon parks the task WITHOUT
|
|
101
108
|
// charging an attempt, which is the most consequential unchargeable decision it makes
|
|
109
|
+
"scope-request": { label: "scope requested", tone: "attention" },
|
|
102
110
|
"scope-authoring": { label: "authoring defect", tone: "attention" },
|
|
103
111
|
"session-reset": { label: "session reset", tone: "attention" },
|
|
104
112
|
"worker-mode-fallback": { label: "dispatch mode", tone: "attention" },
|
|
@@ -124,6 +132,9 @@ export const RAIL_ROWS = {
|
|
|
124
132
|
"phase-start": { label: "gate start", tone: "active" },
|
|
125
133
|
"gate-result": { label: "gate", tone: "pass" },
|
|
126
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" },
|
|
127
138
|
"suite-budget": { label: "suite budget", tone: "attention" },
|
|
128
139
|
"gate-replayed": { label: "gate replayed", tone: "attention" },
|
|
129
140
|
"gate-reused": { label: "gate reused", tone: "neutral" },
|
|
@@ -215,6 +226,8 @@ const RAIL_SALIENT = [
|
|
|
215
226
|
// both journal `gates: string[]` and neither states a scalar the ladder can pick up
|
|
216
227
|
{ key: "gates", render: (v) => (Array.isArray(v) && v.length > 0 ? `gates ${v.join(", ")}` : undefined) },
|
|
217
228
|
{ key: "channel", render: (v) => (typeof v === "string" ? `channel ${v}` : undefined) },
|
|
229
|
+
// the pane a board was lost on or reopened in — nothing on the ladder names it
|
|
230
|
+
{ key: "pane", render: (v) => (typeof v === "string" ? `pane ${v}` : undefined) },
|
|
218
231
|
{ key: "status", render: (v) => (typeof v === "string" ? `status ${v}` : undefined) },
|
|
219
232
|
{ key: "cause", render: (v) => (typeof v === "string" ? `cause ${v}` : undefined) },
|
|
220
233
|
{ key: "silentMs", render: (v) => (typeof v === "number" ? `silent ${Math.round(v / 1000)}s` : undefined) },
|
|
@@ -311,6 +324,7 @@ const RAIL_PROJECTION = {
|
|
|
311
324
|
? `to ${d.chosen}${typeof d.static === "string" ? ` over ${d.static}` : ""}`
|
|
312
325
|
: undefined,
|
|
313
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,
|
|
314
328
|
"graph-rehash": (d) => (typeof d.to === "string" ? `to ${d.to.slice(0, 12)}` : undefined),
|
|
315
329
|
"repair-dispatch": (d) => typeof d.diffBytes === "number" ? `diff ${d.diffBytes}B${d.capped === true ? ", capped" : ""}` : undefined,
|
|
316
330
|
"tip-moved": (d) => typeof d.gatedCommit === "string" && typeof d.branchTip === "string"
|
|
@@ -319,6 +333,8 @@ const RAIL_PROJECTION = {
|
|
|
319
333
|
"trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
|
|
320
334
|
// SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
|
|
321
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,
|
|
322
338
|
"suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
|
|
323
339
|
? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
|
|
324
340
|
: undefined,
|
|
@@ -493,6 +509,9 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
493
509
|
if (lints.length)
|
|
494
510
|
throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
|
|
495
511
|
}
|
|
512
|
+
await assertRefsWritable(cwd, "run");
|
|
513
|
+
const host = classifyHost();
|
|
514
|
+
preflightHostDriver(cfg, driverOverride, host);
|
|
496
515
|
// The run id is minted HERE rather than inside the daemon, because the narration sink has to know
|
|
497
516
|
// which run it is narrating before the first event arrives (the daemon's `narrate` callback is
|
|
498
517
|
// handed an event and nothing else, and `run-start` carries no run id). `runDaemon` uses the id
|
|
@@ -502,7 +521,7 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
502
521
|
const s = await runDaemon(cwd, {
|
|
503
522
|
runId,
|
|
504
523
|
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
505
|
-
driver: bindNarration(pickDriver(cfg, driverOverride), narrate),
|
|
524
|
+
driver: bindNarration(pickDriver(cfg, driverOverride, host), narrate),
|
|
506
525
|
mode: flagMode,
|
|
507
526
|
supersedes: values.supersedes,
|
|
508
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.",
|
|
@@ -28,6 +28,8 @@ export declare function classifyScopeOffenders(taskId: string, hard: ReadonlyArr
|
|
|
28
28
|
/**
|
|
29
29
|
* Return human-readable scope-lint lines for plan output (no `!` prefix — plan owns that).
|
|
30
30
|
* Each line names the task id and at least one missing collateral test path.
|
|
31
|
+
* OBS-971: uncapped — every predicted path is listed, with direct importers of an owned source
|
|
32
|
+
* sorted first, followed by the total count. The runtime map (collateralHits) remains unchanged.
|
|
31
33
|
*/
|
|
32
34
|
export declare function collateralLints(tasks: ReadonlyArray<Pick<Task, "id" | "files">>, repoRoot: string): string[];
|
|
33
35
|
/**
|