tickmarkr 2.1.6 → 2.1.8
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/cli/commands/beat.js +37 -10
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +23 -1
- package/dist/cli/commands/plan.d.ts +6 -2
- package/dist/cli/commands/plan.js +94 -1
- package/dist/cli/commands/run.js +21 -2
- package/dist/cli/commands/stats.js +4 -14
- package/dist/cli/commands/status.js +73 -26
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +24 -5
- package/dist/compile/native.js +14 -0
- package/dist/compile/ownership.d.ts +35 -0
- package/dist/compile/ownership.js +213 -0
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +7 -6
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +54 -14
- package/dist/gates/acceptance.d.ts +13 -0
- package/dist/gates/acceptance.js +43 -16
- package/dist/gates/baseline.js +12 -1
- package/dist/gates/run-gates.js +26 -23
- package/dist/run/daemon.js +42 -7
- package/dist/run/journal.js +21 -1
- package/dist/run/outcome.js +8 -1
- package/dist/run/supervision.d.ts +15 -1
- package/dist/run/supervision.js +112 -14
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -1
- package/skills/tickmarkr-loop/SKILL.md +2 -1
- package/skills/tickmarkr-overseer/SKILL.md +9 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +46 -32
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
4
|
-
import { SUPERVISION_BEAT_MS, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
4
|
+
import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, supervisionStandDownPath, } from "../../run/supervision.js";
|
|
5
5
|
// SUP-04: the writer side of supervision, as a VERB. `beatSupervision` and `SUPERVISION_BEAT_MS` shipped
|
|
6
6
|
// with exactly one in-repo caller — the daemon, on one tier — so `status` printed
|
|
7
7
|
// `orchestrator ARMED / overseer ABSENT / watch ABSENT` while a real overseer worked the run: two thirds
|
|
@@ -18,16 +18,22 @@ import { SUPERVISION_BEAT_MS, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSuper
|
|
|
18
18
|
// reads UNREADABLE. Here the writes are unguarded: a failure leaves the process with a non-zero exit
|
|
19
19
|
// and a message instead of a claim. The LOOP belongs to the caller, which is what makes the beat
|
|
20
20
|
// evidence: stop calling it and the tier ages into STALE on its own.
|
|
21
|
+
const VALUE_OPTIONS = ["--seat", "--arm-id", "--pct", "--threshold-pct"];
|
|
21
22
|
export async function beat(argv, cwd = process.cwd()) {
|
|
22
23
|
const standDown = argv.includes("--stand-down");
|
|
23
24
|
const seat = seatOf(argv);
|
|
24
|
-
const
|
|
25
|
+
const pct = percentageOf(argv, "--pct");
|
|
26
|
+
const thresholdPct = percentageOf(argv, "--threshold-pct") ?? SUPERVISION_DEFAULT_THRESHOLD_PCT;
|
|
27
|
+
const armId = optionOf(argv, "--arm-id");
|
|
28
|
+
const named = argv.find((a, i) => !a.startsWith("--") && !VALUE_OPTIONS.includes(argv[i - 1] ?? ""));
|
|
25
29
|
if (!isTier(named)) {
|
|
26
|
-
throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity>
|
|
30
|
+
throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity> ` +
|
|
31
|
+
`[--arm-id <identity> --pct <0..100> --threshold-pct <0..100>] [--stand-down] — ` +
|
|
32
|
+
`got ${named ? `\`${named}\`` : "no tier"}`);
|
|
27
33
|
}
|
|
28
34
|
// SUP-05: NO SEAT, NO BEAT — and the refusal comes before any write, so a refused invocation leaves
|
|
29
|
-
// the tier exactly as it found it. This verb is one-shot: the
|
|
30
|
-
// already exited by the time anyone reads the record, so tier +
|
|
35
|
+
// the tier exactly as it found it. This verb is one-shot: the exitedWriterPid it records has
|
|
36
|
+
// already exited by the time anyone reads the record, so tier + writer + instant is a beat nobody can
|
|
31
37
|
// attribute to a seat. Measured 2026-08-26: a consult seat ran the documented loop verbatim and the
|
|
32
38
|
// board read that tier ARMED with no seat of that tier having armed anything, and a seatless ARMED
|
|
33
39
|
// reads as coverage — worse than ABSENT, because ABSENT sends someone to look.
|
|
@@ -42,16 +48,35 @@ export async function beat(argv, cwd = process.cwd()) {
|
|
|
42
48
|
// uncleared marker would render DISARMED while this verb claimed ARMED, so the removal is unguarded
|
|
43
49
|
// too — `force` makes the ordinary "no marker" case a no-op, and anything else is a real failure.
|
|
44
50
|
rmSync(supervisionStandDownPath(cwd, named), { force: true, recursive: true });
|
|
45
|
-
beatSupervision(cwd, named, seat);
|
|
51
|
+
beatSupervision(cwd, named, seat, pct === undefined ? undefined : { armId: armId ?? seat, pct, thresholdPct });
|
|
46
52
|
return `${named} ARMED as ${seat} — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
|
|
47
53
|
}
|
|
48
54
|
/** `--seat <identity>` or `--seat=<identity>`; blank and missing are the same answer — none. */
|
|
49
55
|
function seatOf(argv) {
|
|
50
|
-
const
|
|
51
|
-
const spaced = argv[argv.indexOf("--seat") + 1];
|
|
52
|
-
const seat = (inline ?? (argv.includes("--seat") ? spaced : undefined))?.trim();
|
|
56
|
+
const seat = optionOf(argv, "--seat")?.trim();
|
|
53
57
|
return seat && !seat.startsWith("--") ? seat : undefined;
|
|
54
58
|
}
|
|
59
|
+
/** A `--name value` or `--name=value` option, excluding a missing value or the next flag. */
|
|
60
|
+
function optionOf(argv, name) {
|
|
61
|
+
const inline = argv.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1).trim();
|
|
62
|
+
const spaced = argv[argv.indexOf(name) + 1]?.trim();
|
|
63
|
+
const value = inline ?? (argv.includes(name) ? spaced : undefined);
|
|
64
|
+
return value && !value.startsWith("--") ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
function percentageOf(argv, name) {
|
|
67
|
+
const raw = optionOf(argv, name);
|
|
68
|
+
if (raw === undefined) {
|
|
69
|
+
if (argv.includes(name) || argv.some((a) => a.startsWith(`${name}=`))) {
|
|
70
|
+
throw new Error(`${name} needs a number from 0 through 100`);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const value = Number(raw);
|
|
75
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
76
|
+
throw new Error(`${name} must be a number from 0 through 100`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
55
80
|
// Stand-down is a RECORDED act, not a silence: the marker tells a reader this watcher left on purpose,
|
|
56
81
|
// so the tier reads DISARMED rather than ageing out as a death. Published atomically — written aside,
|
|
57
82
|
// renamed over — because a torn marker is rejected by the reader, and a rejected stand-down reports a
|
|
@@ -63,7 +88,9 @@ function standDownTier(repoRoot, tier, seat) {
|
|
|
63
88
|
mkdirSync(dirname(p), { recursive: true });
|
|
64
89
|
// The marker names the seat for the same reason the beat does: "someone stood this tier down" is not
|
|
65
90
|
// a hand-off anyone can act on, and on a seat tier the reader rejects an anonymous one outright.
|
|
66
|
-
writeFileSync(tmp, JSON.stringify({
|
|
91
|
+
writeFileSync(tmp, JSON.stringify({
|
|
92
|
+
tier, seat, exitedWriterPid: process.pid, disarmedAt: new Date().toISOString(),
|
|
93
|
+
}) + "\n");
|
|
67
94
|
renameSync(tmp, p);
|
|
68
95
|
return `${tier} DISARMED — ${seat} handed off; status reads it stood down, not dead`;
|
|
69
96
|
}
|
|
@@ -3,6 +3,7 @@ import type { WorkerAdapter } from "../../adapters/types.js";
|
|
|
3
3
|
import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
|
|
4
4
|
import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
|
|
5
5
|
import { type ShResult } from "../../run/git.js";
|
|
6
|
+
import { type VitestListResult } from "../../gates/acceptance.js";
|
|
6
7
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
7
8
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
8
9
|
export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -20,6 +21,8 @@ export type DoctorOpts = {
|
|
|
20
21
|
orcaStatusProbe?: (cwd: string, binary: string) => Promise<ShResult>;
|
|
21
22
|
/** Test seam for shell-path discovery; absence remains a normal doctor row, never an exception. */
|
|
22
23
|
resolveOrcaBinary?: (cwd: string) => string | undefined;
|
|
24
|
+
/** Test seam for the runner-owned JSON listing used by the acceptance-oracle report row. */
|
|
25
|
+
listTests?: (cwd: string) => Promise<VitestListResult>;
|
|
23
26
|
};
|
|
24
27
|
type OrcaCapability = {
|
|
25
28
|
verdict: "pass" | "fail";
|
|
@@ -8,7 +8,7 @@ import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnin
|
|
|
8
8
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
9
9
|
import { shq } from "../../adapters/types.js";
|
|
10
10
|
import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
11
|
-
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
11
|
+
import { graphPath, loadGraph, tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
12
12
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
13
13
|
import { loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
14
14
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
@@ -17,6 +17,7 @@ import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
|
17
17
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
18
18
|
import { LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
19
19
|
import { sh } from "../../run/git.js";
|
|
20
|
+
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
20
21
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
21
22
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
22
23
|
export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -362,6 +363,27 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
362
363
|
const healthy = h.installed && (a.id !== kimi.id || h.authed);
|
|
363
364
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
364
365
|
});
|
|
366
|
+
if (existsSync(graphPath(cwd))) {
|
|
367
|
+
try {
|
|
368
|
+
const graph = loadGraph(cwd);
|
|
369
|
+
const items = graph.tasks.flatMap((task) => task.acceptance);
|
|
370
|
+
if (items.some((item) => typeof item === "object" && item.oracle === "test")) {
|
|
371
|
+
const listing = await (opts.listTests ?? listVitestTests)(cwd);
|
|
372
|
+
if (listing.status === "failed") {
|
|
373
|
+
rows.push(alignedStatusRow("fail", "acceptance-oracles", `runner listing failed — ${listing.error.split("\n")[0]}`));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const audited = auditNamedTestOracles(items, listing.tests);
|
|
377
|
+
const resolved = audited.filter((row) => row.matches.length === 1).length;
|
|
378
|
+
const verdict = resolved === audited.length ? "pass" : "fail";
|
|
379
|
+
rows.push(alignedStatusRow(verdict, "acceptance-oracles", `${resolved}/${audited.length} resolved — each resolved oracle's shipped filter matches exactly one runner-listed test`));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
rows.push(alignedStatusRow("fail", "acceptance-oracles", `graph unreadable — ${error instanceof Error ? error.message : String(error)}`));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
365
387
|
if (catalog.warning) {
|
|
366
388
|
rows.push(attentionRow(`model catalog cache unreadable — ${catalog.warning}; using vendored fallback (advisory — routing unchanged)`));
|
|
367
389
|
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type VitestListResult } from "../../gates/acceptance.js";
|
|
2
|
+
import { type WorkerAdapter } from "../../adapters/types.js";
|
|
3
|
+
export type PlanOpts = {
|
|
4
|
+
listTests?: (cwd: string) => Promise<VitestListResult>;
|
|
5
|
+
};
|
|
6
|
+
export declare function plan(argv: string[], cwd?: string, adapters?: WorkerAdapter[], harnessFrom?: string | undefined, opts?: PlanOpts): Promise<string>;
|
|
@@ -3,15 +3,20 @@ import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints,
|
|
|
3
3
|
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
6
|
+
import { classifyContextPath } from "../../compile/native.js";
|
|
6
7
|
import { DEFAULT_CONFIG, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
7
8
|
import { loadGraph } from "../../graph/graph.js";
|
|
9
|
+
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
8
10
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
9
11
|
import { excludedChannels, exclusionLine } from "../../route/preference.js";
|
|
10
12
|
import { staffLedEvidence } from "../../route/profile.js";
|
|
11
13
|
import { route, RoutingError } from "../../route/router.js";
|
|
14
|
+
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
12
15
|
import { modelId } from "../../gates/review.js";
|
|
13
16
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
14
17
|
import { harnessLine, resolveHarness } from "../harness.js";
|
|
18
|
+
import { shq } from "../../adapters/types.js";
|
|
19
|
+
import { shGit } from "../../run/git.js";
|
|
15
20
|
// T4 (v1.50): TTY-only brand pass — the title helper frames the routing table, lint/unroutable
|
|
16
21
|
// markers carry the attention glyph, section labels dim to chrome (the doctor/status system).
|
|
17
22
|
// Gated on ttyVisual(): the non-TTY surface returns untouched (byte-pinned, machine-consumable).
|
|
@@ -35,12 +40,60 @@ const fleetCanCrossVendorReview = (channels) => {
|
|
|
35
40
|
return true;
|
|
36
41
|
return false;
|
|
37
42
|
};
|
|
43
|
+
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;
|
|
44
|
+
async function taskInputFindings(tasks, cwd) {
|
|
45
|
+
const pathsByTask = tasks.map((task) => {
|
|
46
|
+
const paths = new Set(task.files.map((path) => path.replace(/^\.\//, "")));
|
|
47
|
+
for (const item of task.acceptance) {
|
|
48
|
+
const texts = typeof item === "string"
|
|
49
|
+
? [item]
|
|
50
|
+
: [renderAcceptanceItem(item), ...Object.values(item).filter((value) => typeof value === "string")];
|
|
51
|
+
for (const text of texts) {
|
|
52
|
+
for (const match of text.matchAll(NAMED_CRITERION_PATH))
|
|
53
|
+
paths.add(match[1]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { task, paths: [...paths] };
|
|
57
|
+
});
|
|
58
|
+
if (pathsByTask.every(({ paths }) => paths.length === 0))
|
|
59
|
+
return [];
|
|
60
|
+
const [tree, diff] = await Promise.all([
|
|
61
|
+
shGit("git ls-tree --full-tree -r --name-only -z HEAD", cwd),
|
|
62
|
+
shGit("git diff --name-only -z HEAD --", cwd),
|
|
63
|
+
]);
|
|
64
|
+
if (tree.code !== 0 || diff.code !== 0)
|
|
65
|
+
return [];
|
|
66
|
+
const tracked = new Set(tree.stdout.split("\0").filter(Boolean));
|
|
67
|
+
const changed = new Set(diff.stdout.split("\0").filter(Boolean));
|
|
68
|
+
const findings = [];
|
|
69
|
+
for (const { task, paths } of pathsByTask) {
|
|
70
|
+
for (const path of paths) {
|
|
71
|
+
const state = classifyContextPath(path, tracked, cwd);
|
|
72
|
+
if (state.kind === "untracked") {
|
|
73
|
+
findings.push({
|
|
74
|
+
taskId: task.id,
|
|
75
|
+
severity: "refuse",
|
|
76
|
+
detail: `task path ${JSON.stringify(path)} exists in the working tree but no commit holds it`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else if (state.kind === "ok"
|
|
80
|
+
&& (changed.has(path) || [...changed].some((candidate) => candidate.startsWith(`${path}/`)))) {
|
|
81
|
+
findings.push({
|
|
82
|
+
taskId: task.id,
|
|
83
|
+
severity: "warn",
|
|
84
|
+
detail: `tracked path ${JSON.stringify(path)} differs from HEAD — run git restore -- ${shq(path)} to discard the divergence, or commit it before dispatch`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
38
91
|
// v1.89 T4: harnessFrom is the resolver's INPUT — a caller (the byte-pinned goldens) fixes the location
|
|
39
92
|
// and keeps this machine's absolute paths out of a fixture. The default is the INVOKED entrypoint,
|
|
40
93
|
// `process.argv[1]`: the bin symlink a global install puts on PATH, which resolves to dist/cli/index.js.
|
|
41
94
|
// It is NOT `import.meta.url` — that names dist/cli/commands/plan.js, an internal module of the harness
|
|
42
95
|
// rather than the harness that was invoked, so the banner would identify the wrong file entirely.
|
|
43
|
-
export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(), harnessFrom = process.argv[1]) {
|
|
96
|
+
export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(), harnessFrom = process.argv[1], opts = {}) {
|
|
44
97
|
// ponytail: hardcoded 24h TTL — promote to config when an operator asks. mtime is the signal because
|
|
45
98
|
// doctor.json has no probe timestamp and a schema field would break the existing-files compat invariant.
|
|
46
99
|
const DOCTOR_STALE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -51,6 +104,37 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
51
104
|
throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
|
|
52
105
|
}
|
|
53
106
|
const g = loadGraph(cwd);
|
|
107
|
+
const oracleRefusals = new Map();
|
|
108
|
+
if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
|
|
109
|
+
const listing = await (opts.listTests ?? listVitestTests)(cwd);
|
|
110
|
+
for (const task of g.tasks) {
|
|
111
|
+
const refusals = [];
|
|
112
|
+
if (listing.status === "failed") {
|
|
113
|
+
if (task.acceptance.some((item) => typeof item === "object" && item.oracle === "test")) {
|
|
114
|
+
refusals.push(`acceptance oracle unresolved — runner listing failed: ${listing.error.split("\n")[0]}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
for (const audit of auditNamedTestOracles(task.acceptance, listing.tests)) {
|
|
119
|
+
if (audit.matches.length === 0) {
|
|
120
|
+
refusals.push(`acceptance oracle ${JSON.stringify(audit.criterion)} matches zero runner-listed test names`);
|
|
121
|
+
}
|
|
122
|
+
else if (audit.matches.length > 1) {
|
|
123
|
+
refusals.push(`acceptance oracle ${JSON.stringify(audit.criterion)} matches ${audit.matches.length} runner-listed test names`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (refusals.length)
|
|
128
|
+
oracleRefusals.set(task.id, refusals);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const inputFindings = await taskInputFindings(g.tasks, cwd);
|
|
132
|
+
const inputRefusals = new Map();
|
|
133
|
+
for (const finding of inputFindings) {
|
|
134
|
+
if (finding.severity !== "refuse")
|
|
135
|
+
continue;
|
|
136
|
+
inputRefusals.set(finding.taskId, [...(inputRefusals.get(finding.taskId) ?? []), finding.detail]);
|
|
137
|
+
}
|
|
54
138
|
const { cfg, mode, source } = resolveRunMode(cwd, { flag: values.mode, spec: g.mode });
|
|
55
139
|
// readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
|
|
56
140
|
const cached = readDoctor(cwd);
|
|
@@ -152,6 +236,11 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
152
236
|
let cost = 0;
|
|
153
237
|
const routed = [];
|
|
154
238
|
for (const t of g.tasks) {
|
|
239
|
+
const refusals = [...(oracleRefusals.get(t.id) ?? []), ...(inputRefusals.get(t.id) ?? [])];
|
|
240
|
+
if (refusals.length) {
|
|
241
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! pre-dispatch refusal — ${refusals.join("; ")}`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
155
244
|
try {
|
|
156
245
|
const r = route(t, cfg, channels, dispatchProfile);
|
|
157
246
|
routed.push({ taskId: t.id, adapter: r.assignment.adapter, model: r.assignment.model });
|
|
@@ -192,6 +281,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
192
281
|
lints.push(`${t.id}: unroutable — ${msg}`);
|
|
193
282
|
}
|
|
194
283
|
}
|
|
284
|
+
const inputWarnings = inputFindings.filter((finding) => finding.severity === "warn");
|
|
285
|
+
if (inputWarnings.length) {
|
|
286
|
+
lines.push("", "input warnings:", ...inputWarnings.map((finding) => ` ! ${finding.taskId}: ${finding.detail}`));
|
|
287
|
+
}
|
|
195
288
|
lines.push("", `est. cost (API channels only, rough): ~$${cost.toFixed(2)} + judge/review/consult calls`);
|
|
196
289
|
// VIS-04: summary only when a profile is active AND something deviates. Labeled by the switch — off = preview.
|
|
197
290
|
if (profile && deviations) {
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -167,7 +167,7 @@ const railTone = (event, fallback) => {
|
|
|
167
167
|
// `gate-result` is the one event whose data IS a gate result; every other row carrying a `gate`
|
|
168
168
|
// names a gate without reporting one (a start, a reuse, a retry) and keeps its declared tone.
|
|
169
169
|
if (event.event === "gate-result")
|
|
170
|
-
return GATE_OUTCOME_TONES[normalizeGateOutcome(event.data).kind]
|
|
170
|
+
return GATE_OUTCOME_TONES[normalizeGateOutcome(event.data).kind];
|
|
171
171
|
const verdict = event.data.pass ?? event.data.ok;
|
|
172
172
|
return verdict === true ? "pass" : verdict === false ? "fail" : fallback;
|
|
173
173
|
};
|
|
@@ -226,7 +226,24 @@ const railSalient = (data) => RAIL_SALIENT.flatMap(({ key, render }) => {
|
|
|
226
226
|
* This is one detail vocabulary stated twice, so `brand-surfaces.test.ts` pins the two against each
|
|
227
227
|
* other over the whole event corpus: the pipe's bytes must equal event/taskId/THIS detail put through
|
|
228
228
|
* the formatter's legacy squeeze-and-slice. A ladder that drifts fails there rather than in a tab.
|
|
229
|
+
* Gate-result verdict words are the deliberate exception: the pipe is byte-frozen, while the TTY rail
|
|
230
|
+
* names the normalized outcome so a held selected-test screen is not narrated as a pass.
|
|
229
231
|
*/
|
|
232
|
+
const gateResultDetail = (data) => {
|
|
233
|
+
if (typeof data.gate !== "string")
|
|
234
|
+
return undefined;
|
|
235
|
+
switch (normalizeGateOutcome(data).kind) {
|
|
236
|
+
case "passed":
|
|
237
|
+
return `${data.gate} passed`;
|
|
238
|
+
case "failed":
|
|
239
|
+
return `${data.gate} failed`;
|
|
240
|
+
case "held":
|
|
241
|
+
// The kind is already normalized; the selected-test list only chooses this surface noun.
|
|
242
|
+
return Array.isArray(data.selectedTests) ? `${data.gate} selected-test screen` : `${data.gate} held`;
|
|
243
|
+
default:
|
|
244
|
+
return data.gate;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
230
247
|
const formatterDetail = ({ event, data }) => {
|
|
231
248
|
const assignment = data.assignment;
|
|
232
249
|
const direct = [data.summary, data.reason, data.error, data.step, data.action, data.lint, data.branch, data.from]
|
|
@@ -240,7 +257,9 @@ const formatterDetail = ({ event, data }) => {
|
|
|
240
257
|
}
|
|
241
258
|
if (event === "tip-verify")
|
|
242
259
|
return `${data.gate} passed`;
|
|
243
|
-
|
|
260
|
+
if (event === "gate-result")
|
|
261
|
+
return gateResultDetail(data);
|
|
262
|
+
return `${data.gate}`;
|
|
244
263
|
}
|
|
245
264
|
if (typeof data.code === "number")
|
|
246
265
|
return `exit ${data.code}`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { classifyFailureOutput } from "../../gates/baseline.js";
|
|
4
3
|
import { stateDirName } from "../../graph/graph.js";
|
|
5
4
|
import { Journal } from "../../run/journal.js";
|
|
5
|
+
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
6
6
|
// The operator-facing schema is deliberately closed. Adding a fact to the table therefore requires
|
|
7
7
|
// adding it here as a conscious compatibility change, rather than letting incidental journal fields
|
|
8
8
|
// leak into an analytics surface.
|
|
@@ -35,14 +35,6 @@ const reviewerFrom = (data) => {
|
|
|
35
35
|
return undefined;
|
|
36
36
|
return /\breviewer(?:\s+|:\s*)([\w@./+-]+:[\w@./+-]+)/iu.exec(data.details)?.[1];
|
|
37
37
|
};
|
|
38
|
-
const redEvidence = (data) => {
|
|
39
|
-
const fingerprints = Array.isArray(data.fingerprints)
|
|
40
|
-
? data.fingerprints.filter((value) => typeof value === "string")
|
|
41
|
-
: [];
|
|
42
|
-
const prose = [data.details, data.error, data.reason]
|
|
43
|
-
.filter((value) => typeof value === "string");
|
|
44
|
-
return [...fingerprints, ...prose].join("\n");
|
|
45
|
-
};
|
|
46
38
|
const historyFor = (tasks, taskId) => {
|
|
47
39
|
let history = tasks.get(taskId);
|
|
48
40
|
if (!history) {
|
|
@@ -143,12 +135,10 @@ export function collectChannelStats(cwd = process.cwd()) {
|
|
|
143
135
|
if (reviewer)
|
|
144
136
|
channel.reviewers.add(reviewer);
|
|
145
137
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const infra = event.data.infra === true || classifyFailureOutput(redEvidence(event.data)) === "infra";
|
|
149
|
-
if (infra)
|
|
138
|
+
const outcome = normalizeGateOutcome(event.data);
|
|
139
|
+
if (outcome.kind === "infra")
|
|
150
140
|
channel.infraReds += 1;
|
|
151
|
-
else
|
|
141
|
+
else if (outcome.kind === "failed")
|
|
152
142
|
channel.realReds += 1;
|
|
153
143
|
}
|
|
154
144
|
// A rescue is task-matched, not attempt-matched: one edge says the failed author and delivering
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { GLYPHS, LIVE } from "../../brand.js";
|
|
4
4
|
import { DEFAULT_CONFIG, loadConfig } from "../../config/config.js";
|
|
@@ -7,12 +7,12 @@ import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
|
|
|
7
7
|
import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
8
8
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
9
9
|
import { foldActivity } from "../../run/activity.js";
|
|
10
|
-
import { Journal, engagementComparable, isQualityFailureParkKind, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
11
|
-
import { isPidLive } from "../../run/lock.js";
|
|
10
|
+
import { Journal, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
11
|
+
import { isPidLive, runLockOwner } from "../../run/lock.js";
|
|
12
12
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
13
13
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
14
14
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
15
|
-
import {
|
|
15
|
+
import { armWatchSupervision, readSupervision, supervisionText } from "../../run/supervision.js";
|
|
16
16
|
import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
|
|
17
17
|
import { COCKPIT_COLUMN_FLOOR } from "../../tui/cockpit/layout.js";
|
|
18
18
|
import { cellWidth, fitCells, wrapCells } from "../../tui/cockpit/width.js";
|
|
@@ -109,6 +109,15 @@ const ASCII_SPINNER = ["|", "/", "-", "\\"];
|
|
|
109
109
|
const SAVE_TERMINAL_TITLE = "\x1b[22;0t";
|
|
110
110
|
const RESTORE_TERMINAL_TITLE = "\x1b[23;0t";
|
|
111
111
|
const decisionEvidence = (stateDir, runId, sequence) => `${stateDir}/runs/${runId}/journal.jsonl#L${sequence}`;
|
|
112
|
+
const DECISION_GATE_VERDICTS = {
|
|
113
|
+
passed: "passed",
|
|
114
|
+
failed: "failed",
|
|
115
|
+
skipped: "skipped",
|
|
116
|
+
declined: "skipped",
|
|
117
|
+
held: "unknown",
|
|
118
|
+
unavailable: "unknown",
|
|
119
|
+
infra: "unknown",
|
|
120
|
+
};
|
|
112
121
|
// v1.79 T4: a deterministic JSONL projection over journal truth. Sequence/evidence come from the
|
|
113
122
|
// append-only line position, timestamps and claims come from the row, and no watcher-local clock or
|
|
114
123
|
// filesystem write participates. Re-reading the same bytes therefore returns the same event bytes.
|
|
@@ -126,13 +135,7 @@ export const decisionEventsFromJournal = (events, runId, stateDir = ".tickmarkr"
|
|
|
126
135
|
return [{ ...base, type: "phase-change", tier: "routine", phase: event.data.phase }];
|
|
127
136
|
}
|
|
128
137
|
if (event.event === "gate-result" && typeof event.data.gate === "string") {
|
|
129
|
-
const verdict = event.data.
|
|
130
|
-
? "skipped"
|
|
131
|
-
: event.data.pass === true
|
|
132
|
-
? "passed"
|
|
133
|
-
: event.data.pass === false
|
|
134
|
-
? "failed"
|
|
135
|
-
: "unknown";
|
|
138
|
+
const verdict = DECISION_GATE_VERDICTS[normalizeGateOutcome(event.data).kind];
|
|
136
139
|
return [{
|
|
137
140
|
...base,
|
|
138
141
|
type: "gate-verdict",
|
|
@@ -381,6 +384,13 @@ export const gateBox = (state, unicode) => {
|
|
|
381
384
|
return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
|
|
382
385
|
};
|
|
383
386
|
export const defaultGateStates = (task) => GATE_NAMES.map((gate) => task.gates.includes(gate) ? "open" : "skip");
|
|
387
|
+
const GATE_OUTCOME_STATES = {
|
|
388
|
+
// The board state is a rendering vocabulary over the normalized outcome, not another field reader.
|
|
389
|
+
passed: "pass",
|
|
390
|
+
failed: "fail",
|
|
391
|
+
skipped: "skip",
|
|
392
|
+
declined: "skip",
|
|
393
|
+
};
|
|
384
394
|
const gateSnapshot = (task, events, rehashAt) => {
|
|
385
395
|
const outcomes = new Map();
|
|
386
396
|
const start = attemptStartIdx(events, task.id);
|
|
@@ -389,12 +399,9 @@ const gateSnapshot = (task, events, rehashAt) => {
|
|
|
389
399
|
const e = events[eventIndex];
|
|
390
400
|
if (e.taskId !== task.id || e.event !== "gate-result" || typeof e.data.gate !== "string")
|
|
391
401
|
continue;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
outcomes.set(e.data.gate, { state: "pass", eventIndex });
|
|
396
|
-
else if (e.data.pass === false)
|
|
397
|
-
outcomes.set(e.data.gate, { state: "fail", eventIndex });
|
|
402
|
+
const state = GATE_OUTCOME_STATES[normalizeGateOutcome(e.data).kind];
|
|
403
|
+
if (state)
|
|
404
|
+
outcomes.set(e.data.gate, { state, eventIndex });
|
|
398
405
|
}
|
|
399
406
|
}
|
|
400
407
|
return {
|
|
@@ -519,7 +526,7 @@ const foldTaskEffort = (tasks, events) => {
|
|
|
519
526
|
task.dispatches += 1;
|
|
520
527
|
else if (event.event === "gate-result" && event.data.gate === "review"
|
|
521
528
|
&& event.data.replayMeasurement !== true
|
|
522
|
-
&& REVIEW_VERDICTS.has(normalizeGateOutcome(event.data
|
|
529
|
+
&& REVIEW_VERDICTS.has(normalizeGateOutcome(event.data).kind))
|
|
523
530
|
task.reviews += 1;
|
|
524
531
|
else if (event.event === "task-human")
|
|
525
532
|
task.parks += 1;
|
|
@@ -843,20 +850,56 @@ const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
|
843
850
|
return [];
|
|
844
851
|
}
|
|
845
852
|
});
|
|
853
|
+
// runLockOwner is the single lock payload/liveness reader. Its current path helper also initializes
|
|
854
|
+
// .tickmarkr metadata; an engine-written lock necessarily passed through that initializer already, so
|
|
855
|
+
// status avoids invoking it in stripped reader-purity fixtures where no engine lock can exist.
|
|
856
|
+
const canReadRunLockOwner = (cwd) => existsSync(join(cwd, stateDirName(cwd), ".gitignore"));
|
|
857
|
+
const reportableLockRunId = (owner) => {
|
|
858
|
+
if (!owner?.live || owner.runId === undefined)
|
|
859
|
+
return undefined;
|
|
860
|
+
try {
|
|
861
|
+
return parseRunId(owner.runId);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
// Repository-wide locks can be held for non-run work such as compile; those are not a status run.
|
|
865
|
+
return undefined;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
const readRunJournalRaw = (cwd, runId, allowMissingJournal) => {
|
|
869
|
+
const dir = join(cwd, stateDirName(cwd), "runs", runId);
|
|
870
|
+
try {
|
|
871
|
+
return readFileSync(join(dir, "journal.jsonl"), "utf8");
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
if (error.code === "ENOENT") {
|
|
875
|
+
if (allowMissingJournal)
|
|
876
|
+
return "";
|
|
877
|
+
throw new Error(`no journal for ${runId} at ${dir}`);
|
|
878
|
+
}
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
846
882
|
/**
|
|
847
883
|
* ONE journal read per answer, and every claim about the run folded from THOSE bytes — the board's
|
|
848
884
|
* task rows, activity, phases, gates, liveness and tip verification, and the compact one-line form
|
|
849
885
|
* alike. A second read is what lets two snapshots be presented as one state: a line the daemon
|
|
850
886
|
* appends between them pairs a task count from one instant with a verdict from another.
|
|
851
887
|
*
|
|
852
|
-
* An explicit <runId> is a resolution, not a hint:
|
|
853
|
-
*
|
|
888
|
+
* An explicit <runId> is a resolution, not a hint: it refuses an id without a readable journal, so
|
|
889
|
+
* status fails loudly naming that id instead of rendering any other run. The implicit form first
|
|
890
|
+
* asks the repository lock accessor which run is live, then falls back to the newest journalled run;
|
|
891
|
+
* a lock-selected run may still be in the directory-before-first-journal window, which renders as an
|
|
892
|
+
* empty snapshot for that run rather than borrowing the previous run's journal.
|
|
854
893
|
*/
|
|
855
894
|
const readRunRecord = (cwd, graph, namedRunId) => {
|
|
856
|
-
const
|
|
895
|
+
const explicitRunId = namedRunId === undefined ? undefined : parseRunId(namedRunId);
|
|
896
|
+
const lockedRunId = explicitRunId === undefined && canReadRunLockOwner(cwd)
|
|
897
|
+
? reportableLockRunId(runLockOwner(cwd))
|
|
898
|
+
: undefined;
|
|
899
|
+
const runId = explicitRunId ?? lockedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
857
900
|
if (!runId)
|
|
858
901
|
return undefined;
|
|
859
|
-
const raw =
|
|
902
|
+
const raw = readRunJournalRaw(cwd, runId, lockedRunId === runId);
|
|
860
903
|
const events = parseJournalSnapshot(raw);
|
|
861
904
|
// The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
|
|
862
905
|
// append-only audit that authorizes this status replay after stop-amend-resume.
|
|
@@ -1373,15 +1416,19 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1373
1416
|
let journalCursor = 0;
|
|
1374
1417
|
const consumeDecisionEvents = () => {
|
|
1375
1418
|
// A named run is followed, never re-resolved: --watch <runId> keeps reporting that run even as
|
|
1376
|
-
// newer runs start.
|
|
1377
|
-
const
|
|
1419
|
+
// newer runs start. The no-argument form tracks the live lock first, then latest journal.
|
|
1420
|
+
const explicitRunId = namedRunId === undefined ? undefined : parseRunId(namedRunId);
|
|
1421
|
+
const lockedRunId = explicitRunId === undefined && canReadRunLockOwner(cwd)
|
|
1422
|
+
? reportableLockRunId(runLockOwner(cwd))
|
|
1423
|
+
: undefined;
|
|
1424
|
+
const runId = explicitRunId ?? lockedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
1378
1425
|
if (!runId)
|
|
1379
1426
|
return [];
|
|
1380
1427
|
if (decisionRunId !== runId) {
|
|
1381
1428
|
decisionRunId = runId;
|
|
1382
1429
|
journalCursor = 0;
|
|
1383
1430
|
}
|
|
1384
|
-
const journalEvents =
|
|
1431
|
+
const journalEvents = parseJournalSnapshot(readRunJournalRaw(cwd, runId, lockedRunId === runId));
|
|
1385
1432
|
if (journalEvents.length < journalCursor)
|
|
1386
1433
|
journalCursor = 0;
|
|
1387
1434
|
const fresh = decisionEventsFromJournal(journalEvents, runId, stateDirName(cwd))
|
|
@@ -1408,7 +1455,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
1408
1455
|
// it INVERTS it: armSupervision's interval outlives the board in any host that outlives one board, so
|
|
1409
1456
|
// a dead board keeps writing and reads ARMED. That is the over-claiming direction, the one an operator
|
|
1410
1457
|
// acts on, and the one this instrument exists to close.
|
|
1411
|
-
const armed = bounded ? undefined :
|
|
1458
|
+
const armed = bounded ? undefined : armWatchSupervision(cwd, opts.supervisionBeatMs);
|
|
1412
1459
|
let titleSaved = false;
|
|
1413
1460
|
const restoreTitle = () => {
|
|
1414
1461
|
if (!titleSaved)
|
package/dist/compile/index.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ export type PlanIR = {
|
|
|
10
10
|
tasks: RunGraph["tasks"];
|
|
11
11
|
};
|
|
12
12
|
export type PlanFinalizationHook = (plan: PlanIR) => PlanIR;
|
|
13
|
-
export declare function finalizePlan(plan: PlanIR, src: string): RunGraph;
|
|
13
|
+
export declare function finalizePlan(plan: PlanIR, src: string, repoRoot?: string): RunGraph;
|
|
14
14
|
export declare function compileSource(src: string, type?: SourceType, root?: string, beforeFinalize?: PlanFinalizationHook): RunGraph;
|
|
15
15
|
export {};
|
package/dist/compile/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { validateGraph } from "../graph/schema.js";
|
|
4
4
|
import { taskUnitContractErrors } from "./collateral.js";
|
|
5
|
+
import { blocksCompile, ownershipFindings, renderOwnershipFinding } from "./ownership.js";
|
|
5
6
|
import { CompileError } from "./common.js";
|
|
6
7
|
import { compileGsd, isGsdPhaseDir } from "./gsd.js";
|
|
7
8
|
import { compileNative, TICKMARKR_NATIVE_MARKER } from "./native.js";
|
|
@@ -40,8 +41,8 @@ function enforceTaskUnitContract(g, src) {
|
|
|
40
41
|
}
|
|
41
42
|
return g;
|
|
42
43
|
}
|
|
43
|
-
export function finalizePlan(plan, src) {
|
|
44
|
-
const graph = validateGraph({
|
|
44
|
+
export function finalizePlan(plan, src, repoRoot) {
|
|
45
|
+
const graph = enforceTaskUnitContract(validateGraph({
|
|
45
46
|
version: plan.version,
|
|
46
47
|
...(plan.mode !== undefined ? { mode: plan.mode } : {}),
|
|
47
48
|
spec: {
|
|
@@ -51,8 +52,26 @@ export function finalizePlan(plan, src) {
|
|
|
51
52
|
...(plan.base !== undefined ? { base: plan.base } : {}),
|
|
52
53
|
},
|
|
53
54
|
tasks: plan.tasks,
|
|
54
|
-
});
|
|
55
|
-
|
|
55
|
+
}), src);
|
|
56
|
+
// overseer-217 removal condition, now paid: on this milestone's authored graph the conventional
|
|
57
|
+
// name map emitted 21 raw unowned-test findings; review found 1 real and 20 false, while intersecting
|
|
58
|
+
// with a direct import or command-entry spawn retained the real one and left 0 false positives. That
|
|
59
|
+
// is a precision measurement, NOT a recall claim. The prior halted semantic-contract class has no
|
|
60
|
+
// name/import/ownership relation (one of its three members has no matching literal anywhere), so this
|
|
61
|
+
// rule could not have caught it and does not claim to. The promoted rule already earned a true positive
|
|
62
|
+
// during authoring: assigning the plan command to oracle-preflight left two dedicated plan tests unowned.
|
|
63
|
+
if (repoRoot) {
|
|
64
|
+
const findings = ownershipFindings(graph.tasks, repoRoot);
|
|
65
|
+
const blocking = findings.filter(blocksCompile);
|
|
66
|
+
for (const finding of findings.filter((item) => !blocksCompile(item))) {
|
|
67
|
+
console.warn(renderOwnershipFinding(finding));
|
|
68
|
+
}
|
|
69
|
+
if (blocking.length > 0) {
|
|
70
|
+
throw new CompileError(`${src} violates cross-task test ownership (${blocking.length} error${blocking.length === 1 ? "" : "s"}):\n`
|
|
71
|
+
+ blocking.map((finding) => ` - ${renderOwnershipFinding(finding)}`).join("\n"));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return graph;
|
|
56
75
|
}
|
|
57
76
|
function compilePlan(src, type, root) {
|
|
58
77
|
const kind = type ?? detect(src);
|
|
@@ -75,5 +94,5 @@ function compilePlan(src, type, root) {
|
|
|
75
94
|
};
|
|
76
95
|
}
|
|
77
96
|
export function compileSource(src, type, root, beforeFinalize = (plan) => plan) {
|
|
78
|
-
return finalizePlan(beforeFinalize(compilePlan(src, type, root)), src);
|
|
97
|
+
return finalizePlan(beforeFinalize(compilePlan(src, type, root)), src, root);
|
|
79
98
|
}
|