tickmarkr 2.8.7 → 2.8.9
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/README.md +22 -0
- package/dist/cli/commands/compile.js +1 -1
- 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/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- 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 +221 -42
- package/dist/run/git.d.ts +7 -2
- package/dist/run/git.js +37 -4
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +33 -2
- package/dist/run/wall-budget.js +8 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +58 -3
- package/skills/tickmarkr-loop/SKILL.md +59 -5
- package/skills/tickmarkr-overseer/SKILL.md +134 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +145 -18
|
@@ -15,10 +15,9 @@
|
|
|
15
15
|
#
|
|
16
16
|
# Prints one wake reason and EXITS. Re-arm after every wake.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# wide and unreadable, because closing was a step someone had to remember.
|
|
18
|
+
# Queue row 240; D-2138 add.1 R3, add.4: optional close lists carry ONE recorded address per
|
|
19
|
+
# artifact, in argument order. Completion wakes on delivery; closure additionally requires the
|
|
20
|
+
# current mission's stand-down and live identity matching the spawn ledger. Never close on timeout.
|
|
22
21
|
set -u
|
|
23
22
|
# macOS ships bash 3.2, where `set -u` makes "${arr[@]}" on an EMPTY array a fatal unbound-variable
|
|
24
23
|
# error. Every expansion below therefore uses the ${arr[@]+"${arr[@]}"} guard. Caught by the timeout
|
|
@@ -38,6 +37,19 @@ POLL="${3:?}"
|
|
|
38
37
|
shift 3
|
|
39
38
|
[ "$#" -gt 0 ] || { echo "watch-artifacts: no files given" >&2; exit 2; }
|
|
40
39
|
|
|
40
|
+
# Refuse ambiguous arming before watching anything; preserve empty fields for count validation.
|
|
41
|
+
CLOSE_HOST=""
|
|
42
|
+
CLOSE_ADDRESSES="${TKR_CLOSE_TERMINALS:-${TKR_CLOSE_PANES:-}}"
|
|
43
|
+
if [ -n "${TKR_CLOSE_TERMINALS:-}" ] && [ -n "${TKR_CLOSE_PANES:-}" ]; then
|
|
44
|
+
echo "watch-artifacts: choose one host close list" >&2; exit 2
|
|
45
|
+
fi
|
|
46
|
+
if [ -n "$CLOSE_ADDRESSES" ]; then
|
|
47
|
+
if [ -n "${TKR_CLOSE_TERMINALS:-}" ]; then CLOSE_HOST=orca; else CLOSE_HOST=herdr; fi
|
|
48
|
+
node -e 'const a=process.argv[1].split(","); if(a.length!==Number(process.argv[2]) || a.some(x=>!x.trim())) process.exit(2)' "$CLOSE_ADDRESSES" "$#" || {
|
|
49
|
+
echo "watch-artifacts: close list requires one recorded address per artifact in argument order" >&2; exit 2;
|
|
50
|
+
}
|
|
51
|
+
fi
|
|
52
|
+
|
|
41
53
|
# OBS-848: ownership is a pid-file fact, never an argv/path pattern. Two tiers intentionally watch
|
|
42
54
|
# the same artifacts, so the journal/file argument cannot identify which watcher a seat may retire.
|
|
43
55
|
PID_DIR="${TKR_STATE_DIR:-.tickmarkr}/overseer/pids"
|
|
@@ -55,7 +67,7 @@ trap clear_pid EXIT
|
|
|
55
67
|
# two that happened to be in view at the time. Class, not instance.
|
|
56
68
|
END=$((SECONDS + CAP))
|
|
57
69
|
|
|
58
|
-
#
|
|
70
|
+
# Queue row 240; D-2138 add.4: DONE requires the marker on the final line and stable content.
|
|
59
71
|
#
|
|
60
72
|
# Anchored to the tail on purpose: a report that merely *mentions* its own marker mid-body has not
|
|
61
73
|
# finished, and grepping the whole file would call that done. This brief tells seats to end the file with
|
|
@@ -110,7 +122,7 @@ done
|
|
|
110
122
|
done_file() { # $1 = index into "$@", $2 = path
|
|
111
123
|
local i="$1" f="$2" sig
|
|
112
124
|
[ -s "$f" ] || return 1
|
|
113
|
-
tail -
|
|
125
|
+
tail -n 1 "$f" 2>/dev/null | grep -qF -- "$MARKER" || return 1
|
|
114
126
|
# CONTENT HASH, not size+mtime. The first version of this used `stat` size and mtime and the drill
|
|
115
127
|
# killed it on the incident's own shape: the correction that cost a ruling was `23 WEAK · 19 SOUND`
|
|
116
128
|
# -> `25 WEAK · 17 SOUND`, which is **byte-identical in length**, and mtime is whole seconds. A
|
|
@@ -124,12 +136,136 @@ done_file() { # $1 = index into "$@", $2 = path
|
|
|
124
136
|
return 1
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
# Queue row 240; D-2138 add.4 U04/U05/U08/U09, add.6 V01: both completion exits use
|
|
140
|
+
# this path. A bare address, title alone or old mission's stand-down never licenses a close.
|
|
141
|
+
close_completed() {
|
|
142
|
+
[ -n "$CLOSE_HOST" ] || return 0
|
|
143
|
+
node - "$PID_DIR" "$CLOSE_HOST" "$CLOSE_ADDRESSES" ${READY_INDEXES[@]+"${READY_INDEXES[@]}"} <<'NODE'
|
|
144
|
+
const fs = require("fs");
|
|
145
|
+
const path = require("path");
|
|
146
|
+
const { spawnSync } = require("child_process");
|
|
147
|
+
const [dir, host, addresses, ...ready] = process.argv.slice(2);
|
|
148
|
+
const canonical = p => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
149
|
+
const field = (o) => typeof o === "string" && o ? o : undefined;
|
|
150
|
+
const checkout = o => field(o.worktree) || field(o.worktreePath) || field(o.worktree_path)
|
|
151
|
+
|| field(o.worktree?.path) || (field(o.worktreeId)?.split("::").slice(1).join("::"))
|
|
152
|
+
|| field(o.path) || field(o.cwd);
|
|
153
|
+
const same = (a, b) => !!a && !!b && canonical(a) === canonical(b);
|
|
154
|
+
function record(file) {
|
|
155
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
156
|
+
const entries = raw.trim().split("\n").map(line => { const i = line.indexOf("="); return [line.slice(0, i), line.slice(i + 1)]; });
|
|
157
|
+
if (entries.some(([k]) => !k) || new Set(entries.map(([k]) => k)).size !== entries.length) throw Error("malformed spawn/stand-down record");
|
|
158
|
+
return { raw, values: Object.fromEntries(entries) };
|
|
159
|
+
}
|
|
160
|
+
function call(args, refusal = false) {
|
|
161
|
+
const r = spawnSync(host, args, { encoding: "utf8", timeout: 10000 });
|
|
162
|
+
if (r.error || r.signal) throw Error("host call unreadable or timed out");
|
|
163
|
+
let body; try { body = JSON.parse(r.stdout); } catch { throw Error("host response is not JSON"); }
|
|
164
|
+
if (refusal && r.status === 1 && body.ok === false && body.error?.code === "runtime_error" && body.error?.message === "tab_not_found") return body;
|
|
165
|
+
if (r.status !== 0 || body.ok === false || !body.result) throw Error("host call refused");
|
|
166
|
+
return body;
|
|
167
|
+
}
|
|
168
|
+
function orcaList(visual = true) {
|
|
169
|
+
const body = call(["terminal", "list", ...(visual ? ["--include-visual-layouts"] : []), "--limit", "10000", "--json"]);
|
|
170
|
+
if (body.ok !== true || !body._meta?.runtimeId || body._meta.runtimeId === "none" || body.result.truncated === true
|
|
171
|
+
|| !Array.isArray(body.result.terminals) || (visual && !Array.isArray(body.result.visualLayouts))) throw Error("unverifiable terminal list");
|
|
172
|
+
return body;
|
|
173
|
+
}
|
|
174
|
+
function tabs(node, out = []) {
|
|
175
|
+
if (Array.isArray(node)) node.forEach(n => tabs(n, out));
|
|
176
|
+
else if (node && typeof node === "object") {
|
|
177
|
+
if (Array.isArray(node.tabs)) out.push(...node.tabs);
|
|
178
|
+
tabs(node.first, out); tabs(node.second, out);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function leaves(node, out = []) {
|
|
183
|
+
if (Array.isArray(node)) node.forEach(n => leaves(n, out));
|
|
184
|
+
else if (node && typeof node === "object") {
|
|
185
|
+
if (node.type === "terminal" && field(node.handle)) out.push(node.handle);
|
|
186
|
+
else { leaves(node.panes, out); leaves(node.first, out); leaves(node.second, out); }
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function liveTabs(body) {
|
|
191
|
+
return body.result.visualLayouts.flatMap(layout => tabs(layout.root).map(tab => ({ tab, checkout: checkout(layout), handles: leaves(tab.panes) })));
|
|
192
|
+
}
|
|
193
|
+
function paneList() {
|
|
194
|
+
const result = call(["pane", "list"]).result;
|
|
195
|
+
if (!Array.isArray(result.panes)) throw Error("unverifiable pane list");
|
|
196
|
+
return result.panes;
|
|
197
|
+
}
|
|
198
|
+
for (const index of ready) {
|
|
199
|
+
let attempted = false;
|
|
200
|
+
const address = addresses.split(",")[Number(index)];
|
|
201
|
+
try {
|
|
202
|
+
if (address === process.env.ORCA_TERMINAL_HANDLE || address === process.env.HERDR_PANE_ID) throw Error("own launching terminal");
|
|
203
|
+
const matches = fs.readdirSync(dir).filter(n => n.endsWith(".handle")).map(n => ({ file: path.join(dir, n), seat: n.slice(0, -7), ...record(path.join(dir, n)) }))
|
|
204
|
+
.filter(r => r.values.address === address && r.values.host === host);
|
|
205
|
+
if (matches.length !== 1) throw Error("no unique spawn ledger record names address");
|
|
206
|
+
const r = matches[0], v = r.values;
|
|
207
|
+
if (!v.title || !v.checkout || !v.mission) throw Error("incomplete spawn ledger");
|
|
208
|
+
if (r.seat === process.env.TKR_ARMING_SEAT) throw Error("own arming seat");
|
|
209
|
+
if (v.next_mission || v["next-mission"]) throw Error("named next mission keeps seat open until clear and reassignment");
|
|
210
|
+
const stood = path.join(dir, `${r.seat}.stood-down`);
|
|
211
|
+
const stoodDown = () => {
|
|
212
|
+
if (record(r.file).raw !== r.raw) throw Error("spawn ledger changed during verification");
|
|
213
|
+
if (record(stood).values.mission !== v.mission) throw Error("stand-down does not name current mission");
|
|
214
|
+
};
|
|
215
|
+
stoodDown();
|
|
216
|
+
if (host === "orca") {
|
|
217
|
+
const before = orcaList();
|
|
218
|
+
const rows = before.result.terminals.filter(t => t.handle === address);
|
|
219
|
+
const allTabs = liveTabs(before);
|
|
220
|
+
const owned = allTabs.filter(t => t.tab.title === v.title && same(t.checkout, v.checkout) && t.handles.includes(address));
|
|
221
|
+
if (rows.length !== 1 || !same(checkout(rows[0]), v.checkout) || owned.length !== 1) {
|
|
222
|
+
// Read-only re-resolution by title. Never substitute this candidate into a close command.
|
|
223
|
+
const candidates = allTabs.filter(t => t.tab.title === v.title && same(t.checkout, v.checkout)).flatMap(t => t.handles);
|
|
224
|
+
for (const handle of candidates) call(["terminal", "read", "--terminal", handle, "--screen", "--json"]);
|
|
225
|
+
throw Error("live tab title and checkout mismatch or stale handle; read-only re-resolution by title");
|
|
226
|
+
}
|
|
227
|
+
const tab = owned[0], tabId = tab.tab.id ?? tab.tab.tabId ?? rows[0].tabId;
|
|
228
|
+
if (!tabId) throw Error("owned tab id is unverifiable");
|
|
229
|
+
const finalTerminal = tab.handles.length === 1;
|
|
230
|
+
stoodDown();
|
|
231
|
+
attempted = true;
|
|
232
|
+
const receipt = call(["terminal", "close", "--terminal", address, "--json"], true);
|
|
233
|
+
if (receipt._meta?.runtimeId !== before._meta.runtimeId) throw Error("runtime changed at close");
|
|
234
|
+
if (receipt.ok !== false && (receipt.ok !== true || receipt.result?.close?.handle !== address)) throw Error("close receipt does not name recorded handle");
|
|
235
|
+
const after = orcaList(false), layoutAfter = orcaList();
|
|
236
|
+
if (after._meta.runtimeId !== before._meta.runtimeId || layoutAfter._meta.runtimeId !== before._meta.runtimeId
|
|
237
|
+
|| after.result.terminals.some(t => t.handle === address)
|
|
238
|
+
|| (finalTerminal && liveTabs(layoutAfter).some(t => (t.tab.id ?? t.tab.tabId) === tabId || (t.tab.title === v.title && same(t.checkout, v.checkout))))
|
|
239
|
+
|| (!finalTerminal && tab.handles.filter(h => h !== address).some(h => !after.result.terminals.some(t => t.handle === h)))) throw Error("handle or final owned tab survives close, sibling disappeared, or runtime changed; spec question, never close by title");
|
|
240
|
+
console.log(` CLOSED ${address}: receipt ${receipt.ok === false ? "runtime_error: tab_not_found" : receipt.result.close.handle}; handle absent from terminal list; ${finalTerminal ? `owned tab ${tabId} absent from visual layout` : `shared tab ${tabId} kept for sibling terminals`}`);
|
|
241
|
+
} else {
|
|
242
|
+
const before = paneList(), rows = before.filter(p => p.pane_id === address);
|
|
243
|
+
if (rows.length !== 1 || rows[0].label !== v.title || !same(rows[0].cwd, v.checkout)) throw Error("live pane label and checkout mismatch");
|
|
244
|
+
stoodDown();
|
|
245
|
+
attempted = true;
|
|
246
|
+
call(["pane", "close", address]);
|
|
247
|
+
const after = paneList();
|
|
248
|
+
if (after.some(p => p.pane_id === address)) throw Error("pane survives close");
|
|
249
|
+
// A shared consultant tab belongs to several seats. Only a recorded sole owner reaps its tab.
|
|
250
|
+
if (v.tab && rows[0].tab_id === v.tab && !after.some(p => p.tab_id === v.tab)) {
|
|
251
|
+
call(["tab", "close", v.tab]);
|
|
252
|
+
const listed = call(["tab", "list"]).result.tabs;
|
|
253
|
+
if (!Array.isArray(listed) || listed.some(t => (t.tab_id ?? t.id) === v.tab)) throw Error("owned tab survives close");
|
|
254
|
+
}
|
|
255
|
+
console.log(` CLOSED ${address}: spawn record matched label and cwd; pane absent from pane list`);
|
|
256
|
+
}
|
|
257
|
+
} catch (error) { console.log(` ${attempted ? "CLOSE UNPROVEN" : "LEFT OPEN"} ${address}: ${error.code === "ENOENT" ? "missing spawn or stand-down record" : error.message}`); }
|
|
258
|
+
}
|
|
259
|
+
NODE
|
|
260
|
+
}
|
|
261
|
+
|
|
127
262
|
while :; do
|
|
128
263
|
pending=()
|
|
129
264
|
ready=()
|
|
265
|
+
READY_INDEXES=()
|
|
130
266
|
i=0
|
|
131
267
|
for f in "$@"; do
|
|
132
|
-
if done_file "$i" "$f"; then ready+=("$f"); else pending+=("$f"); fi
|
|
268
|
+
if done_file "$i" "$f"; then ready+=("$f"); READY_INDEXES+=("$i"); else pending+=("$f"); fi
|
|
133
269
|
i=$((i + 1))
|
|
134
270
|
done
|
|
135
271
|
|
|
@@ -152,6 +288,7 @@ while :; do
|
|
|
152
288
|
if [ -s "$f" ]; then echo " PARTIAL $(wc -c <"$f" | tr -d ' ') bytes, no marker yet $f last-line: $(tail -n 1 "$f" | tr -d '\0' | cut -c1-72)"
|
|
153
289
|
else echo " NOT STARTED $f <- check whether that seat is BLOCKED; a stalled seat writes nothing"; fi
|
|
154
290
|
done
|
|
291
|
+
close_completed
|
|
155
292
|
exit 0
|
|
156
293
|
fi
|
|
157
294
|
|
|
@@ -161,17 +298,7 @@ while :; do
|
|
|
161
298
|
echo " RE-HASH BEFORE YOU QUOTE IT. The marker means the file ENDS with '$MARKER', never that its"
|
|
162
299
|
echo " author has stopped: a rewrite-in-place keeps the marker terminal at every instant. If shasum"
|
|
163
300
|
echo " now differs from the value above, you are reading a superseded file."
|
|
164
|
-
|
|
165
|
-
# not. Closing here is safe precisely because the marker — not `done`, not a size — is the trigger,
|
|
166
|
-
# so this can never reap a seat mid-write. Only on the COMPLETE path: on a timeout the seats are
|
|
167
|
-
# still working and closing one would destroy the work being waited for.
|
|
168
|
-
if [ -n "${TKR_CLOSE_PANES:-}" ]; then
|
|
169
|
-
for pane in ${TKR_CLOSE_PANES//,/ }; do
|
|
170
|
-
# `herdr pane close` exits 0 even for a pane that is already gone, so report the body rather
|
|
171
|
-
# than the status — an exit code here would claim a close that may never have happened.
|
|
172
|
-
printf ' CLOSED %s -> %s\n' "$pane" "$(herdr pane close "$pane" 2>&1 | head -c 80)"
|
|
173
|
-
done
|
|
174
|
-
fi
|
|
301
|
+
close_completed
|
|
175
302
|
exit 0
|
|
176
303
|
fi
|
|
177
304
|
|