tickmarkr 2.8.6 → 2.8.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/adapters/grok.js +5 -9
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +13 -3
- package/dist/cli/commands/report.js +21 -12
- package/dist/cli/commands/status.js +14 -2
- package/dist/cli/commands/verify.js +29 -5
- package/dist/cli/commands/version.d.ts +1 -1
- package/dist/cli/commands/version.js +36 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +6 -3
- package/dist/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- package/dist/gates/baseline.js +47 -7
- package/dist/gates/run-gates.js +31 -22
- package/dist/report/bundle.d.ts +5 -2
- package/dist/report/bundle.js +11 -6
- package/dist/report/compare.d.ts +9 -0
- package/dist/report/compare.js +58 -40
- package/dist/report/operator-record.js +4 -3
- package/dist/run/daemon.d.ts +7 -3
- package/dist/run/daemon.js +157 -58
- package/dist/run/git.d.ts +5 -0
- package/dist/run/git.js +51 -1
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +2 -2
- package/dist/run/repair-disposition.js +35 -4
- package/dist/run/stall.d.ts +2 -0
- package/dist/run/stall.js +12 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -8
- package/skills/tickmarkr-loop/SKILL.md +10 -8
- package/skills/tickmarkr-overseer/SKILL.md +10 -8
package/dist/adapters/grok.js
CHANGED
|
@@ -115,16 +115,12 @@ export const grok = {
|
|
|
115
115
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
116
116
|
},
|
|
117
117
|
parse: parseWorkerResult, // prompt.ts — last-valid-trailer, hard-wrap tolerant. No grok fork.
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// has stalled before launch (OBS-414's own sweep lists grok's gap as MCP suppression, not trust).
|
|
122
|
-
// FALSIFIER, and it is cheap: dispatch grok into a never-seen worktree and read the pane. One
|
|
123
|
-
// capture of a trust prompt replaces this with {fingerprint, key} — the capture is the only
|
|
124
|
-
// admissible evidence, and a guess here would press a key on whatever it happened to match.
|
|
118
|
+
// Queue row 215 (D-2118 add.1, D-2125): recorded Grok Build 1.0.62 and 1.0.67 directory-trust
|
|
119
|
+
// modal, quoted in GROK_TRUST_PANE. Enter is the measured acceptance key (y also accepts).
|
|
120
|
+
// Accepted residual: answering persists grok's trust for the main checkout root.
|
|
125
121
|
trustDialog: {
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
fingerprint: "Grok Build may run or modify contents in this directory",
|
|
123
|
+
key: "Enter",
|
|
128
124
|
},
|
|
129
125
|
// v1.5 MODEL-01: fail OPEN to [] (advisory detection, unlike gates). Parses the model LIST only —
|
|
130
126
|
// the auth banner on line 1 is ignored BY CONSTRUCTION (parseGrokModels anchors on the header).
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -12,13 +12,21 @@ import { CAPACITY_RE } from "./types.js";
|
|
|
12
12
|
// and the allowlist stays exactly as immutable during a run as it was before this text changed.
|
|
13
13
|
export function buildTaskPrompt(task, feedback = "", nonce = "") {
|
|
14
14
|
const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
15
|
+
// Queue row 234 (D-2130 add.1, D-2131): only the worker brief carries the suite;
|
|
16
|
+
// the judge and the review brief keep the shared criterion text byte-equal.
|
|
17
|
+
const criteria = task.acceptance.map((item) => typeof item !== "string" && item.oracle === "test" && item.landing !== undefined
|
|
18
|
+
? `${renderAcceptanceItem(item)} | suite: ${item.landing}`
|
|
19
|
+
: renderAcceptanceItem(item));
|
|
20
|
+
const suiteNote = task.acceptance.some((item) => typeof item !== "string" && item.oracle === "test" && item.landing !== undefined)
|
|
21
|
+
? '\n\nFor test criteria with ` | suite: `, the test title is the text before the marker, and the test must pass in the named file.'
|
|
22
|
+
: "";
|
|
15
23
|
return `You are an autonomous coding worker dispatched by tickmarkr into an isolated git worktree.
|
|
16
24
|
|
|
17
25
|
## Task ${task.id}: ${task.title}
|
|
18
26
|
Goal: ${task.goal}
|
|
19
27
|
|
|
20
28
|
## Acceptance criteria (you will be judged against these, verbatim)
|
|
21
|
-
${list(
|
|
29
|
+
${list(criteria)}${suiteNote}
|
|
22
30
|
${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(task.files)}\n` : ""}${task.context.length ? `\n## Context (read these first)\n${list(task.context)}\n` : ""}${task.outOfScope?.length ? `\n## Out of scope\nStay within the task’s declared bounds; do not pursue these items:\n${list(task.outOfScope)}\n` : ""}
|
|
23
31
|
## Rules
|
|
24
32
|
- Work only inside the current directory (your isolated worktree). Never push. Never switch branches.
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ export declare const TRUST_DIALOG_BLANK_MESSAGE = "trust-dialog fingerprint must
|
|
|
117
117
|
export declare const CLAUDE_TRUST_PANE: string;
|
|
118
118
|
export declare const CODEX_TRUST_PANE: string;
|
|
119
119
|
export declare const CURSOR_TRUST_PANE = "Workspace Trust Required\nTrust this folder?";
|
|
120
|
+
export declare const GROK_TRUST_PANE: string;
|
|
120
121
|
export declare const KIMI_TRUST_PANE: string;
|
|
121
122
|
export declare const KIMI_TRUST_PANE_029: string;
|
|
122
123
|
export declare const KIMI_MCP_TRUST_PANE: string;
|
package/dist/adapters/types.js
CHANGED
|
@@ -59,6 +59,16 @@ export const CODEX_TRUST_PANE = [
|
|
|
59
59
|
].join("\n");
|
|
60
60
|
// v1.22 T5 / OBS-19, cursor-agent's per-worktree dialog.
|
|
61
61
|
export const CURSOR_TRUST_PANE = "Workspace Trust Required\nTrust this folder?";
|
|
62
|
+
// Queue row 215 (D-2118 add.1, D-2125): quoted from the recorded Grok Build 1.0.62 modal in
|
|
63
|
+
// tests/fixtures/grok-trust/grok-1.0.62.screen.txt; 1.0.67 renders these same modal rows centered.
|
|
64
|
+
export const GROK_TRUST_PANE = [
|
|
65
|
+
"Do you trust the contents of this directory?",
|
|
66
|
+
"/Users/…/repo",
|
|
67
|
+
"Grok Build may run or modify contents in this directory,",
|
|
68
|
+
"posing security risks.",
|
|
69
|
+
"Yes, proceed y",
|
|
70
|
+
"No, quit n",
|
|
71
|
+
].join("\n");
|
|
62
72
|
// Queue row 198, kimi 2.2.0's folder-trust modal on a fresh task worktree (run 1394 T3, read off the
|
|
63
73
|
// Orca screen before its one Enter). The worktree path is shortened as above; every other row is as
|
|
64
74
|
// rendered. 2.2.0 renamed the selected option, so the 0.29.0 fingerprint below never matched it and the
|
|
@@ -92,6 +102,7 @@ export const KIMI_MCP_TRUST_PANE = [
|
|
|
92
102
|
].join("\n");
|
|
93
103
|
export const RECORDED_TRUST_PANES = [
|
|
94
104
|
CLAUDE_TRUST_PANE, CODEX_TRUST_PANE, CURSOR_TRUST_PANE, KIMI_TRUST_PANE, KIMI_TRUST_PANE_029, KIMI_MCP_TRUST_PANE,
|
|
105
|
+
GROK_TRUST_PANE,
|
|
95
106
|
];
|
|
96
107
|
// v1.89 T1 / OBS-414 round 4 — EXACT, not "leading bytes of a recorded line". Round 3 accepted any
|
|
97
108
|
// prefix of a captured line, and every prefix of a trust prompt is also a substring of unrelated
|
|
@@ -111,6 +122,7 @@ export const APPROVED_TRUST_FINGERPRINTS = [
|
|
|
111
122
|
"Workspace Trust Required", // cursor-agent, OBS-19
|
|
112
123
|
"❯ Trust this folder", // kimi 0.29.0, OBS-358 + OBS-406 — the cursor glyph is load-bearing
|
|
113
124
|
"❯ Trust and continue", // kimi 2.2.0, queue row 198 — the same gate with its option renamed
|
|
125
|
+
"Grok Build may run or modify contents in this directory", // queue row 215, D-2125
|
|
114
126
|
];
|
|
115
127
|
const isApprovedFingerprint = (fingerprint) => APPROVED_TRUST_FINGERPRINTS.includes(fingerprint)
|
|
116
128
|
&& RECORDED_TRUST_PANES.some((pane) => pane.includes(fingerprint));
|
|
@@ -67,7 +67,7 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
67
67
|
// Both modes reach the same pure compiler; --dry-run removes every state write below.
|
|
68
68
|
const g = compileSource(sourcePath, values.type, cwd, // repo root: gsd stores context[0] repo-relative so workers resolve it inside their worktree
|
|
69
69
|
(plan) => plan, { strict: values.strict });
|
|
70
|
-
const sourceFindings = sourceScopeFindings(g.tasks, cwd);
|
|
70
|
+
const sourceFindings = sourceScopeFindings(g.tasks, cwd, { failClosed: g.spec.source === "native" });
|
|
71
71
|
const scopeLints = [
|
|
72
72
|
...collateralLints(g.tasks, cwd),
|
|
73
73
|
...sourceScopeLints(g.tasks, cwd, sourceFindings),
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
interface SkillDrift {
|
|
2
|
+
skill: string;
|
|
3
|
+
dir: string;
|
|
4
|
+
missing: string[];
|
|
5
|
+
modified: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function skillDrift(cwd: string): SkillDrift[];
|
|
8
|
+
export declare const docsDrift: (cwd: string) => string[];
|
|
9
|
+
export declare function installedAgentGuidance(cwd: string): string[];
|
|
1
10
|
export type DetectedHost = "herdr" | "orca" | "subprocess";
|
|
2
11
|
export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
|
|
3
12
|
/** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
|
|
@@ -6,3 +15,4 @@ export type InitIO = {
|
|
|
6
15
|
output?: NodeJS.WriteStream;
|
|
7
16
|
};
|
|
8
17
|
export declare function init(argv: string[], cwd?: string, io?: InitIO): Promise<string>;
|
|
18
|
+
export {};
|
|
@@ -107,7 +107,11 @@ Outside multi-agent environments, run the loop directly.
|
|
|
107
107
|
|
|
108
108
|
### Version preflight
|
|
109
109
|
|
|
110
|
-
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version
|
|
110
|
+
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version --check\` and stop on a non-zero exit. In the tickmarkr source repository (\`package.json\` named \`tickmarkr\`), stop if the versions differ anywhere (major, minor, or patch); the binary and repository must agree on the entire version, so binary \`2.1.0\` versus repository \`2.1.1\` is a stop. In a consumer repository, the check compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version; refresh drifted guidance with \`tickmarkr init --agent --force\`. Tell the operator to update or link the correct binary when the source version differs. Never proceed on hope — stale binaries silently skip daemon gates. Also verify no run is live in this repository before starting one: lead with this repository's \`.tickmarkr/graph.lock\`, read its holder pid, and treat it as live until \`kill -0 <pid>\` proves that holder dead. Never require a machine-wide process pattern to be empty: a lawful run in another repository — or the probing shell's own argv — can match. If you use a process probe as secondary evidence, exclude the probing process, resolve every candidate's own cwd (for example with \`lsof -a -p <pid> -d cwd\`), and count only candidates whose cwd is this repository root.
|
|
111
|
+
|
|
112
|
+
### Seat hygiene
|
|
113
|
+
|
|
114
|
+
In a multi-tier session, the tiers clear each other at 50% context or more, after writing a handoff. A new spec or run is a mission boundary: write the handoff and clear before taking the next mission. Copy this duty into every brief and handoff. On a single-tier plain host, write your handoff and ask the operator to clear the session.
|
|
111
115
|
|
|
112
116
|
### Tip-verify-before-green
|
|
113
117
|
|
|
@@ -184,7 +188,7 @@ function skillFileList(root, prefix = "") {
|
|
|
184
188
|
}
|
|
185
189
|
// Drift is only computed for skills that ARE installed. A skill never installed is not drift — `--agent`
|
|
186
190
|
// installs it and says so, and nagging about it would bury the case this exists for.
|
|
187
|
-
function skillDrift(cwd) {
|
|
191
|
+
export function skillDrift(cwd) {
|
|
188
192
|
const drifted = [];
|
|
189
193
|
for (const { skillsDir } of hostTargets(cwd)) {
|
|
190
194
|
for (const skill of AGENT_SKILLS) {
|
|
@@ -215,7 +219,7 @@ function skillDrift(cwd) {
|
|
|
215
219
|
// superseded preflight rules (patch-version drift tolerated, a machine-wide process match required)
|
|
216
220
|
// through the very release that fixed the tracked copy, and a consultation was launched under the stale
|
|
217
221
|
// text with nobody able to see it. Same rule as skillDrift: a doc that was never written is not drift.
|
|
218
|
-
const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
222
|
+
export const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
219
223
|
if (!existsSync(docPath))
|
|
220
224
|
return false;
|
|
221
225
|
const current = readFileSync(docPath, "utf8");
|
|
@@ -227,6 +231,12 @@ const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter
|
|
|
227
231
|
return true; // a truncated block is stale by definition
|
|
228
232
|
return current.slice(begin, end + DOCS_END.length) !== AGENT_DOCS.trimEnd();
|
|
229
233
|
});
|
|
234
|
+
export function installedAgentGuidance(cwd) {
|
|
235
|
+
return hostTargets(cwd).flatMap(({ skillsDir, docPath }) => [
|
|
236
|
+
...AGENT_SKILLS.map((skill) => join(skillsDir, skill, "SKILL.md")).filter((path) => existsSync(path)),
|
|
237
|
+
...(existsSync(docPath) && readFileSync(docPath, "utf8").includes(DOCS_BEGIN) ? [docPath] : []),
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
230
240
|
const describeDrift = (d) => [
|
|
231
241
|
d.missing.length ? `missing ${d.missing.join(", ")}` : "",
|
|
232
242
|
d.modified.length ? `modified ${d.modified.join(", ")}` : "",
|
|
@@ -13,6 +13,7 @@ import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChann
|
|
|
13
13
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
14
14
|
import { effectiveEvents, fatalRunEndCause, foldOwedChecks, Journal, loadRoutingProfile, OWED_DISCHARGE_EVENT, } from "../../run/journal.js";
|
|
15
15
|
import { baselineProvenanceOf, fingerprintsOf, forgivenFingerprints, forgivenGateRow, formatBaselineProvenance, formatFingerprints, formatForgiven, formatTipProof, runEndTipProof, } from "../../run/daemon.js";
|
|
16
|
+
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
16
17
|
import { formatSpan, WALL_PRIORITY, WALL_PRIORITY_TEXT, wallBudget, wallBudgetFacts } from "../../run/wall-budget.js";
|
|
17
18
|
import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
|
|
18
19
|
import { cellWidth, wrapCells } from "../../tui/cockpit/width.js";
|
|
@@ -382,12 +383,15 @@ const verificationOf = (runId, events) => {
|
|
|
382
383
|
return "failed";
|
|
383
384
|
if (tip?.state !== "pass")
|
|
384
385
|
return "absent";
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
// Queue 35 (D-2134 R6): the same per-gate proof names both the lead and verification line.
|
|
387
|
+
const proof = runEndTipProof(cycle);
|
|
388
|
+
if (proof.gates?.length) {
|
|
389
|
+
if (proof.gates.every(({ kind }) => kind === "reused"))
|
|
390
|
+
return "cached";
|
|
391
|
+
if (proof.gates.some(({ kind }) => kind === "reused"))
|
|
392
|
+
return "mixed";
|
|
393
|
+
}
|
|
394
|
+
return "passed";
|
|
391
395
|
};
|
|
392
396
|
// Absent is a state to render, never a zero to invent — and no reading here calls a run green.
|
|
393
397
|
// NOTE: every reading below is byte-pinned by tests/fixtures/brand-surfaces/report-md.md — the
|
|
@@ -396,6 +400,7 @@ const verificationOf = (runId, events) => {
|
|
|
396
400
|
const VERIFICATION_READING = {
|
|
397
401
|
passed: "passed — tickmarkr verified this run's integration tip",
|
|
398
402
|
cached: "cached — carried forward from an earlier verified tip, not re-run for this one",
|
|
403
|
+
mixed: "mixed — some tip gates ran fresh and others were carried forward",
|
|
399
404
|
failed: "FAILED — the run did not verify its own tip",
|
|
400
405
|
absent: "absent — no tip verification recorded: neither passed nor failed",
|
|
401
406
|
};
|
|
@@ -406,9 +411,9 @@ function forgivenLines(events) {
|
|
|
406
411
|
function verificationReading(runId, events) {
|
|
407
412
|
const state = verificationOf(runId, events);
|
|
408
413
|
const proof = runEndTipProof(closedCycle(events));
|
|
409
|
-
if ((state === "passed" || state === "cached") && proof.gates?.length) {
|
|
410
|
-
|
|
411
|
-
return `${
|
|
414
|
+
if ((state === "passed" || state === "cached" || state === "mixed") && proof.gates?.length) {
|
|
415
|
+
// Queue 35 (D-2134 R6): mixed describes the lead's provenance; per-gate details retain it here.
|
|
416
|
+
return `${state === "mixed" ? "passed" : state} — ${formatTipProof(proof)}`;
|
|
412
417
|
}
|
|
413
418
|
return VERIFICATION_READING[state];
|
|
414
419
|
}
|
|
@@ -520,9 +525,13 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = [], suit
|
|
|
520
525
|
lines.push("- **tickmarks:**");
|
|
521
526
|
if (gates.length) {
|
|
522
527
|
for (const g of gates) {
|
|
523
|
-
//
|
|
524
|
-
let pass =
|
|
528
|
+
// Queue 35 (D-2134 R6): only held changes the existing tickmark vocabulary.
|
|
529
|
+
let pass = normalizeGateOutcome(g.data).kind === "held" ? "held"
|
|
530
|
+
: gateDeclined(g.data) ? "declined" : g.data.pass === true ? "pass" : g.data.pass === false ? "fail" : EM;
|
|
525
531
|
const gate = typeof g.data.gate === "string" ? g.data.gate : EM;
|
|
532
|
+
if (gate === "acceptance" && g.data.judgeIndependent === false) {
|
|
533
|
+
pass += " (independence not established)";
|
|
534
|
+
}
|
|
526
535
|
if (gate === "review" && pass === "pass") {
|
|
527
536
|
const reviewer = reviewerIdentity(g.data);
|
|
528
537
|
const before = events.slice(0, events.indexOf(g) + 1).reverse()
|
|
@@ -616,7 +625,7 @@ function textReport(runId, events, rows, cwd, suite) {
|
|
|
616
625
|
// T11: gates that declined never ran — out of both the pass count and the rate base.
|
|
617
626
|
const declinedCount = gateResults.filter((e) => gateDeclined(e.data)).length;
|
|
618
627
|
const gateRan = gateResults.length - declinedCount;
|
|
619
|
-
const gatePass = gateResults.filter((e) => e.data.
|
|
628
|
+
const gatePass = gateResults.filter((e) => normalizeGateOutcome(e.data).kind === "passed" && !gateDeclined(e.data)).length;
|
|
620
629
|
const escalations = events.filter((e) => e.event === "escalation").length;
|
|
621
630
|
const consults = events.filter((e) => e.event === "consult-verdict").length;
|
|
622
631
|
const failovers = events.filter((e) => e.event === "quota-failover").length;
|
|
@@ -968,6 +968,16 @@ const recordedTasks = (graph, rows) => graph.tasks.filter((task) => {
|
|
|
968
968
|
|| row.parkKind !== undefined);
|
|
969
969
|
});
|
|
970
970
|
const recordedDone = (tasks, rows) => tasks.filter((task) => graphTaskStatus(rows.get(task.id)?.state, task.status) === "done").length;
|
|
971
|
+
const judgeIndependenceNotes = (events) => {
|
|
972
|
+
// Queue row 34(a) (D-2134 R5): only the newest acceptance row owns this note.
|
|
973
|
+
const acceptance = new Map();
|
|
974
|
+
for (const event of events) {
|
|
975
|
+
if (event.taskId && event.event === "gate-result" && event.data.gate === "acceptance")
|
|
976
|
+
acceptance.set(event.taskId, event);
|
|
977
|
+
}
|
|
978
|
+
return [...acceptance].filter(([, event]) => event.data.judgeIndependent === false)
|
|
979
|
+
.map(([id]) => `${sanitizeTaskText(id)} judge not independent`);
|
|
980
|
+
};
|
|
971
981
|
const renderFrame = (cwd,
|
|
972
982
|
// No default: the version is the binary's, resolved once by its caller, and a frame that had to
|
|
973
983
|
// invent one would be naming a version nobody can check.
|
|
@@ -976,6 +986,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
976
986
|
const record = readRunRecord(cwd, g, namedRunId);
|
|
977
987
|
const runId = record?.runId;
|
|
978
988
|
const events = record?.events ?? [];
|
|
989
|
+
const judgeNotes = judgeIndependenceNotes(events);
|
|
979
990
|
const runLine = runId && record ? runStatusLine(cwd, runId, events) : null;
|
|
980
991
|
// The header and every live-phase consumer share this exact process reading. Proved death retires
|
|
981
992
|
// stale phase markers; unknown identity deliberately does not, preserving pre-identity journals.
|
|
@@ -1171,7 +1182,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1171
1182
|
// At its machine width and wider the plain print keeps each machine row whole — one row per task and
|
|
1172
1183
|
// its evidence line after it, which consumers parse. A configured width narrower than that holds for
|
|
1173
1184
|
// EVERY row: one that overflows wraps beneath its own indent, its identity kept on the first.
|
|
1174
|
-
const body = [header, legendLine, ` ${supervisionText(supervision, divider)}`, ...rows];
|
|
1185
|
+
const body = [header, legendLine, ` ${supervisionText(supervision, divider)}`, ...rows, ...judgeNotes.map((note) => ` ${note}`)];
|
|
1175
1186
|
const wrapRow = (line) => wrapCells(line, width, { continuationPrefix: `${/^ */u.exec(line)[0]} ` });
|
|
1176
1187
|
return {
|
|
1177
1188
|
content: [
|
|
@@ -1383,6 +1394,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1383
1394
|
"",
|
|
1384
1395
|
...tableRows(),
|
|
1385
1396
|
...recoverySection,
|
|
1397
|
+
...judgeNotes.flatMap((note) => wrapCells(` ${note}`, boardColumns, { continuationPrefix: " " })),
|
|
1386
1398
|
gatesLegend,
|
|
1387
1399
|
"",
|
|
1388
1400
|
rule(boardColumns),
|
|
@@ -1436,7 +1448,7 @@ const oneLine = (cwd, namedRunId) => {
|
|
|
1436
1448
|
// A fatal run-end leads right after the run id: no completion fraction or verify state reads first.
|
|
1437
1449
|
const crash = fatalRunEndCause(events);
|
|
1438
1450
|
return sanitizeTaskText([
|
|
1439
|
-
record.runId, ...(crash ? [`run crashed — ${crash}`] : []), ...claims, firstPassText(endToEndFirstPass(events)), wallText(events), needsYouText(events, currentOwed(events, cwd)),
|
|
1451
|
+
record.runId, ...(crash ? [`run crashed — ${crash}`] : []), ...claims, firstPassText(endToEndFirstPass(events)), wallText(events), needsYouText(events, currentOwed(events, cwd)), ...judgeIndependenceNotes(events),
|
|
1440
1452
|
].join(" · "));
|
|
1441
1453
|
};
|
|
1442
1454
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
@@ -370,8 +370,8 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
370
370
|
}
|
|
371
371
|
// Baseline: --baseline file > cached capture for this merge-base > fresh capture on a detached
|
|
372
372
|
// temp worktree of the merge-base (so pre-existing failures on base are forgiven, exactly as a run).
|
|
373
|
-
//
|
|
374
|
-
//
|
|
373
|
+
// Queue row 31 / D-2134: gate-time state lives outside the repo; a validated discharge's durable
|
|
374
|
+
// proof is copied into its run only after the battery's final dirty-worktree check.
|
|
375
375
|
// ponytail: tmpdir means the baseline cache dies on reboot/cleanup — worst case is one re-capture.
|
|
376
376
|
const stateDir = verifyStateDir(cwd);
|
|
377
377
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -600,6 +600,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
600
600
|
console.error(`verify: evidence capture failed for ${artifactPath}`);
|
|
601
601
|
}
|
|
602
602
|
const review = reviewRows.at(-1);
|
|
603
|
+
let recordedArtifactPath = artifactPath;
|
|
603
604
|
// C1: ONE discharge row names what the fold re-reads — the artifact by hash, the range mapping, the
|
|
604
605
|
// criteria and each resolved channel — and it is appended only when the fold, the single decider,
|
|
605
606
|
// already accepts it: verify never writes a proof that would turn known debt into unknown debt.
|
|
@@ -624,8 +625,31 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
624
625
|
dischargeRefusal = "the results artifact is unavailable";
|
|
625
626
|
else if (!candidate.known || !owed.every((o) => candidate.discharged.includes(o.id)))
|
|
626
627
|
dischargeRefusal = candidate.unknown.at(-1)?.reason ?? "the proof does not discharge the check";
|
|
627
|
-
else
|
|
628
|
-
|
|
628
|
+
else {
|
|
629
|
+
// Queue row 31 / D-2134: validate before writing under the run, then retain the exact bytes.
|
|
630
|
+
// Exclusive creation accepts an identical existing proof, never overwrites different bytes.
|
|
631
|
+
const relativePath = join("discharges", `${artifactSha256}.json`);
|
|
632
|
+
const durablePath = join(recordJournal.dir, relativePath);
|
|
633
|
+
try {
|
|
634
|
+
mkdirSync(dirname(durablePath), { recursive: true });
|
|
635
|
+
try {
|
|
636
|
+
writeFileSync(durablePath, artifactBytes, { flag: "wx" });
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (error.code !== "EEXIST")
|
|
640
|
+
throw error;
|
|
641
|
+
if (!readFileSync(durablePath).equals(artifactBytes))
|
|
642
|
+
throw new Error("existing durable proof has different bytes");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
dischargeRefusal = "durable results artifact unavailable or has different bytes";
|
|
647
|
+
}
|
|
648
|
+
if (!dischargeRefusal) {
|
|
649
|
+
recordedArtifactPath = relativePath;
|
|
650
|
+
recordJournal.append(OWED_DISCHARGE_EVENT, values.task, { ...data, artifactPath: recordedArtifactPath });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
629
653
|
}
|
|
630
654
|
const owedAfter = recordJournal ? foldOwedChecks(recordJournal.read(), cwd) : undefined;
|
|
631
655
|
const code = green && !dischargeRefusal ? 0 : 2;
|
|
@@ -633,7 +657,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
633
657
|
? [...(dischargeRefusal ? [`verify --record refused discharge: ${dischargeRefusal}`] : []), debtLine(owedAfter)] : [];
|
|
634
658
|
if (recordJournal && review) {
|
|
635
659
|
recordJournal.append("review-leg2", values.task ?? "VERIFY", {
|
|
636
|
-
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath, artifactSha256, artifactAvailability,
|
|
660
|
+
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath: recordedArtifactPath, artifactSha256, artifactAvailability,
|
|
637
661
|
...review,
|
|
638
662
|
});
|
|
639
663
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function distFingerprint(dir?: string): string;
|
|
2
|
-
export declare function version(argv?: string[], dir?: string): Promise<string>;
|
|
2
|
+
export declare function version(argv?: string[], dir?: string, cwd?: string): Promise<string>;
|
|
@@ -23,7 +23,41 @@ export function distFingerprint(dir = distDir) {
|
|
|
23
23
|
hash.update(relative(dir, file)).update("\0").update(readFileSync(file)).update("\0");
|
|
24
24
|
return hash.digest("hex");
|
|
25
25
|
}
|
|
26
|
-
export async function version(argv = [], dir = distDir) {
|
|
26
|
+
export async function version(argv = [], dir = distDir, cwd = process.cwd()) {
|
|
27
27
|
const { version: v } = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
28
|
-
|
|
28
|
+
const stamp = argv.includes("--dist") ? `${v} dist:${resolve(dir)} fingerprint:${distFingerprint(dir)}` : v;
|
|
29
|
+
if (!argv.includes("--check"))
|
|
30
|
+
return stamp;
|
|
31
|
+
let manifest;
|
|
32
|
+
try {
|
|
33
|
+
manifest = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
34
|
+
}
|
|
35
|
+
catch { /* no source manifest — consumer guidance check */ }
|
|
36
|
+
if (manifest?.name === "tickmarkr") {
|
|
37
|
+
const repoVersion = manifest.version ?? "missing";
|
|
38
|
+
const equal = v === repoVersion;
|
|
39
|
+
const out = `tickmarkr binary ${stamp}: compared tickmarkr source repository package.json version ${repoVersion} — ${equal ? "entire version matches" : "versions differ; update or link the correct binary"}`;
|
|
40
|
+
if (!equal)
|
|
41
|
+
throw new Error(out);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
const { docsDrift, installedAgentGuidance, skillDrift } = await import("./init.js");
|
|
45
|
+
const installed = installedAgentGuidance(cwd);
|
|
46
|
+
if (!installed.length)
|
|
47
|
+
return `tickmarkr binary ${stamp}: no installed agent guidance to compare`;
|
|
48
|
+
const drift = [
|
|
49
|
+
...skillDrift(cwd).flatMap(({ dir: skillDir, missing, modified }) => [
|
|
50
|
+
...missing.map((file) => `missing ${relative(cwd, join(skillDir, file))}`),
|
|
51
|
+
...modified.map((file) => `modified ${relative(cwd, join(skillDir, file))}`),
|
|
52
|
+
]),
|
|
53
|
+
...docsDrift(cwd).map((file) => `stale agent-docs block in ${relative(cwd, file)}`),
|
|
54
|
+
];
|
|
55
|
+
const out = [
|
|
56
|
+
`tickmarkr binary ${stamp}: compared installed agent guidance with this binary's packaged block and skills:`,
|
|
57
|
+
...installed.map((file) => ` ${relative(cwd, file)}`),
|
|
58
|
+
...(drift.length ? [...drift, "refresh: tickmarkr init --agent --force"] : ["installed guidance matches"]),
|
|
59
|
+
].join("\n");
|
|
60
|
+
if (drift.length)
|
|
61
|
+
throw new Error(out);
|
|
62
|
+
return out;
|
|
29
63
|
}
|
package/dist/cli/help.d.ts
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -169,9 +169,12 @@ export const COMMAND_HELP = {
|
|
|
169
169
|
],
|
|
170
170
|
},
|
|
171
171
|
version: {
|
|
172
|
-
usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
|
173
|
-
options: {
|
|
174
|
-
|
|
172
|
+
usage: "version [--dist] [--check]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
|
173
|
+
options: {
|
|
174
|
+
"--dist": "Also print the resolved build directory and distribution fingerprint.",
|
|
175
|
+
"--check": "Compare the entire source version when package.json is named tickmarkr; otherwise check installed agent guidance against this binary. Exit non-zero on drift; no installed guidance exits zero.",
|
|
176
|
+
},
|
|
177
|
+
examples: ["version", "version --dist", "version --check"],
|
|
175
178
|
},
|
|
176
179
|
verify: {
|
|
177
180
|
usage: "verify [--base <ref>] [--criteria <file> | --task <id>] [options]",
|
|
@@ -55,7 +55,9 @@ export interface SourceScopeFinding {
|
|
|
55
55
|
paths: string[];
|
|
56
56
|
}
|
|
57
57
|
/** Structured OBS-76 findings shared by human rendering and native compile enforcement. */
|
|
58
|
-
export declare function sourceScopeFindings(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string
|
|
58
|
+
export declare function sourceScopeFindings(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string, options?: {
|
|
59
|
+
failClosed?: boolean;
|
|
60
|
+
}): SourceScopeFinding[];
|
|
59
61
|
/** Human display is derived from the structured finding; no enforcement path parses this text. */
|
|
60
62
|
export declare function renderSourceScopeFinding(finding: SourceScopeFinding): string;
|
|
61
63
|
/**
|
|
@@ -3,18 +3,15 @@ import { extname, join, posix, relative } from "node:path";
|
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
5
5
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
6
|
+
import { CompileError } from "./common.js";
|
|
6
7
|
// Advisory scan (OBS-12/13/14/21, OBS-76). NEVER expands files[] or fails compile — a warning the
|
|
7
8
|
// author acts on. Plain-text (no AST), capped + sorted for DISPLAY only.
|
|
8
9
|
// OBS-547: the collateral prediction is no longer plan-time-only. The daemon computes one full
|
|
9
10
|
// (uncapped) map per run and hands each task's slice to its scope gate, which classifies a red
|
|
10
11
|
// against it — the prediction never causes a refusal, it only says whether a red the gate already
|
|
11
12
|
// found was foreseeable (authoring defect) or not (chargeable quality failure).
|
|
12
|
-
/** Max files walked per root (sorted walk; rest ignored). */
|
|
13
|
-
const MAX_WALK_FILES = 400;
|
|
14
13
|
/** Max collateral test paths listed per task. */
|
|
15
14
|
const MAX_HITS_PER_TASK = 20;
|
|
16
|
-
/** Skip giant fixtures / snapshots. */
|
|
17
|
-
const MAX_READ_BYTES = 512 * 1024;
|
|
18
15
|
const EXPORT_MANIFEST_TEST = "tests/repo/export-manifest.test.ts";
|
|
19
16
|
const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
20
17
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
@@ -33,8 +30,6 @@ function walkCode(repoRoot, subdir) {
|
|
|
33
30
|
const root = join(repoRoot, subdir);
|
|
34
31
|
const out = [];
|
|
35
32
|
const walk = (dir) => {
|
|
36
|
-
if (out.length >= MAX_WALK_FILES)
|
|
37
|
-
return;
|
|
38
33
|
let entries;
|
|
39
34
|
try {
|
|
40
35
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -45,8 +40,6 @@ function walkCode(repoRoot, subdir) {
|
|
|
45
40
|
// deterministic order
|
|
46
41
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
47
42
|
for (const e of entries) {
|
|
48
|
-
if (out.length >= MAX_WALK_FILES)
|
|
49
|
-
return;
|
|
50
43
|
if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith("."))
|
|
51
44
|
continue;
|
|
52
45
|
const full = join(dir, e.name);
|
|
@@ -78,22 +71,21 @@ function mentions(content, needles) {
|
|
|
78
71
|
return false;
|
|
79
72
|
}
|
|
80
73
|
// cache file bodies (one read per path)
|
|
81
|
-
function makeReader(repoRoot) {
|
|
74
|
+
function makeReader(repoRoot, failClosed = false) {
|
|
82
75
|
const body = new Map();
|
|
83
76
|
return (rel) => {
|
|
84
77
|
if (body.has(rel))
|
|
85
78
|
return body.get(rel);
|
|
86
79
|
try {
|
|
87
|
-
const st = statSync(join(repoRoot, rel));
|
|
88
|
-
if (st.size > MAX_READ_BYTES) {
|
|
89
|
-
body.set(rel, null);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
80
|
const text = readFileSync(join(repoRoot, rel), "utf8");
|
|
93
81
|
body.set(rel, text);
|
|
94
82
|
return text;
|
|
95
83
|
}
|
|
96
84
|
catch {
|
|
85
|
+
if (failClosed) {
|
|
86
|
+
throw new CompileError(`${rel} is unreadable, so the source-scope scan is incomplete — restore read access `
|
|
87
|
+
+ `or remove the path and recompile (queue row 27, D-2134).`);
|
|
88
|
+
}
|
|
97
89
|
body.set(rel, null);
|
|
98
90
|
return null;
|
|
99
91
|
}
|
|
@@ -502,7 +494,7 @@ export function newDirectoryLints(tasks, repoRoot) {
|
|
|
502
494
|
return lines;
|
|
503
495
|
}
|
|
504
496
|
/** Structured OBS-76 findings shared by human rendering and native compile enforcement. */
|
|
505
|
-
export function sourceScopeFindings(tasks, repoRoot) {
|
|
497
|
+
export function sourceScopeFindings(tasks, repoRoot, options = {}) {
|
|
506
498
|
const perTask = tasks
|
|
507
499
|
.map((t) => ({ t, needles: criteriaSymbols(t.acceptance) }))
|
|
508
500
|
.filter((x) => x.needles.length);
|
|
@@ -511,7 +503,7 @@ export function sourceScopeFindings(tasks, repoRoot) {
|
|
|
511
503
|
const srcFiles = walkCode(repoRoot, "src");
|
|
512
504
|
if (!srcFiles.length)
|
|
513
505
|
return [];
|
|
514
|
-
const read = makeReader(repoRoot);
|
|
506
|
+
const read = makeReader(repoRoot, options.failClosed);
|
|
515
507
|
const findings = [];
|
|
516
508
|
for (const { t, needles } of perTask) {
|
|
517
509
|
// OBS-22: scopeGate accepts picomatch globs; advisory warnings must agree.
|
package/dist/compile/native.js
CHANGED
|
@@ -272,6 +272,11 @@ function testsAtHead(root) {
|
|
|
272
272
|
if (cached)
|
|
273
273
|
return cached;
|
|
274
274
|
const grep = gitSync(root, ["grep", "-I", "-n", "-e", "", "HEAD", "--", "tests/*.test.ts", "tests/**/*.test.ts"]);
|
|
275
|
+
if (grep.error || grep.signal || grep.status === null) {
|
|
276
|
+
const reason = grep.error?.message ?? (grep.signal ? `signal ${grep.signal}` : "no exit status");
|
|
277
|
+
throw new CompileError(`cannot complete the test corpus read (git grep HEAD tests) in ${root}: ${reason} — `
|
|
278
|
+
+ `restore the corpus read and recompile (queue row 27, D-2134).`);
|
|
279
|
+
}
|
|
275
280
|
if (grep.status !== 0 || typeof grep.stdout !== "string") {
|
|
276
281
|
headTestCache.set(root, []);
|
|
277
282
|
return [];
|
|
@@ -387,8 +392,8 @@ function criterionScopeFinding(task, text, criterion, id, tests) {
|
|
|
387
392
|
// OBS-898: the fence lint's corpus is listed ONCE per compile by git (tracked plus untracked-not-ignored,
|
|
388
393
|
// the same set a checkout walk sees minus the ignored trees) and filtered per task. The walk it replaces
|
|
389
394
|
// descended every dot-directory (.tickmarkr/runs alone held 3 799 files here) once PER TASK, so a
|
|
390
|
-
// 20-task spec compiled in seconds and the committed-spec corpus test timed out.
|
|
391
|
-
//
|
|
395
|
+
// 20-task spec compiled in seconds and the committed-spec corpus test timed out.
|
|
396
|
+
// Queue row 27 (D-2134): this advisory listing keeps its empty answer on git failure.
|
|
392
397
|
function repoFileList(root) {
|
|
393
398
|
const ls = gitSync(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
394
399
|
if (ls.status !== 0 || typeof ls.stdout !== "string")
|
|
@@ -49,7 +49,7 @@ export class SubprocessDriver {
|
|
|
49
49
|
n = 0;
|
|
50
50
|
async slot(cwd, name) {
|
|
51
51
|
const id = `sp-${++this.n}`;
|
|
52
|
-
this.slots.set(id, { buf: "", exited: false });
|
|
52
|
+
this.slots.set(id, { buf: "", receivedBytes: 0, exited: false });
|
|
53
53
|
return { id, name, cwd };
|
|
54
54
|
}
|
|
55
55
|
state(slot) {
|
|
@@ -72,8 +72,13 @@ export class SubprocessDriver {
|
|
|
72
72
|
detached: true,
|
|
73
73
|
});
|
|
74
74
|
s.proc = p;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// Queue row 233 (D-2130): count both received streams before retention truncates the tail.
|
|
76
|
+
const receive = (d) => {
|
|
77
|
+
s.receivedBytes += d.length;
|
|
78
|
+
s.buf = (s.buf + d).slice(-MAX_BUF);
|
|
79
|
+
};
|
|
80
|
+
p.stdout.on("data", receive);
|
|
81
|
+
p.stderr.on("data", receive);
|
|
77
82
|
p.on("close", () => (s.exited = true));
|
|
78
83
|
p.on("error", (e) => { s.buf = (s.buf + `\n[tickmarkr subprocess error] ${e}\n`).slice(-MAX_BUF); s.exited = true; });
|
|
79
84
|
// Queue row 108 (D-1622): THIS run's transport state. The slot's `exited` outlives an invocation (a reused slot) and
|
|
@@ -117,7 +122,7 @@ export class SubprocessDriver {
|
|
|
117
122
|
async transportState(slot) {
|
|
118
123
|
const s = this.state(slot);
|
|
119
124
|
const run = s.invocation;
|
|
120
|
-
return { closed: run?.closed ?? false, bytes:
|
|
125
|
+
return { closed: run?.closed ?? false, bytes: s.receivedBytes, exitCode: run?.exitCode ?? null, signal: run?.signal ?? null };
|
|
121
126
|
}
|
|
122
127
|
async read(slot, lines) {
|
|
123
128
|
return this.state(slot).buf.split("\n").slice(-lines).join("\n");
|
|
@@ -21,6 +21,14 @@ export interface JudgeVerdict {
|
|
|
21
21
|
}>;
|
|
22
22
|
}
|
|
23
23
|
export declare function judgeCriterionId(index: number): string;
|
|
24
|
+
export interface OracleFailure {
|
|
25
|
+
index: number;
|
|
26
|
+
kind: "command" | "test";
|
|
27
|
+
criterion: string;
|
|
28
|
+
landing?: string;
|
|
29
|
+
failure: "absent" | "elsewhere" | "untitled" | "red" | "unverified" | "no-runner";
|
|
30
|
+
output: string;
|
|
31
|
+
}
|
|
24
32
|
export declare function testFilterPattern(name: string): string;
|
|
25
33
|
export declare function testFiltered(testCmd: string, name: string): string;
|
|
26
34
|
export interface VitestListedTest {
|