tickmarkr 2.4.0 → 2.4.2
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/claude-code.js +7 -2
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +68 -8
- package/dist/adapters/qwen.js +3 -3
- package/dist/adapters/registry.js +13 -1
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +33 -0
- 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
|
@@ -15,13 +15,14 @@ import { HerdrDriver } from "../../drivers/herdr.js";
|
|
|
15
15
|
import { ORCA_FIXTURE_VERSION, parseEnvelope, resolveOrcaCliBinary } from "../../drivers/orca.js";
|
|
16
16
|
import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
17
17
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
18
|
-
import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
18
|
+
import { CATALOG_REFRESH_TIMEOUT_MS, LIVEBENCH_TABLE_DATE, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
19
19
|
import { sh } from "../../run/git.js";
|
|
20
20
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
21
21
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
22
22
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
23
23
|
export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
24
24
|
export const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
|
|
25
|
+
const initialFetch = globalThis.fetch;
|
|
25
26
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
26
27
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
27
28
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
@@ -348,19 +349,23 @@ export function liveBenchStalenessFinding(now) {
|
|
|
348
349
|
}
|
|
349
350
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
|
|
350
351
|
if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
|
|
351
|
-
const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
|
|
352
|
+
const refreshed = await refreshCatalogCommand({ repoRoot: cwd, fetcher: opts.catalogFetcher, now: opts.catalogNow });
|
|
352
353
|
return refreshed.updated
|
|
353
|
-
? `tickmarkr doctor --refresh-catalog: model catalog refreshed${refreshed.warning ? `; ${refreshed.warning}` : ""}`
|
|
354
|
-
: `tickmarkr doctor --refresh-catalog: catalog refresh unavailable — ${refreshed.warning
|
|
354
|
+
? `tickmarkr doctor --refresh-catalog: model catalog refreshed — ${formatCatalogRefreshLegs(refreshed.legs)}${refreshed.warning ? `; ${refreshed.warning}` : ""}`
|
|
355
|
+
: `tickmarkr doctor --refresh-catalog: catalog refresh unavailable — ${formatCatalogRefreshLegs(refreshed.legs)}${refreshed.warning ? `; ${refreshed.warning}` : ""}`;
|
|
355
356
|
}
|
|
356
357
|
// Operator directive 2026-08-12 (declutter): long per-model lists render only when asked for.
|
|
357
358
|
const listAllModels = _argv.includes("--models");
|
|
358
359
|
const cfg = loadConfig(cwd);
|
|
359
360
|
let catalog = opts.catalog ?? readCachedCatalog(cwd, { now: opts.catalogNow });
|
|
360
|
-
let
|
|
361
|
+
let catalogRefreshLine;
|
|
361
362
|
// RULING-222-17 reverses cache-only for these two operator-facing commands only. The refresh
|
|
362
363
|
// remains age-guarded; compile, plan, and run never import this path.
|
|
363
|
-
|
|
364
|
+
const catalogRefreshAllowed = process.env.VITEST !== "true"
|
|
365
|
+
|| opts.catalogFetcher !== undefined
|
|
366
|
+
|| opts.catalogNow !== undefined
|
|
367
|
+
|| globalThis.fetch !== initialFetch;
|
|
368
|
+
if (!opts.catalog && catalog.stale && catalogRefreshAllowed) {
|
|
364
369
|
const refreshed = await refreshCatalogCommand({
|
|
365
370
|
repoRoot: cwd,
|
|
366
371
|
fetcher: opts.catalogFetcher,
|
|
@@ -368,7 +373,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
368
373
|
now: opts.catalogNow,
|
|
369
374
|
});
|
|
370
375
|
catalog = refreshed.catalog;
|
|
371
|
-
|
|
376
|
+
catalogRefreshLine = formatCatalogRefreshLegs(refreshed.legs);
|
|
372
377
|
}
|
|
373
378
|
// banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
|
|
374
379
|
if (opts.banner !== false && visual())
|
|
@@ -466,8 +471,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
466
471
|
if (catalog.warning) {
|
|
467
472
|
rows.push(attentionRow(`model catalog cache unreadable — ${catalog.warning}; using vendored fallback (advisory — routing unchanged)`));
|
|
468
473
|
}
|
|
469
|
-
if (
|
|
470
|
-
rows.push(attentionRow(`model catalog auto-refresh
|
|
474
|
+
if (catalogRefreshLine) {
|
|
475
|
+
rows.push(attentionRow(`model catalog auto-refresh — ${catalogRefreshLine}`));
|
|
471
476
|
}
|
|
472
477
|
// v1.48 T1 / v1.86 T12: advisory sweep for known agent CLIs with no drive contract. Presence is
|
|
473
478
|
// resolved through the worker's login shell; advisory targets are never executed or written to health.
|
|
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthExclusions } from "../../adapters/registry.js";
|
|
5
5
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
|
-
import { CATALOG_REFRESH_TIMEOUT_MS, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
6
|
+
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
7
7
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
8
8
|
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, overlayBytesLoadError, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
9
|
import { projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
@@ -14,6 +14,7 @@ import { route } from "../../route/router.js";
|
|
|
14
14
|
import { disallowedBy } from "../../route/preference.js";
|
|
15
15
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
16
16
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
17
|
+
const initialFetch = globalThis.fetch;
|
|
17
18
|
const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
|
|
18
19
|
const QUIT = "fleet: quit without writing";
|
|
19
20
|
// v1.60 T3: every preview surface ranks with the SAME exploration setting as the candidate picker
|
|
@@ -98,16 +99,21 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
98
99
|
const interactive = input.isTTY === true && output.isTTY === true;
|
|
99
100
|
let catalog = readCachedCatalog(cwd, { now: io.catalogNow });
|
|
100
101
|
let refreshReason = "";
|
|
101
|
-
|
|
102
|
+
let catalogRefreshAttempted = false;
|
|
103
|
+
const catalogRefreshAllowed = process.env.VITEST !== "true"
|
|
104
|
+
|| io.catalogFetcher !== undefined
|
|
105
|
+
|| io.catalogNow !== undefined
|
|
106
|
+
|| globalThis.fetch !== initialFetch;
|
|
107
|
+
if (catalog.stale && catalogRefreshAllowed) {
|
|
102
108
|
const refreshed = await refreshCatalogCommand({
|
|
103
109
|
repoRoot: cwd,
|
|
104
110
|
fetcher: io.catalogFetcher,
|
|
105
111
|
timeoutMs: CATALOG_REFRESH_TIMEOUT_MS,
|
|
106
112
|
now: io.catalogNow,
|
|
107
113
|
});
|
|
114
|
+
catalogRefreshAttempted = true;
|
|
108
115
|
catalog = refreshed.catalog;
|
|
109
|
-
|
|
110
|
-
refreshReason = `fleet: catalog auto-refresh failed open — ${refreshed.warning}; retained ${catalog.source} catalog`;
|
|
116
|
+
refreshReason = `fleet: catalog auto-refresh — ${formatCatalogRefreshLegs(refreshed.legs)}`;
|
|
111
117
|
}
|
|
112
118
|
if (print) {
|
|
113
119
|
// v1.51 T4: the print surface names the mode and its source layer right under the header —
|
|
@@ -129,7 +135,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
129
135
|
if (!why && interactive) {
|
|
130
136
|
const { reuse } = initDoctorReuse(cwd, values.fresh ?? false);
|
|
131
137
|
if (!reuse) {
|
|
132
|
-
output.write(`${await doctor([], cwd, adapters, { banner: false, compact: true })}\n`);
|
|
138
|
+
output.write(`${await doctor([], cwd, adapters, { banner: false, compact: true, ...(catalogRefreshAttempted ? { catalog } : {}), catalogFetcher: io.catalogFetcher, catalogNow: io.catalogNow })}\n`);
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
const assembled = await assembleFleetEditor(cwd, adapters, io, { globalDir, catalog });
|
|
@@ -11,6 +11,7 @@ import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
|
11
11
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
12
12
|
import { orcaHostDetected } from "../../drivers/index.js";
|
|
13
13
|
import { Journal } from "../../run/journal.js";
|
|
14
|
+
import { runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
14
15
|
import { doctor } from "./doctor.js";
|
|
15
16
|
import { assembleFleetEditor } from "./fleet.js";
|
|
16
17
|
const SCAFFOLD_SPEC = "tickmarkr.spec.md";
|
|
@@ -24,20 +25,18 @@ const ENVIRONMENTS_FOOTER = [
|
|
|
24
25
|
" claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively",
|
|
25
26
|
" anywhere — no herdr or Orca terminal? same fail-closed gates, headless subprocess driver (legacy: no herdr? same fail-closed gates, headless subprocess driver when Orca markers are absent)",
|
|
26
27
|
].join("\n");
|
|
27
|
-
/**
|
|
28
|
-
function
|
|
29
|
-
const
|
|
28
|
+
/** Current run truth, lock first; falls back to an abandoned journal. */
|
|
29
|
+
function activeRunLine(cwd) {
|
|
30
|
+
const locked = runLockRunId(cwd);
|
|
31
|
+
const runId = locked ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
30
32
|
if (!runId)
|
|
31
33
|
return null;
|
|
34
|
+
let events = [];
|
|
32
35
|
try {
|
|
33
|
-
|
|
34
|
-
if (events.some((e) => e.event === "run-end"))
|
|
35
|
-
return null;
|
|
36
|
-
return runId;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
36
|
+
events = Journal.open(cwd, runId).read();
|
|
40
37
|
}
|
|
38
|
+
catch { /* lock may exist before first journal row */ }
|
|
39
|
+
return runStatusLine(cwd, runId, events);
|
|
41
40
|
}
|
|
42
41
|
/** Relative paths of specs/*.spec.md already in the repo. */
|
|
43
42
|
function existingSpecs(cwd) {
|
|
@@ -56,9 +55,9 @@ function existingSpecs(cwd) {
|
|
|
56
55
|
}
|
|
57
56
|
/** Context-aware next-steps line (operator-approved 2026-07-17). */
|
|
58
57
|
function nextSteps(cwd, scaffoldedSpec) {
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
return
|
|
58
|
+
const runLine = activeRunLine(cwd);
|
|
59
|
+
if (runLine)
|
|
60
|
+
return `${runLine} — tickmarkr status`;
|
|
62
61
|
const specs = existingSpecs(cwd);
|
|
63
62
|
if (specs.length > 0) {
|
|
64
63
|
const listed = specs.length <= 3 ? specs.join(", ") : `${specs.slice(0, 3).join(", ")}, …`;
|
|
@@ -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 [
|