tickmarkr 2.4.0 → 2.4.1
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/catalog-remote.d.ts +11 -0
- package/dist/adapters/catalog-remote.js +55 -13
- package/dist/adapters/codex.js +6 -7
- package/dist/adapters/qwen.js +3 -3
- package/dist/adapters/registry.js +13 -1
- package/dist/cli/commands/compile.d.ts +3 -0
- package/dist/cli/commands/compile.js +77 -46
- package/dist/cli/commands/doctor.js +14 -9
- package/dist/cli/commands/fleet.js +11 -5
- package/dist/cli/commands/init.js +12 -13
- package/dist/cli/commands/plan.js +47 -7
- package/dist/cli/commands/run.js +20 -1
- package/dist/cli/commands/status.js +20 -21
- package/dist/cli/commands/version.js +2 -2
- package/dist/compile/collateral.d.ts +14 -5
- package/dist/compile/collateral.js +32 -26
- package/dist/compile/index.js +17 -6
- package/dist/compile/native.js +10 -2
- package/dist/compile/ownership.js +15 -9
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +32 -3
- package/dist/drivers/orca.d.ts +5 -1
- package/dist/drivers/orca.js +51 -2
- package/dist/drivers/types.d.ts +1 -1
- package/dist/gates/baseline.d.ts +26 -2
- package/dist/gates/baseline.js +90 -7
- package/dist/gates/review.d.ts +2 -2
- package/dist/gates/review.js +2 -18
- package/dist/graph/graph.d.ts +20 -0
- package/dist/graph/graph.js +66 -1
- package/dist/route/preference.d.ts +3 -1
- package/dist/route/preference.js +4 -4
- package/dist/run/consult.js +1 -0
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +151 -22
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +18 -4
- package/dist/run/journal.d.ts +1 -1
- package/dist/run/journal.js +1 -1
- package/dist/run/lock.d.ts +6 -0
- package/dist/run/lock.js +41 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +39 -4
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +79 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +90 -4
|
@@ -6,7 +6,7 @@ import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints,
|
|
|
6
6
|
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
7
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
8
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
9
|
-
import { DEFAULT_CONFIG, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
9
|
+
import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
10
10
|
import { loadGraph } from "../../graph/graph.js";
|
|
11
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
12
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
@@ -14,11 +14,12 @@ import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines }
|
|
|
14
14
|
import { staffLedEvidence } from "../../route/profile.js";
|
|
15
15
|
import { route, RoutingError } from "../../route/router.js";
|
|
16
16
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
17
|
-
import { modelId, modelProvider } from "../../gates/review.js";
|
|
18
|
-
import { loadRoutingProfile } from "../../run/journal.js";
|
|
17
|
+
import { modelId, modelProvider, pickReviewer } from "../../gates/review.js";
|
|
18
|
+
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
19
19
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
20
|
-
import { shq } from "../../adapters/types.js";
|
|
20
|
+
import { channelKey, shq } from "../../adapters/types.js";
|
|
21
21
|
import { shGit } from "../../run/git.js";
|
|
22
|
+
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
22
23
|
import { driverEvidence, pickDriver } from "../../drivers/index.js";
|
|
23
24
|
// T4 (v1.50): TTY-only brand pass — the title helper frames the routing table, lint/unroutable
|
|
24
25
|
// markers carry the attention glyph, section labels dim to chrome (the doctor/status system).
|
|
@@ -44,6 +45,19 @@ const fleetCanCrossVendorReview = (channels) => {
|
|
|
44
45
|
return false;
|
|
45
46
|
};
|
|
46
47
|
const NAMED_CRITERION_PATH = /(?:^|[\s("'`])((?:src|tests|fixtures|scripts)\/[A-Za-z0-9_@{}*?.,/+-]+\.(?:tsx|ts|jsx|json|js|mjs|cjs|md|txt))(?![A-Za-z0-9])/g;
|
|
48
|
+
const judgeOracle = (task) => task.acceptance.some((item) => typeof item === "string" || (typeof item === "object" && item.oracle === "judge"));
|
|
49
|
+
const latestRunStatusLine = (cwd) => {
|
|
50
|
+
const locked = runLockRunId(cwd);
|
|
51
|
+
const runId = locked ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
52
|
+
if (!runId)
|
|
53
|
+
return null;
|
|
54
|
+
let events = [];
|
|
55
|
+
try {
|
|
56
|
+
events = Journal.open(cwd, runId).read();
|
|
57
|
+
}
|
|
58
|
+
catch { /* lock may exist before first journal row */ }
|
|
59
|
+
return runStatusLine(cwd, runId, events);
|
|
60
|
+
};
|
|
47
61
|
async function taskInputFindings(tasks, cwd) {
|
|
48
62
|
const pathsByTask = tasks.map((task) => {
|
|
49
63
|
const paths = new Set(task.files.map((path) => path.replace(/^\.\//, "")));
|
|
@@ -169,11 +183,13 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
169
183
|
// source, and the explore posture; each task row carries a floor-derivation line below.
|
|
170
184
|
// v1.89 T4: the harness names itself ABOVE the routing table — the table is only as trustworthy as the
|
|
171
185
|
// binary that produced it, and version equality cannot tell an installed package from a checkout.
|
|
186
|
+
const runLine = latestRunStatusLine(cwd);
|
|
172
187
|
const lines = [
|
|
173
188
|
`tickmarkr plan — dry run (${channels.length} channels available)`,
|
|
174
189
|
`mode: ${mode.mode} (${source}) · explore ${cfg.routing.explore?.mode ?? "on"}`,
|
|
175
190
|
`driver: ${driverEvidence(cfg, selectedDriver, values.driver)}`,
|
|
176
191
|
harnessLine(resolveHarness(harnessFrom)),
|
|
192
|
+
...(runLine ? [runLine] : []),
|
|
177
193
|
"",
|
|
178
194
|
];
|
|
179
195
|
const judgeDeny = disallowedBy(cfg.judge, cfg.routing, "judge");
|
|
@@ -273,6 +289,28 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
273
289
|
lints.push("review: no cross-vendor reviewer pair in fleet — set review.required: false to waive");
|
|
274
290
|
}
|
|
275
291
|
let cost = 0;
|
|
292
|
+
const seatCost = (seat) => seat?.channel === "api" ? cfg.pricing[seat.tier] : 0;
|
|
293
|
+
const judgeSeat = (task) => {
|
|
294
|
+
const shapeGates = cfg.gates.byShape?.[task.shape];
|
|
295
|
+
if (!task.gates.includes("acceptance") || shapeGates?.acceptance === false)
|
|
296
|
+
return { line: "none — acceptance gate disabled", cost: 0 };
|
|
297
|
+
if (!judgeOracle(task))
|
|
298
|
+
return { line: "none — no judge oracle", cost: 0 };
|
|
299
|
+
const seat = pools.judge.find((c) => c.adapter === cfg.judge.adapter && c.model === cfg.judge.model);
|
|
300
|
+
return { line: `${cfg.judge.adapter}:${cfg.judge.model}`, cost: seatCost(seat) };
|
|
301
|
+
};
|
|
302
|
+
const reviewSeat = (task, author) => {
|
|
303
|
+
const shapeGates = cfg.gates.byShape?.[task.shape];
|
|
304
|
+
if (!task.gates.includes("review") || shapeGates?.review === false)
|
|
305
|
+
return { line: "none — review gate disabled", cost: 0 };
|
|
306
|
+
const policy = effectiveReviewPolicy(task.files, cfg.review);
|
|
307
|
+
if (policy === "judge-only")
|
|
308
|
+
return { line: "none — reviewPolicy judge-only: declared leaf paths", cost: 0 };
|
|
309
|
+
const reviewer = pickReviewer(author, pools.review, [], cfg.review.prefer ?? [], task.routingHints?.floor);
|
|
310
|
+
if (!reviewer)
|
|
311
|
+
return { line: "none — no cross-vendor reviewer available", cost: 0 };
|
|
312
|
+
return { line: `${channelKey(reviewer)} — reviewPolicy full`, cost: seatCost(reviewer) };
|
|
313
|
+
};
|
|
276
314
|
const routed = [];
|
|
277
315
|
for (const t of g.tasks) {
|
|
278
316
|
const refusals = [...(oracleRefusals.get(t.id) ?? []), ...(inputRefusals.get(t.id) ?? [])];
|
|
@@ -288,8 +326,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
288
326
|
lints.push(suf ? `${l}${suf}` : l);
|
|
289
327
|
}
|
|
290
328
|
const est = r.assignment.channel === "sub" ? 0 : cfg.pricing[r.assignment.tier];
|
|
291
|
-
|
|
292
|
-
|
|
329
|
+
const judge = judgeSeat(t);
|
|
330
|
+
const review = reviewSeat(t, r.assignment);
|
|
331
|
+
cost += est + judge.cost + review.cost;
|
|
332
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`);
|
|
293
333
|
const d = derivation(t.shape);
|
|
294
334
|
if (d)
|
|
295
335
|
lines.push(d);
|
|
@@ -326,7 +366,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
326
366
|
if (inputWarnings.length) {
|
|
327
367
|
lines.push("", "input warnings:", ...inputWarnings.map((finding) => ` ! ${finding.taskId}: ${finding.detail}`));
|
|
328
368
|
}
|
|
329
|
-
lines.push("", `est. cost (API
|
|
369
|
+
lines.push("", `est. cost (API LLM seats, rough): ~$${cost.toFixed(2)} + consult calls`);
|
|
330
370
|
// VIS-04: summary only when a profile is active AND something deviates. Labeled by the switch — off = preview.
|
|
331
371
|
if (profile && deviations) {
|
|
332
372
|
lines.push(cfg.routing.learned === "off"
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -2,7 +2,7 @@ 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
4
|
import { parseDriverOverride, pickDriver } from "../../drivers/index.js";
|
|
5
|
-
import { loadGraph } from "../../graph/graph.js";
|
|
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";
|
|
8
8
|
import { route, NO_EXPLORE_ENV } from "../../route/router.js";
|
|
@@ -424,7 +424,26 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
424
424
|
if (values.quality)
|
|
425
425
|
console.warn(QUALITY_ALIAS_NOTICE);
|
|
426
426
|
const flagMode = values.mode ?? (values.quality ? "partner-led" : undefined);
|
|
427
|
+
const refusal = (() => {
|
|
428
|
+
try {
|
|
429
|
+
return readCompileRefusal(cwd);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
throw new Error(`refusing to run: ${error instanceof Error ? error.message : String(error)}. `
|
|
433
|
+
+ "Recompile successfully with `tickmarkr compile <src>` before running.");
|
|
434
|
+
}
|
|
435
|
+
})();
|
|
436
|
+
if (refusal) {
|
|
437
|
+
throw new Error(`refusing to run the prior graph: the last compile of ${refusal.source} was refused at ${refusal.refusedAt}:\n`
|
|
438
|
+
+ `${refusal.error}\n`
|
|
439
|
+
+ "Recompile successfully with `tickmarkr compile <src>` to clear the refusal record before running.");
|
|
440
|
+
}
|
|
427
441
|
const graph = loadGraph(cwd);
|
|
442
|
+
const diskSpec = onDiskSpecHash(cwd, graph);
|
|
443
|
+
if (diskSpec && diskSpec.hash !== graph.spec.hash) {
|
|
444
|
+
throw new Error(`refusing to run a stale graph: ${diskSpec.path} content hash ${diskSpec.hash} differs from `
|
|
445
|
+
+ `the graph's recorded spec hash ${graph.spec.hash}. Recompile with \`tickmarkr compile <src>\` before running.`);
|
|
446
|
+
}
|
|
428
447
|
const { cfg, conflict } = resolveRunMode(cwd, { flag: flagMode, spec: graph.mode });
|
|
429
448
|
if (conflict) {
|
|
430
449
|
// Loud, never silent: live intent (the flag) may override compiled intent (the spec) — strict refuses.
|
|
@@ -9,7 +9,7 @@ import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../.
|
|
|
9
9
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
10
10
|
import { foldActivity } from "../../run/activity.js";
|
|
11
11
|
import { Journal, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
12
|
-
import { isPidLive,
|
|
12
|
+
import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
13
13
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
14
14
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
15
15
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
@@ -860,17 +860,6 @@ const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
|
860
860
|
// .tickmarkr metadata; an engine-written lock necessarily passed through that initializer already, so
|
|
861
861
|
// status avoids invoking it in stripped reader-purity fixtures where no engine lock can exist.
|
|
862
862
|
const canReadRunLockOwner = (cwd) => existsSync(join(cwd, stateDirName(cwd), ".gitignore"));
|
|
863
|
-
const reportableLockRunId = (owner) => {
|
|
864
|
-
if (!owner?.live || owner.runId === undefined)
|
|
865
|
-
return undefined;
|
|
866
|
-
try {
|
|
867
|
-
return parseRunId(owner.runId);
|
|
868
|
-
}
|
|
869
|
-
catch {
|
|
870
|
-
// Repository-wide locks can be held for non-run work such as compile; those are not a status run.
|
|
871
|
-
return undefined;
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
863
|
const readRunJournalRaw = (cwd, runId, allowMissingJournal) => {
|
|
875
864
|
const dir = join(cwd, stateDirName(cwd), "runs", runId);
|
|
876
865
|
try {
|
|
@@ -900,7 +889,7 @@ const readRunJournalRaw = (cwd, runId, allowMissingJournal) => {
|
|
|
900
889
|
const readRunRecord = (cwd, graph, namedRunId) => {
|
|
901
890
|
const explicitRunId = namedRunId === undefined ? undefined : parseRunId(namedRunId);
|
|
902
891
|
const lockedRunId = explicitRunId === undefined && canReadRunLockOwner(cwd)
|
|
903
|
-
?
|
|
892
|
+
? runLockRunId(cwd)
|
|
904
893
|
: undefined;
|
|
905
894
|
const runId = explicitRunId ?? lockedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
906
895
|
if (!runId)
|
|
@@ -947,6 +936,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
947
936
|
const record = readRunRecord(cwd, g, namedRunId);
|
|
948
937
|
const runId = record?.runId;
|
|
949
938
|
const events = record?.events ?? [];
|
|
939
|
+
const runLine = runId && record ? runStatusLine(cwd, runId, events) : null;
|
|
950
940
|
// The header and every live-phase consumer share this exact process reading. Proved death retires
|
|
951
941
|
// stale phase markers; unknown identity deliberately does not, preserving pre-identity journals.
|
|
952
942
|
const daemon = daemonLiveness(events);
|
|
@@ -1091,7 +1081,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1091
1081
|
});
|
|
1092
1082
|
const zone = journalRowsOnly ? `${divider}zone ${localZoneLabel(zoneReference)}` : "";
|
|
1093
1083
|
const header = runId
|
|
1094
|
-
? `tickmarkr status${divider}run ${runId}${zone}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, daemon, now).replaceAll(" · ", divider)}${verify ? `${divider}${verify}` : ""}${divider}${tipPhase ? `${done}/${total} tasks done${divider}run not verified` : `${done}/${total} done`}`
|
|
1084
|
+
? `tickmarkr status${divider}${runLine ?? `run ${runId}`}${zone}${supersededBy ? `${divider}superseded by ${supersededBy}` : ""}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events, daemon, now).replaceAll(" · ", divider)}${verify ? `${divider}${verify}` : ""}${divider}${tipPhase ? `${done}/${total} tasks done${divider}run not verified` : `${done}/${total} done`}`
|
|
1095
1085
|
: `tickmarkr status${zone}${divider}no runs yet${divider}${done}/${total} done`;
|
|
1096
1086
|
const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
1097
1087
|
return {
|
|
@@ -1115,7 +1105,8 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1115
1105
|
const tally = tipPhase
|
|
1116
1106
|
? `${progressTone(`${progressGlyph} ${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
|
|
1117
1107
|
: done === total && total > 0 ? ok(`${done}/${total} done`) : information(`${done}/${total} done`);
|
|
1118
|
-
const
|
|
1108
|
+
const load1 = os.loadavg()[0] ?? 0;
|
|
1109
|
+
const live = `${liveness(events, daemon, now)} · load1 ${load1.toFixed(2)}`
|
|
1119
1110
|
.replace(/\bdead\b/u, fail("dead"))
|
|
1120
1111
|
.replace(/\bfinished\b/u, dim("finished"))
|
|
1121
1112
|
.replace(/\balive\b/u, running("alive"))
|
|
@@ -1133,6 +1124,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1133
1124
|
? ok(verify)
|
|
1134
1125
|
: tipFailed ? fail(`${GLYPHS.fail} ${verify}`) : warn(`${GLYPHS.attention} ${verify}`)]),
|
|
1135
1126
|
...(runId ? [live] : []),
|
|
1127
|
+
...(runLine && runLine !== `run ${runId}` ? [runLine] : []),
|
|
1136
1128
|
...(journalRowsOnly ? [`zone ${localZoneLabel(zoneReference)}`] : []),
|
|
1137
1129
|
].join(separator);
|
|
1138
1130
|
// The brand lockup is two rows and stays two rows at every width: the muted chip, and the running
|
|
@@ -1427,7 +1419,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1427
1419
|
// newer runs start. The no-argument form tracks the live lock first, then latest journal.
|
|
1428
1420
|
const explicitRunId = namedRunId === undefined ? undefined : parseRunId(namedRunId);
|
|
1429
1421
|
const lockedRunId = explicitRunId === undefined && canReadRunLockOwner(cwd)
|
|
1430
|
-
?
|
|
1422
|
+
? runLockRunId(cwd)
|
|
1431
1423
|
: undefined;
|
|
1432
1424
|
const runId = explicitRunId ?? lockedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
1433
1425
|
if (!runId)
|
|
@@ -1494,16 +1486,22 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1494
1486
|
process.stdout.write(`\x1b]0;⏳ ${hotPhase.taskId} ${phaseLabel(hotPhase)} ${fmtElapsed(nowMs - hotPhase.startedAt)}\x07`);
|
|
1495
1487
|
};
|
|
1496
1488
|
const signalHandlers = [];
|
|
1489
|
+
let exitingFromSignal = false;
|
|
1497
1490
|
const exitFromSignal = (code) => () => {
|
|
1491
|
+
if (exitingFromSignal)
|
|
1492
|
+
return;
|
|
1493
|
+
exitingFromSignal = true;
|
|
1498
1494
|
if (titleSaved) {
|
|
1499
1495
|
process.removeListener("exit", restoreTitle);
|
|
1500
1496
|
restoreTitle();
|
|
1501
1497
|
}
|
|
1502
|
-
if (alternateScreen) {
|
|
1503
|
-
process.
|
|
1504
|
-
|
|
1498
|
+
if (!alternateScreen) {
|
|
1499
|
+
process.exit(code);
|
|
1500
|
+
return;
|
|
1505
1501
|
}
|
|
1506
|
-
process.exit
|
|
1502
|
+
process.removeListener("exit", leaveAlternateScreen);
|
|
1503
|
+
alternateScreen = false;
|
|
1504
|
+
process.stdout.write(LEAVE_ALTERNATE_SCREEN, () => process.exit(code));
|
|
1507
1505
|
};
|
|
1508
1506
|
if (!bounded && !eventStream && tty) {
|
|
1509
1507
|
process.stdout.write(ENTER_ALTERNATE_SCREEN);
|
|
@@ -1527,7 +1525,8 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1527
1525
|
if (bounded)
|
|
1528
1526
|
eventLines.push(line);
|
|
1529
1527
|
}
|
|
1530
|
-
|
|
1528
|
+
if (decisionEvents.length === 0)
|
|
1529
|
+
process.stderr.write("status: keepalive\n");
|
|
1531
1530
|
}
|
|
1532
1531
|
else {
|
|
1533
1532
|
frame = renderFrame(cwd, binaryVersion, nowMs, i, workerLiveness, true, namedRunId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
-
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../../package.json");
|
|
6
6
|
const distDir = join(dirname(pkgPath), "dist");
|
|
@@ -25,5 +25,5 @@ export function distFingerprint(dir = distDir) {
|
|
|
25
25
|
}
|
|
26
26
|
export async function version(argv = [], dir = distDir) {
|
|
27
27
|
const { version: v } = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
28
|
-
return argv.includes("--dist") ? `${v} dist:${distFingerprint(dir)}` : v;
|
|
28
|
+
return argv.includes("--dist") ? `${v} dist:${resolve(dir)} fingerprint:${distFingerprint(dir)}` : v;
|
|
29
29
|
}
|
|
@@ -47,12 +47,21 @@ export declare function collateralLints(tasks: ReadonlyArray<Pick<Task, "id" | "
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function criterionReadsOnly(text: string, target: string): boolean;
|
|
49
49
|
export declare function newDirectoryLints(tasks: ReadonlyArray<Pick<Task, "id" | "files">>, repoRoot: string): string[];
|
|
50
|
+
export interface SourceScopeFinding {
|
|
51
|
+
taskId: string;
|
|
52
|
+
/** Complete sorted hit set. Rendering may cap it; compile-time enforcement never does. */
|
|
53
|
+
paths: string[];
|
|
54
|
+
}
|
|
55
|
+
/** Structured OBS-76 findings shared by human rendering and native compile enforcement. */
|
|
56
|
+
export declare function sourceScopeFindings(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string): SourceScopeFinding[];
|
|
57
|
+
/** Human display is derived from the structured finding; no enforcement path parses this text. */
|
|
58
|
+
export declare function renderSourceScopeFinding(finding: SourceScopeFinding): string;
|
|
50
59
|
/**
|
|
51
60
|
* OBS-76 class: sweep src/ for out-of-scope source files that reference a symbol the acceptance
|
|
52
61
|
* criteria name — the v1.52 router.ts omission, named at plan time instead of one judge round in.
|
|
53
62
|
* Advisory plan output only, same contract as collateralLints.
|
|
54
63
|
*/
|
|
55
|
-
export declare function sourceScopeLints(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string): string[];
|
|
64
|
+
export declare function sourceScopeLints(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string, findings?: readonly SourceScopeFinding[]): string[];
|
|
56
65
|
/** Max acceptance items per task. Consult-converged (copus/csol/cfable R2). */
|
|
57
66
|
export declare const MAX_ACCEPTANCE_ITEMS = 6;
|
|
58
67
|
/** Max files[] patterns per task. */
|
|
@@ -141,15 +150,15 @@ export type ReviewParticipation = TickmarkrConfig["review"] & {
|
|
|
141
150
|
export declare function reviewParticipationErrors(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "shape">>, review: ReviewParticipation): string[];
|
|
142
151
|
/**
|
|
143
152
|
* Every Task Unit Contract violation in one pass, ready to throw. `repoRoot` backs the
|
|
144
|
-
* symbol-ownership lint and
|
|
145
|
-
*
|
|
153
|
+
* symbol-ownership lint and participation overlay, and defaults to the invocation directory for
|
|
154
|
+
* programmatic compiles. The CLI passes its target repository explicitly.
|
|
146
155
|
*
|
|
147
156
|
* The read declaration (`context:`) rides on this parameter, on the task objects themselves — the
|
|
148
157
|
* aggregator never infers it from the repo, the config, or a sibling task. It is OPTIONAL: a caller
|
|
149
158
|
* whose task objects carry no `context` gets byte-identical verdicts to the ones it got before the
|
|
150
159
|
* field existed, because an absent declaration grants no authority at all.
|
|
151
160
|
*
|
|
152
|
-
* The compile seam in src/compile/index.ts threads `compileSource`'s repo root here
|
|
153
|
-
*
|
|
161
|
+
* The compile seam in src/compile/index.ts threads `compileSource`'s repo root here when supplied;
|
|
162
|
+
* otherwise this function deliberately checks process.cwd().
|
|
154
163
|
*/
|
|
155
164
|
export declare function taskUnitContractErrors(tasks: ReadonlyArray<Pick<Task, "id" | "goal" | "files" | "deps" | "acceptance" | "shape"> & Partial<Pick<Task, "context">>>, repoRoot?: string, review?: ReviewParticipation): string[];
|
|
@@ -460,23 +460,18 @@ export function newDirectoryLints(tasks, repoRoot) {
|
|
|
460
460
|
}
|
|
461
461
|
return lines;
|
|
462
462
|
}
|
|
463
|
-
/**
|
|
464
|
-
|
|
465
|
-
* criteria name — the v1.52 router.ts omission, named at plan time instead of one judge round in.
|
|
466
|
-
* Advisory plan output only, same contract as collateralLints.
|
|
467
|
-
*/
|
|
468
|
-
export function sourceScopeLints(tasks, repoRoot) {
|
|
469
|
-
const newDirLints = newDirectoryLints(tasks, repoRoot);
|
|
463
|
+
/** Structured OBS-76 findings shared by human rendering and native compile enforcement. */
|
|
464
|
+
export function sourceScopeFindings(tasks, repoRoot) {
|
|
470
465
|
const perTask = tasks
|
|
471
466
|
.map((t) => ({ t, needles: criteriaSymbols(t.acceptance) }))
|
|
472
467
|
.filter((x) => x.needles.length);
|
|
473
468
|
if (!perTask.length)
|
|
474
|
-
return
|
|
469
|
+
return [];
|
|
475
470
|
const srcFiles = walkCode(repoRoot, "src");
|
|
476
471
|
if (!srcFiles.length)
|
|
477
|
-
return
|
|
472
|
+
return [];
|
|
478
473
|
const read = makeReader(repoRoot);
|
|
479
|
-
const
|
|
474
|
+
const findings = [];
|
|
480
475
|
for (const { t, needles } of perTask) {
|
|
481
476
|
// OBS-22: scopeGate accepts picomatch globs; advisory warnings must agree.
|
|
482
477
|
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
@@ -495,11 +490,23 @@ export function sourceScopeLints(tasks, repoRoot) {
|
|
|
495
490
|
if (!hits.length)
|
|
496
491
|
continue;
|
|
497
492
|
hits.sort();
|
|
498
|
-
|
|
499
|
-
const tail = hits.length > MAX_HITS_PER_TASK ? " (capped)" : "";
|
|
500
|
-
lines.push(`${t.id}: criteria implicate out-of-scope source not in files[]: ${listed}${tail}`);
|
|
493
|
+
findings.push({ taskId: t.id, paths: hits });
|
|
501
494
|
}
|
|
502
|
-
return
|
|
495
|
+
return findings;
|
|
496
|
+
}
|
|
497
|
+
/** Human display is derived from the structured finding; no enforcement path parses this text. */
|
|
498
|
+
export function renderSourceScopeFinding(finding) {
|
|
499
|
+
const listed = finding.paths.slice(0, MAX_HITS_PER_TASK).join(", ");
|
|
500
|
+
const tail = finding.paths.length > MAX_HITS_PER_TASK ? " (capped)" : "";
|
|
501
|
+
return `${finding.taskId}: criteria implicate out-of-scope source not in files[]: ${listed}${tail}`;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* OBS-76 class: sweep src/ for out-of-scope source files that reference a symbol the acceptance
|
|
505
|
+
* criteria name — the v1.52 router.ts omission, named at plan time instead of one judge round in.
|
|
506
|
+
* Advisory plan output only, same contract as collateralLints.
|
|
507
|
+
*/
|
|
508
|
+
export function sourceScopeLints(tasks, repoRoot, findings = sourceScopeFindings(tasks, repoRoot)) {
|
|
509
|
+
return [...newDirectoryLints(tasks, repoRoot), ...findings.map(renderSourceScopeFinding)];
|
|
503
510
|
}
|
|
504
511
|
// ── Task Unit Contract (OBS-212 / OBS-214) ────────────────────────────────────────────────────
|
|
505
512
|
// These are ERRORS, not advisories. Everything above this line is a plan-time warning the author
|
|
@@ -835,9 +842,9 @@ export function symbolOwnershipErrors(tasks, repoRoot) {
|
|
|
835
842
|
* cannot be a warning, because nothing downstream would stop it — the gate would honour the policy.
|
|
836
843
|
*/
|
|
837
844
|
export function reviewParticipationErrors(tasks, review) {
|
|
838
|
-
// The shipped critical paths are a FLOOR, not a default that a config replaces:
|
|
839
|
-
//
|
|
840
|
-
//
|
|
845
|
+
// The shipped critical paths are a FLOOR, not a default that a config replaces: a programmatic
|
|
846
|
+
// compile may rely on taskUnitContractErrors' process.cwd() default, and the one direction that
|
|
847
|
+
// must never happen is a mismatched invocation root lowering enforcement below what tickmarkr
|
|
841
848
|
// ships. Union is monotone — a repo-local list can only add.
|
|
842
849
|
const critical = [...new Set([...DEFAULT_REVIEW_CRITICAL_PATHS, ...(review.criticalPaths ?? [])])];
|
|
843
850
|
const errors = [];
|
|
@@ -871,11 +878,10 @@ export function reviewParticipationErrors(tasks, review) {
|
|
|
871
878
|
return errors;
|
|
872
879
|
}
|
|
873
880
|
/**
|
|
874
|
-
* The ACTIVE participation config for a compile rooted at `repoRoot`.
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
* this seam loads the same config itself and reports a malformed one loudly.
|
|
881
|
+
* The ACTIVE participation config for a compile rooted at `repoRoot`. CLI compilation supplies its
|
|
882
|
+
* target root; a programmatic caller that omits it uses taskUnitContractErrors' process.cwd() default,
|
|
883
|
+
* so its active overlay is the invocation repository. Native CLI compiles independently read the
|
|
884
|
+
* overlay fail-closed at the finalization seam, so malformed YAML is loud.
|
|
879
885
|
*/
|
|
880
886
|
function activeReviewParticipation(repoRoot) {
|
|
881
887
|
try {
|
|
@@ -888,16 +894,16 @@ function activeReviewParticipation(repoRoot) {
|
|
|
888
894
|
}
|
|
889
895
|
/**
|
|
890
896
|
* Every Task Unit Contract violation in one pass, ready to throw. `repoRoot` backs the
|
|
891
|
-
* symbol-ownership lint and
|
|
892
|
-
*
|
|
897
|
+
* symbol-ownership lint and participation overlay, and defaults to the invocation directory for
|
|
898
|
+
* programmatic compiles. The CLI passes its target repository explicitly.
|
|
893
899
|
*
|
|
894
900
|
* The read declaration (`context:`) rides on this parameter, on the task objects themselves — the
|
|
895
901
|
* aggregator never infers it from the repo, the config, or a sibling task. It is OPTIONAL: a caller
|
|
896
902
|
* whose task objects carry no `context` gets byte-identical verdicts to the ones it got before the
|
|
897
903
|
* field existed, because an absent declaration grants no authority at all.
|
|
898
904
|
*
|
|
899
|
-
* The compile seam in src/compile/index.ts threads `compileSource`'s repo root here
|
|
900
|
-
*
|
|
905
|
+
* The compile seam in src/compile/index.ts threads `compileSource`'s repo root here when supplied;
|
|
906
|
+
* otherwise this function deliberately checks process.cwd().
|
|
901
907
|
*/
|
|
902
908
|
export function taskUnitContractErrors(tasks, repoRoot = process.cwd(), review = activeReviewParticipation(repoRoot)) {
|
|
903
909
|
return [
|
package/dist/compile/index.js
CHANGED
|
@@ -37,12 +37,16 @@ function detect(src) {
|
|
|
37
37
|
function repoOverlayMode(repoRoot) {
|
|
38
38
|
if (!repoRoot)
|
|
39
39
|
return undefined;
|
|
40
|
+
const path = join(repoRoot, ".tickmarkr", "config.yaml");
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
return undefined;
|
|
40
43
|
try {
|
|
41
|
-
const cfg = parse(readFileSync(
|
|
44
|
+
const cfg = parse(readFileSync(path, "utf8"));
|
|
42
45
|
return typeof cfg?.routing?.mode === "string" ? cfg.routing.mode : undefined;
|
|
43
46
|
}
|
|
44
|
-
catch {
|
|
45
|
-
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new CompileError(`${path} does not parse, so compile cannot prove the repository routing overlay: `
|
|
49
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
function hasModeOverride(src) {
|
|
@@ -58,9 +62,13 @@ function hasModeOverride(src) {
|
|
|
58
62
|
return false;
|
|
59
63
|
}
|
|
60
64
|
function enforceModeOverlay(graph, src, repoRoot) {
|
|
61
|
-
if (graph.spec.source !== "native"
|
|
65
|
+
if (graph.spec.source !== "native")
|
|
62
66
|
return;
|
|
67
|
+
// Read every native compile's overlay before checking whether either side declares a mode. A
|
|
68
|
+
// corrupt file cannot masquerade as an absent mode and silently skip the disagreement gate.
|
|
63
69
|
const overlayMode = repoOverlayMode(repoRoot);
|
|
70
|
+
if (graph.mode === undefined)
|
|
71
|
+
return;
|
|
64
72
|
if (overlayMode === undefined || overlayMode === graph.mode || hasModeOverride(src))
|
|
65
73
|
return;
|
|
66
74
|
throw new CompileError(`${src} front-matter mode ${graph.mode} disagrees with repository routing.mode ${overlayMode}; `
|
|
@@ -75,7 +83,7 @@ function enforceTaskUnitContract(g, src, repoRoot) {
|
|
|
75
83
|
return g;
|
|
76
84
|
}
|
|
77
85
|
export function finalizePlan(plan, src, repoRoot) {
|
|
78
|
-
const graph =
|
|
86
|
+
const graph = validateGraph({
|
|
79
87
|
version: plan.version,
|
|
80
88
|
...(plan.mode !== undefined ? { mode: plan.mode } : {}),
|
|
81
89
|
spec: {
|
|
@@ -85,8 +93,11 @@ export function finalizePlan(plan, src, repoRoot) {
|
|
|
85
93
|
...(plan.base !== undefined ? { base: plan.base } : {}),
|
|
86
94
|
},
|
|
87
95
|
tasks: plan.tasks,
|
|
88
|
-
})
|
|
96
|
+
});
|
|
97
|
+
// Overlay readability is compile truth, not a mode-disagreement optimization. Check it before
|
|
98
|
+
// other repo-dependent lints so malformed native config cannot be hidden by an earlier finding.
|
|
89
99
|
enforceModeOverlay(graph, src, repoRoot);
|
|
100
|
+
enforceTaskUnitContract(graph, src, repoRoot);
|
|
90
101
|
// overseer-217 removal condition, now paid: on this milestone's authored graph the conventional
|
|
91
102
|
// name map emitted 21 raw unowned-test findings; review found 1 real and 20 false, while intersecting
|
|
92
103
|
// with a direct import or command-entry spawn retained the real one and left 0 false positives. That
|
package/dist/compile/native.js
CHANGED
|
@@ -286,11 +286,19 @@ export function authoringLintFindings(tasks, file) {
|
|
|
286
286
|
if (scope)
|
|
287
287
|
findings.push(scope);
|
|
288
288
|
for (const symbol of fencedIdentifiers(text)) {
|
|
289
|
-
|
|
289
|
+
// Empty files[] is the deliberately unrestricted scope and a non-repository programmatic
|
|
290
|
+
// compile has no disk corpus to prove against. But a declared files[] that expands to zero
|
|
291
|
+
// repository files is evidence, not absence: its preservation fence cannot be satisfied.
|
|
292
|
+
if (!root || task.files.length === 0)
|
|
290
293
|
continue;
|
|
294
|
+
if ([...taskTexts.values()].some((body) => body.includes(symbol)))
|
|
295
|
+
continue;
|
|
296
|
+
const matchDetail = taskFiles.length === 0
|
|
297
|
+
? `this task's files[] matched zero files on disk (${task.files.join(", ")})`
|
|
298
|
+
: `that symbol has zero hits in this task's files[] (${taskFiles.join(", ")})`;
|
|
291
299
|
findings.push({
|
|
292
300
|
code: "fence-symbol-absent", fixtureId: id, taskId: task.id, criterion: index + 1,
|
|
293
|
-
detail: `preservation fence cites \`${symbol}\` but
|
|
301
|
+
detail: `preservation fence cites \`${symbol}\` but ${matchDetail} — OBS-604`,
|
|
294
302
|
});
|
|
295
303
|
}
|
|
296
304
|
if (/line[- ]count|physical line|not greater than (?:the|\d)|no larger than|at most \d+ (?:lines|bytes)/i.test(text)) {
|
|
@@ -34,24 +34,29 @@ function sourceFiles(repoRoot) {
|
|
|
34
34
|
return [];
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
// OBS-898:
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// OBS-898: expand the task's source declarations against ONE source-tree listing, once per compile.
|
|
38
|
+
// Tests then query this in-memory index; neither a test file nor a second corroboration pass can
|
|
39
|
+
// trigger another source walk or glob expansion.
|
|
40
|
+
function namedSourceIndex(tasks, allSources) {
|
|
40
41
|
const matches = new Map();
|
|
41
42
|
for (const task of tasks) {
|
|
42
43
|
for (const entry of task.files.map(normalize).filter((path) => path.startsWith("src/"))) {
|
|
43
44
|
const fromGlob = /[*?{[]/.test(entry);
|
|
44
45
|
const entries = fromGlob ? allSources.filter(filesGlob(entry)) : [entry];
|
|
45
46
|
for (const sourcePath of entries) {
|
|
46
|
-
|
|
47
|
-
if (stem === source || stem.startsWith(`${source}-`)) {
|
|
48
|
-
matches.set(`${task.id}:${sourcePath}`, { taskId: task.id, source: sourcePath, fromGlob });
|
|
49
|
-
}
|
|
47
|
+
matches.set(`${task.id}:${sourcePath}`, { taskId: task.id, source: sourcePath, fromGlob });
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
return [...matches.values()];
|
|
54
52
|
}
|
|
53
|
+
function namedSources(test, index) {
|
|
54
|
+
const stem = basename(test).replace(/\.test\.ts$/, "");
|
|
55
|
+
return index.filter(({ source }) => {
|
|
56
|
+
const sourceStem = basename(source, extname(source));
|
|
57
|
+
return stem === sourceStem || stem.startsWith(`${sourceStem}-`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
55
60
|
const moduleKey = (path) => normalize(path).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
56
61
|
function directImportSpecifiers(text) {
|
|
57
62
|
// Comments cannot create an edge. Keep strings intact because they are the import target.
|
|
@@ -167,9 +172,10 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
167
172
|
}
|
|
168
173
|
const globOwnedTests = new Map();
|
|
169
174
|
const allSources = sourceFiles(repoRoot);
|
|
175
|
+
const namedIndex = namedSourceIndex(tasks, allSources);
|
|
170
176
|
for (const source of sources) {
|
|
171
177
|
const ids = predictedBy.get(source.path) ?? new Set();
|
|
172
|
-
for (const named of namedSources(source.path,
|
|
178
|
+
for (const named of namedSources(source.path, namedIndex)) {
|
|
173
179
|
ids.add(named.taskId);
|
|
174
180
|
if (named.fromGlob) {
|
|
175
181
|
const owners = globOwnedTests.get(source.path) ?? new Set();
|
|
@@ -185,7 +191,7 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
185
191
|
if (owners(test).length === 0 && !(globOwnedTests.get(test)?.size)) {
|
|
186
192
|
const ids = [...taskIds].sort();
|
|
187
193
|
const source = sourceByPath.get(test);
|
|
188
|
-
const evidence = corroboration(source, namedSources(test,
|
|
194
|
+
const evidence = corroboration(source, namedSources(test, namedIndex));
|
|
189
195
|
findings.push({
|
|
190
196
|
code: "unowned-test",
|
|
191
197
|
test,
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
71
71
|
private watches;
|
|
72
72
|
constructor(bin?: string, workersPerTab?: number, time?: HerdrTimeSource, journal?: DriverJournal | undefined);
|
|
73
73
|
private appendDispatchRetry;
|
|
74
|
+
private appendPaneClose;
|
|
74
75
|
private openRunJournal;
|
|
75
76
|
private liveSupervisionSeats;
|
|
76
77
|
private journalReconcile;
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -196,6 +196,29 @@ export class HerdrDriver {
|
|
|
196
196
|
// recovery to the file and the pipe while the operator's rail stays silent about it.
|
|
197
197
|
Journal.open(repoRoot, owned.runId, this.narrate).append("dispatch-retry", owned.taskId, data);
|
|
198
198
|
}
|
|
199
|
+
// OBS-906: close() is the harvest path, not a reconcile path, so the sweep's close row cannot
|
|
200
|
+
// describe it. Persist the worker slot identity beside the exact pane/tab address this path used.
|
|
201
|
+
// A driver used outside a daemon has no bound run journal; the injectable sink remains the unit
|
|
202
|
+
// seam there, while every daemon-created worktree is bound by worktree() below.
|
|
203
|
+
appendPaneClose(slot, placement) {
|
|
204
|
+
const owned = parseOwnedName(slot.name);
|
|
205
|
+
if (owned?.role !== "worker")
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
const data = { slot: slot.name, ...placement };
|
|
209
|
+
if (this.journal) {
|
|
210
|
+
this.journal("pane-close", slot.name, data);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const repoRoot = this.journalRoots.get(slot.cwd);
|
|
214
|
+
if (!repoRoot)
|
|
215
|
+
return;
|
|
216
|
+
Journal.open(repoRoot, owned.runId, this.narrate).append("pane-close", owned.taskId, data);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* close is best-effort; a journal failure must not make a successful pane reap fatal */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
199
222
|
openRunJournal(runId) {
|
|
200
223
|
const roots = new Set(this.journalRoots.values());
|
|
201
224
|
roots.add(process.cwd());
|
|
@@ -1101,11 +1124,15 @@ export class HerdrDriver {
|
|
|
1101
1124
|
return this.serial(() => this.closeGrouped(slot));
|
|
1102
1125
|
}
|
|
1103
1126
|
if (slot.tabId) {
|
|
1104
|
-
await this.herdr(`tab close ${shq(slot.tabId)}`); // reaps the slot's whole tab, best-effort
|
|
1127
|
+
const closed = await this.herdr(`tab close ${shq(slot.tabId)}`); // reaps the slot's whole tab, best-effort
|
|
1128
|
+
if (closed.code === 0)
|
|
1129
|
+
this.appendPaneClose(slot, { tabId: slot.tabId });
|
|
1105
1130
|
return;
|
|
1106
1131
|
}
|
|
1107
1132
|
const pane = await this.paneId(slot);
|
|
1108
|
-
await this.herdr(`pane close ${shq(pane)}`); // best-effort
|
|
1133
|
+
const closed = await this.herdr(`pane close ${shq(pane)}`); // best-effort
|
|
1134
|
+
if (closed.code === 0)
|
|
1135
|
+
this.appendPaneClose(slot, { paneId: pane });
|
|
1109
1136
|
}
|
|
1110
1137
|
// D-08 ref-counted teardown, PER GENERATION (VIS-09 item 2): pane close per member; a generation's
|
|
1111
1138
|
// tab closes only when ITS OWN last member leaves; the group entry dies when all generations are gone.
|
|
@@ -1119,7 +1146,9 @@ export class HerdrDriver {
|
|
|
1119
1146
|
if (!gen)
|
|
1120
1147
|
return; // generation already torn down — its tab is gone
|
|
1121
1148
|
const pane = await this.paneId(slot);
|
|
1122
|
-
await this.herdr(`pane close ${shq(pane)}`); // best-effort
|
|
1149
|
+
const closed = await this.herdr(`pane close ${shq(pane)}`); // best-effort
|
|
1150
|
+
if (closed.code === 0)
|
|
1151
|
+
this.appendPaneClose(slot, { paneId: pane, tabId: gen.tabId });
|
|
1123
1152
|
gen.members = gen.members.filter((m) => m.name !== slot.name);
|
|
1124
1153
|
await this.renameGroupTab(gen);
|
|
1125
1154
|
if (gen.members.length === 0) {
|