tickmarkr 2.6.0 → 2.6.2
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 +14 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +3 -1
- package/dist/adapters/prompt.js +21 -3
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +13 -4
- package/dist/adapters/types.js +16 -0
- package/dist/cli/commands/approve.d.ts +5 -1
- package/dist/cli/commands/approve.js +66 -23
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +71 -8
- package/dist/cli/commands/plan.js +7 -3
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +37 -22
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.js +11 -2
- package/dist/config/config.d.ts +20 -0
- package/dist/config/config.js +47 -8
- package/dist/config/fleet-overlay.d.ts +1 -0
- package/dist/config/fleet-overlay.js +56 -0
- package/dist/drivers/orca.d.ts +26 -1
- package/dist/drivers/orca.js +199 -61
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +2 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +12 -1
- package/dist/gates/baseline.js +11 -4
- package/dist/gates/cache.d.ts +3 -1
- package/dist/gates/cache.js +10 -3
- package/dist/gates/llm.d.ts +5 -4
- package/dist/gates/llm.js +17 -14
- package/dist/gates/review.d.ts +8 -0
- package/dist/gates/review.js +47 -15
- package/dist/gates/run-gates.d.ts +6 -1
- package/dist/gates/run-gates.js +34 -17
- package/dist/gates/test-manifest.d.ts +14 -0
- package/dist/gates/test-manifest.js +33 -6
- package/dist/graph/graph.d.ts +6 -2
- package/dist/graph/graph.js +15 -4
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/role-pick.d.ts +16 -0
- package/dist/route/role-pick.js +15 -0
- package/dist/route/router.d.ts +14 -0
- package/dist/route/router.js +39 -16
- package/dist/run/consult.d.ts +13 -1
- package/dist/run/consult.js +19 -14
- package/dist/run/daemon.d.ts +46 -2
- package/dist/run/daemon.js +964 -190
- package/dist/run/git.d.ts +44 -1
- package/dist/run/git.js +103 -5
- package/dist/run/host-health.d.ts +20 -0
- package/dist/run/host-health.js +64 -0
- package/dist/run/journal.d.ts +126 -3
- package/dist/run/journal.js +418 -36
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-state.d.ts +24 -2
- package/dist/run/operator-state.js +41 -5
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +31 -1
- package/dist/run/protocol.js +3 -1
- package/dist/run/stall.d.ts +38 -2
- package/dist/run/stall.js +276 -6
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/tui/cockpit/board.d.ts +1 -1
- package/dist/tui/cockpit/board.js +30 -22
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.d.ts +2 -0
- package/dist/tui/cockpit/derive.js +17 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/live-store.d.ts +1 -0
- package/dist/tui/cockpit/live-store.js +31 -8
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +28 -2
- package/dist/tui/cockpit/run-view.d.ts +11 -7
- package/dist/tui/cockpit/run-view.js +69 -15
- package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
- package/dist/tui/cockpit/setup-cockpit.js +6 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +2 -1
- package/schema/config.schema.json +818 -0
- package/skills/tickmarkr-loop/SKILL.md +8 -2
- package/skills/tickmarkr-overseer/SKILL.md +85 -6
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
|
|
2
|
+
import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, parkToken, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
|
|
3
3
|
import { stateDirName } from "../../graph/graph.js";
|
|
4
|
-
import { Journal } from "../../run/journal.js";
|
|
4
|
+
import { bindingToken, effectiveDecisions, Journal, physicalLine, recordedBinding } from "../../run/journal.js";
|
|
5
5
|
const parkDiagnostic = (park) => {
|
|
6
6
|
if (park.kind === "scope-request")
|
|
7
7
|
return park.approveCommand ?? "scope-request requires tickmarkr approve with --files <glob,…>";
|
|
@@ -50,6 +50,8 @@ export function decisionArgv(command, { runId, by }) {
|
|
|
50
50
|
const flag = { approve: [], waive: ["--waive"], uphold: ["--uphold"], recheck: ["--recheck"] }[command.verb];
|
|
51
51
|
return [
|
|
52
52
|
runId, command.taskId, ...flag,
|
|
53
|
+
...(command.park === undefined ? [] : ["--park", command.park]),
|
|
54
|
+
...(command.gate === undefined ? [] : ["--gate", command.gate]),
|
|
53
55
|
...(command.reviewRounds === undefined ? [] : ["--review-rounds", String(command.reviewRounds)]),
|
|
54
56
|
"--by", by,
|
|
55
57
|
...(command.reason === undefined ? [] : ["--reason", command.reason]),
|
|
@@ -62,10 +64,9 @@ export const journalFileFor = (cwd, runId) => join(stateDirName(cwd), "runs", ru
|
|
|
62
64
|
* the named verb is outside the park's table, so a confirm inset can never promise a decision the
|
|
63
65
|
* command would refuse. The enactor is read from the run lock now, and read again at the receipt.
|
|
64
66
|
*/
|
|
65
|
-
export function previewDecision(
|
|
66
|
-
let argv;
|
|
67
|
+
export function previewDecision(named, { cwd, runId, by }) {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
decisionArgv(named, { runId, by });
|
|
69
70
|
}
|
|
70
71
|
catch (e) {
|
|
71
72
|
return { ok: false, refusal: e.message };
|
|
@@ -78,15 +79,29 @@ export function previewDecision(command, { cwd, runId, by }) {
|
|
|
78
79
|
catch (e) {
|
|
79
80
|
return { ok: false, refusal: e.message };
|
|
80
81
|
}
|
|
81
|
-
const park = newestPark(events,
|
|
82
|
+
const park = newestPark(events, named.taskId, sourceIndexes);
|
|
82
83
|
if (!park)
|
|
83
|
-
return { ok: false, refusal: `task ${
|
|
84
|
+
return { ok: false, refusal: `task ${named.taskId} has no park in run ${runId} — nothing to decide` };
|
|
85
|
+
// OBS-1178: the argv binds the park this preview displays; approve refuses it once another opens.
|
|
86
|
+
const token = parkToken(park);
|
|
87
|
+
if (named.park !== undefined && named.park !== token)
|
|
88
|
+
return { ok: false, refusal: `task ${named.taskId}'s open park is ${token ?? "untimestamped"}, not ${named.park} — refresh and decide the open park` };
|
|
89
|
+
if (named.gate !== undefined && named.gate !== park.failedGate)
|
|
90
|
+
return { ok: false, refusal: `refusing mismatched gate for ${named.taskId}: --gate ${named.gate} but park ${token} failed ${park.failedGate ?? "no gate"} — nothing appended` };
|
|
91
|
+
const command = { ...named, ...(token === undefined ? {} : { park: token }), ...(named.verb === "waive" && named.gate === undefined && park.failedGate !== undefined ? { gate: park.failedGate } : {}) };
|
|
92
|
+
let argv;
|
|
93
|
+
try {
|
|
94
|
+
argv = decisionArgv(command, { runId, by });
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return { ok: false, refusal: e.message };
|
|
98
|
+
}
|
|
84
99
|
const verbs = permittedDecisionVerbs(park);
|
|
85
100
|
if (!verbs.includes(command.verb)) {
|
|
86
101
|
const why = parkDiagnostic(park) ?? `permitted: ${verbs.join(", ")}`;
|
|
87
102
|
return { ok: false, refusal: `${command.verb} is not a decision for ${command.taskId}'s ${park.kind ?? "unknown"} park (${why})` };
|
|
88
103
|
}
|
|
89
|
-
const released = events
|
|
104
|
+
const released = releaseSince(events, park.index, command.taskId);
|
|
90
105
|
if (released) {
|
|
91
106
|
return { ok: false, refusal: `task ${command.taskId} was already released at #L${sourceIndexes[events.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} — nothing to confirm` };
|
|
92
107
|
}
|
|
@@ -100,28 +115,12 @@ export function previewDecision(command, { cwd, runId, by }) {
|
|
|
100
115
|
};
|
|
101
116
|
}
|
|
102
117
|
const parkLine = (park) => `#L${park.line} ${park.kind ?? "unknown kind"}${park.failedGate ? ` · failed gate ${park.failedGate}` : ""}${park.reason ? ` · ${park.reason}` : ""}`;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (verb === "waive")
|
|
108
|
-
lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
|
|
109
|
-
if (verb === "recheck")
|
|
110
|
-
lines.push(" marks no gate satisfied; the whole declared battery re-runs");
|
|
111
|
-
if (verb === "uphold")
|
|
112
|
-
lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
|
|
113
|
-
lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
|
|
114
|
-
return lines;
|
|
115
|
-
}
|
|
116
|
-
/** Who enacts it — the three lock states, each stated by the command's own sentence. */
|
|
117
|
-
export function enactmentLines(run, disposition) {
|
|
118
|
-
const owner = run.live
|
|
119
|
-
? "enactment matching live daemon — pending daemon enactment at its next task boundary"
|
|
120
|
-
: run.blockingRunId
|
|
121
|
-
? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
|
|
122
|
-
: `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
|
|
123
|
-
return [owner, ` ${approvalEnactment(disposition, run)}`];
|
|
118
|
+
/** The decision already open on this park, if any. OBS-1178: a refused or unsound row released nothing. */
|
|
119
|
+
function releaseSince(events, parkIndex, taskId) {
|
|
120
|
+
const effective = effectiveDecisions(events);
|
|
121
|
+
return events.find((e, i) => i > parkIndex && e.event === "task-approved" && e.taskId === taskId && effective.has(physicalLine(events, i)));
|
|
124
122
|
}
|
|
123
|
+
const quoteArg = (arg) => (/^[\w.:/@=-]+$/u.test(arg) ? arg : JSON.stringify(arg));
|
|
125
124
|
/** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
|
|
126
125
|
export function decisionConfirmLines(preview) {
|
|
127
126
|
return [
|
|
@@ -157,7 +156,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
|
|
|
157
156
|
if (!park || park.index !== preview.park.index || park.line !== preview.park.line || park.kind !== preview.park.kind) {
|
|
158
157
|
return { ok: false, preview, stale: true, refusal: `task ${named.taskId}'s park changed since preview (${park ? `now #L${park.line} ${park.kind ?? "unknown"}` : "no park now"}) — refresh and preview again` };
|
|
159
158
|
}
|
|
160
|
-
const released = before
|
|
159
|
+
const released = releaseSince(before, park.index, named.taskId);
|
|
161
160
|
if (released) {
|
|
162
161
|
return { ok: false, preview, stale: true, refusal: `task ${named.taskId} was released at #L${beforeSourceIndexes[before.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} after this preview — no second decision appended; refresh` };
|
|
163
162
|
}
|
|
@@ -208,6 +207,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
|
|
|
208
207
|
via: "cli",
|
|
209
208
|
gate: named.verb === "waive" ? park.failedGate : named.verb === "uphold" ? "review" : undefined,
|
|
210
209
|
reviewRoundCeiling: named.reviewRounds,
|
|
210
|
+
park: parkToken(park),
|
|
211
211
|
};
|
|
212
212
|
const actual = {
|
|
213
213
|
release,
|
|
@@ -216,6 +216,7 @@ export async function executeDecision(preview, { cwd, command = approve }) {
|
|
|
216
216
|
via: appended.event.data.via,
|
|
217
217
|
gate: appended.event.data.gate,
|
|
218
218
|
reviewRoundCeiling: appended.event.data.reviewRoundCeiling,
|
|
219
|
+
park: recordedBinding(appended.event.data.park) === undefined ? undefined : bindingToken(recordedBinding(appended.event.data.park)),
|
|
219
220
|
};
|
|
220
221
|
const mismatch = Object.keys(expected)
|
|
221
222
|
.find((field) => actual[field] !== expected[field]);
|
|
@@ -264,8 +265,7 @@ export const initialDecisionSession = () => ({ menu: null, confirming: null, rec
|
|
|
264
265
|
/**
|
|
265
266
|
* The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
|
|
266
267
|
* selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
|
|
267
|
-
* verb
|
|
268
|
-
* cancel the deepest layer. Enter on a confirm is inert by construction.
|
|
268
|
+
* verb opens the confirm inset; `y` names the write; `n`/Esc cancel the deepest layer. Enter on a confirm is inert.
|
|
269
269
|
*/
|
|
270
270
|
export function applyDecisionKey(session, event, selected) {
|
|
271
271
|
if (session.confirming !== null) {
|
|
@@ -315,3 +315,26 @@ export function decisionKeybar(session, selected) {
|
|
|
315
315
|
return session.menu.verbs.length === 0 ? "Esc Close" : "↑↓ Choose · Enter Preview · Esc Close";
|
|
316
316
|
return selected === undefined ? "" : "a Actions";
|
|
317
317
|
}
|
|
318
|
+
// Consequence and enactor lines, shared by the confirm inset and the receipt above. Kept below the key
|
|
319
|
+
// grammar so the operator walkthrough's line citations stay put.
|
|
320
|
+
/** What a confirmed write buys, from the command's own table — never a phrase of this file's own. */
|
|
321
|
+
function consequenceLines(verb, park, release, disposition) {
|
|
322
|
+
const lines = [`consequence disposition ${disposition}; appends one task-approved${release ? ` with release ${release}` : " with no release"}`];
|
|
323
|
+
if (verb === "waive")
|
|
324
|
+
lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
|
|
325
|
+
if (verb === "recheck")
|
|
326
|
+
lines.push(" marks no gate satisfied; the whole declared battery re-runs");
|
|
327
|
+
if (verb === "uphold")
|
|
328
|
+
lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
|
|
329
|
+
lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
|
|
330
|
+
return lines;
|
|
331
|
+
}
|
|
332
|
+
/** Who enacts it — the three lock states, each stated by the command's own sentence. */
|
|
333
|
+
export function enactmentLines(run, disposition) {
|
|
334
|
+
const owner = run.live
|
|
335
|
+
? "enactment matching live daemon — pending daemon enactment at its next task boundary"
|
|
336
|
+
: run.blockingRunId
|
|
337
|
+
? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
|
|
338
|
+
: `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
|
|
339
|
+
return [owner, ` ${approvalEnactment(disposition, run)}`];
|
|
340
|
+
}
|
|
@@ -115,6 +115,8 @@ export type TaskRow = {
|
|
|
115
115
|
readonly state?: TaskState;
|
|
116
116
|
readonly attempts?: number;
|
|
117
117
|
readonly actor?: string;
|
|
118
|
+
/** Distinct merged-author channels from task-done; legacy rows say ["unknown"]. Not the actor. */
|
|
119
|
+
readonly authors?: readonly string[];
|
|
118
120
|
readonly lastEventTime?: string;
|
|
119
121
|
/** Complete UTC instant paired with `lastEventTime`; additive for existing consumers. */
|
|
120
122
|
readonly lastEventTimestamp?: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Consolidated C1/C6 readers use the incremental lifecycle contract; legacy captures keep their API.
|
|
2
2
|
export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
|
|
3
|
+
import { doneAuthors } from "../../run/operator-state.js";
|
|
3
4
|
import { channelKey } from "../../adapters/types.js";
|
|
4
5
|
import { newestPark, permittedDecisionVerbs } from "../../cli/commands/approve.js";
|
|
5
6
|
import { parseOwnedName } from "../../drivers/types.js";
|
|
6
7
|
import { projectActivity } from "../../run/activity.js";
|
|
8
|
+
import { effectiveDecisions, withPhysicalLine } from "../../run/journal.js";
|
|
7
9
|
import { isPidLive } from "../../run/lock.js";
|
|
8
10
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
9
11
|
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
@@ -78,6 +80,15 @@ function parseSource(raw) {
|
|
|
78
80
|
}
|
|
79
81
|
return { events, defects };
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* OBS-1178: the rows the task-state folds read. A refused or unsound decision released nothing, so its
|
|
85
|
+
* task-approved row is dropped here; the one decision fold keys every row by its physical line. The
|
|
86
|
+
* journal rows the cockpit draws keep every line as evidence.
|
|
87
|
+
*/
|
|
88
|
+
function decidedEvents(events) {
|
|
89
|
+
const effective = effectiveDecisions(events.map((event) => withPhysicalLine(event, event.line)));
|
|
90
|
+
return events.filter((event) => event.event !== "task-approved" || effective.has(event.line));
|
|
91
|
+
}
|
|
81
92
|
function assignmentFrom(value) {
|
|
82
93
|
const assignment = recordValue(value);
|
|
83
94
|
return assignment
|
|
@@ -276,6 +287,8 @@ function deriveTasks(events, interrupted) {
|
|
|
276
287
|
if (state) {
|
|
277
288
|
const fact = task(event.taskId, index, event.ts);
|
|
278
289
|
fact.state = state;
|
|
290
|
+
if (event.event === "task-done")
|
|
291
|
+
fact.authors = doneAuthors(event.data);
|
|
279
292
|
// The park kind travels with the park: `task-human` records it, and the
|
|
280
293
|
// next recorded state — approved, done, failed — retires it with the park
|
|
281
294
|
// it named. A resolved state must not leave its badge behind (OBS-256).
|
|
@@ -641,6 +654,7 @@ function taskRows(events, tasks, graph, runId) {
|
|
|
641
654
|
...(state === undefined ? {} : { state }),
|
|
642
655
|
...(attempts === 0 ? {} : { attempts }),
|
|
643
656
|
...(assignment === undefined ? {} : { actor: channelKey(assignment) }),
|
|
657
|
+
...(fact?.authors === undefined ? {} : { authors: fact.authors }),
|
|
644
658
|
...(lastTs === undefined ? {} : { lastEventTime: eventTime(lastTs) }),
|
|
645
659
|
...(lastTs === undefined ? {} : { lastEventTimestamp: lastTs }),
|
|
646
660
|
...(title === undefined ? {} : { title }),
|
|
@@ -840,7 +854,8 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
840
854
|
// alive in the lock — the cockpit called a live run interrupted.
|
|
841
855
|
: pid !== undefined && (options.isDaemonAlive ?? isPidLive)(pid);
|
|
842
856
|
const interrupted = !alive;
|
|
843
|
-
const
|
|
857
|
+
const decided = decidedEvents(events);
|
|
858
|
+
const tasks = deriveTasks(decided, interrupted);
|
|
844
859
|
const taskFacts = [...tasks.values()];
|
|
845
860
|
const done = taskFacts.filter((task) => task.state === "done").length;
|
|
846
861
|
const failed = taskFacts.filter((task) => task.state === "failed").length;
|
|
@@ -869,7 +884,7 @@ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
|
869
884
|
// run-end — the run's own closing statement is enough.
|
|
870
885
|
const runStatus = failed > 0
|
|
871
886
|
? "failed"
|
|
872
|
-
: human > 0 || unresolvedSummaryParks(
|
|
887
|
+
: human > 0 || unresolvedSummaryParks(decided).length > 0
|
|
873
888
|
? "parked"
|
|
874
889
|
: lifecycle === "superseded" || hasInterruptedTask
|
|
875
890
|
? "interrupted"
|
|
@@ -46,5 +46,15 @@ export interface ConsolidatedOptions {
|
|
|
46
46
|
environment?: NodeJS.ProcessEnv;
|
|
47
47
|
diagnostics?: readonly HomeNeedsYouTarget[];
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
|
|
51
|
+
* decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
|
|
52
|
+
* fold shows now, since a later run-end restores a park but not the gate evidence a rejected recheck
|
|
53
|
+
* cleared — the operator snapshot is refolded over the decided rows, each keeping its physical line and
|
|
54
|
+
* the store's evidence identity.
|
|
55
|
+
* ponytail: a whole-journal read per observed journal change (parsed only when it holds an approval);
|
|
56
|
+
* move the decision fold into OperatorStateFold if that ever measures slow.
|
|
57
|
+
*/
|
|
58
|
+
export declare function decidedLiveStore(store: LiveStore): LiveStore;
|
|
49
59
|
/** The live entry mounts the dependency-owned leaves, using only C2 snapshots/history/page. */
|
|
50
60
|
export declare function runConsolidatedCockpit(options: ConsolidatedOptions): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { existsSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { renderMarkdownRecord } from "../../cli/commands/report.js";
|
|
5
5
|
import { loadConfig } from "../../config/config.js";
|
|
@@ -20,9 +20,10 @@ import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF }
|
|
|
20
20
|
import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPreview, withDecisionReceipt, decisionConfirmLines } from "./decision-actions.js";
|
|
21
21
|
import { approvalRunOwner } from "../../cli/commands/approve.js";
|
|
22
22
|
import { GLYPHS } from "../../brand.js";
|
|
23
|
-
import { Journal } from "../../run/journal.js";
|
|
23
|
+
import { effectiveEvents, Journal, parseJournalText, physicalLine } from "../../run/journal.js";
|
|
24
24
|
import { observeNamedRun, WATCH_OWNER_ENV } from "../../run/supervision.js";
|
|
25
25
|
import { formatOwnedName } from "../../drivers/types.js";
|
|
26
|
+
import { readOperatorState } from "../../run/operator-state.js";
|
|
26
27
|
import { cellWidth, wrapCells } from "./width.js";
|
|
27
28
|
import { resolveShellColourMode } from "./theme.js";
|
|
28
29
|
let timelineUsers = 0;
|
|
@@ -58,6 +59,52 @@ export function borrowRuntimeTimeline() {
|
|
|
58
59
|
}
|
|
59
60
|
export const SHELL_TERMINAL_ENTER = "\x1b[?1049h\x1b[22;0t\x1b]0;tickmarkr cockpit\x07" + POINTER_TRACKING_ON;
|
|
60
61
|
export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25h\x1b[?1049l";
|
|
62
|
+
/**
|
|
63
|
+
* OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
|
|
64
|
+
* decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
|
|
65
|
+
* fold shows now, since a later run-end restores a park but not the gate evidence a rejected recheck
|
|
66
|
+
* cleared — the operator snapshot is refolded over the decided rows, each keeping its physical line and
|
|
67
|
+
* the store's evidence identity.
|
|
68
|
+
* ponytail: a whole-journal read per observed journal change (parsed only when it holds an approval);
|
|
69
|
+
* move the decision fold into OperatorStateFold if that ever measures slow.
|
|
70
|
+
*/
|
|
71
|
+
export function decidedLiveStore(store) {
|
|
72
|
+
let key;
|
|
73
|
+
let operator;
|
|
74
|
+
const decided = (snap) => {
|
|
75
|
+
const { journal, graph } = snap;
|
|
76
|
+
const observed = `${journal.source}:${journal.generation}:${journal.offset}:${graph.identity}:${graph.status}`;
|
|
77
|
+
if (observed !== key) {
|
|
78
|
+
key = observed;
|
|
79
|
+
operator = undefined;
|
|
80
|
+
let bytes;
|
|
81
|
+
try {
|
|
82
|
+
bytes = readFileSync(journal.source).subarray(0, journal.offset);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return snap;
|
|
86
|
+
}
|
|
87
|
+
if (bytes.includes("task-approved")) {
|
|
88
|
+
const rows = parseJournalText(bytes.subarray(0, bytes.lastIndexOf(10) + 1).toString("utf8"))
|
|
89
|
+
.filter(e => e !== null && typeof e === "object" && typeof e.event === "string" && typeof e.data === "object" && e.data !== null);
|
|
90
|
+
const kept = effectiveEvents(rows);
|
|
91
|
+
if (kept.length !== rows.length) {
|
|
92
|
+
const events = kept.map((event, i) => {
|
|
93
|
+
const line = physicalLine(kept, i);
|
|
94
|
+
return { source: journal.source, line, id: `${journal.source}#L${line}`, generation: journal.generation, event };
|
|
95
|
+
});
|
|
96
|
+
operator = readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
|
|
97
|
+
readable: journal.status === "readable" && journal.backlogBytes === 0 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!operator)
|
|
102
|
+
return snap;
|
|
103
|
+
const { sequence, observedAt, graphAvailability } = snap.operator;
|
|
104
|
+
return { ...snap, operator: { ...operator, sequence, observedAt, graphAvailability } };
|
|
105
|
+
};
|
|
106
|
+
return { ...store, snapshot: () => decided(store.snapshot()), refresh: () => decided(store.refresh()) };
|
|
107
|
+
}
|
|
61
108
|
/** Preserve a policy-disabled gate from C2 even when C5's generic event
|
|
62
109
|
* mapper calls an unpassed result unknown. Lead with the gate name so a long
|
|
63
110
|
* task identity cannot clip the label that explains the disabled glyph. */
|
|
@@ -97,7 +144,7 @@ export async function runConsolidatedCockpit(options) {
|
|
|
97
144
|
output.write(SHELL_TERMINAL_ENTER);
|
|
98
145
|
}
|
|
99
146
|
store = createLiveStore({ cwd, runId, now: options.now });
|
|
100
|
-
const source = store;
|
|
147
|
+
const source = decidedLiveStore(store);
|
|
101
148
|
if (options.observeRun !== false)
|
|
102
149
|
observation = observeNamedRun(cwd, runId, options.environment);
|
|
103
150
|
// BD-1 (RULING-231-19 §2): the daemon-placed board carries the watch owner token and mounts
|
|
@@ -2,6 +2,7 @@ import { type JournalEvent } from "../../run/journal.js";
|
|
|
2
2
|
import { type OperatorRecord } from "../../run/operator-state.js";
|
|
3
3
|
export declare const OBSERVATION_INTERVAL_MS = 1000;
|
|
4
4
|
export declare const STORE_LIMITS: {
|
|
5
|
+
readonly graphBytes: number;
|
|
5
6
|
readonly history: 256;
|
|
6
7
|
readonly historyBytes: number;
|
|
7
8
|
readonly recordBytes: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, fstatSync, openSync,
|
|
1
|
+
import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { graphPath, stateDirName } from "../../graph/graph.js";
|
|
4
4
|
import { validateGraph } from "../../graph/schema.js";
|
|
@@ -7,7 +7,8 @@ import { isPidLive, STALE_MS } from "../../run/lock.js";
|
|
|
7
7
|
import { readTierLiveness, SUPERVISION_TIERS } from "../../run/supervision.js";
|
|
8
8
|
import { OperatorStateFold } from "../../run/operator-state.js";
|
|
9
9
|
export const OBSERVATION_INTERVAL_MS = 1_000;
|
|
10
|
-
|
|
10
|
+
// Graph declarations have a separate 16 MiB bound; journal retention remains unchanged.
|
|
11
|
+
export const STORE_LIMITS = { graphBytes: 16 * 1024 * 1024, history: 256, historyBytes: 2 * 1024 * 1024, recordBytes: 1024 * 1024, readBytes: 1024 * 1024, subscribers: 64, metrics: 12, errors: 32 };
|
|
11
12
|
const errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
12
13
|
const fileIdentity = (st) => `${st.dev}:${st.ino}`;
|
|
13
14
|
const stamp = (st) => `${fileIdentity(st)}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`;
|
|
@@ -192,10 +193,12 @@ export class JournalTail {
|
|
|
192
193
|
class JsonSource {
|
|
193
194
|
path;
|
|
194
195
|
parse;
|
|
196
|
+
maxBytes;
|
|
195
197
|
cached;
|
|
196
|
-
constructor(path, parse) {
|
|
198
|
+
constructor(path, parse, maxBytes = STORE_LIMITS.recordBytes) {
|
|
197
199
|
this.path = path;
|
|
198
200
|
this.parse = parse;
|
|
201
|
+
this.maxBytes = maxBytes;
|
|
199
202
|
}
|
|
200
203
|
read(now) {
|
|
201
204
|
try {
|
|
@@ -203,9 +206,29 @@ class JsonSource {
|
|
|
203
206
|
const id = stamp(st);
|
|
204
207
|
if (this.cached?.identity === id && this.cached.status === "readable")
|
|
205
208
|
return this.cached = { ...this.cached, observedAt: now };
|
|
206
|
-
if (!st.isFile()
|
|
207
|
-
throw new Error("source is not a
|
|
208
|
-
|
|
209
|
+
if (!st.isFile())
|
|
210
|
+
throw new Error("source is not a regular file");
|
|
211
|
+
if (st.size > BigInt(this.maxBytes))
|
|
212
|
+
throw new Error(`source exceeds ${this.maxBytes} byte cap`);
|
|
213
|
+
// Bound the read itself too: a writer can grow the file after stat.
|
|
214
|
+
const fd = openSync(this.path, "r");
|
|
215
|
+
let value;
|
|
216
|
+
try {
|
|
217
|
+
const bytes = Buffer.alloc(Number(st.size) + 1);
|
|
218
|
+
let length = 0;
|
|
219
|
+
while (length < bytes.length) {
|
|
220
|
+
const n = readSync(fd, bytes, length, bytes.length - length, length);
|
|
221
|
+
if (!n)
|
|
222
|
+
break;
|
|
223
|
+
length += n;
|
|
224
|
+
}
|
|
225
|
+
if (length !== Number(st.size) || stamp(fstatSync(fd, { bigint: true })) !== id)
|
|
226
|
+
throw new Error("source changed during read; retry observation");
|
|
227
|
+
value = this.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
closeSync(fd);
|
|
231
|
+
}
|
|
209
232
|
return this.cached = { source: this.path, identity: id, value, status: "readable", observedAt: now };
|
|
210
233
|
}
|
|
211
234
|
catch (e) {
|
|
@@ -222,7 +245,7 @@ export function createLiveStore(options) {
|
|
|
222
245
|
const tail = new JournalTail(join(state, "runs", parseRunId(options.runId), "journal.jsonl"), {
|
|
223
246
|
reset: () => { fold = new OperatorStateFold(); }, record: record => fold.apply(record),
|
|
224
247
|
});
|
|
225
|
-
const graph = new JsonSource(graphPath(options.cwd), raw => validateGraph(JSON.parse(raw)));
|
|
248
|
+
const graph = new JsonSource(graphPath(options.cwd), raw => validateGraph(JSON.parse(raw)), STORE_LIMITS.graphBytes);
|
|
226
249
|
const config = new JsonSource(options.configPath ?? join(state, "config.yaml"), raw => raw);
|
|
227
250
|
const cache = new JsonSource(options.cachePath ?? join(state, "doctor.json"), JSON.parse);
|
|
228
251
|
const lock = new JsonSource(join(state, "graph.lock"), raw => {
|
|
@@ -256,7 +279,7 @@ export function createLiveStore(options) {
|
|
|
256
279
|
const readable = journal.status === "readable" && journal.backlogBytes === 0;
|
|
257
280
|
return {
|
|
258
281
|
sequence: ++sequence, observedAt, delayed, freshness: errors.length ? "failed" : delayed ? "delayed" : "fresh",
|
|
259
|
-
operator: fold.snapshot({ graph: graphReading.status === "readable" ? graphReading.value : undefined, sequence, observedAt, readable }),
|
|
282
|
+
operator: fold.snapshot({ graph: graphReading.status === "readable" ? graphReading.value : undefined, sequence, observedAt, readable, graphAvailability: { status: graphReading.status, error: graphReading.error } }),
|
|
260
283
|
journal, graph: graphReading, config: configReading, cache: cacheReading,
|
|
261
284
|
lock: { ...owner, state: lockState, alive, expired: owner.identity ? observedAt - Number(owner.identity.split(":")[3]) / 1e6 > STALE_MS : undefined },
|
|
262
285
|
supervision, errors, actionsEnabled: readable && !errors.length && !delayed,
|
|
@@ -34,6 +34,9 @@ export declare function deriveRunViewRows(data: RunCockpitData, viewId: RunViewI
|
|
|
34
34
|
* task row — identity, phase, build evidence, blocker, next action — each unrecorded field saying so,
|
|
35
35
|
* each recorded one citing the journal line it was read from, and the OBS-1048 stall marker when the
|
|
36
36
|
* newest attempt's harvest is suspect. No attempt label is printed here: the ruler is the row's own.
|
|
37
|
+
* A task with no recorded dispatch folds identity, phase, and build into one missing-evidence clause
|
|
38
|
+
* (OBS-1104). A recorded blocker or next action — a pre-dispatch park, a dependency wait — stays,
|
|
39
|
+
* cited to the journal row that produced it.
|
|
37
40
|
*/
|
|
38
41
|
export declare function taskProjectionText(row: TaskRow, journal?: readonly JournalRow[]): string;
|
|
39
42
|
/**
|
|
@@ -3,8 +3,9 @@ import { Box, useApp, useStdout } from "ink";
|
|
|
3
3
|
import { cloneElement, createContext, useContext, useEffect, useInsertionEffect, useMemo, useRef, useSyncExternalStore, } from "react";
|
|
4
4
|
import { GLYPHS, PLAIN_COMPACT_LOCKUP, } from "../../brand.js";
|
|
5
5
|
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, keyRosterLines, Panel, PANEL_CHROME_ROWS, ProgressMeter, Sparkline, StatTile, StatusStrip, } from "./components.js";
|
|
6
|
+
import { authorsNote } from "../../run/operator-state.js";
|
|
6
7
|
import { fieldReading } from "./derive.js";
|
|
7
|
-
import { STALL_MARKER } from "./run-view.js";
|
|
8
|
+
import { MISSING_EVIDENCE, STALL_MARKER } from "./run-view.js";
|
|
8
9
|
import { initialRunInteractionState, projectRunKeyEntries, reconcileRunInteraction, RUN_SIDE_RAIL_COLUMN_FLOOR, runPanelFocusOrder, runSideRailVisible, selectableRunViewRowIds, } from "./keys.js";
|
|
9
10
|
import { DECISIONS_TAB_TITLE, RUN_VIEWS, runViewById, setupSectionAt, SETUP_SECTIONS, } from "./views.js";
|
|
10
11
|
import { FRAME_HEADER_GAP, FRAME_NESTED_ROW_BANDS, FULL_JOURNAL_ROWS, planFrame, RUN_PANEL_HORIZONTAL_CHROME, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
|
|
@@ -280,7 +281,7 @@ function promotedViewRows(data, viewId) {
|
|
|
280
281
|
time: fieldReading(row.lastEventTime),
|
|
281
282
|
...(row.lastEventTimestamp === undefined ? {} : { timestamp: row.lastEventTimestamp }),
|
|
282
283
|
state: taskRowState(row),
|
|
283
|
-
text: `${row.taskId} · ${fieldReading(row.state)} · ${runAttemptLabel(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`} · ${taskProjectionText(row, data.journalRows)}`,
|
|
284
|
+
text: `${row.taskId} · ${fieldReading(row.state)} · ${runAttemptLabel(row.attempts)} · ${fieldReading(row.actor)}${authorsNote(row.authors, row.actor) === undefined ? "" : ` · ${authorsNote(row.authors, row.actor)}`}${row.title === undefined ? "" : ` · ${row.title}`} · ${taskProjectionText(row, data.journalRows)}`,
|
|
284
285
|
}));
|
|
285
286
|
}
|
|
286
287
|
if (viewId === "gates") {
|
|
@@ -308,8 +309,13 @@ function promotedViewRows(data, viewId) {
|
|
|
308
309
|
* task row — identity, phase, build evidence, blocker, next action — each unrecorded field saying so,
|
|
309
310
|
* each recorded one citing the journal line it was read from, and the OBS-1048 stall marker when the
|
|
310
311
|
* newest attempt's harvest is suspect. No attempt label is printed here: the ruler is the row's own.
|
|
312
|
+
* A task with no recorded dispatch folds identity, phase, and build into one missing-evidence clause
|
|
313
|
+
* (OBS-1104). A recorded blocker or next action — a pre-dispatch park, a dependency wait — stays,
|
|
314
|
+
* cited to the journal row that produced it.
|
|
311
315
|
*/
|
|
312
316
|
export function taskProjectionText(row, journal = []) {
|
|
317
|
+
if ((row.attempts ?? 0) === 0)
|
|
318
|
+
return undispatchedProjectionText(row, journal);
|
|
313
319
|
const identity = row.identities?.at(-1);
|
|
314
320
|
const blocker = row.summary?.blocker;
|
|
315
321
|
const harvest = row.harvests?.at(-1);
|
|
@@ -330,6 +336,26 @@ export function taskProjectionText(row, journal = []) {
|
|
|
330
336
|
...(harvest?.suspectedStalledHarvest ? [`${STALL_MARKER} · launch ${at(evidence.launch)} · ${harvest.nudgeFailures} nudge failed ${at(evidence.nudge)} · ${harvest.pageCount} paged ${at(evidence.page)} · no worker-result`] : []),
|
|
331
337
|
].reduce((text, part) => `${text} · ${part}`);
|
|
332
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Identity, phase, and build are unrecorded until a dispatch. Append the blocker and next action
|
|
341
|
+
* only when the summary recorded one — an unknown placeholder with no journal row is the missing clause.
|
|
342
|
+
*/
|
|
343
|
+
function undispatchedProjectionText(row, journal) {
|
|
344
|
+
const blocker = row.summary?.blocker;
|
|
345
|
+
if (blocker == null)
|
|
346
|
+
return MISSING_EVIDENCE;
|
|
347
|
+
const evidence = projectionEvidence(row, journal);
|
|
348
|
+
const at = (line) => line === undefined ? "(no journal row)" : `#L${line}`;
|
|
349
|
+
const showBlocker = blocker.kind !== "unknown" || evidence.blocker !== undefined;
|
|
350
|
+
const showNext = blocker.nextAction != null;
|
|
351
|
+
if (!showBlocker && !showNext)
|
|
352
|
+
return MISSING_EVIDENCE;
|
|
353
|
+
return [
|
|
354
|
+
MISSING_EVIDENCE,
|
|
355
|
+
...(showBlocker ? [`blocker ${blocker.kind} ${at(evidence.blocker)}`] : []),
|
|
356
|
+
...(showNext ? [`next ${blocker.nextAction} ${at(evidence.nextAction)}`] : []),
|
|
357
|
+
].reduce((text, part) => `${text} · ${part}`);
|
|
358
|
+
}
|
|
333
359
|
/**
|
|
334
360
|
* Correlate each already-derived field with the row that can have produced that value. This is
|
|
335
361
|
* intentionally not a "newest row with a plausible name" lookup: launch identity is paired by
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { ApprovalRunOwner } from "../../cli/commands/approve.js";
|
|
3
3
|
import { type GateName, type RunGraph } from "../../graph/schema.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type JournalEvent } from "../../run/journal.js";
|
|
5
5
|
import { type GateOutcome } from "../../run/outcome.js";
|
|
6
6
|
import type { OperatorGateState, OperatorSnapshot, OperatorTask } from "../../run/operator-state.js";
|
|
7
7
|
import { type DecisionCommand, type DecisionPreview, type DecisionSession, type RunDecision } from "./decision-actions.js";
|
|
@@ -35,10 +35,8 @@ export interface RunGateCell {
|
|
|
35
35
|
/** The verdict text the evidence row carries, split into lines for paging. */
|
|
36
36
|
readonly verdict: readonly string[];
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
* that give a cell its inherited/satisfied label; only rows for this task after the evidence line count.
|
|
41
|
-
*/
|
|
38
|
+
/** Current-attempt cells in declaration order; later task rows supply inherited/satisfied labels.
|
|
39
|
+
* Only rows after each cell's own evidence line count. */
|
|
42
40
|
export declare function runGateCells(task: OperatorTask, evidence: EvidenceLookup, rows?: readonly RunEvidenceRow[]): readonly RunGateCell[];
|
|
43
41
|
export declare const VERDICT_WINDOW = 10;
|
|
44
42
|
export interface RunViewSession {
|
|
@@ -95,8 +93,9 @@ export interface TaskProjection {
|
|
|
95
93
|
readonly build: ProjectionField;
|
|
96
94
|
readonly blocker: ProjectionField;
|
|
97
95
|
readonly nextAction: ProjectionField;
|
|
98
|
-
/** OBS-1048: present only when the newest launched attempt has failed nudges, pages and no worker-result. */
|
|
96
|
+
/** OBS-1048: present only when the newest launched attempt has failed nudges, pages and no worker-result. OBS-1104: neverDispatched collapses that task's line. */
|
|
99
97
|
readonly stalled?: ProjectionField;
|
|
98
|
+
readonly neverDispatched?: true;
|
|
100
99
|
}
|
|
101
100
|
export declare const STALL_MARKER = "\u26A0 stalled harvest suspected";
|
|
102
101
|
/**
|
|
@@ -105,7 +104,12 @@ export declare const STALL_MARKER = "\u26A0 stalled harvest suspected";
|
|
|
105
104
|
* Nothing is inferred from a title, a provider or a task id: an unrecorded field says so.
|
|
106
105
|
*/
|
|
107
106
|
export declare function projectRunTasks(snapshot: OperatorSnapshot, rows: readonly RunEvidenceRow[], graph: RunGraph | undefined, decisions: readonly RunDecision[], runId: string): readonly TaskProjection[];
|
|
108
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* OBS-1104: a never-dispatched task has no identity, phase, or build row. Those fold into one
|
|
109
|
+
* missing-evidence clause. A substantive or journal-cited blocker and next action stay — a
|
|
110
|
+
* pre-dispatch park and a dependency wait are recorded facts, and dropping them hides the park token.
|
|
111
|
+
*/
|
|
112
|
+
export declare const MISSING_EVIDENCE = "missing evidence";
|
|
109
113
|
export declare const projectionLine: (p: TaskProjection) => string;
|
|
110
114
|
export interface PaneLocator {
|
|
111
115
|
readonly status: "recorded" | "unavailable";
|