tickmarkr 2.6.7 → 2.6.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/dist/adapters/catalog-remote.js +5 -2
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.js +18 -10
- package/dist/adapters/model-lints.js +21 -11
- package/dist/adapters/model-windows.js +0 -1
- package/dist/adapters/registry.js +7 -3
- package/dist/adapters/types.d.ts +2 -0
- package/dist/cli/commands/beat.d.ts +3 -3
- package/dist/cli/commands/beat.js +39 -19
- package/dist/cli/commands/doctor.js +3 -0
- package/dist/cli/commands/fleet.js +2 -0
- package/dist/cli/commands/plan.js +16 -18
- package/dist/cli/commands/verify.js +4 -2
- package/dist/cli/help.js +5 -2
- package/dist/config/config.js +6 -5
- package/dist/drivers/herdr.js +2 -2
- package/dist/drivers/orca.d.ts +12 -4
- package/dist/drivers/orca.js +200 -39
- package/dist/gates/evidence.js +3 -3
- package/dist/gates/review.d.ts +1 -1
- package/dist/gates/review.js +62 -13
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +115 -20
- package/dist/gates/test-manifest.d.ts +36 -2
- package/dist/gates/test-manifest.js +55 -10
- package/dist/run/beat-lifecycle.d.ts +109 -24
- package/dist/run/beat-lifecycle.js +1173 -216
- package/dist/run/daemon.d.ts +4 -0
- package/dist/run/daemon.js +111 -33
- package/dist/run/git-trust.d.ts +78 -3
- package/dist/run/git-trust.js +175 -19
- package/dist/run/git.d.ts +24 -3
- package/dist/run/git.js +140 -21
- package/dist/run/merge.js +2 -2
- package/dist/run/verification-job.d.ts +2 -0
- package/dist/run/verification-job.js +32 -10
- package/dist/tui/ink/fleet-app.d.ts +7 -2
- package/dist/tui/ink/fleet-app.js +81 -23
- package/package.json +4 -1
- package/skills/tickmarkr-auto/SKILL.md +58 -14
- package/skills/tickmarkr-loop/SKILL.md +65 -14
- package/skills/tickmarkr-overseer/SKILL.md +97 -28
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +9 -7
|
@@ -44,12 +44,24 @@ function ledgerChannelId(line) {
|
|
|
44
44
|
const slash = head.indexOf("/");
|
|
45
45
|
return slash === -1 ? undefined : `${head.slice(0, slash)}:${head.slice(slash + 1)}`;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Space picks exactly as Enter does in every single-select overlay (operator field report 2026-10-08,
|
|
49
|
+
* "can't choose here space key doesn't select" — the reach picker even OPENS on Space). Space keeps its
|
|
50
|
+
* own meaning only where it has one: the multi-select chains (candidates, prefer) toggle on it, the text
|
|
51
|
+
* stages (add-model, the classify note) type it, and the review diff confirms only on `y`. The lift
|
|
52
|
+
* listing is a confirm, not a pick: it clears a deny entry for every channel that entry covers, so only
|
|
53
|
+
* Enter lifts — a stray Space there does nothing.
|
|
54
|
+
*/
|
|
55
|
+
const spacePicks = (overlay) => overlay.kind === "classify"
|
|
56
|
+
? overlay.stage !== "note"
|
|
57
|
+
: overlay.kind === "presets" || overlay.kind === "assign" || overlay.kind === "poolmode"
|
|
58
|
+
|| overlay.kind === "judge" || overlay.kind === "reach";
|
|
47
59
|
const REACH_CHOICES = [
|
|
48
60
|
{ id: "in", label: "in", gloss: "every seat may route here" },
|
|
49
61
|
{ id: "out-workers", label: "out · workers", gloss: "workers skip it; judge/review/consult unaffected" },
|
|
50
62
|
{ id: "out-all", label: "out · all seats", gloss: "no seat routes here" },
|
|
51
63
|
];
|
|
52
|
-
export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
|
|
64
|
+
export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts = {}, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, layerStamp, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], viewRows = Number.POSITIVE_INFINITY, frameColumns = 100, frameRows, ...initialDenyProps }) {
|
|
53
65
|
const { exit } = useApp();
|
|
54
66
|
const [, setRevision] = useState(0);
|
|
55
67
|
// the props are the fleet command's contract, one per schema-enumerated scope (initialDeny…),
|
|
@@ -137,21 +149,51 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
137
149
|
// the STAGED policy (fleet loads the candidate bytes; see stagedRouting) and the recorded
|
|
138
150
|
// identity — never literal membership of the staged sets, which missed identity, bare-model and
|
|
139
151
|
// family entries and could not tell an allowlist exclusion from a deny.
|
|
140
|
-
|
|
152
|
+
// B-KEYS (D-1338): the staged CONTENT every row and the staged policy derive from — the deny sets,
|
|
153
|
+
// the allow complement, classifications, efforts and seeds, read raw off ui (the provenance a
|
|
154
|
+
// review folds is a pure function of them). Computed per call, never per render: a handler
|
|
155
|
+
// mutates ui and reads rows in the same tick, before bump.
|
|
156
|
+
const stagedKey = () => JSON.stringify([
|
|
157
|
+
DENY_SCOPES.map((scope) => [...ui[scope.key]].sort()),
|
|
158
|
+
[...ui.allowOut].sort(),
|
|
159
|
+
ui.classifications,
|
|
160
|
+
ui.efforts,
|
|
161
|
+
ui.seededAllowOut,
|
|
162
|
+
]);
|
|
163
|
+
// ponytail: both memos live in refs keyed on stagedKey — a render-local memo reset on every keypress,
|
|
164
|
+
// so each cursor move reloaded the policy and re-derived ~800 rows' reach (B-KEYS). A cursor move,
|
|
165
|
+
// a filter, showAll or an overlay never recompute a row; any staged edit changes the key and
|
|
166
|
+
// recomputes every group. Ceiling: one JSON.stringify of the staged sets per lookup (µs); a dirty
|
|
167
|
+
// counter bumped at every mutation site if a fleet ever outgrows it.
|
|
168
|
+
const routingMemo = useRef(null);
|
|
141
169
|
const stagedPolicy = () => {
|
|
170
|
+
const key = memoKey();
|
|
171
|
+
const memo = routingMemo.current;
|
|
172
|
+
if (memo?.key === key)
|
|
173
|
+
return memo.policy;
|
|
142
174
|
const deny = stagedDeny();
|
|
143
175
|
const stage = stagedMetadata();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
? stagedRouting(deny, stage)
|
|
150
|
-
: { ok: true, routing: { deny: denyBlockFrom(denyLists()) } },
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return routingMemo.policy;
|
|
176
|
+
const policy = stagedRouting
|
|
177
|
+
? stagedRouting(deny, stage)
|
|
178
|
+
: { ok: true, routing: { deny: denyBlockFrom(denyLists()) } };
|
|
179
|
+
routingMemo.current = { key, policy };
|
|
180
|
+
return policy;
|
|
154
181
|
};
|
|
182
|
+
const rowsMemo = useRef({ key: "", byAdapter: new Map() });
|
|
183
|
+
// B-KEYS r2 (D-1446): staged content alone does not identify a row — the rows also derive from the loaded
|
|
184
|
+
// overlay layers (stagedRouting and holderOf read both files) and the props they read. Once per render a
|
|
185
|
+
// generation moves when the layer bytes or one of those props changes, and both memos key on it beside the
|
|
186
|
+
// staged content: a layer written mid-session shows on the next frame, as on the base, while a cursor move
|
|
187
|
+
// over unchanged layers still derives nothing. Cost: one read of each overlay per frame (the B2 previewCfg key).
|
|
188
|
+
const layers = useRef({ stamp: "", inputs: [], gen: 0 });
|
|
189
|
+
{
|
|
190
|
+
const stamp = layerStamp?.() ?? "";
|
|
191
|
+
const inputs = [modelGroups, stagedRouting, holderOf];
|
|
192
|
+
if (stamp !== layers.current.stamp || inputs.some((input, at) => input !== layers.current.inputs[at])) {
|
|
193
|
+
layers.current = { stamp, inputs, gen: layers.current.gen + 1 };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const memoKey = () => `${layers.current.gen}\u0000${stagedKey()}`;
|
|
155
197
|
// OBS-1099 add.1: the browser's deny discovery ranges over the schema-enumerated scopes (the
|
|
156
198
|
// route collector names its four paths by hand, so a scope the schema gains would never render
|
|
157
199
|
// or lift here). A flat scope reaches every seat; a nested one only the seat its block names
|
|
@@ -321,7 +363,19 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
321
363
|
const found = members.map(coveringFor).filter((covering) => covering !== undefined);
|
|
322
364
|
return found.find((covering) => covering.shared) ?? found[0];
|
|
323
365
|
};
|
|
366
|
+
// B-KEYS: one derivation per group per staged state — the memo above hands every later pass
|
|
367
|
+
// (visibleCount per rail row, modelRows, the hidden and folded counts, a handler's lookup) the
|
|
368
|
+
// same rows; the returned array is read, never mutated, by every caller.
|
|
324
369
|
const groupRows = (group) => {
|
|
370
|
+
const key = memoKey();
|
|
371
|
+
if (rowsMemo.current.key !== key)
|
|
372
|
+
rowsMemo.current = { key, byAdapter: new Map() };
|
|
373
|
+
const memo = rowsMemo.current.byAdapter;
|
|
374
|
+
const rows = memo.get(group.adapter) ?? deriveGroupRows(group);
|
|
375
|
+
memo.set(group.adapter, rows);
|
|
376
|
+
return rows;
|
|
377
|
+
};
|
|
378
|
+
const deriveGroupRows = (group) => {
|
|
325
379
|
const rows = group.rows.map((row) => {
|
|
326
380
|
const members = membersOf(group.adapter, row);
|
|
327
381
|
const staged = ui.classifications.find((classification) => classification.adapter === group.adapter
|
|
@@ -949,6 +1003,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
949
1003
|
return true;
|
|
950
1004
|
};
|
|
951
1005
|
if (overlay) {
|
|
1006
|
+
const pick = key.return || (input === " " && spacePicks(overlay));
|
|
952
1007
|
if (overlay.kind === "presets") {
|
|
953
1008
|
const rowCount = modeOptions.length + (overlay.home ? 1 : 0);
|
|
954
1009
|
if (key.escape) {
|
|
@@ -973,7 +1028,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
973
1028
|
bump();
|
|
974
1029
|
return;
|
|
975
1030
|
}
|
|
976
|
-
if (
|
|
1031
|
+
if (pick) {
|
|
977
1032
|
if (overlay.home && overlay.at === modeOptions.length) {
|
|
978
1033
|
setOverlay(null); // custom → the browser IS the full editor
|
|
979
1034
|
return;
|
|
@@ -1023,8 +1078,10 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1023
1078
|
finish({ kind: "discard" });
|
|
1024
1079
|
return;
|
|
1025
1080
|
}
|
|
1081
|
+
// the review screen holds every staged edit by definition — `q` takes the same two presses
|
|
1082
|
+
// as every other screen, never one keystroke to lose the session (`n` is the explicit discard)
|
|
1026
1083
|
if (input === "q") {
|
|
1027
|
-
|
|
1084
|
+
guardedQuit(armed);
|
|
1028
1085
|
return;
|
|
1029
1086
|
}
|
|
1030
1087
|
if (input === "y") {
|
|
@@ -1055,7 +1112,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1055
1112
|
bump();
|
|
1056
1113
|
return;
|
|
1057
1114
|
}
|
|
1058
|
-
if (
|
|
1115
|
+
if (pick) {
|
|
1059
1116
|
const channel = CHANNELS[overlay.channelAt];
|
|
1060
1117
|
ui.channelByAdapter[overlay.adapter] = channel;
|
|
1061
1118
|
// OBS-508: a bulk stage waiting on the one first-touch channel answer resumes here.
|
|
@@ -1083,7 +1140,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1083
1140
|
bump();
|
|
1084
1141
|
return;
|
|
1085
1142
|
}
|
|
1086
|
-
if (
|
|
1143
|
+
if (pick) {
|
|
1087
1144
|
// OBS-508: keep the suggested band → the evidence note arrives pre-typed; override the
|
|
1088
1145
|
// band → the note starts empty (the suggestion argues for a DIFFERENT tier).
|
|
1089
1146
|
const chosen = TIERS[overlay.tierAt];
|
|
@@ -1166,7 +1223,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1166
1223
|
bump();
|
|
1167
1224
|
return;
|
|
1168
1225
|
}
|
|
1169
|
-
if (
|
|
1226
|
+
if (pick && rows[overlay.at]) {
|
|
1170
1227
|
const shape = rows[overlay.at].id;
|
|
1171
1228
|
ui.map = { ...ui.map, [shape]: { pin: { via: overlay.adapter, model: overlay.model } } };
|
|
1172
1229
|
setOverlay(null);
|
|
@@ -1263,7 +1320,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1263
1320
|
bump();
|
|
1264
1321
|
return;
|
|
1265
1322
|
}
|
|
1266
|
-
if (
|
|
1323
|
+
if (pick) {
|
|
1267
1324
|
const { shape, chain } = overlay.picker;
|
|
1268
1325
|
// a pool is the shape's WHOLE declaration: pin and prefer leave with it (schema
|
|
1269
1326
|
// exclusivity — pin+pool and pool+prefer both fail config load)
|
|
@@ -1341,7 +1398,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1341
1398
|
bump();
|
|
1342
1399
|
return;
|
|
1343
1400
|
}
|
|
1344
|
-
if (
|
|
1401
|
+
if (pick) {
|
|
1345
1402
|
const choice = REACH_CHOICES[overlay.at].id;
|
|
1346
1403
|
setOverlay(null);
|
|
1347
1404
|
applyReach(overlay.target, choice);
|
|
@@ -1378,7 +1435,8 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
1378
1435
|
bump();
|
|
1379
1436
|
return;
|
|
1380
1437
|
}
|
|
1381
|
-
|
|
1438
|
+
// Space picks too (spacePicks): a judge picker that swallowed it read as "can't choose"
|
|
1439
|
+
if (pick && rows[overlay.at]) {
|
|
1382
1440
|
// single-select: one seat or back to the config default — never a chain
|
|
1383
1441
|
const picked = rows[overlay.at];
|
|
1384
1442
|
ui.judgeSeat = picked === judgeKeepRow ? null : picked;
|
|
@@ -2003,7 +2061,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
2003
2061
|
const rows = [judgeKeepRow, ...judgeSeats].filter((row) => matches(row, ui.filter));
|
|
2004
2062
|
const { visible, start, above, below } = windowRows(rows, overlay.at, capacity);
|
|
2005
2063
|
const selectedSeat = ui.judgeSeat ?? judgeKeepRow;
|
|
2006
|
-
return (_jsxs(OverlayPanel, { title: "pick \u00B7 judge", width: bodyW, children: [_jsx(Text, { dimColor: true, children: "one seat judges acceptance criteria
|
|
2064
|
+
return (_jsxs(OverlayPanel, { title: "pick \u00B7 judge", width: bodyW, children: [_jsx(Text, { dimColor: true, wrap: "truncate", children: clip("Space or Enter picks · Esc keeps · one seat judges acceptance criteria — failover stays runtime (GATE-09)", bodyW - 4) }), _jsx(SearchRow, { filter: ui.filter, active: true }), above > 0 && _jsx(ElisionMark, { count: above, side: "above" }), visible.map((label, index) => (_jsxs(Text, { children: [_jsx(Pointer, { on: start + index === overlay.at }), label === selectedSeat ? _jsx(Glyph, { kind: "on" }) : _jsx(Text, { children: " " }), _jsx(Text, { bold: start + index === overlay.at, children: ` ${clip(label, bodyW - 10)}` })] }, label))), below > 0 && _jsx(ElisionMark, { count: below, side: "below" })] }));
|
|
2007
2065
|
})();
|
|
2008
2066
|
const modelVisibilityLine = () => {
|
|
2009
2067
|
const hidden = hiddenModelCount();
|
|
@@ -2180,7 +2238,7 @@ export function FleetApp({ ageMs, agents, initialAllowOut = [], initialEfforts =
|
|
|
2180
2238
|
? _jsx(Text, { color: INK.warn, children: ` ! ${clip(ui.notice, width - 4)}` })
|
|
2181
2239
|
: details.map((line) => _jsx(Text, { dimColor: true, children: ` ${clip(line, width - 2)}` }, line)), _jsx(KeyBar, { binds: keyBinds() })] }));
|
|
2182
2240
|
}
|
|
2183
|
-
export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
|
|
2241
|
+
export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowOut = [], initialEfforts, modelGroups, initialMode, modeOptions, initialMap, modePreview, shapeRows, candidatesForShape, preferOptionsForShape, initialSteering, steeringOptionsFor, reviewAdvisories = [], reviewOverlay, layerStamp, reloadGuard, stagedRouting, holderOf, entry = "probe", initialJudge = "", judgeSeats = [], initialInput = [], input, output, debug = false, ...initialDenyProps }) {
|
|
2184
2242
|
const agents = adapters.flatMap((adapter) => {
|
|
2185
2243
|
const state = health[adapter.id];
|
|
2186
2244
|
if (!state?.installed)
|
|
@@ -2204,7 +2262,7 @@ export async function runFleetInkEditor({ ageMs, adapters, health, initialAllowO
|
|
|
2204
2262
|
// rebuilt from output.columns/rows so a pane resize re-renders at the new size. Same root
|
|
2205
2263
|
// element type on every rerender ⇒ React keeps the component instance, so staged edits,
|
|
2206
2264
|
// cursor, and overlays all survive the resize.
|
|
2207
|
-
const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, reloadGuard: reloadGuard, stagedRouting: stagedRouting, holderOf: holderOf, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
|
|
2265
|
+
const editorElement = () => (_jsx(FleetApp, { ageMs: ageMs, agents: agents, ...initialDenyProps, initialAllowOut: initialAllowOut, initialEfforts: initialEfforts, modelGroups: declaredModelGroups, initialMode: initialMode, modeOptions: modeOptions, initialMap: initialMap, modePreview: modePreview, shapeRows: shapeRows, candidatesForShape: candidatesForShape, preferOptionsForShape: preferOptionsForShape, initialSteering: initialSteering, steeringOptionsFor: steeringOptionsFor, reviewAdvisories: reviewAdvisories, reviewOverlay: reviewOverlay, layerStamp: layerStamp, reloadGuard: reloadGuard, stagedRouting: stagedRouting, holderOf: holderOf, entry: entry, initialJudge: initialJudge, judgeSeats: judgeSeats,
|
|
2208
2266
|
// v1.90.9: cap every list to the terminal minus chrome so the cursor and header can
|
|
2209
2267
|
// never scroll off-screen (omp's 218-model list did exactly that pre-cap).
|
|
2210
2268
|
// OBS-523: chrome is ≤12 rows worst case (header, borders, padding, scope+search,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickmarkr",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.9",
|
|
4
4
|
"description": "Spec in, verified work out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -114,6 +114,9 @@
|
|
|
114
114
|
"scripts/export-public.sh",
|
|
115
115
|
"scripts/verify-export.sh",
|
|
116
116
|
"tests/scripts/verify-export.test.ts",
|
|
117
|
+
"scripts/check-analysis.mjs",
|
|
118
|
+
"scripts/atlas.mjs",
|
|
119
|
+
"tests/docs/analysis-library.test.ts",
|
|
117
120
|
".github/workflows/ci.yml",
|
|
118
121
|
"**/*.local.*"
|
|
119
122
|
]
|
|
@@ -12,8 +12,9 @@ Use the canonical loop skill's [cockpit, parked decisions and printed twins](../
|
|
|
12
12
|
for each target: delivered views are **1 Home, 4 Run, 5 Evidence**; Fleet/Bootstrap and
|
|
13
13
|
Plan/Health remain follow-ons with CLI entries. The recorded 1/3 merged, human T2, blocked
|
|
14
14
|
T3 case stays PARTIAL despite tip pass. Review the Run confirmation, read back the appended
|
|
15
|
-
`task-approved` receipt, and explicitly launch `tickmarkr resume <runId>` with the
|
|
16
|
-
approval records permission and never dispatches work itself.
|
|
15
|
+
`task-approved` receipt, and explicitly launch `tickmarkr resume <runId>` with the detached launch below when no live owner exists;
|
|
16
|
+
approval records permission and never dispatches work itself. A waive needs an `OOB: executed`
|
|
17
|
+
read at the task head (the walkthrough's record example); a static CLEAN never funds one. Non-TTY readers keep `status`,
|
|
17
18
|
`report` and default-watch line output, with `--watch --plain` as the TTY fallback.
|
|
18
19
|
Keep `name: tickmarkr-auto` and the installed links to canonical `skills/` intact.
|
|
19
20
|
|
|
@@ -92,26 +93,41 @@ After sending, **confirm delivery** by reading the target pane and verifying the
|
|
|
92
93
|
1. **Prepare** — confirm the target list. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
93
94
|
2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
|
|
94
95
|
3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
|
|
95
|
-
4. **Run** — launch `tickmarkr run` using the [
|
|
96
|
+
4. **Run** — launch `tickmarkr run` using the [detached launch](#host-owned-daemon-and-detached-beats), and start detached beats with their run-end stop items. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary's execution clauses for you — `EXECUTION COMPLETE` or `NOT GREEN`, never green, because a run-end record is history; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
|
|
96
97
|
5. **Verify and consolidate** — continue only after a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty, and CURRENT `tickmarkr status <runId>` reads its owed checks outstanding empty AND known (`outstanding 0`) — a run with a parked task is partial, not green. Empty execution buckets alone are not green either (D-660): every bucket empty and the tip passed, but an operator waived one review, leaves one accepted-risk review check owed — status reads `outstanding 1 (T7 review)` and `run`/`resume` still exit 0 on execution alone, so the run is execution complete, not green. It turns green only when a `tickmarkr verify --record <runId>` discharge moves CURRENT status to `outstanding 0`, including a discharge landing after run-end — the historical run-end record keeps the old count, so never read debt from it; `outstanding unknown` is never green. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
|
|
97
98
|
6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout; redirect explicitly beside the spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records.
|
|
98
99
|
7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
|
|
99
100
|
|
|
100
101
|
## Host-owned daemon and detached beats
|
|
101
102
|
|
|
102
|
-
Launch the daemon
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
Launch the daemon DETACHED, with no pane of its own: one `tickmarkr run` or `tickmarkr resume <runId>`
|
|
104
|
+
process in its own session, its stdout and stderr appended to `<state-dir>/daemon.log`, and the
|
|
105
|
+
ORCH's recorded address as its board anchor, so the daemon self-places its one board beside the
|
|
106
|
+
ORCH in the ORCH tab. Never launch it in a visible split, pane or tab, as an agent harness
|
|
107
|
+
background task, or tied to an agent session: the launching shell may end, and the daemon keeps
|
|
108
|
+
running and logging. The detach wrapper is `node`, which tickmarkr already requires — no private
|
|
109
|
+
script, no extra visible daemon pane and no CLI log flag:
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
```bash
|
|
112
|
+
# On Orca: <ORCH handle> is the ORCH terminal handle
|
|
113
|
+
cd <repo> && ORCA_TERMINAL_HANDLE="<ORCH handle>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr run >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
114
|
+
cd <repo> && ORCA_TERMINAL_HANDLE="<ORCH handle>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr resume <runId> >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
115
|
+
# On herdr: <ORCH pane id> is the ORCH pane id
|
|
116
|
+
cd <repo> && HERDR_PANE_ID="<ORCH pane id>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr run >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
117
|
+
cd <repo> && HERDR_PANE_ID="<ORCH pane id>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr resume <runId> >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: run the
|
|
121
|
+
`ORCA_TERMINAL_HANDLE` lines; `<ORCH handle>` is the ORCH's own `$ORCA_TERMINAL_HANDLE`.
|
|
122
|
+
- **On herdr (`HERDR_ENV=1`)**: run the `HERDR_PANE_ID` lines; `<ORCH pane id>` is the ORCH's own
|
|
123
|
+
`$HERDR_PANE_ID`.
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
`
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
After either launch, read this repository's lock pid from `<state-dir>/graph.lock` and confirm it:
|
|
126
|
+
`kill -0 <pid>` succeeds, `ps -o pgid= -p <pid>` prints that same pid (its own session group;
|
|
127
|
+
ppid 1 is never proof on its own), walk its ppid chain and verify no agent session is an ancestor,
|
|
128
|
+
and read `<state-dir>/daemon.log`. No live holder means the launch failed: read the log, fix the
|
|
129
|
+
cause and run the same line again — never a second daemon beside a live holder, and never a
|
|
130
|
+
visible split to watch it; the daemon-placed board is the live surface.
|
|
115
131
|
|
|
116
132
|
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
117
133
|
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
@@ -133,5 +149,33 @@ generation and identity still match, and reads back DISARMED. Never put beats in
|
|
|
133
149
|
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
134
150
|
on failure, park or handoff, and require its DISARMED read-back.
|
|
135
151
|
|
|
152
|
+
**Legacy beat migration.** `beat start` refuses a legacy (pre-lifecycle, unowned) arm nonzero and
|
|
153
|
+
prints its exit under the RECORDED seat; `beat stop` refuses it too, so a stop followed by a start
|
|
154
|
+
is a dead end, never a migration. When the refusal names `stop pid <N> first`, that pid is the old
|
|
155
|
+
`--loop` writer: stop exactly that pid yourself (`kill <N>`, then confirm `kill -0 <N>` fails) —
|
|
156
|
+
tickmarkr never signals a process it did not launch. Then run the printed step under the seat it
|
|
157
|
+
names, start again and read back ARMED:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
cd <repo> && tickmarkr beat <tier> --seat <recorded seat> --stand-down
|
|
161
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
162
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Crash recovery.** A crashed owner, a writer killed inside its claimed tick or a killed recovery
|
|
166
|
+
taker leaves its claim, removal lock or stage directory behind. `beat status` only observes: it
|
|
167
|
+
exits nonzero, names the dead pids, prints the command that recovers them and changes no file —
|
|
168
|
+
status never recovers. Run the command it prints; the next ordinary `stop` or `start` recovers what
|
|
169
|
+
was left, with a notice naming each dead pid, and the start reads back ARMED:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
173
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
174
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Never delete a claim, lock or stage file by hand. A live or unprovable holder refuses BUSY naming
|
|
178
|
+
it: wait for that holder to finish or exit, then run the same command.
|
|
179
|
+
|
|
136
180
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
137
181
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|
|
@@ -88,25 +88,40 @@ When spawning consultants (agents gathering synthesis input for decisions like S
|
|
|
88
88
|
1. **Prepare** — start from the requested spec. Run the [binary preflight](#binary-preflight-before-compile-or-run). Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
89
89
|
2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
|
|
90
90
|
3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
|
|
91
|
-
4. **Run** — launch `tickmarkr run` using the [
|
|
91
|
+
4. **Run** — launch `tickmarkr run` using the [detached launch](#host-owned-daemon-and-detached-beats), and start detached beats with their run-end stop items. A watch ending the seat's turn is no watch: keep a **blocking journal consumer** alive for the run's terminal events — the shipped watcher below, or a foreground `until grep` on the run's terminal events — and ensure it is re-armed at most every twenty minutes. Never rely on a `Monitor`-only wake. Watch the run journal rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary's execution clauses for you — `EXECUTION COMPLETE` or `NOT GREEN`, never green, because a run-end record is history; the debt clause is yours — read CURRENT `tickmarkr status <runId>` (step 5). Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
|
|
92
92
|
5. **Verify and consolidate** — accept only a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty, and CURRENT `tickmarkr status <runId>` reads its owed checks outstanding empty AND known (`outstanding 0`) — a run with a parked task is partial, not green. Empty execution buckets alone are not green either (D-660): every bucket empty and the tip passed, but an operator waived one review, leaves one accepted-risk review check owed — status reads `outstanding 1 (T7 review)` and `run`/`resume` still exit 0 on execution alone, so the run is execution complete, not green. It turns green only when a `tickmarkr verify --record <runId>` discharge moves CURRENT status to `outstanding 0`, including a discharge landing after run-end — the historical run-end record keeps the old count, so never read debt from it; `outstanding unknown` is never green. Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
|
|
93
93
|
6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout. Redirect it explicitly beside the source spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records. Then [stand down](#stand-down-mission-end-and-retirement).
|
|
94
94
|
|
|
95
95
|
## Host-owned daemon and detached beats
|
|
96
96
|
|
|
97
|
-
Launch the daemon
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
Launch the daemon DETACHED, with no pane of its own: one `tickmarkr run` or `tickmarkr resume <runId>`
|
|
98
|
+
process in its own session, its stdout and stderr appended to `<state-dir>/daemon.log`, and the
|
|
99
|
+
ORCH's recorded address as its board anchor, so the daemon self-places its one board beside the
|
|
100
|
+
ORCH in the ORCH tab. Never launch it in a visible split, pane or tab, as an agent harness
|
|
101
|
+
background task, or tied to an agent session: the launching shell may end, and the daemon keeps
|
|
102
|
+
running and logging. The detach wrapper is `node`, which tickmarkr already requires — no private
|
|
103
|
+
script, no extra visible daemon pane and no CLI log flag:
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
```bash
|
|
106
|
+
# On Orca: <ORCH handle> is the ORCH terminal handle
|
|
107
|
+
cd <repo> && ORCA_TERMINAL_HANDLE="<ORCH handle>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr run >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
108
|
+
cd <repo> && ORCA_TERMINAL_HANDLE="<ORCH handle>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr resume <runId> >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
109
|
+
# On herdr: <ORCH pane id> is the ORCH pane id
|
|
110
|
+
cd <repo> && HERDR_PANE_ID="<ORCH pane id>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr run >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
111
|
+
cd <repo> && HERDR_PANE_ID="<ORCH pane id>" node -e "require('child_process').spawn(process.argv[1], process.argv.slice(2), { detached: true, stdio: 'inherit' }).unref()" tickmarkr resume <runId> >> <state-dir>/daemon.log 2>&1 < /dev/null
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: run the
|
|
115
|
+
`ORCA_TERMINAL_HANDLE` lines; `<ORCH handle>` is the ORCH's own `$ORCA_TERMINAL_HANDLE`.
|
|
116
|
+
- **On herdr (`HERDR_ENV=1`)**: run the `HERDR_PANE_ID` lines; `<ORCH pane id>` is the ORCH's own
|
|
117
|
+
`$HERDR_PANE_ID`.
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
After either launch, read this repository's lock pid from `<state-dir>/graph.lock` and confirm it:
|
|
120
|
+
`kill -0 <pid>` succeeds, `ps -o pgid= -p <pid>` prints that same pid (its own session group;
|
|
121
|
+
ppid 1 is never proof on its own), walk its ppid chain and verify no agent session is an ancestor,
|
|
122
|
+
and read `<state-dir>/daemon.log`. No live holder means the launch failed: read the log, fix the
|
|
123
|
+
cause and run the same line again — never a second daemon beside a live holder, and never a
|
|
124
|
+
visible split to watch it; the daemon-placed board is the live surface.
|
|
110
125
|
|
|
111
126
|
Detached beats are product-owned, one per (tier, seat), from the repository root, through the
|
|
112
127
|
shipped lifecycle verbs — never a hand-rolled setsid plus nohup wrapper, pidfile or shell loop:
|
|
@@ -128,6 +143,34 @@ generation and identity still match, and reads back DISARMED. Never put beats in
|
|
|
128
143
|
background task or a visible tab. Include a run-end `beat stop` item for every beat started, also
|
|
129
144
|
on failure, park or handoff, and require its DISARMED read-back.
|
|
130
145
|
|
|
146
|
+
**Legacy beat migration.** `beat start` refuses a legacy (pre-lifecycle, unowned) arm nonzero and
|
|
147
|
+
prints its exit under the RECORDED seat; `beat stop` refuses it too, so a stop followed by a start
|
|
148
|
+
is a dead end, never a migration. When the refusal names `stop pid <N> first`, that pid is the old
|
|
149
|
+
`--loop` writer: stop exactly that pid yourself (`kill <N>`, then confirm `kill -0 <N>` fails) —
|
|
150
|
+
tickmarkr never signals a process it did not launch. Then run the printed step under the seat it
|
|
151
|
+
names, start again and read back ARMED:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd <repo> && tickmarkr beat <tier> --seat <recorded seat> --stand-down
|
|
155
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
156
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Crash recovery.** A crashed owner, a writer killed inside its claimed tick or a killed recovery
|
|
160
|
+
taker leaves its claim, removal lock or stage directory behind. `beat status` only observes: it
|
|
161
|
+
exits nonzero, names the dead pids, prints the command that recovers them and changes no file —
|
|
162
|
+
status never recovers. Run the command it prints; the next ordinary `stop` or `start` recovers what
|
|
163
|
+
was left, with a notice naming each dead pid, and the start reads back ARMED:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd <repo> && tickmarkr beat status <tier> --seat <seat>
|
|
167
|
+
cd <repo> && tickmarkr beat stop <tier> --seat <seat>
|
|
168
|
+
cd <repo> && tickmarkr beat start <tier> --seat <seat>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Never delete a claim, lock or stage file by hand. A live or unprovable holder refuses BUSY naming
|
|
172
|
+
it: wait for that holder to finish or exit, then run the same command.
|
|
173
|
+
|
|
131
174
|
Keep only watchers that must WAKE the seat in the harness. Re-arm them on each wake and at
|
|
132
175
|
their harness cap; daemon and beat lifetimes must not depend on that cap.
|
|
133
176
|
|
|
@@ -146,7 +189,7 @@ run/task, original park `#L`, actor/reason, exact argv, consequence and enactor.
|
|
|
146
189
|
confirms; `n`/Esc cancel; Enter never confirms. Read the receipt's appended `task-approved`
|
|
147
190
|
line and actor/reason back from the journal. Approval records permission, never dispatch,
|
|
148
191
|
a passed gate or task completion. With no live owner it says **approved; resume required**:
|
|
149
|
-
exit the observer and launch `tickmarkr resume <runId>` explicitly with the same
|
|
192
|
+
exit the observer and launch `tickmarkr resume <runId>` explicitly with the same detached launch above. A matching live daemon
|
|
150
193
|
enacts at its next task boundary; if a different live run owns the repository lock, wait
|
|
151
194
|
for that run to end before resuming this one.
|
|
152
195
|
|
|
@@ -174,7 +217,15 @@ Run offers only validated park verbs: human/attempt-cap/other non-gate parks all
|
|
|
174
217
|
infra allows approve or `--recheck`; review gate-fail allows `--waive`, `--uphold` or
|
|
175
218
|
`--recheck`; other gate-fail allows waive/recheck. Waive satisfies only the identified
|
|
176
219
|
failed gate, uphold funds a fixed attempt carrying review findings, and recheck reruns the
|
|
177
|
-
declared battery without satisfying a gate. A
|
|
220
|
+
declared battery without satisfying a gate. A waive needs EXECUTED evidence: mark every
|
|
221
|
+
out-of-band (OOB) read behind it `OOB: static` (diff and source read, nothing run) or
|
|
222
|
+
`OOB: executed` (the finding's claim run at the task's head commit, exit code and log on file).
|
|
223
|
+
Static CLEAN never backs a waive — a static read that missed an executed material defect looks
|
|
224
|
+
like one that found none, and a static read relabelled executed is a false record. When the
|
|
225
|
+
finding's reproduction, executed at the task head, fails, the ruling is uphold or recheck; waive
|
|
226
|
+
only when it passes and the record names it, keeping the park/gate binding and the owed check
|
|
227
|
+
(`outstanding 1 (T2 review)` until `tickmarkr verify --record <runId>`):
|
|
228
|
+
`D-NNN WAIVE T2 review — park <line>@<ts> --gate review — OOB: executed — <reproduction> at <task-head-sha>: exit 0, log <path>`. A stall park that recorded a `reapFailure`
|
|
178
229
|
(unreadable or surviving worker census) allows approve or `--recheck --park <line>@<ts>`:
|
|
179
230
|
recheck re-verifies that attempt's owned census and gates its harvested commits with no
|
|
180
231
|
worker only with an explicitly recorded empty survivors array (`[]`) — a missing,
|