tickmarkr 1.43.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/status.js +22 -13
- package/dist/compile/native.js +9 -0
- package/dist/gates/acceptance.js +9 -3
- package/dist/run/daemon.js +1 -1
- package/dist/run/journal.d.ts +2 -0
- package/dist/run/journal.js +12 -0
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BANNER } from "../../brand.js";
|
|
2
2
|
import { blockedTasks, loadGraph, pendingTasks } from "../../graph/graph.js";
|
|
3
3
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
4
|
-
import { Journal } from "../../run/journal.js";
|
|
4
|
+
import { Journal, journalComparable } from "../../run/journal.js";
|
|
5
5
|
// ponytail: fixed 2s refresh; promote to config.visibility.* only when an operator asks.
|
|
6
6
|
const REFRESH_MS = 2000;
|
|
7
|
+
const NOT_COMPARABLE_NOTICE = "graph recompiled since this run — task states not comparable; run `tickmarkr run` to execute";
|
|
7
8
|
// The timer must keep the process ALIVE: an unref'd timer here let the event loop drain after the
|
|
8
9
|
// first frame, so a live `--watch` printed once and exited 0 (OBS-11). Never unref this.
|
|
9
10
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -40,6 +41,7 @@ const gateBox = (state, unicode) => {
|
|
|
40
41
|
return state === "pass" ? "✓" : state === "fail" ? "✗" : state === "skip" ? "·" : "☐";
|
|
41
42
|
return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
|
|
42
43
|
};
|
|
44
|
+
const defaultGateStates = (task) => GATE_NAMES.map((gate) => task.gates.includes(gate) ? "open" : "skip");
|
|
43
45
|
const gateStates = (task, events) => {
|
|
44
46
|
const outcomes = new Map();
|
|
45
47
|
const start = attemptStartIdx(events, task.id);
|
|
@@ -121,18 +123,23 @@ const renderFrame = (cwd) => {
|
|
|
121
123
|
let replayed = null;
|
|
122
124
|
let events = [];
|
|
123
125
|
const contexts = new Map();
|
|
126
|
+
let comparable = false;
|
|
124
127
|
if (runId) {
|
|
125
128
|
const j = Journal.open(cwd, runId);
|
|
126
129
|
events = j.read();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
// OBS-52: refuse journal join when the run's recorded graphHash ≠ loaded graph — bare task-id replay lied after recompile.
|
|
131
|
+
comparable = journalComparable(events, g.spec.hash);
|
|
132
|
+
if (comparable) {
|
|
133
|
+
replayed = j.replayStatuses();
|
|
134
|
+
for (const e of events) {
|
|
135
|
+
if (e.event === "task-dispatch" && e.taskId) {
|
|
136
|
+
const a = e.data.assignment;
|
|
137
|
+
if (typeof a.adapter === "string" && typeof a.model === "string")
|
|
138
|
+
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
139
|
+
}
|
|
140
|
+
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
141
|
+
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
142
|
+
}
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
}
|
|
@@ -148,7 +155,7 @@ const renderFrame = (cwd) => {
|
|
|
148
155
|
const label = starved.has(t.id) ? " starved" : waiting.has(t.id) ? " dep-waiting" : "";
|
|
149
156
|
const channel = assignments.get(t.id) ?? "-";
|
|
150
157
|
const assignCol = contexts.has(t.id) ? `${channel}${divider}ctx ${contexts.get(t.id)}` : channel;
|
|
151
|
-
return { t, st, label, assignCol, states: gateStates(t, events) };
|
|
158
|
+
return { t, st, label, assignCol, states: comparable ? gateStates(t, events) : defaultGateStates(t) };
|
|
152
159
|
});
|
|
153
160
|
const statusColor = (st) => st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 36;
|
|
154
161
|
if (!unicode) {
|
|
@@ -160,7 +167,7 @@ const renderFrame = (cwd) => {
|
|
|
160
167
|
return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
161
168
|
});
|
|
162
169
|
const header = runId
|
|
163
|
-
? `tickmarkr status${divider}run ${runId}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
170
|
+
? `tickmarkr status${divider}run ${runId}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
164
171
|
: `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
|
|
165
172
|
const legend = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
166
173
|
return [header, legend, ...rows].join("\n");
|
|
@@ -181,7 +188,9 @@ const renderFrame = (cwd) => {
|
|
|
181
188
|
.replaceAll(" · ", dot);
|
|
182
189
|
const tally = `${done}/${g.tasks.length} done`;
|
|
183
190
|
const header = ` ${color("✓", 32, true)} ${color("tickmarkr", 1, true)}${dot}` +
|
|
184
|
-
(runId
|
|
191
|
+
(runId
|
|
192
|
+
? `run ${runId}${dot}${!comparable ? `${NOT_COMPARABLE_NOTICE}${dot}` : ""}${live}${dot}`
|
|
193
|
+
: `no runs yet${dot}`) +
|
|
185
194
|
`${gauge} ${done === g.tasks.length && g.tasks.length > 0 ? color(tally, 32, true) : tally}`;
|
|
186
195
|
const rule = dim("─".repeat(Math.min(width, 100)));
|
|
187
196
|
const legend = dim(` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(" · ")}`);
|
package/dist/compile/native.js
CHANGED
|
@@ -144,6 +144,15 @@ export function compileNative(file) {
|
|
|
144
144
|
if (plainCount > 0) {
|
|
145
145
|
console.warn(`tickmarkr: ${plainCount} acceptance item${plainCount === 1 ? "" : "s"} in ${file} ${plainCount === 1 ? "is a plain string" : "are plain strings"} — compiled as judge oracle${plainCount === 1 ? "" : "s"}. Prefix with command:/test:/judge: to make the oracle explicit.`);
|
|
146
146
|
}
|
|
147
|
+
// OBS-51: semicolon-joined judge criteria invite intermittent clause-split verdicts — warn per item.
|
|
148
|
+
for (const draft of drafts) {
|
|
149
|
+
for (const item of draft.acceptance) {
|
|
150
|
+
const text = typeof item === "string" ? item : item.oracle === "judge" ? item.text : null;
|
|
151
|
+
if (text?.includes(";")) {
|
|
152
|
+
console.warn(`tickmarkr: OBS-51: task ${draft.id} judge criterion contains semicolon-joined clauses — split into separate acceptance items: ${JSON.stringify(text)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
147
156
|
return result;
|
|
148
157
|
}
|
|
149
158
|
// Commented native spec written to tickmarkr.spec.md by `tickmarkr init`. Documented via HTML comments (which the
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -93,12 +93,18 @@ Respond with ONLY this JSON (no prose before or after):
|
|
|
93
93
|
meta: { unparseable: true, judge: channelKey({ adapter: judge.adapter.id, model: judge.model }) } };
|
|
94
94
|
}
|
|
95
95
|
const inconsistencies = [];
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const expected = judgeItems.length;
|
|
97
|
+
const received = v.criteria.length;
|
|
98
|
+
// OBS-51: judges sometimes split one semicolon-joined criterion into multiple verdict items.
|
|
99
|
+
const clauseSplitAllPass = received > expected
|
|
100
|
+
&& v.criteria.every((c) => c && typeof c === "object" && c.met === true);
|
|
101
|
+
if (received !== expected && !clauseSplitAllPass) {
|
|
102
|
+
inconsistencies.push(`judge verdict inconsistent: criteria count mismatch — expected ${expected}, received ${received}`);
|
|
98
103
|
}
|
|
99
104
|
v.criteria.forEach((c, i) => {
|
|
100
105
|
if (!c || typeof c !== "object" || c.met !== true) {
|
|
101
|
-
|
|
106
|
+
const unmet = c && typeof c === "object" && c.criterion ? ` — unmet clause: ${c.criterion}` : "";
|
|
107
|
+
inconsistencies.push(`judge verdict inconsistent: criteria[${i}].met must be true${unmet}`);
|
|
102
108
|
}
|
|
103
109
|
});
|
|
104
110
|
const pass = v.pass === true && inconsistencies.length === 0;
|
package/dist/run/daemon.js
CHANGED
|
@@ -87,7 +87,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
87
87
|
baseRef = await gitHead(repoRoot);
|
|
88
88
|
baseline = await captureBaseline(repoRoot, commands);
|
|
89
89
|
writeFileSync(join(journal.dir, "baseline.json"), JSON.stringify(baseline, null, 2));
|
|
90
|
-
journal.append("run-start", undefined, { pid: process.pid, baseRef, commands, channels: channels.map(channelKey), branch }); // pid: v1.13 (VIS-11) liveness
|
|
90
|
+
journal.append("run-start", undefined, { pid: process.pid, baseRef, commands, channels: channels.map(channelKey), branch, graphHash: graph.spec.hash }); // graphHash: OBS-52 status join guard; pid: v1.13 (VIS-11) liveness
|
|
91
91
|
}
|
|
92
92
|
// T6: open the narrator AFTER run-start/run-resume is journaled so the watch surface has a run to
|
|
93
93
|
// show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
62
62
|
}>>;
|
|
63
63
|
}, z.core.$strip>;
|
|
64
64
|
export type TelemetryRow = z.infer<typeof TelemetryRowSchema>;
|
|
65
|
+
export declare function recordedGraphHash(events: JournalEvent[]): string | undefined;
|
|
66
|
+
export declare function journalComparable(events: JournalEvent[], loadedHash: string): boolean;
|
|
65
67
|
export declare function newRunId(now?: Date): string;
|
|
66
68
|
export declare function readAllTelemetry(repoRoot: string, lastK: number, opts?: {
|
|
67
69
|
after?: string;
|
package/dist/run/journal.js
CHANGED
|
@@ -72,6 +72,18 @@ export const TelemetryRowSchema = z.object({
|
|
|
72
72
|
// v1.29 additive: mode of the attempt represented by this row. Absent on old telemetry.
|
|
73
73
|
retryMode: z.enum(RETRY_MODES).optional(),
|
|
74
74
|
});
|
|
75
|
+
// OBS-52: graph hash on run-start binds status replay to the compiled graph that started the run.
|
|
76
|
+
export function recordedGraphHash(events) {
|
|
77
|
+
for (const e of events) {
|
|
78
|
+
if (e.event === "run-start" && typeof e.data.graphHash === "string")
|
|
79
|
+
return e.data.graphHash;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
export function journalComparable(events, loadedHash) {
|
|
84
|
+
const recorded = recordedGraphHash(events);
|
|
85
|
+
return recorded !== undefined && recorded === loadedHash;
|
|
86
|
+
}
|
|
75
87
|
export function newRunId(now = new Date()) {
|
|
76
88
|
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
77
89
|
return `run-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|