tickmarkr 2.5.5 → 2.5.7
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 +3 -1
- package/dist/adapters/prompt.js +21 -1
- package/dist/cli/commands/approve.js +69 -8
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/fleet.js +4 -0
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/status.js +95 -34
- package/dist/cli/commands/verify.js +108 -85
- package/dist/config/config.d.ts +11 -0
- package/dist/config/config.js +21 -12
- package/dist/config/fleet-overlay.js +55 -17
- package/dist/drivers/index.js +2 -1
- package/dist/drivers/orca.d.ts +21 -1
- package/dist/drivers/orca.js +209 -27
- package/dist/gates/baseline.d.ts +21 -5
- package/dist/gates/baseline.js +67 -17
- package/dist/gates/cache.d.ts +14 -13
- package/dist/gates/cache.js +17 -5
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +118 -15
- package/dist/gates/run-gates.d.ts +10 -2
- package/dist/gates/run-gates.js +362 -108
- package/dist/gates/test-manifest.d.ts +33 -1
- package/dist/gates/test-manifest.js +132 -40
- package/dist/gates/test-reporter.js +20 -7
- package/dist/graph/graph.d.ts +4 -0
- package/dist/graph/graph.js +50 -1
- package/dist/run/activity.d.ts +28 -0
- package/dist/run/activity.js +194 -0
- package/dist/run/consult.js +5 -4
- package/dist/run/daemon.d.ts +11 -0
- package/dist/run/daemon.js +663 -128
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +46 -1
- package/dist/run/git.js +149 -12
- package/dist/run/journal.d.ts +23 -5
- package/dist/run/journal.js +98 -25
- package/dist/run/lease.d.ts +44 -0
- package/dist/run/lease.js +226 -3
- package/dist/run/operator-page-summary.d.ts +56 -0
- package/dist/run/operator-page-summary.js +68 -0
- package/dist/run/operator-summary.d.ts +69 -0
- package/dist/run/operator-summary.js +77 -0
- package/dist/run/protocol.d.ts +71 -0
- package/dist/run/protocol.js +32 -0
- package/dist/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -3
- package/dist/tui/cockpit/board.d.ts +9 -0
- package/dist/tui/cockpit/board.js +10 -0
- package/dist/tui/cockpit/derive.d.ts +35 -0
- package/dist/tui/cockpit/derive.js +152 -10
- package/dist/tui/cockpit/evidence-view.d.ts +2 -0
- package/dist/tui/cockpit/evidence-view.js +42 -12
- package/dist/tui/cockpit/run-cockpit.d.ts +8 -1
- package/dist/tui/cockpit/run-cockpit.js +95 -1
- package/dist/tui/cockpit/run-view.d.ts +37 -0
- package/dist/tui/cockpit/run-view.js +189 -2
- package/dist/tui/ink/fleet-app.d.ts +10 -2
- package/dist/tui/ink/fleet-app.js +33 -15
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +55 -3
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ prefer, every example below works with either.
|
|
|
52
52
|
|
|
53
53
|
- **macOS or Linux.** Every command shells out through `bash`, so native Windows is not
|
|
54
54
|
supported — use WSL (untested).
|
|
55
|
-
- Node ≥ 20 and `git` on PATH.
|
|
55
|
+
- Node ≥ 20.3 and `git` on PATH.
|
|
56
56
|
- At least one agent CLI on PATH (`claude`, `codex`, `cursor-agent`, `opencode`, `grok`, `pi`,
|
|
57
57
|
or `kimi`), authenticated through its own login. tickmarkr never handles vendor API keys itself.
|
|
58
58
|
|
|
@@ -476,6 +476,8 @@ accepted contributions are credited via `Co-authored-by:` on the release commit.
|
|
|
476
476
|
|
|
477
477
|
## Documentation
|
|
478
478
|
|
|
479
|
+
- [Experimental execution policy](docs/experimental-execution-policy.md) — bounded recovery, cumulative task time, and repair selection.
|
|
480
|
+
|
|
479
481
|
- **[FLEET.md](https://github.com/alzahrani-khalid/tickmarkr/blob/main/FLEET.md)** — routing modes, steering syntax, tier provenance, and run flags (advanced reference)
|
|
480
482
|
- **[LICENSE](LICENSE)** — MIT license
|
|
481
483
|
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — development setup and contribution guidelines
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -71,9 +71,29 @@ function workerVerdictWitness(raw, nonce, positions) {
|
|
|
71
71
|
// these — a result carrying any other summary is a PARSED trailer, i.e. the worker speaking.
|
|
72
72
|
export const NO_TRAILER_SUMMARY = "worker produced no TICKMARKR_RESULT trailer";
|
|
73
73
|
export const UNPARSEABLE_TRAILER_SUMMARY = "unparseable TICKMARKR_RESULT trailer";
|
|
74
|
+
// OBS-1062: every interactive TUI echoes the brief into the pane, and the brief carries the trailer
|
|
75
|
+
// TEMPLATE above — so the nonce token is on screen before the worker has said anything. A token
|
|
76
|
+
// whose object opens with the template's literal `"ok":true|false` (not a bool) is the brief being
|
|
77
|
+
// displayed, never the worker speaking; it must not count as trailer participation, or the daemon
|
|
78
|
+
// reaps the attempt as malformed at its first poll (v2.5.7 run …115246, both workers at 34 s).
|
|
79
|
+
const TEMPLATE_BODY = '{"ok":true|false';
|
|
80
|
+
// Bounded to THIS occurrence (review finding, Leg 0a R2): the `{` must sit before the next nonce
|
|
81
|
+
// token, or a malformed worker token followed by a later template redraw would be erased as echo.
|
|
82
|
+
function isTemplateEcho(raw, at, end) {
|
|
83
|
+
const open = raw.indexOf("{", at);
|
|
84
|
+
if (open === -1 || open >= end)
|
|
85
|
+
return false;
|
|
86
|
+
const joined = raw
|
|
87
|
+
.slice(open, open + 64)
|
|
88
|
+
.split("\n")
|
|
89
|
+
.map((l) => l.replace(/^[\s│|]+/, "").replace(/[\s│|]+$/, ""))
|
|
90
|
+
.join("");
|
|
91
|
+
return joined.startsWith(TEMPLATE_BODY);
|
|
92
|
+
}
|
|
74
93
|
export function parseWorkerResult(raw, nonce) {
|
|
75
94
|
const fail = (summary, cause) => ({ ok: false, summary, deviations: [], raw, cause });
|
|
76
|
-
const
|
|
95
|
+
const all = trailerTokenPositions(raw, nonce);
|
|
96
|
+
const positions = all.filter((at, i) => !isTemplateEcho(raw, at, all[i + 1] ?? raw.length));
|
|
77
97
|
// TUIs echo the prompt template, redraw lines, and HARD-wrap the JSON with per-line margins
|
|
78
98
|
// (cursor does; recent-unwrapped can't rejoin hard newlines). Scan occurrences backward — last
|
|
79
99
|
// parseable wins — joining wrapped lines, stripping margin/box chrome, and growing the candidate
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { graphDefinitionHash, loadGraph, saveGraph } from "../../graph/graph.js";
|
|
1
|
+
import { graphDefinitionHash, loadGraph, saveGraph, taskDefinitionFingerprint } from "../../graph/graph.js";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { userInfo } from "node:os";
|
|
4
|
+
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { integrationBranch } from "../../run/merge.js";
|
|
3
6
|
import { separabilityErrors } from "../../compile/collateral.js";
|
|
4
7
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
5
|
-
import { applyScopeAmendments, engagementComparable, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
8
|
+
import { applyScopeAmendments, engagementComparable, engagementReleased, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
6
9
|
export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
|
|
7
10
|
/**
|
|
8
11
|
* What each disposition's release actually buys — the clause every operator-facing sentence about
|
|
@@ -85,6 +88,8 @@ export function permittedDecisionVerbs(park) {
|
|
|
85
88
|
}
|
|
86
89
|
if (park.kind === "infra")
|
|
87
90
|
return ["approve", "recheck"];
|
|
91
|
+
if (park.kind === "diff-cap")
|
|
92
|
+
return ["recheck"]; // OBS-1007: a cap trip is re-gated under a raised cap, never re-bought
|
|
88
93
|
return ["approve"];
|
|
89
94
|
}
|
|
90
95
|
/** The release marker this command appends for a verb on a park — the fact a read-back must match. */
|
|
@@ -138,6 +143,21 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
138
143
|
if (status === undefined) {
|
|
139
144
|
throw new Error(`task ${taskId} has no events in run ${runId} — unknown task or never dispatched`);
|
|
140
145
|
}
|
|
146
|
+
// OBS-1022: a task-failed task whose preserved ref (or task branch) carries commits ahead of the
|
|
147
|
+
// integration base owns verified-able work; --recheck re-gates that tree with no worker. Any other
|
|
148
|
+
// verb on a failed task, or a recheck over nothing landed, is refused naming why.
|
|
149
|
+
if (status === "failed" && recheck) {
|
|
150
|
+
const ahead = failedTaskCommitsAhead(cwd, journal, runId, taskId);
|
|
151
|
+
if (ahead.count === 0) {
|
|
152
|
+
throw new Error(`task ${taskId} is failed and ${ahead.ref} carries no commits ahead of ${ahead.base} — nothing to re-gate; use resume --retry-failed to fund a worker`);
|
|
153
|
+
}
|
|
154
|
+
journal.append("task-approved", taskId, {
|
|
155
|
+
by, ...(reason ? { reason } : {}), via: "cli", release: RECHECK_RELEASE,
|
|
156
|
+
recheckedRef: ahead.ref, commitsAhead: ahead.count,
|
|
157
|
+
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
158
|
+
});
|
|
159
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking failed ${taskId} in ${runId} — by ${by}; ${ahead.count} commit(s) on ${ahead.ref} ahead of ${ahead.base}; no worker funded`, serialization.contended);
|
|
160
|
+
}
|
|
141
161
|
if (status !== "human") {
|
|
142
162
|
// a silent no-op would be worse than a loud refusal — name the actual status (D-05)
|
|
143
163
|
throw new Error(`task ${taskId} is ${status}, not a parked human gate — refusing (a silent no-op would be worse)`);
|
|
@@ -149,7 +169,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
149
169
|
const lastHuman = park === undefined ? undefined : events[park.index];
|
|
150
170
|
const capPark = park?.kind === ATTEMPT_CAP_RELEASE;
|
|
151
171
|
const gateFailPark = park?.kind === "gate-fail";
|
|
152
|
-
const infraPark = park?.kind === "infra";
|
|
172
|
+
const infraPark = park?.kind === "infra" || park?.kind === "diff-cap";
|
|
153
173
|
const failedGate = gateFailPark ? park?.failedGate : undefined;
|
|
154
174
|
if ((park?.kind === "scope-request" && !decisions) || files !== undefined) {
|
|
155
175
|
if (park?.kind !== "scope-request")
|
|
@@ -158,7 +178,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
158
178
|
throw new Error(`scope-request for ${taskId} requires --files <glob,…>`);
|
|
159
179
|
if (decisions)
|
|
160
180
|
throw new Error("--files cannot be combined with --waive, --uphold or --recheck");
|
|
161
|
-
const graph = applyScopeAmendments(loadGraph(cwd), journal);
|
|
181
|
+
const graph = applyScopeAmendments(loadGraph(cwd), journal, false, engagementReleased(events));
|
|
162
182
|
const from = graphDefinitionHash(graph);
|
|
163
183
|
if (!engagementComparable(journal.read(), from).comparable
|
|
164
184
|
|| (lastHuman?.data.graphDefinitionHash !== undefined && lastHuman.data.graphDefinitionHash !== from)) {
|
|
@@ -176,11 +196,11 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
176
196
|
throw new Error(`refusing scope-request approval for ${taskId}: ${conflicts.join("\n")}`);
|
|
177
197
|
journal.append("task-approved", taskId, {
|
|
178
198
|
by, ...(reason ? { reason } : {}), via: "cli", release: "scope-request",
|
|
179
|
-
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line },
|
|
199
|
+
amendment: { from, to: graphDefinitionHash(amended), beforeFiles: task.files, files: amendedFiles, parkLine: park.line, definition: taskDefinitionFingerprint(task) },
|
|
180
200
|
});
|
|
181
201
|
// Do not write graph.json from this process while the daemon owns it: its sweep materializes
|
|
182
202
|
// the amendment without replacing a sibling's running state with this command's snapshot.
|
|
183
|
-
const projected = applyScopeAmendments(graph, journal);
|
|
203
|
+
const projected = applyScopeAmendments(graph, journal, false, engagementReleased(events));
|
|
184
204
|
const owner = approvalRunOwner(cwd, runId);
|
|
185
205
|
if (!owner.live && !owner.blockingRunId)
|
|
186
206
|
saveGraph(cwd, projected);
|
|
@@ -205,7 +225,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
205
225
|
}
|
|
206
226
|
if (recheck) {
|
|
207
227
|
if ((!gateFailPark || !failedGate) && !infraPark) {
|
|
208
|
-
throw new Error(`--recheck applies to a gate-fail or
|
|
228
|
+
throw new Error(`--recheck applies to a gate-fail, infra or diff-cap park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
209
229
|
}
|
|
210
230
|
journal.append("task-approved", taskId, {
|
|
211
231
|
by,
|
|
@@ -214,7 +234,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
214
234
|
release: RECHECK_RELEASE,
|
|
215
235
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
216
236
|
});
|
|
217
|
-
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` :
|
|
237
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; ${failedGate ? `failed gate ${failedGate}` : `${park.kind} park`}; no gate marked satisfied`, serialization.contended);
|
|
218
238
|
}
|
|
219
239
|
if (waive) {
|
|
220
240
|
if (!gateFailPark || !failedGate) {
|
|
@@ -242,6 +262,11 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
242
262
|
if (park?.tombstone) {
|
|
243
263
|
throw new Error(`task ${taskId}'s newest park is a tombstone (${park.reason ?? "no reason"}) — permanent by design; no verb releases it`);
|
|
244
264
|
}
|
|
265
|
+
// OBS-1007: a cap trip is re-gated under a raised cap, never re-bought — plain approve would fund a
|
|
266
|
+
// worker the production oracle (permittedDecisionVerbs) says this park cannot buy.
|
|
267
|
+
if (park?.kind === "diff-cap") {
|
|
268
|
+
throw new Error(`task ${taskId} is parked on a diff cap; plain approve would fund another worker — raise gates.diffCap and pass --recheck (disposition re-dispatch)`);
|
|
269
|
+
}
|
|
245
270
|
journal.append("task-approved", taskId, {
|
|
246
271
|
by,
|
|
247
272
|
...(reason ? { reason } : {}),
|
|
@@ -256,6 +281,42 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
256
281
|
serialization.release();
|
|
257
282
|
}
|
|
258
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* OBS-1022: what a failed task has landed. A preserved snapshot is authoritative only when it contains
|
|
286
|
+
* the current task-branch tip; an older preservation can remain in the journal after a later retry has
|
|
287
|
+
* advanced/rebuilt the branch. Missing task branches may still recover from a preserved ref. The selected
|
|
288
|
+
* ref is counted against the run's integration branch; unreadable refs count as zero and fail closed.
|
|
289
|
+
*/
|
|
290
|
+
function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
|
|
291
|
+
const cfg = loadConfig(cwd);
|
|
292
|
+
const base = integrationBranch(cfg, runId);
|
|
293
|
+
const taskRef = `${base}--${taskId}`;
|
|
294
|
+
const preserved = [...journal.read()].reverse().find((e) => e.taskId === taskId && e.event === "worktree-preserved" && typeof e.data.ref === "string");
|
|
295
|
+
const preservedRef = preserved?.data.ref;
|
|
296
|
+
let ref = taskRef;
|
|
297
|
+
if (preservedRef) {
|
|
298
|
+
try {
|
|
299
|
+
execFileSync("git", ["rev-parse", "--verify", taskRef], { cwd, stdio: "ignore" });
|
|
300
|
+
execFileSync("git", ["merge-base", "--is-ancestor", taskRef, preservedRef], { cwd, stdio: "ignore" });
|
|
301
|
+
ref = preservedRef;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
try {
|
|
305
|
+
execFileSync("git", ["rev-parse", "--verify", taskRef], { cwd, stdio: "ignore" });
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
ref = preservedRef;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const out = execFileSync("git", ["rev-list", "--count", `${base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
314
|
+
return { ref, base, count: Number.parseInt(out.trim(), 10) || 0 };
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return { ref, base, count: 0 };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
259
320
|
// The status is command OUTPUT, not a typed sibling result: the registered approve function returns
|
|
260
321
|
// one primitive string and the dispatcher prints those bytes unchanged. A compact sentinel followed
|
|
261
322
|
// by JSON makes the contract unambiguous to machines without widening the shared command result type.
|
|
@@ -203,6 +203,11 @@ export declare function doctorProbePreflight(cwd?: string, cfg?: {
|
|
|
203
203
|
worker: "interactive" | "print";
|
|
204
204
|
workersPerTab: number;
|
|
205
205
|
};
|
|
206
|
+
executionPolicy?: {
|
|
207
|
+
boundedInfrastructure: boolean;
|
|
208
|
+
repairSelection: boolean;
|
|
209
|
+
taskExecutionLimitMs: number;
|
|
210
|
+
} | undefined;
|
|
206
211
|
setup?: string | undefined;
|
|
207
212
|
cost?: {
|
|
208
213
|
models?: Record<string, {
|
|
@@ -322,6 +327,11 @@ export declare function cachedDoctorDiagnostics(cwd?: string, adapters?: WorkerA
|
|
|
322
327
|
worker: "interactive" | "print";
|
|
323
328
|
workersPerTab: number;
|
|
324
329
|
};
|
|
330
|
+
executionPolicy?: {
|
|
331
|
+
boundedInfrastructure: boolean;
|
|
332
|
+
repairSelection: boolean;
|
|
333
|
+
taskExecutionLimitMs: number;
|
|
334
|
+
} | undefined;
|
|
325
335
|
setup?: string | undefined;
|
|
326
336
|
cost?: {
|
|
327
337
|
models?: Record<string, {
|
|
@@ -324,6 +324,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
324
324
|
...structuredClone(initial),
|
|
325
325
|
denyAdapters: deny.adapters,
|
|
326
326
|
denyModels: deny.models,
|
|
327
|
+
// OBS-1046: the allow complement is staged beside the deny lists, never folded into them
|
|
328
|
+
allowOut: deny.allowOut ?? initial.allowOut,
|
|
327
329
|
denyWorkersAdapters: deny.workersAdapters,
|
|
328
330
|
denyWorkersModels: deny.workersModels,
|
|
329
331
|
map,
|
|
@@ -644,6 +646,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
644
646
|
const staged = structuredClone(initial);
|
|
645
647
|
staged.denyAdapters = state.denyAdapters;
|
|
646
648
|
staged.denyModels = state.denyModels;
|
|
649
|
+
staged.allowOut = state.allowOut ?? initial.allowOut;
|
|
647
650
|
// OBS-994/FL-1: the workers-only deny scope rides the same review/write funnel as the flat one.
|
|
648
651
|
staged.denyWorkersAdapters = state.denyWorkersAdapters;
|
|
649
652
|
staged.denyWorkersModels = state.denyWorkersModels;
|
|
@@ -699,6 +702,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
699
702
|
health,
|
|
700
703
|
initialDenyAdapters: editable.denyAdapters,
|
|
701
704
|
initialDenyModels: editable.denyModels,
|
|
705
|
+
initialAllowOut: editable.allowOut,
|
|
702
706
|
initialDenyWorkersAdapters: editable.denyWorkersAdapters,
|
|
703
707
|
initialDenyWorkersModels: editable.denyWorkersModels,
|
|
704
708
|
modelGroups,
|
|
@@ -7,7 +7,7 @@ import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
|
7
7
|
import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
|
|
8
8
|
import { classifyContextPath } from "../../compile/native.js";
|
|
9
9
|
import { DEFAULT_CONFIG, effectiveReviewPolicy, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
10
|
-
import { graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
10
|
+
import { chainDepth, dispatchWaves, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
11
11
|
import { renderAcceptanceItem } from "../../graph/schema.js";
|
|
12
12
|
import { resolveRunMode } from "../../run/daemon.js";
|
|
13
13
|
import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
|
|
@@ -364,11 +364,27 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
364
364
|
: "";
|
|
365
365
|
return { line: `${channelKey(reviewer)}${rotationNote} — reviewPolicy full`, cost: seatCost(reviewer) };
|
|
366
366
|
};
|
|
367
|
+
// OBS-1018: depth and wave come from the same functions the daemon admits by, so the plan shows the
|
|
368
|
+
// critical path the run will actually follow at this concurrency.
|
|
369
|
+
const depths = chainDepth(g);
|
|
370
|
+
const waves = dispatchWaves(g, cfg.concurrency);
|
|
371
|
+
const byId = new Map(g.tasks.map((t) => [t.id, t]));
|
|
372
|
+
const chainLine = (t) => {
|
|
373
|
+
const wave = waves.get(t.id);
|
|
374
|
+
// Map absence never implies status: a pending task stays wave-less when a dependency is
|
|
375
|
+
// running/gated/failed/human, so render the real status and name the blocking deps.
|
|
376
|
+
const tail = wave !== undefined
|
|
377
|
+
? `wave ${wave} at concurrency ${cfg.concurrency}`
|
|
378
|
+
: t.status !== "pending"
|
|
379
|
+
? `no wave (${t.status})`
|
|
380
|
+
: `no wave (pending; blocked by ${t.deps.filter((d) => byId.get(d)?.status !== "done").map((d) => `${d}:${byId.get(d)?.status ?? "missing"}`).join(", ")})`;
|
|
381
|
+
return ` chain: depth ${depths.get(t.id)} · ${tail}`;
|
|
382
|
+
};
|
|
367
383
|
const routed = [];
|
|
368
384
|
for (const t of g.tasks) {
|
|
369
385
|
const refusals = [...(oracleRefusals.get(t.id) ?? []), ...(inputRefusals.get(t.id) ?? [])];
|
|
370
386
|
if (refusals.length) {
|
|
371
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! pre-dispatch refusal — ${refusals.join("; ")}
|
|
387
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! pre-dispatch refusal — ${refusals.join("; ")}`, chainLine(t));
|
|
372
388
|
continue;
|
|
373
389
|
}
|
|
374
390
|
try {
|
|
@@ -382,7 +398,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
382
398
|
const judge = judgeSeat(t);
|
|
383
399
|
const review = reviewSeat(t, r.assignment);
|
|
384
400
|
cost += est + judge.cost + review.cost;
|
|
385
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}
|
|
401
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`, ` judge: ${judge.line}${judge.cost ? ` ~$${judge.cost.toFixed(2)}` : ""}`, ` review: ${review.line}${review.cost ? ` ~$${review.cost.toFixed(2)}` : ""}`, chainLine(t));
|
|
386
402
|
const d = derivation(t.shape);
|
|
387
403
|
if (d)
|
|
388
404
|
lines.push(d);
|
|
@@ -406,7 +422,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
406
422
|
if (!(e instanceof RoutingError))
|
|
407
423
|
throw e;
|
|
408
424
|
const msg = `${e.message}${exclusionReason(e.message)}`;
|
|
409
|
-
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}
|
|
425
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}`, chainLine(t));
|
|
410
426
|
const d = derivation(t.shape);
|
|
411
427
|
if (d)
|
|
412
428
|
lines.push(d);
|
|
@@ -7,8 +7,11 @@ import { HerdrDriver } from "../../drivers/herdr.js";
|
|
|
7
7
|
import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
|
|
8
8
|
import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
9
9
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { projectActivity } from "../../run/activity.js";
|
|
11
|
+
import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
12
|
+
import { trackJournalRows } from "../../run/protocol.js";
|
|
13
|
+
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
14
|
+
import { Journal, formatJournalNarration, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
12
15
|
import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
13
16
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
14
17
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
@@ -96,8 +99,8 @@ const taskClockReference = (events, taskId, content, fallback) => {
|
|
|
96
99
|
}
|
|
97
100
|
return fallback;
|
|
98
101
|
};
|
|
99
|
-
// Every attempt number a cell phrase carries is the engagement's own ladder (
|
|
100
|
-
//
|
|
102
|
+
// Every attempt number a cell phrase carries is the engagement's own ladder (journal dispatch
|
|
103
|
+
// labels are displayed from 1). Name that ruler wherever a bare one appears, not only in the one phrase
|
|
101
104
|
// shipping today — a phrase added upstream must not reach the operator unruled.
|
|
102
105
|
const labelAttemptRuler = (text) => text.replace(/(?<!engagement |run )\battempt (?=\d)/gu, "engagement attempt ");
|
|
103
106
|
// The timer must keep the process ALIVE: an unref'd timer here let the event loop drain after the
|
|
@@ -907,9 +910,58 @@ const readRunRecord = (cwd, graph, namedRunId) => {
|
|
|
907
910
|
* catch an empty or torn first write. Those snapshots own no task fold yet; once run-start exists,
|
|
908
911
|
* however, a fold failure is unexpected and must surface instead of becoming a false empty run.
|
|
909
912
|
*/
|
|
910
|
-
const recordTaskRows = (record, graph) => record.comparable && record.events.some((event) => event.event === "run-start")
|
|
911
|
-
? new Map(deriveRunCockpitData({ fileName: `${record.runId}.journal.jsonl`, raw: record.raw }, "status", { graph }).taskRows.map((row) => [row.taskId, row]))
|
|
913
|
+
const recordTaskRows = (record, graph, isDaemonAlive) => record.comparable && record.events.some((event) => event.event === "run-start")
|
|
914
|
+
? new Map(deriveRunCockpitData({ fileName: `${record.runId}.journal.jsonl`, raw: record.raw }, "status", { graph, isDaemonAlive }).taskRows.map((row) => [row.taskId, row]))
|
|
912
915
|
: new Map();
|
|
916
|
+
/** Adapt the single journal snapshot to the shared, evidence-only projections. */
|
|
917
|
+
const recordProjection = (record, graph, isDaemonAlive) => {
|
|
918
|
+
const rows = record ? recordTaskRows(record, graph, isDaemonAlive) : new Map();
|
|
919
|
+
const events = record?.comparable ? record.events : [];
|
|
920
|
+
const activity = projectActivity(record?.runId ?? "", trackJournalRows(record?.runId ?? "", events.map((raw, sourceIndex) => ({ raw, sourceIndex }))), graph.tasks);
|
|
921
|
+
const tasks = graph.tasks.map(task => {
|
|
922
|
+
const row = rows.get(task.id);
|
|
923
|
+
const evidence = events.filter(event => event.taskId === task.id);
|
|
924
|
+
const last = evidence.at(-1);
|
|
925
|
+
const recorded = activity.get(task.id);
|
|
926
|
+
let phase = last ? recorded.state : undefined;
|
|
927
|
+
// A completed battery is evidence of readiness, never evidence that merge started.
|
|
928
|
+
const gates = gateSnapshot(task, events, record?.rehashAt);
|
|
929
|
+
const boundary = events.reduce((index, event, at) => ["run-start", "run-resume", "run-end"].includes(event.event)
|
|
930
|
+
|| (event.taskId === task.id && (event.event === "task-dispatch"
|
|
931
|
+
|| (event.event === "phase-start" && event.data.phase === "gates"))) ? at : index, -1);
|
|
932
|
+
// Read the latest outcome, including non-verdicts. The gate rail retains earlier
|
|
933
|
+
// verdicts while a screen or infrastructure retry is pending; that historical
|
|
934
|
+
// pass must not make the current battery look ready to merge.
|
|
935
|
+
const currentResults = new Map(events.slice(boundary + 1).filter(event => event.taskId === task.id && event.event === "gate-result")
|
|
936
|
+
.map(event => [event.data.gate, normalizeGateOutcome(event.data).kind]));
|
|
937
|
+
if (recorded.state === "unconfirmed" && task.gates.length > 0 && !gates.priorGraph
|
|
938
|
+
&& task.gates.every(gate => currentResults.get(gate) === "passed")) {
|
|
939
|
+
phase = "awaiting merge phase";
|
|
940
|
+
}
|
|
941
|
+
const responsibility = [...evidence].reverse().find(event => typeof event.data.role === "string" || typeof event.data.agent === "string");
|
|
942
|
+
return {
|
|
943
|
+
id: task.id, deps: task.deps, status: row?.state ?? task.status,
|
|
944
|
+
phase, lastEvidenceAt: last?.ts,
|
|
945
|
+
responsible: responsibility ? {
|
|
946
|
+
role: typeof responsibility.data.role === "string" ? responsibility.data.role : undefined,
|
|
947
|
+
agent: typeof responsibility.data.agent === "string" ? responsibility.data.agent : undefined,
|
|
948
|
+
} : undefined,
|
|
949
|
+
};
|
|
950
|
+
});
|
|
951
|
+
const decisions = tasks.flatMap(task => {
|
|
952
|
+
const park = task.status === "human" ? newestPark(events, task.id) : undefined;
|
|
953
|
+
return park ? [{ taskId: task.id, park, verbs: permittedDecisionVerbs(park) }] : [];
|
|
954
|
+
});
|
|
955
|
+
return { rows, activity, summaries: new Map(projectOperatorSummary(tasks, decisions).map(task => [task.taskId, task])) };
|
|
956
|
+
};
|
|
957
|
+
const summaryText = (summary) => sanitizeTaskText([
|
|
958
|
+
`phase ${summary.phase ?? "unrecorded"}`,
|
|
959
|
+
`last evidence ${summary.lastEvidenceAt ?? "unrecorded"}`,
|
|
960
|
+
`responsible ${[summary.responsible?.role, summary.responsible?.agent].filter(Boolean).join(" / ") || "unrecorded"}`,
|
|
961
|
+
`blocker ${summary.blocker?.kind ?? "none"}`,
|
|
962
|
+
`next action ${summary.blocker?.nextAction ?? "unrecorded"}`,
|
|
963
|
+
`human-decision ${summary.blocker?.decisionRequired ? "yes" : "no"}`,
|
|
964
|
+
].join(" · "));
|
|
913
965
|
/** The graph tasks this run's record actually speaks about — a row with no recorded fact is silence. */
|
|
914
966
|
const recordedTasks = (graph, rows) => graph.tasks.filter((task) => {
|
|
915
967
|
const row = rows.get(task.id);
|
|
@@ -936,14 +988,11 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
936
988
|
const comparable = record?.comparable ?? false;
|
|
937
989
|
const rehashAt = record?.rehashAt;
|
|
938
990
|
const assignments = new Map();
|
|
939
|
-
let replayed = null;
|
|
940
991
|
const contexts = new Map();
|
|
941
992
|
// v1.53 T5: this run is dead — a newer run replaced it
|
|
942
993
|
const supersededBy = [...events].reverse()
|
|
943
994
|
.find((e) => e.event === "superseded" && typeof e.data.by === "string")?.data.by;
|
|
944
995
|
if (record && comparable) {
|
|
945
|
-
if (!journalRowsOnly)
|
|
946
|
-
replayed = Journal.open(cwd, record.runId).replayStatuses();
|
|
947
996
|
for (const e of events) {
|
|
948
997
|
if (!journalRowsOnly && e.event === "task-dispatch" && e.taskId) {
|
|
949
998
|
const a = e.data.assignment;
|
|
@@ -956,7 +1005,12 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
956
1005
|
}
|
|
957
1006
|
}
|
|
958
1007
|
}
|
|
959
|
-
const
|
|
1008
|
+
const projection = recordProjection(record, g, () => daemon.state !== "dead");
|
|
1009
|
+
const taskRows = projection.rows;
|
|
1010
|
+
// Preserve the plain table's compact status vocabulary using the same snapshot as its evidence.
|
|
1011
|
+
const replayed = new Map([...taskRows].flatMap(([id, row]) => row.state === undefined ? [] : [[id,
|
|
1012
|
+
row.state === "running" || row.state === "interrupted" ? "pending" : graphTaskStatus(row.state, "pending"),
|
|
1013
|
+
]]));
|
|
960
1014
|
const clockFallback = new Date(now);
|
|
961
1015
|
const recordedClock = new Date(events.at(-1)?.ts ?? now);
|
|
962
1016
|
const zoneReference = Number.isFinite(recordedClock.getTime()) ? recordedClock : clockFallback;
|
|
@@ -973,14 +1027,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
973
1027
|
};
|
|
974
1028
|
const renderedTasks = journalRowsOnly ? recordedTasks(g, taskRows) : g.tasks;
|
|
975
1029
|
const starved = new Set(blockedTasks(effective).map((t) => t.id));
|
|
976
|
-
//
|
|
977
|
-
// (a recompiled graph's journal must not animate the wrong tasks); with no or stale journal the
|
|
978
|
-
// dep-waiting cells still derive from the effective graph statuses.
|
|
979
|
-
const activity = foldActivity(comparable ? events : [], effective.tasks);
|
|
980
|
-
// RULING-v189-scope §14c: a card answers BOTH supervisor questions. `foldActivity` names what a
|
|
981
|
-
// task waits FOR; this names what waits ON it. Read from `effective.tasks` — whose statuses are
|
|
982
|
-
// the journal's replay, never the compiled graph's — so a dependent the record says is done stops
|
|
983
|
-
// being named, and a task all of whose dependents are done acquires no entry at all.
|
|
1030
|
+
// Reverse dependencies share the effective task statuses with the projection.
|
|
984
1031
|
const dependents = new Map();
|
|
985
1032
|
for (const task of effective.tasks) {
|
|
986
1033
|
if (task.status === "done")
|
|
@@ -1039,7 +1086,18 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1039
1086
|
|| (st === "failed" && failureKind === undefined));
|
|
1040
1087
|
const livePhase = phases.get(t.id);
|
|
1041
1088
|
const isStarved = !livePhase && starved.has(t.id);
|
|
1042
|
-
const
|
|
1089
|
+
const summary = projection.summaries.get(t.id);
|
|
1090
|
+
// Attempt labels can restart at zero in legacy engagements. Pair the preparing caption's
|
|
1091
|
+
// counter and timestamp from the same dispatch, rather than retaining a prior higher label.
|
|
1092
|
+
const dispatch = [...events].reverse().find(event => event.taskId === t.id && event.event === "task-dispatch");
|
|
1093
|
+
const projectedPhrase = summary.blocker?.kind === "dependency-wait"
|
|
1094
|
+
? `dep-waiting on ${summary.blocker.prerequisites.join(", ")}`
|
|
1095
|
+
: summary.phase === "terminal"
|
|
1096
|
+
? st === "human" ? `parked${failureKind ? ` (${failureKind})` : ""}` : undefined
|
|
1097
|
+
: summary.phase === "preparing" && dispatch
|
|
1098
|
+
? `preparing · attempt ${(Number.isInteger(dispatch.data.attempt) ? dispatch.data.attempt : 0) + 1} since ${dispatch.ts.slice(11, 19)}`
|
|
1099
|
+
: summary.phase ?? undefined;
|
|
1100
|
+
const rawPhrase = livePhase ? phaseDetail(livePhase, now, workerLiveness.get(t.id)) : projectedPhrase;
|
|
1043
1101
|
const phrase = rawPhrase === undefined
|
|
1044
1102
|
? undefined
|
|
1045
1103
|
: journalRowsOnly
|
|
@@ -1053,12 +1111,12 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1053
1111
|
? gateSnapshot(t, events, rehashAt)
|
|
1054
1112
|
: { states: defaultGateStates(t), priorGraph: false };
|
|
1055
1113
|
const pane = panes.get(t.id);
|
|
1056
|
-
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, pane, ...gates };
|
|
1114
|
+
return { t, st, merged, failureKind, redTier, label, assignCol, isStarved, phrase, channel, ctx, livePhase, pane, summary, ...gates };
|
|
1057
1115
|
});
|
|
1058
1116
|
if (!unicode) {
|
|
1059
|
-
// machine
|
|
1060
|
-
// use
|
|
1061
|
-
const rows = cells.flatMap(({ t, st, merged, label, assignCol, livePhase, states, priorGraph, pane }) => {
|
|
1117
|
+
// Keep the machine task-title columns intact; projection details occupy their own line.
|
|
1118
|
+
// Phase-aware frames use ASCII spinners so pipes never receive terminal-only braille/ANSI.
|
|
1119
|
+
const rows = cells.flatMap(({ t, st, merged, label, assignCol, livePhase, states, priorGraph, pane, summary }) => {
|
|
1062
1120
|
const chain = gateChain(states, false);
|
|
1063
1121
|
const prefix = livePhase ? ` ${ASCII_SPINNER[animationFrame % ASCII_SPINNER.length]} ${t.id} ` : ` ${surfaceTaskBox(st, merged)} ${t.id} `;
|
|
1064
1122
|
const suffix = ` ${chain}${priorGraph ? ` ${PRIOR_GRAPH_MARKER}` : ""} ${livePhase ? "running" : surfaceStatusWord(st)}${label} ${assignCol}${pane ? ` pane ${pane}` : ""}`;
|
|
@@ -1069,6 +1127,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1069
1127
|
// (a pane name is 60 columns on its own), so the floor costs wrapping, never the graph.
|
|
1070
1128
|
return [
|
|
1071
1129
|
`${prefix}${shortGoal(t.title, Math.max(MACHINE_TITLE_FLOOR, width - prefix.length - suffix.length))}${suffix}`,
|
|
1130
|
+
` ${summaryText(summary)}`,
|
|
1072
1131
|
...recoveryLinesForTask(t.id).map((line) => ` ${line}`),
|
|
1073
1132
|
];
|
|
1074
1133
|
});
|
|
@@ -1142,7 +1201,8 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1142
1201
|
besideLockup(fillLockup(dim(versionText), versionCells), factLines[1]),
|
|
1143
1202
|
...factLines.slice(2).map((line) => `${" ".repeat(lockupCells + lockupGapCells)}${line}`),
|
|
1144
1203
|
];
|
|
1145
|
-
const
|
|
1204
|
+
const lastEvent = comparable ? events.at(-1) : undefined;
|
|
1205
|
+
const nowLine = lastEvent ? [legend(` now: ${formatJournalNarration(lastEvent)}`)] : [];
|
|
1146
1206
|
const supervisionLegend = legend(` ${supervisionText(supervision)}`);
|
|
1147
1207
|
const taskSectionSummary = `${done} of ${total} merged · rows in graph order · gates left→right in pipeline order`;
|
|
1148
1208
|
const taskSection = ` ${ok("▌")} ${title("TASKS")} ${dim(taskSectionSummary)}`;
|
|
@@ -1220,6 +1280,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1220
1280
|
` ${tone(cell.t.id)} ${dim(taskArea(cell.t))} ${sanitizeTaskText(cell.t.title)}`,
|
|
1221
1281
|
...wrapCells(` ${dim(machinery)}`, boardColumns, { continuationPrefix: " " }),
|
|
1222
1282
|
...wrapCells(` ${dim(noteFor(cell))}`, boardColumns, { continuationPrefix: " " }).filter((line) => line.trim()),
|
|
1283
|
+
...wrapCells(` ${dim(summaryText(cell.summary))}`, boardColumns, { continuationPrefix: " " }),
|
|
1223
1284
|
"",
|
|
1224
1285
|
];
|
|
1225
1286
|
});
|
|
@@ -1249,15 +1310,15 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1249
1310
|
? running
|
|
1250
1311
|
: dim;
|
|
1251
1312
|
const titleTone = cell.redTier || cell.livePhase || (effort?.parks ?? 0) >= 3 ? title : dim;
|
|
1252
|
-
return Array.from({ length: height }, (_, index) => ` ${idTone(fitColumn(columns[0][index] ?? "", idWidth, 1))}`
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1313
|
+
return [...Array.from({ length: height }, (_, index) => ` ${idTone(fitColumn(columns[0][index] ?? "", idWidth, 1))}`
|
|
1314
|
+
+ dim(fitColumn(columns[1][index] ?? "", areaWidth))
|
|
1315
|
+
+ dim(fitColumn(columns[2][index] ?? "", depsWidth))
|
|
1316
|
+
+ titleTone(fitColumn(columns[3][index] ?? "", taskWidth))
|
|
1317
|
+
+ fitCells(columns[4][index] ?? "", gatesWidth)
|
|
1318
|
+
+ " "
|
|
1319
|
+
+ dim(fitColumn(columns[5][index] ?? "", channelWidth))
|
|
1320
|
+
+ dim(fitColumn(columns[6][index] ?? "", attemptWidth, 1))
|
|
1321
|
+
+ dim(fitCells(columns[7][index] ?? "", noteWidth))), ...wrapCells(` ${dim(summaryText(cell.summary))}`, boardColumns, { continuationPrefix: " " })];
|
|
1261
1322
|
});
|
|
1262
1323
|
return [legend(headerRow), dim(ruleRow), ...rows];
|
|
1263
1324
|
};
|
|
@@ -1305,7 +1366,7 @@ const oneLine = (cwd, namedRunId) => {
|
|
|
1305
1366
|
const record = readRunRecord(cwd, graph, namedRunId);
|
|
1306
1367
|
if (!record)
|
|
1307
1368
|
return sanitizeTaskText(`tickmarkr · no runs yet · 0/${graph.tasks.length} done`);
|
|
1308
|
-
const rows =
|
|
1369
|
+
const { rows } = recordProjection(record, graph);
|
|
1309
1370
|
const claims = record.comparable
|
|
1310
1371
|
? [
|
|
1311
1372
|
`${recordedDone(recordedTasks(graph, rows), rows)}/${graph.tasks.length} done`,
|