tickmarkr 2.5.1 → 2.5.3
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/qwen.d.ts +1 -0
- package/dist/adapters/qwen.js +8 -0
- package/dist/adapters/types.d.ts +1 -0
- package/dist/cli/commands/approve.js +1 -1
- package/dist/cli/commands/doctor.d.ts +8 -0
- package/dist/cli/commands/doctor.js +50 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/run.d.ts +5 -0
- package/dist/cli/commands/run.js +16 -1
- package/dist/cli/commands/status.js +8 -14
- package/dist/compile/native.js +3 -0
- package/dist/compile/ownership.d.ts +9 -0
- package/dist/compile/ownership.js +65 -2
- package/dist/config/config.d.ts +6 -0
- package/dist/config/config.js +6 -0
- package/dist/drivers/subprocess.d.ts +2 -3
- package/dist/drivers/subprocess.js +2 -3
- package/dist/gates/baseline.d.ts +13 -0
- package/dist/gates/baseline.js +99 -30
- package/dist/gates/llm.d.ts +1 -1
- package/dist/gates/llm.js +35 -13
- package/dist/gates/review.d.ts +11 -0
- package/dist/gates/review.js +67 -18
- package/dist/gates/run-gates.js +2 -2
- package/dist/run/daemon.d.ts +8 -3
- package/dist/run/daemon.js +3198 -2934
- package/dist/run/git.d.ts +4 -2
- package/dist/run/git.js +11 -23
- package/dist/run/journal.js +16 -7
- package/dist/run/merge.d.ts +1 -1
- package/dist/run/merge.js +126 -18
- package/dist/run/operator-state.d.ts +1 -1
- package/dist/run/operator-state.js +5 -9
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +61 -4
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +5 -2
- package/skills/tickmarkr-overseer/scripts/watch-launch.sh +38 -0
package/dist/adapters/qwen.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ClassifiedWorkerResult } from "./prompt.js";
|
|
2
2
|
import { type WorkerAdapter } from "./types.js";
|
|
3
3
|
export declare const QWEN_VERSION_IDENTITY: RegExp;
|
|
4
|
+
export declare const QWEN_HARNESS_BANNER_ROWS: readonly ["⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice."];
|
|
4
5
|
export declare function parseQwenResult(raw: string, nonce: string): ClassifiedWorkerResult;
|
|
5
6
|
export declare const qwen: WorkerAdapter;
|
package/dist/adapters/qwen.js
CHANGED
|
@@ -3,6 +3,13 @@ import { parseWorkerResult } from "./prompt.js";
|
|
|
3
3
|
import { channelsFromConfig, shq, } from "./types.js";
|
|
4
4
|
export const QWEN_VERSION_IDENTITY = /^\d+\.\d+\.\d+/;
|
|
5
5
|
const QWEN_SKIP_UPDATE = "QWEN_CODE_SKIP_UPDATE_CHECK_ONCE=true";
|
|
6
|
+
// Verbatim stderr from tests/fixtures/qwen/safe-mode.stderr (qwen 0.21.15). These bytes are launch
|
|
7
|
+
// harness, not evidence that a reviewer took a turn. Keep the declaration closed and row-shaped so
|
|
8
|
+
// llm.ts can sweep partial terminal paints without a broad SAFE MODE/warning regex.
|
|
9
|
+
export const QWEN_HARNESS_BANNER_ROWS = [
|
|
10
|
+
"⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.",
|
|
11
|
+
"Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.",
|
|
12
|
+
];
|
|
6
13
|
function decodeQwenEvents(events) {
|
|
7
14
|
const text = [];
|
|
8
15
|
let failed = false;
|
|
@@ -136,6 +143,7 @@ export const qwen = {
|
|
|
136
143
|
channels: (cfg) => channelsFromConfig("qwen", cfg),
|
|
137
144
|
hardcodedFlags: { binary: "qwen", flags: ["--safe-mode", "--approval-mode", "-m", "-o", "-p"] },
|
|
138
145
|
headlessCommand: (promptFile, model) => `${QWEN_SKIP_UPDATE} qwen --safe-mode --approval-mode yolo -m ${shq(model)} -o json -p '' < ${shq(promptFile)}`,
|
|
146
|
+
harnessBannerRows: QWEN_HARNESS_BANNER_ROWS,
|
|
139
147
|
// OBS-905: qwen has NO interactive form. The `-i "$(cat prompt)"` TUI launch put the whole prompt in
|
|
140
148
|
// argv (the OBS-889 leak-and-census shape) and produced a rendered transcript the JSON decoder above
|
|
141
149
|
// can never read — under the herdr driver every qwen task read "unparseable" and merged only by harvest.
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -171,6 +171,7 @@ export interface WorkerAdapter {
|
|
|
171
171
|
probe(): Promise<AuthHealth>;
|
|
172
172
|
channels(cfg: TickmarkrConfig): BillingChannel[];
|
|
173
173
|
headlessCommand(promptFile: string, model: string): string;
|
|
174
|
+
harnessBannerRows?: readonly string[];
|
|
174
175
|
interactiveCommand(promptFile: string, model: string): string | null;
|
|
175
176
|
interactiveSeed?: InteractiveSeed;
|
|
176
177
|
resumeCommand?(sessionId: string, promptFile: string, model: string): string;
|
|
@@ -114,7 +114,7 @@ export function approvalRunOwner(cwd, runId) {
|
|
|
114
114
|
/** The one sentence that says who enacts this release and what it buys. */
|
|
115
115
|
export function approvalEnactment(token, run) {
|
|
116
116
|
if (run.live) {
|
|
117
|
-
return `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}`;
|
|
117
|
+
return `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}, including in the approval window or by cancelling an in-progress tip verify`;
|
|
118
118
|
}
|
|
119
119
|
if (run.blockingRunId) {
|
|
120
120
|
return `release recorded; live run \`${run.blockingRunId}\` holds the repository lock, so resume \`${run.runId}\` after it ends to ${APPROVAL_ENACTS[token]}`;
|
|
@@ -8,6 +8,12 @@ import { type VitestListResult } from "../../gates/acceptance.js";
|
|
|
8
8
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
9
9
|
export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
10
10
|
export declare const LIVEBENCH_TABLE_MAX_AGE_DAYS = 90;
|
|
11
|
+
type ReviewDemotionSummary = {
|
|
12
|
+
reviewer: string;
|
|
13
|
+
count: number;
|
|
14
|
+
causes: Record<string, number>;
|
|
15
|
+
};
|
|
16
|
+
export declare function recentReviewDemotions(cwd: string, lastRuns?: number): ReviewDemotionSummary[];
|
|
11
17
|
export type DoctorOpts = {
|
|
12
18
|
banner?: boolean;
|
|
13
19
|
kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
|
|
@@ -162,6 +168,7 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
162
168
|
gates: {
|
|
163
169
|
build?: string | undefined;
|
|
164
170
|
test?: string | undefined;
|
|
171
|
+
tipTest?: string | undefined;
|
|
165
172
|
lint?: string | undefined;
|
|
166
173
|
diffCap?: number | undefined;
|
|
167
174
|
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
@@ -278,6 +285,7 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
278
285
|
gates: {
|
|
279
286
|
build?: string | undefined;
|
|
280
287
|
test?: string | undefined;
|
|
288
|
+
tipTest?: string | undefined;
|
|
281
289
|
lint?: string | undefined;
|
|
282
290
|
diffCap?: number | undefined;
|
|
283
291
|
byShape?: Partial<Record<"plan" | "spec" | "implement" | "tests" | "docs" | "migration" | "ui" | "refactor" | "chore", {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { detectPackageManager, turboContinueFindings } from "../../gates/baseline.js";
|
|
6
6
|
import { version } from "./version.js";
|
|
@@ -26,6 +26,46 @@ const initialFetch = globalThis.fetch;
|
|
|
26
26
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
27
27
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
28
28
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
29
|
+
// Use the routing profile's established 50-run recency horizon without importing journal machinery
|
|
30
|
+
// into doctor. Torn/malformed rows are ignored one at a time: diagnostics must survive a killed write.
|
|
31
|
+
export function recentReviewDemotions(cwd, lastRuns = 50) {
|
|
32
|
+
const runs = join(cwd, stateDirName(cwd), "runs");
|
|
33
|
+
if (!existsSync(runs))
|
|
34
|
+
return [];
|
|
35
|
+
const grouped = new Map();
|
|
36
|
+
const ids = readdirSync(runs, { withFileTypes: true })
|
|
37
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("run-")
|
|
38
|
+
&& existsSync(join(runs, entry.name, "journal.jsonl")))
|
|
39
|
+
.map((entry) => entry.name).sort().slice(-lastRuns);
|
|
40
|
+
for (const id of ids) {
|
|
41
|
+
const lines = readFileSync(join(runs, id, "journal.jsonl"), "utf8").split("\n");
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (!line.trim())
|
|
44
|
+
continue;
|
|
45
|
+
let row;
|
|
46
|
+
try {
|
|
47
|
+
row = JSON.parse(line);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!row || typeof row !== "object")
|
|
53
|
+
continue;
|
|
54
|
+
const event = row;
|
|
55
|
+
if (event.event !== "review-pool-demotion" || !event.data || typeof event.data !== "object")
|
|
56
|
+
continue;
|
|
57
|
+
const data = event.data;
|
|
58
|
+
if (typeof data.reviewer !== "string" || !data.reviewer.trim())
|
|
59
|
+
continue;
|
|
60
|
+
const cause = typeof data.cause === "string" && data.cause.trim() ? data.cause : "unknown";
|
|
61
|
+
const summary = grouped.get(data.reviewer) ?? { reviewer: data.reviewer, count: 0, causes: {} };
|
|
62
|
+
summary.count++;
|
|
63
|
+
summary.causes[cause] = (summary.causes[cause] ?? 0) + 1;
|
|
64
|
+
grouped.set(data.reviewer, summary);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...grouped.values()].sort((a, b) => a.reviewer.localeCompare(b.reviewer));
|
|
68
|
+
}
|
|
29
69
|
const ORCA_HOOK_ADAPTERS = {
|
|
30
70
|
claude: "claude-code",
|
|
31
71
|
"claude-code": "claude-code",
|
|
@@ -581,6 +621,15 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
581
621
|
const healthy = h.installed && (a.id !== kimi.id || h.authed);
|
|
582
622
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
583
623
|
});
|
|
624
|
+
const reviewDemotions = recentReviewDemotions(cwd);
|
|
625
|
+
if (reviewDemotions.length) {
|
|
626
|
+
rows.push(legend("recent review-seat demotions:"));
|
|
627
|
+
for (const demotion of reviewDemotions) {
|
|
628
|
+
const causes = Object.entries(demotion.causes)
|
|
629
|
+
.map(([cause, count]) => `${cause}${count === demotion.count ? "" : ` ×${count}`}`).join(", ");
|
|
630
|
+
rows.push(alignedStatusRow("warn", demotion.reviewer, `${demotion.count} review seat${demotion.count === 1 ? "" : "s"} demoted · cause ${causes}`));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
584
633
|
if (existsSync(graphPath(cwd))) {
|
|
585
634
|
try {
|
|
586
635
|
const graph = loadGraph(cwd);
|
|
@@ -114,7 +114,7 @@ A run is green only when ALL of these hold: the run-end event exists in the jour
|
|
|
114
114
|
|
|
115
115
|
### Verified handoffs
|
|
116
116
|
|
|
117
|
-
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the
|
|
117
|
+
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, send-keys enter to that pane once and read again (OBS-975); never report "relayed" without read-back.
|
|
118
118
|
|
|
119
119
|
### Orient before you act — this block may be the ONLY guidance your host loaded
|
|
120
120
|
|
|
@@ -4,6 +4,11 @@ import { type JournalEvent } from "../../run/journal.js";
|
|
|
4
4
|
* it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
|
|
5
5
|
export declare const TTY_NOISE_EVENTS: readonly ["worker-contact", "worker-status"];
|
|
6
6
|
type RailTone = "pass" | "fail" | "attention" | "active" | "neutral";
|
|
7
|
+
/** Approval-close lifecycle labels extend the established closed rail vocabulary. */
|
|
8
|
+
export declare const APPROVAL_RAIL_ROWS: Record<string, {
|
|
9
|
+
label: string;
|
|
10
|
+
tone: RailTone;
|
|
11
|
+
}>;
|
|
7
12
|
/** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
|
|
8
13
|
* own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
|
|
9
14
|
* or `ok` datum on the event overrides the default tone, so one gate row can read either way.
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -33,6 +33,13 @@ const RAIL_TONES = {
|
|
|
33
33
|
active: { glyph: GLYPHS.pointer, paint: LIVE.running },
|
|
34
34
|
neutral: { glyph: GLYPHS.neutral, paint: LIVE.chrome },
|
|
35
35
|
};
|
|
36
|
+
/** Approval-close lifecycle labels extend the established closed rail vocabulary. */
|
|
37
|
+
export const APPROVAL_RAIL_ROWS = {
|
|
38
|
+
"end-condition-held": { label: "close held", tone: "attention" },
|
|
39
|
+
"approval-window-start": { label: "approval window", tone: "attention" },
|
|
40
|
+
"approval-window-expired": { label: "approval window expired", tone: "attention" },
|
|
41
|
+
"tip-verify-cancelled": { label: "tip verify cancelled", tone: "attention" },
|
|
42
|
+
};
|
|
36
43
|
/** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
|
|
37
44
|
* own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
|
|
38
45
|
* or `ok` datum on the event overrides the default tone, so one gate row can read either way.
|
|
@@ -116,6 +123,9 @@ export const RAIL_ROWS = {
|
|
|
116
123
|
// gate starts and verdicts
|
|
117
124
|
"phase-start": { label: "gate start", tone: "active" },
|
|
118
125
|
"gate-result": { label: "gate", tone: "pass" },
|
|
126
|
+
"baseline-wait": { label: "baseline wait", tone: "active" },
|
|
127
|
+
"suite-budget": { label: "suite budget", tone: "attention" },
|
|
128
|
+
"gate-replayed": { label: "gate replayed", tone: "attention" },
|
|
119
129
|
"gate-reused": { label: "gate reused", tone: "neutral" },
|
|
120
130
|
"judge-retry": { label: "judge retry", tone: "attention" },
|
|
121
131
|
"review-no-verdict": { label: "review unavailable", tone: "attention" },
|
|
@@ -307,6 +317,11 @@ const RAIL_PROJECTION = {
|
|
|
307
317
|
? `gated ${d.gatedCommit.slice(0, 12)}, tip ${d.branchTip.slice(0, 12)}`
|
|
308
318
|
: undefined,
|
|
309
319
|
"trust-auto-answer": (d) => typeof d.adapter === "string" ? `${d.adapter}${typeof d.phase === "string" ? ` ${d.phase}` : ""}` : undefined,
|
|
320
|
+
// SB-1: the census the ceiling released beside, and the budget the round ran under versus the one it
|
|
321
|
+
// would have had on an empty census — none of `{count, occupancyCap, conservativeCap}` is on the ladder
|
|
322
|
+
"suite-budget": (d) => typeof d.count === "number" && typeof d.conservativeCap === "number" && typeof d.occupancyCap === "number"
|
|
323
|
+
? `beside ${d.count}, cap ${d.conservativeCap} not ${d.occupancyCap}`
|
|
324
|
+
: undefined,
|
|
310
325
|
};
|
|
311
326
|
// The row's text: the formatter's detail first, then the salient fields that detail could not carry.
|
|
312
327
|
// Appending (never prefixing) keeps the operator's prose at the front of the row, so the clip a
|
|
@@ -333,7 +348,7 @@ const clipCells = (text, cells) => cells <= 0 ? "" : cellWidth(text) <= cells ?
|
|
|
333
348
|
export function narrationRow(event, runId, columns = process.stdout.columns ?? 80) {
|
|
334
349
|
if (TTY_NOISE_EVENTS.includes(event.event))
|
|
335
350
|
return null;
|
|
336
|
-
const row = RAIL_ROWS[event.event];
|
|
351
|
+
const row = APPROVAL_RAIL_ROWS[event.event] ?? RAIL_ROWS[event.event];
|
|
337
352
|
if (!row)
|
|
338
353
|
return null;
|
|
339
354
|
if (event.event === "phase-start" && typeof event.data.gate !== "string")
|
|
@@ -825,23 +825,17 @@ const liveness = (events, daemon, now = Date.now()) => {
|
|
|
825
825
|
const state = daemon.state === "alive" ? "alive" : ended ? "finished" : "dead";
|
|
826
826
|
return `last event ${age} ago · daemon pid ${daemon.pid} ${state}${cause ? ` · ${cause}` : ""}`;
|
|
827
827
|
};
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
828
|
+
// OBS-978: the one comparator resume, plan and the operator fold read decides the join — it already
|
|
829
|
+
// audits the rehash chain. When it binds and the journal holds a graph-rehash, the newest row is the
|
|
830
|
+
// audit that bound the loaded graph; rehashAt marks the facts recorded against a prior graph.
|
|
831
831
|
const statusEngagement = (events, loadedHash) => {
|
|
832
|
-
|
|
832
|
+
if (!engagementComparable(events, loadedHash).comparable)
|
|
833
|
+
return { comparable: false };
|
|
833
834
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
continue;
|
|
837
|
-
const auditsBaseline = baseline.comparable || baseline.reason === "mismatch"
|
|
838
|
-
? event.data.from === baseline.recorded
|
|
839
|
-
: event.data.from === null;
|
|
840
|
-
return event.data.to === loadedHash && auditsBaseline
|
|
841
|
-
? { comparable: true, rehashAt: i }
|
|
842
|
-
: { comparable: false };
|
|
835
|
+
if (events[i].event === "graph-rehash")
|
|
836
|
+
return { comparable: true, rehashAt: i };
|
|
843
837
|
}
|
|
844
|
-
return { comparable:
|
|
838
|
+
return { comparable: true };
|
|
845
839
|
};
|
|
846
840
|
// The journal's own reader rule (src/run/journal.ts readJsonl), applied to bytes already in hand:
|
|
847
841
|
// skip blanks, drop a line that will not parse (a torn trailing write after a crash), keep the rest.
|
package/dist/compile/native.js
CHANGED
|
@@ -847,6 +847,9 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
847
847
|
hard value anywhere in the domain, the criterion asserts a universal that may be FALSE ABOUT THE
|
|
848
848
|
WORLD — bound it or say where it stops holding, rather than demanding a value that does not exist.
|
|
849
849
|
|
|
850
|
+
- A millisecond ceiling in a test is a BUDGET for the SLOWEST RUNNER.
|
|
851
|
+
Any ceiling under one second must carry a SLOWEST-RUNNER note and a member that OVERRUNS it.
|
|
852
|
+
|
|
850
853
|
PICK THE CRITERION FORM FROM WHO COULD BE WRONG:
|
|
851
854
|
- When the WORKER could be wrong because it can choose the value, use "test:" and pin the exact
|
|
852
855
|
literal it could otherwise choose; an example selected by its implementer proves only itself.
|
|
@@ -13,6 +13,12 @@ export type OwnershipFinding = {
|
|
|
13
13
|
taskIds: string[];
|
|
14
14
|
corroboration?: OwnershipCorroboration;
|
|
15
15
|
detail: string;
|
|
16
|
+
} | {
|
|
17
|
+
code: "unowned-shape-oracle";
|
|
18
|
+
taskId: string;
|
|
19
|
+
source: string;
|
|
20
|
+
oracle: string;
|
|
21
|
+
detail: string;
|
|
16
22
|
} | {
|
|
17
23
|
code: "test-path-outside-allowlist";
|
|
18
24
|
taskId: string;
|
|
@@ -26,6 +32,9 @@ export type OwnershipFinding = {
|
|
|
26
32
|
path: string;
|
|
27
33
|
detail: string;
|
|
28
34
|
};
|
|
35
|
+
export declare const SHAPE_ORACLES: readonly ["tests/run/narration.test.ts", "tests/cli/brand-surfaces.test.ts", "tests/run/notify-identity.test.ts", "tests/run/outcome-projections.test.ts", "tests/cockpit/setup.test.ts"];
|
|
36
|
+
export declare const SHAPE_ORACLE_SOURCES: readonly ["src/run/daemon.ts", "src/cli/commands/run.ts"];
|
|
37
|
+
export declare const SHAPE_ORACLE_MAP: Record<string, readonly string[]>;
|
|
29
38
|
/**
|
|
30
39
|
* Cross-task ownership evidence. Findings are data: this checker never throws or changes the graph;
|
|
31
40
|
* the compile seam promotes only a corroborated unowned-test finding and reports every other shape.
|
|
@@ -2,6 +2,38 @@ import { readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { basename, extname, join, posix } from "node:path";
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { collateralHits } from "./collateral.js";
|
|
5
|
+
export const SHAPE_ORACLES = [
|
|
6
|
+
"tests/run/narration.test.ts",
|
|
7
|
+
"tests/cli/brand-surfaces.test.ts",
|
|
8
|
+
"tests/run/notify-identity.test.ts",
|
|
9
|
+
"tests/run/outcome-projections.test.ts",
|
|
10
|
+
"tests/cockpit/setup.test.ts",
|
|
11
|
+
];
|
|
12
|
+
export const SHAPE_ORACLE_SOURCES = [
|
|
13
|
+
"src/run/daemon.ts",
|
|
14
|
+
"src/cli/commands/run.ts",
|
|
15
|
+
];
|
|
16
|
+
export const SHAPE_ORACLE_MAP = {
|
|
17
|
+
"src/run/daemon.ts": SHAPE_ORACLES,
|
|
18
|
+
"src/cli/commands/run.ts": SHAPE_ORACLES,
|
|
19
|
+
};
|
|
20
|
+
// Anchored-glob only: a files[] entry touches a mapped source when it names the source (or its
|
|
21
|
+
// extensionless stem) exactly, or when its glob's literal head — everything before the first
|
|
22
|
+
// wildcard — is the stem plus a literal dot, i.e. the wildcard only ever spans the extension
|
|
23
|
+
// ("src/run/daemon.*"). A broad multi-file glob like "src/**" that merely happens to cover the
|
|
24
|
+
// source is an unrelated task casting a wide net, not one touching daemon narration — that
|
|
25
|
+
// distinction is what broke every fixture using tests/fixtures/sample.prd.md's files: src/** task.
|
|
26
|
+
function touchesSource(files, source) {
|
|
27
|
+
const stem = source.replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
28
|
+
return files.some((entry) => {
|
|
29
|
+
if (entry === source || entry === stem)
|
|
30
|
+
return true;
|
|
31
|
+
const special = entry.search(/[*?{[]/);
|
|
32
|
+
// Leg-2 v2.5.3: the anchored head is necessary, not sufficient — the pattern must also MATCH the
|
|
33
|
+
// source under the shared matcher, or "src/run/daemon.{js,jsx}" (never daemon.ts) would count as a touch.
|
|
34
|
+
return special !== -1 && entry.slice(0, special) === `${stem}.` && filesGlob([entry])(source);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
5
37
|
const normalize = (path) => path.replace(/^\.\//, "").split("\\").join("/");
|
|
6
38
|
function testSources(repoRoot) {
|
|
7
39
|
const root = join(repoRoot, "tests");
|
|
@@ -155,6 +187,7 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
155
187
|
const context = task.context.map(normalize);
|
|
156
188
|
return {
|
|
157
189
|
task,
|
|
190
|
+
files,
|
|
158
191
|
owns: files.length === 0 ? () => false : filesGlob(files),
|
|
159
192
|
allows: files.length === 0 ? () => true : filesGlob([...files, ...context]),
|
|
160
193
|
};
|
|
@@ -204,6 +237,23 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
204
237
|
});
|
|
205
238
|
}
|
|
206
239
|
}
|
|
240
|
+
for (const entry of indexed) {
|
|
241
|
+
for (const [source, oracles] of Object.entries(SHAPE_ORACLE_MAP)) {
|
|
242
|
+
if (!touchesSource(entry.files, source))
|
|
243
|
+
continue;
|
|
244
|
+
for (const oracle of oracles) {
|
|
245
|
+
if (!entry.owns(oracle)) {
|
|
246
|
+
findings.push({
|
|
247
|
+
code: "unowned-shape-oracle",
|
|
248
|
+
taskId: entry.task.id,
|
|
249
|
+
source,
|
|
250
|
+
oracle,
|
|
251
|
+
detail: `${entry.task.id} touches ${source} without owning shape oracle ${oracle}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
207
257
|
for (const source of sources) {
|
|
208
258
|
for (const owner of owners(source.path)) {
|
|
209
259
|
for (const path of repositoryPaths(source.text)) {
|
|
@@ -234,11 +284,24 @@ export function ownershipFindings(tasks, repoRoot) {
|
|
|
234
284
|
}
|
|
235
285
|
}
|
|
236
286
|
}
|
|
237
|
-
|
|
287
|
+
const target = (f) => {
|
|
288
|
+
switch (f.code) {
|
|
289
|
+
case "unowned-test": return f.test;
|
|
290
|
+
case "unowned-shape-oracle": return f.oracle;
|
|
291
|
+
case "test-path-outside-allowlist": return f.test;
|
|
292
|
+
case "unordered-context-write": return f.path;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
return findings.sort((a, b) => {
|
|
296
|
+
const taskA = "taskId" in a ? a.taskId : "";
|
|
297
|
+
const taskB = "taskId" in b ? b.taskId : "";
|
|
298
|
+
return `${a.code}:${target(a)}:${taskA}`.localeCompare(`${b.code}:${target(b)}:${taskB}`);
|
|
299
|
+
});
|
|
238
300
|
}
|
|
239
301
|
export function renderOwnershipFinding(finding) {
|
|
240
302
|
return `tickmarkr: ownership-lint[${finding.code}]: ${finding.detail}`;
|
|
241
303
|
}
|
|
242
304
|
export function blocksCompile(finding) {
|
|
243
|
-
return finding.code === "unowned-test" && finding.corroboration !== undefined
|
|
305
|
+
return (finding.code === "unowned-test" && finding.corroboration !== undefined)
|
|
306
|
+
|| finding.code === "unowned-shape-oracle";
|
|
244
307
|
}
|
package/dist/config/config.d.ts
CHANGED
|
@@ -240,6 +240,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
240
240
|
gates: z.ZodObject<{
|
|
241
241
|
build: z.ZodOptional<z.ZodString>;
|
|
242
242
|
test: z.ZodOptional<z.ZodString>;
|
|
243
|
+
tipTest: z.ZodOptional<z.ZodString>;
|
|
243
244
|
lint: z.ZodOptional<z.ZodString>;
|
|
244
245
|
diffCap: z.ZodOptional<z.ZodNumber>;
|
|
245
246
|
byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
@@ -339,6 +340,11 @@ export type InitConfigOverlay = {
|
|
|
339
340
|
llm?: TickmarkrConfig["visibility"]["llm"];
|
|
340
341
|
};
|
|
341
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Default config overlay template.
|
|
345
|
+
* Seam: gates.test defines the per-task gate and baseline test command;
|
|
346
|
+
* gates.tipTest optionally defines the integration tip verify test command (defaults to gates.test).
|
|
347
|
+
*/
|
|
342
348
|
export declare function configTemplate(overlay?: InitConfigOverlay): string;
|
|
343
349
|
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
|
|
344
350
|
export type { FleetOverlayWrite } from "./fleet-overlay.js";
|
package/dist/config/config.js
CHANGED
|
@@ -337,6 +337,7 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
337
337
|
gates: z.object({
|
|
338
338
|
build: z.string(),
|
|
339
339
|
test: z.string(),
|
|
340
|
+
tipTest: z.string(),
|
|
340
341
|
lint: z.string(),
|
|
341
342
|
diffCap: z.number().int().positive(),
|
|
342
343
|
byShape: z.partialRecord(z.enum(SHAPES), ShapeGateParticipationSchema).optional(),
|
|
@@ -756,6 +757,11 @@ export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
|
|
|
756
757
|
return e.message;
|
|
757
758
|
}
|
|
758
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Default config overlay template.
|
|
762
|
+
* Seam: gates.test defines the per-task gate and baseline test command;
|
|
763
|
+
* gates.tipTest optionally defines the integration tip verify test command (defaults to gates.test).
|
|
764
|
+
*/
|
|
759
765
|
export function configTemplate(overlay) {
|
|
760
766
|
const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
|
|
761
767
|
# concurrency: 3
|
|
@@ -3,9 +3,8 @@ export declare const MAX_BUF: number;
|
|
|
3
3
|
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH", "ORCA_TERMINAL_HANDLE", "ORCA_PANE_KEY", "ORCA_TAB_ID"];
|
|
4
4
|
/**
|
|
5
5
|
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* than a flat constant. The operator's own export still wins.
|
|
6
|
+
* Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
|
|
7
|
+
* verification's suite-window budget. The operator's own export still wins.
|
|
9
8
|
*/
|
|
10
9
|
export declare function sealHerdrEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
11
10
|
/** Pane/login-shell form of the same host-neutral worker env seal (herdr seed + daemon setup). */
|
|
@@ -22,9 +22,8 @@ export const HERDR_CONTROL_VARS = [
|
|
|
22
22
|
];
|
|
23
23
|
/**
|
|
24
24
|
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* than a flat constant. The operator's own export still wins.
|
|
25
|
+
* Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
|
|
26
|
+
* verification's suite-window budget. The operator's own export still wins.
|
|
28
27
|
*/
|
|
29
28
|
export function sealHerdrEnv(env = process.env) {
|
|
30
29
|
const out = { ...env };
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface BaselineCommand {
|
|
|
31
31
|
durationMs?: number;
|
|
32
32
|
/** Sum of the per-file durations named by the runner; null when its output names none. */
|
|
33
33
|
fileDurationSumMs?: number | null;
|
|
34
|
+
/** Total files reported by the runner summary; null when unavailable. */
|
|
35
|
+
fileCount?: number | null;
|
|
34
36
|
/** fileDurationSumMs / durationMs — average implied file concurrency, not a configured fork count. */
|
|
35
37
|
impliedParallelism?: number | null;
|
|
36
38
|
/** The slowest per-file entry named by the runner; null when per-file timing is unavailable. */
|
|
@@ -158,6 +160,8 @@ export declare function detectVacuousOracles(cwd: string, tasks: ReadonlyArray<{
|
|
|
158
160
|
}>): Promise<VacuousOracleWarning[]>;
|
|
159
161
|
export declare function compareToBaseline(cwd: string, commands: Record<string, string>, baseline: Baseline, enabled: string[], opts?: {
|
|
160
162
|
rerunOf?: HostStarvedRerun;
|
|
163
|
+
infraRerun?: HostStarvedRerun;
|
|
164
|
+
selected?: readonly string[];
|
|
161
165
|
}): Promise<GateResult[]>;
|
|
162
166
|
export interface HostStarvedRerun {
|
|
163
167
|
durationMs: number;
|
|
@@ -178,4 +182,13 @@ interface CalmWindow {
|
|
|
178
182
|
}
|
|
179
183
|
export declare function setCalmWindowForTests(over: Partial<CalmWindow>): void;
|
|
180
184
|
export declare function resetCalmWindowForTests(): void;
|
|
185
|
+
export declare function waitForCalmWindow(): Promise<number>;
|
|
186
|
+
/** Summary totals survive digit-normalized fingerprints and exclude test-owned echoed output. */
|
|
187
|
+
export declare function runnerFileCount(raw: string): number | null;
|
|
188
|
+
export declare function fileCountDeficit(entry: BaselineCommand | undefined, raw: string, opts?: {
|
|
189
|
+
name?: string;
|
|
190
|
+
selected?: readonly string[];
|
|
191
|
+
}): string | undefined;
|
|
192
|
+
/** Both readers classify fresh evidence first, retaining the whole-output guard against infra forgiveness. */
|
|
193
|
+
export declare function classifyFreshRunnerOutput(entry: BaselineCommand | undefined, raw: string, code: number): FailureClassification | undefined;
|
|
181
194
|
export {};
|