tickmarkr 2.6.4 → 2.6.5

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.
Files changed (63) hide show
  1. package/dist/adapters/catalog-remote.d.ts +23 -0
  2. package/dist/adapters/catalog-remote.js +38 -0
  3. package/dist/adapters/claude-code.d.ts +1 -1
  4. package/dist/adapters/claude-code.js +4 -1
  5. package/dist/adapters/codex-commit-check.d.ts +25 -0
  6. package/dist/adapters/codex-commit-check.js +108 -0
  7. package/dist/adapters/model-discovery-check.d.ts +33 -0
  8. package/dist/adapters/model-discovery-check.js +123 -0
  9. package/dist/adapters/model-lints.d.ts +1 -1
  10. package/dist/adapters/model-lints.js +4 -0
  11. package/dist/adapters/prompt.js +3 -2
  12. package/dist/adapters/types.d.ts +7 -0
  13. package/dist/cli/commands/approve.js +10 -4
  14. package/dist/cli/commands/doctor.d.ts +2 -0
  15. package/dist/cli/commands/doctor.js +14 -2
  16. package/dist/cli/commands/fleet.d.ts +10 -1
  17. package/dist/cli/commands/fleet.js +55 -38
  18. package/dist/cli/commands/init.js +5 -1
  19. package/dist/cli/commands/plan.js +65 -4
  20. package/dist/cli/commands/verify.d.ts +1 -1
  21. package/dist/cli/commands/verify.js +28 -6
  22. package/dist/config/fleet-overlay.d.ts +2 -1
  23. package/dist/config/fleet-overlay.js +77 -4
  24. package/dist/drivers/orca.d.ts +19 -1
  25. package/dist/drivers/orca.js +161 -31
  26. package/dist/gates/cache.d.ts +4 -0
  27. package/dist/gates/cache.js +10 -2
  28. package/dist/gates/llm.d.ts +4 -1
  29. package/dist/gates/llm.js +6 -1
  30. package/dist/gates/review.js +8 -3
  31. package/dist/gates/run-gates.d.ts +3 -2
  32. package/dist/gates/run-gates.js +91 -50
  33. package/dist/run/daemon.js +150 -19
  34. package/dist/run/git.js +9 -0
  35. package/dist/run/journal.d.ts +6 -2
  36. package/dist/run/journal.js +22 -7
  37. package/dist/run/launchservices-check.d.ts +56 -0
  38. package/dist/run/launchservices-check.js +117 -0
  39. package/dist/run/operator-state.d.ts +22 -2
  40. package/dist/run/operator-state.js +20 -3
  41. package/dist/run/title-environment.d.ts +45 -0
  42. package/dist/run/title-environment.js +127 -0
  43. package/dist/run/title-preload.cjs +38 -0
  44. package/dist/run/title-preload.d.cts +1 -0
  45. package/dist/tui/cockpit/board.js +12 -6
  46. package/dist/tui/cockpit/keys.d.ts +8 -0
  47. package/dist/tui/cockpit/keys.js +4 -1
  48. package/dist/tui/cockpit/layout.d.ts +2 -2
  49. package/dist/tui/cockpit/layout.js +2 -2
  50. package/dist/tui/cockpit/live-runtime.js +46 -20
  51. package/dist/tui/cockpit/live-store.d.ts +24 -1
  52. package/dist/tui/cockpit/live-store.js +99 -8
  53. package/dist/tui/cockpit/log-view.d.ts +28 -0
  54. package/dist/tui/cockpit/log-view.js +60 -0
  55. package/dist/tui/cockpit/run-view.js +4 -0
  56. package/dist/tui/cockpit/shell.d.ts +2 -1
  57. package/dist/tui/cockpit/shell.js +6 -4
  58. package/dist/tui/ink/fleet-app.d.ts +12 -6
  59. package/dist/tui/ink/fleet-app.js +17 -11
  60. package/package.json +1 -1
  61. package/skills/tickmarkr-auto/SKILL.md +32 -2
  62. package/skills/tickmarkr-loop/SKILL.md +46 -2
  63. package/skills/tickmarkr-overseer/SKILL.md +83 -8
@@ -1,5 +1,5 @@
1
1
  import { type RunGraph } from "../graph/schema.js";
2
- import { type JournalEvent } from "./journal.js";
2
+ import { type JournalEvent, type OwedFold } from "./journal.js";
3
3
  /** An evidence identity is a physical journal line, never a filtered row ordinal. */
4
4
  export interface EvidenceIdentity {
5
5
  source: string;
@@ -74,6 +74,15 @@ export interface OperatorSnapshot {
74
74
  };
75
75
  latestRunEnd?: EvidenceIdentity;
76
76
  approvedResumeRequired: boolean;
77
+ /** I2: CURRENT owed-check debt from the authority this observation supplied; "unknown" when absent or stale. */
78
+ debt: number | "unknown";
79
+ outstanding: readonly string[];
80
+ }
81
+ /** I2: foldOwedChecks over the complete effective journal, tagged with the journal basis it read. A result
82
+ * whose basis is not the observation's own is stale and counts as unknown debt, never as zero. */
83
+ export interface OwedAuthority {
84
+ basis: string;
85
+ fold: OwedFold;
77
86
  }
78
87
  /** Author channels recorded on a task-done row. A row written before authors were recorded carries none and
79
88
  * projects nothing (never the dispatch); unattributed preserved work arrives as an explicit "unknown" entry. */
@@ -99,12 +108,20 @@ export declare class OperatorStateFold {
99
108
  private total;
100
109
  apply(record: OperatorRecord): void;
101
110
  private task;
102
- snapshot({ graph, sequence, observedAt, readable, graphAvailability }?: {
111
+ /** A run-end is the latest lifecycle boundary: the only state whose headline consults owed-check debt. */
112
+ get ended(): boolean;
113
+ /** `daemonDead`: this run's lock holder was observed provably dead (lock.ts liveness). It is an observation,
114
+ * not a journal row, so it settles the projection only and the fold keeps no trace of it.
115
+ * `owed`/`basis` (I2): COMPLETE needs a known-empty debt computed over this very journal basis. */
116
+ snapshot({ graph, sequence, observedAt, readable, graphAvailability, daemonDead, owed, basis }?: {
103
117
  graph?: RunGraph;
104
118
  sequence?: number;
105
119
  observedAt?: number;
106
120
  readable?: boolean;
107
121
  graphAvailability?: OperatorSnapshot["graphAvailability"];
122
+ daemonDead?: boolean;
123
+ owed?: OwedAuthority;
124
+ basis?: string;
108
125
  }): OperatorSnapshot;
109
126
  private comparableTo;
110
127
  }
@@ -116,4 +133,7 @@ export declare function readOperatorState({ events, source, ...options }: {
116
133
  sequence?: number;
117
134
  observedAt?: number;
118
135
  readable?: boolean;
136
+ daemonDead?: boolean;
137
+ owed?: OwedAuthority;
138
+ basis?: string;
119
139
  }): OperatorSnapshot;
@@ -5,6 +5,9 @@ const bucketNames = ["failed", "human", "blocked", "pending"];
5
5
  const strings = (value) => Array.isArray(value) && value.every(v => typeof v === "string") ? [...value] : undefined;
6
6
  const identity = ({ source, line, id, generation }) => ({ source, line, id, generation });
7
7
  const emptyGates = () => Object.fromEntries(GATE_NAMES.map(g => [g, { state: "not-run" }]));
8
+ /** I1: at a terminal boundary nothing will finish a queued or running cell, so it reads unknown (keeping the
9
+ * row that started it); a measured passed/failed cell is never touched. Returns copies. */
10
+ const settled = (gates, settle = true) => Object.fromEntries(Object.entries(gates).map(([g, cell]) => [g, settle && (cell.state === "queued" || cell.state === "running") ? { ...cell, state: "unknown" } : { ...cell }]));
8
11
  /** Author channels recorded on a task-done row. A row written before authors were recorded carries none and
9
12
  * projects nothing (never the dispatch); unattributed preserved work arrives as an explicit "unknown" entry. */
10
13
  export const doneAuthors = (data) => strings(data.authors);
@@ -60,6 +63,10 @@ export class OperatorStateFold {
60
63
  };
61
64
  this.active = false;
62
65
  this.approved = false;
66
+ for (const t of this.tasks.values()) {
67
+ t.gates = settled(t.gates);
68
+ t.gateActivity = undefined;
69
+ }
63
70
  for (const key of bucketNames)
64
71
  for (const id of this.end.buckets[key] ?? []) {
65
72
  const task = this.task(id);
@@ -132,6 +139,8 @@ export class OperatorStateFold {
132
139
  t.gateActivity = undefined;
133
140
  if (["task-done", "merge", "task-failed", "task-human", "task-blocked"].includes(e.event))
134
141
  t.gateActivity = undefined;
142
+ if (["task-failed", "task-human", "task-blocked"].includes(e.event))
143
+ t.gates = settled(t.gates);
135
144
  const phaseGate = e.event === "phase-start" && typeof e.data.phase === "string"
136
145
  ? e.data.phase.startsWith("gate:") ? e.data.phase.slice(5) : e.data.phase === "judge" ? "acceptance" : e.data.phase === "review" ? "review" : undefined
137
146
  : undefined;
@@ -162,13 +171,18 @@ export class OperatorStateFold {
162
171
  }
163
172
  return t;
164
173
  }
165
- snapshot({ graph, sequence = 0, observedAt = Date.now(), readable = true, graphAvailability } = {}) {
174
+ /** A run-end is the latest lifecycle boundary: the only state whose headline consults owed-check debt. */
175
+ get ended() { return !!this.end && !this.active; }
176
+ /** `daemonDead`: this run's lock holder was observed provably dead (lock.ts liveness). It is an observation,
177
+ * not a journal row, so it settles the projection only and the fold keeps no trace of it.
178
+ * `owed`/`basis` (I2): COMPLETE needs a known-empty debt computed over this very journal basis. */
179
+ snapshot({ graph, sequence = 0, observedAt = Date.now(), readable = true, graphAvailability, daemonDead = false, owed, basis } = {}) {
166
180
  const hash = graph && graphDefinitionHash(graph);
167
181
  const comparable = this.comparableTo(hash);
168
182
  const ids = comparable ? [...graph.tasks.map(t => t.id), ...[...this.tasks.keys()].filter(id => !graph.tasks.some(t => t.id === id))] : [...this.tasks.keys()];
169
183
  const tasks = ids.map(id => {
170
184
  const t = this.tasks.get(id) ?? { id, state: "unknown", merged: false, dispatches: 0, reviewRounds: 0, parks: 0, gates: emptyGates() };
171
- return { ...t, title: comparable ? graph.tasks.find(g => g.id === id)?.title : undefined, gates: Object.fromEntries(Object.entries(t.gates).map(([g, cell]) => [g, { ...cell }])) };
185
+ return { ...t, title: comparable ? graph.tasks.find(g => g.id === id)?.title : undefined, gates: settled(t.gates, daemonDead), ...(daemonDead ? { gateActivity: undefined } : {}) };
172
186
  });
173
187
  const buckets = Object.fromEntries(bucketNames.map(k => [k, this.end?.buckets[k] === undefined ? undefined : [...this.end.buckets[k]]]));
174
188
  // A matching graph contributes planned tasks even when the journal has never named them.
@@ -179,9 +193,11 @@ export class OperatorStateFold {
179
193
  const unresolved = tasks.some(t => ["unknown", "failed", "human", "blocked", "pending", "running"].includes(t.state));
180
194
  const allPlannedMerged = comparable && merged === planned;
181
195
  const currentTip = this.active ? "pending" : this.tipFailed ? "failed" : this.end?.tip ?? "unknown";
196
+ const current = owed !== undefined && basis !== undefined && owed.basis === basis && owed.fold.known && typeof owed.fold.debt === "number";
197
+ const debt = current ? owed.fold.debt : "unknown";
182
198
  const green = readable && comparable && allPlannedMerged && !!this.start && !!this.end && !this.approved && !unresolved
183
199
  && (currentTip === "passed" || currentTip === "not required")
184
- && bucketNames.every(k => buckets[k]?.length === 0);
200
+ && bucketNames.every(k => buckets[k]?.length === 0) && debt === 0;
185
201
  const lifecycle = !readable || !this.start ? "UNKNOWN" : this.active ? "RUNNING" : this.approved ? "APPROVED" : green ? "COMPLETE" : this.end ? "PARTIAL" : "PENDING";
186
202
  return {
187
203
  sequence, observedAt, lastEventAt: this.lastEventAt, firstEventAt: this.firstEventAt, resumes: this.resumes, escalations: this.escalations, lifecycle,
@@ -191,6 +207,7 @@ export class OperatorStateFold {
191
207
  planned, tasks, buckets,
192
208
  gatesRan: { passed: this.passed, total: this.total }, latestRunEnd: this.end?.evidence,
193
209
  approvedResumeRequired: this.approved,
210
+ debt, outstanding: current ? owed.fold.outstanding.map(o => `${o.taskId} ${o.gate}`) : [],
194
211
  };
195
212
  }
196
213
  comparableTo(hash) {
@@ -0,0 +1,45 @@
1
+ import { spawnSync } from "node:child_process";
2
+ /**
3
+ * A2 (D-787): the process-title mode every shell child receives. On Darwin, children get one
4
+ * `--require` of the shipped title-preload.cjs appended to their NODE_OPTIONS, so a Node child's
5
+ * process.title writes stay in JavaScript instead of each checking in with LaunchServices (npm's own
6
+ * entry keeps its native, secret-hiding title — see title-preload.cts). Everything here fails OPEN to
7
+ * the native mode, which is exactly what children received before A2: another platform, the operator's
8
+ * opt-out, a missing artifact, inherited options the preflight cannot vouch for, or a preflight the
9
+ * runner's own node refused.
10
+ *
11
+ * `shell` (git.ts) applies this to the child it spawns and `runnerInputsHash` (cache.ts) hashes the same
12
+ * value, so a cached verdict or baseline is only ever answered by one produced under the identical mode
13
+ * and preload content.
14
+ */
15
+ export declare const NATIVE_PROCESS_TITLE_ENV = "TICKMARKR_NATIVE_PROCESS_TITLE";
16
+ /** The preflight's kill ceiling, never a predicted duration: one `node -e ""` start. */
17
+ export declare const TITLE_PREFLIGHT_TIMEOUT_MS = 10000;
18
+ export type TitleMode = "native" | "preload";
19
+ export interface TitleEnvironment {
20
+ mode: TitleMode;
21
+ /** Content identity of the preload a child loads; null in native mode. */
22
+ identity: string | null;
23
+ /** The NODE_OPTIONS the child receives: the inherited value less any managed preload in native mode;
24
+ * undefined when nothing is left, so the child's NODE_OPTIONS is unset rather than empty. */
25
+ nodeOptions: string | undefined;
26
+ }
27
+ interface TitleSeams {
28
+ platform: NodeJS.Platform;
29
+ preloadPath: string;
30
+ /** Undefined until a test sets it: the standard library binding is read at CALL time, so a suite that
31
+ * mocks `node:child_process` without `spawnSync` still imports this module. */
32
+ probe?: typeof spawnSync;
33
+ }
34
+ export declare const setTitleEnvironmentForTests: (overrides: Partial<TitleSeams>) => void;
35
+ export declare const resetTitleEnvironmentForTests: () => void;
36
+ /** NODE_OPTIONS splits on spaces outside double quotes; inside them a backslash escapes `"` and `\`. */
37
+ export declare const quoteNodeOption: (value: string) => string;
38
+ /**
39
+ * The title mode a child spawned from `env` receives, and the NODE_OPTIONS that carries it. A nested shell
40
+ * inherits its parent's preload, so every mode first removes it: native gets none (the opt-out, options the
41
+ * probe cannot vouch for, a refusal or a missing artifact must hold in a nested shell too) and preload mode
42
+ * exactly one, this one.
43
+ */
44
+ export declare function titleEnvironment(env: NodeJS.ProcessEnv): TitleEnvironment;
45
+ export {};
@@ -0,0 +1,127 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ /**
7
+ * A2 (D-787): the process-title mode every shell child receives. On Darwin, children get one
8
+ * `--require` of the shipped title-preload.cjs appended to their NODE_OPTIONS, so a Node child's
9
+ * process.title writes stay in JavaScript instead of each checking in with LaunchServices (npm's own
10
+ * entry keeps its native, secret-hiding title — see title-preload.cts). Everything here fails OPEN to
11
+ * the native mode, which is exactly what children received before A2: another platform, the operator's
12
+ * opt-out, a missing artifact, inherited options the preflight cannot vouch for, or a preflight the
13
+ * runner's own node refused.
14
+ *
15
+ * `shell` (git.ts) applies this to the child it spawns and `runnerInputsHash` (cache.ts) hashes the same
16
+ * value, so a cached verdict or baseline is only ever answered by one produced under the identical mode
17
+ * and preload content.
18
+ */
19
+ export const NATIVE_PROCESS_TITLE_ENV = "TICKMARKR_NATIVE_PROCESS_TITLE";
20
+ /** The preflight's kill ceiling, never a predicted duration: one `node -e ""` start. */
21
+ export const TITLE_PREFLIGHT_TIMEOUT_MS = 10_000;
22
+ const shipped = () => ({
23
+ platform: process.platform,
24
+ preloadPath: fileURLToPath(new URL("./title-preload.cjs", import.meta.url)),
25
+ });
26
+ let seams = shipped();
27
+ const preflights = new Map();
28
+ export const setTitleEnvironmentForTests = (overrides) => {
29
+ seams = { ...seams, ...overrides };
30
+ preflights.clear();
31
+ };
32
+ export const resetTitleEnvironmentForTests = () => {
33
+ seams = shipped();
34
+ preflights.clear();
35
+ };
36
+ /** NODE_OPTIONS splits on spaces outside double quotes; inside them a backslash escapes `"` and `\`. */
37
+ export const quoteNodeOption = (value) => `"${value.replace(/[\\"]/g, "\\$&")}"`;
38
+ /** The one option this install adds: its preload, at an absolute path. */
39
+ const preloadOption = () => `--require ${quoteNodeOption(seams.preloadPath)}`;
40
+ /**
41
+ * D-827: does the runner's own node load this preload? Probed at most once per process per (node execPath,
42
+ * preload content identity, mode) — neither the operator's options nor a payload's cwd key it — so a
43
+ * refusal is sticky for that triple. The probe certifies only the context it reproduces: the preload alone,
44
+ * at its absolute path, run in the preload's own directory. `certifiable` sends every payload whose own
45
+ * options could change how that loads — the permission model, other code loaders, a policy or config file,
46
+ * any cwd-relative path — to native before any probe, so what reaches a payload loads the same from the
47
+ * probe's cwd and from the payload's (D-835: an absolute preload with none of those certifies every cwd).
48
+ * A bounded synchronous probe that never goes through `shell` or its spawn seam, so payload start timing,
49
+ * recorded spawn commands and the lease census see payloads only. A refusal — nonzero exit, signal, the ceiling, or a spawn error — selects native payloads without
50
+ * re-probing; a payload is never started twice to find out.
51
+ */
52
+ function preflight(env, identity) {
53
+ const key = [process.execPath, identity, "preload"].join("\0");
54
+ let accepted = preflights.get(key);
55
+ if (accepted === undefined) {
56
+ try {
57
+ const probe = (seams.probe ?? spawnSync)(process.execPath, ["-e", ""], {
58
+ cwd: dirname(seams.preloadPath), env: { ...env, NODE_OPTIONS: preloadOption() },
59
+ stdio: "ignore", timeout: TITLE_PREFLIGHT_TIMEOUT_MS, killSignal: "SIGKILL",
60
+ });
61
+ accepted = probe.status === 0 && !probe.error;
62
+ }
63
+ catch {
64
+ accepted = false;
65
+ }
66
+ preflights.set(key, accepted);
67
+ }
68
+ return accepted;
69
+ }
70
+ /** NODE_OPTIONS as Node splits it: on spaces outside double quotes, each quoted segment unescaped. */
71
+ const optionTokens = (options) => (options.match(/(?:[^\s"]|"(?:[^"\\]|\\.)*")+/g) ?? [])
72
+ .map((token) => token.replace(/"((?:[^"\\]|\\.)*)"/g, (_, quoted) => quoted.replace(/\\(.)/g, "$1")));
73
+ /** Node accepts `_` for `-` in an option's name (`--experimental_permission` is Node 20's permission flag), so
74
+ * a name is classified in its hyphen spelling; the value after `=` is left as written. */
75
+ const hyphenated = (token) => token.replace(/^--[^=]*/, (name) => name.replace(/_/g, "-"));
76
+ /** Options that gate file access, load other code, or read a policy or config file. */
77
+ const LOADING_OPTION = /^(?:-r|--(?:require|import|(?:experimental-)?loader|(?:experimental-)?permission[\w-]*|allow-[\w-]+|(?:experimental-)?policy[\w-]*|experimental-(?:default-)?config-file))(?:=|$)/;
78
+ /** A path a cwd resolves: not absolute, and a directory step or a leading `.` or `~` (Node expands neither). */
79
+ const cwdRelative = (value) => !value.startsWith("/") && /^[.~]|\//.test(value);
80
+ /** Can the one canonical probe vouch for a payload under the operator's own options? No when any token
81
+ * loads, permits or resolves against the cwd — a separate value token or an `=` value alike. */
82
+ const certifiable = (own) => optionTokens(own ?? "").every((token) => !LOADING_OPTION.test(hyphenated(token)) && !cwdRelative(/^-[^=]*=/.test(token) ? token.slice(token.indexOf("=") + 1) : token));
83
+ /** One `--require "<path>"` option in the quoted form quoteNodeOption writes. */
84
+ const QUOTED_REQUIRE = /(?:^|\s+)--require "((?:[^"\\]|\\.)*)"(?=\s|$)/g;
85
+ /** A tickmarkr-managed preload, positively identified: this install's, or another install's shipped
86
+ * `dist/run/title-preload.cjs` whose package.json names it tickmarkr (a nested shell under another install).
87
+ * A user's own file of the same name is never one, so their hook keeps running in every mode. */
88
+ const managed = (path) => {
89
+ if (path === seams.preloadPath)
90
+ return true;
91
+ if (!/[\\/]dist[\\/]run[\\/]title-preload\.cjs$/.test(path))
92
+ return false;
93
+ try {
94
+ return JSON.parse(readFileSync(join(dirname(path), "../../package.json"), "utf8")).name === "tickmarkr";
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ };
100
+ /** The operator's own options: every inherited managed preload removed; every other option byte-identical,
101
+ * and the value untouched without one. */
102
+ const withoutManaged = (options) => {
103
+ const stripped = options?.replace(QUOTED_REQUIRE, (whole, quoted) => managed(quoted.replace(/\\(.)/g, "$1")) ? "" : whole);
104
+ return stripped === options ? options : stripped.trim() || undefined;
105
+ };
106
+ /**
107
+ * The title mode a child spawned from `env` receives, and the NODE_OPTIONS that carries it. A nested shell
108
+ * inherits its parent's preload, so every mode first removes it: native gets none (the opt-out, options the
109
+ * probe cannot vouch for, a refusal or a missing artifact must hold in a nested shell too) and preload mode
110
+ * exactly one, this one.
111
+ */
112
+ export function titleEnvironment(env) {
113
+ const own = withoutManaged(env.NODE_OPTIONS);
114
+ const native = { mode: "native", identity: null, nodeOptions: own };
115
+ if (seams.platform !== "darwin" || env[NATIVE_PROCESS_TITLE_ENV] === "1" || !certifiable(own))
116
+ return native;
117
+ let identity;
118
+ try {
119
+ identity = createHash("sha256").update(readFileSync(seams.preloadPath)).digest("hex").slice(0, 16);
120
+ }
121
+ catch {
122
+ return native; // a missing or unreadable artifact keeps the native setter
123
+ }
124
+ if (!preflight(env, identity))
125
+ return native;
126
+ return { mode: "preload", identity, nodeOptions: [own?.trim(), preloadOption()].filter(Boolean).join(" ") };
127
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // A2 (D-787): loaded through NODE_OPTIONS into Darwin shell children (title-environment.ts decides
4
+ // when). Node's native process.title setter checks in with LaunchServices on every write, and test
5
+ // runners write it per worker; a burst of those check-ins has restarted launchservicesd. Here a title
6
+ // write stays in JavaScript, so `ps` keeps the original command line.
7
+ //
8
+ // npm is the exception: its entry sets the native title first thing so `ps` never shows secrets given
9
+ // on its command line, and that hiding must survive. The entry is recognised by the script it runs —
10
+ // `bin/npm-cli.js` / `bin/npx-cli.js` in every npm major's package layout, or corepack's npm/npx shim —
11
+ // resolved through symlinks, since the `npm` command on PATH is a link to it.
12
+ //
13
+ // CommonJS (.cts → .cjs) so `--require` loads it in every supported Node, ESM package or not. It never
14
+ // throws: a preload that throws kills the payload it was meant to protect.
15
+ const node_fs_1 = require("node:fs");
16
+ const NPM_ENTRY = /[\\/](?:bin[\\/]np[mx]-cli|corepack[\\/]dist[\\/]np[mx])\.js$/;
17
+ // The script node was asked to run; an unresolvable one (`node -e`, a deleted file) is simply not npm.
18
+ const entry = () => {
19
+ try {
20
+ return (0, node_fs_1.realpathSync)(process.argv[1]);
21
+ }
22
+ catch {
23
+ return process.argv[1] ?? "";
24
+ }
25
+ };
26
+ try {
27
+ const native = Object.getOwnPropertyDescriptor(process, "title");
28
+ if (native?.configurable && !NPM_ENTRY.test(entry())) {
29
+ let title = process.title;
30
+ Object.defineProperty(process, "title", {
31
+ configurable: true,
32
+ enumerable: native.enumerable,
33
+ get: () => title,
34
+ set: (value) => { title = String(value); },
35
+ });
36
+ }
37
+ }
38
+ catch { /* fail open: the native setter stays */ }
@@ -0,0 +1 @@
1
+ export {};
@@ -136,13 +136,16 @@ export function boardFrame(input, width) {
136
136
  const unknown = buckets.filter(([, v]) => v === undefined).map(([k]) => k);
137
137
  const parked = buckets.filter(([, v]) => v !== undefined && v.length > 0).map(([k, v]) => `${k} ${v.join(",")}`);
138
138
  const tally = `${doneIds.size} merged · ${(s.buckets.human ?? []).length} parked · ${(s.buckets.blocked ?? []).length} dep-blocked`;
139
- const green = !unknown.length && !parked.length && (s.currentTip === "passed" || s.currentTip === "not required");
139
+ // I2: the same CURRENT owed-check authority as the lifecycle — debt must be known and empty.
140
+ const owed = s.debt === 0 ? undefined : s.debt === "unknown" ? "outstanding unknown" : `outstanding ${s.debt} (${s.outstanding.join(", ")})`;
141
+ const tipOk = s.currentTip === "passed" || s.currentTip === "not required";
142
+ const green = !unknown.length && !parked.length && tipOk && !owed;
140
143
  const now = green
141
144
  ? `${pass("RUN ENDED — GREEN")} ${mute(`tip ${s.currentTip} · ${tally}`)}`
142
145
  : s.currentTip === "failed"
143
146
  ? `${fail("RUN ENDED — TIP VERIFY FAILED")} ${mute(tally)}`
144
- : parked.length || unknown.length
145
- ? `${fail("RUN ENDED — NOT GREEN")} ${mute(`${[...parked, ...unknown.map((k) => `${k} bucket unknown`)].join(" · ")} · ${tally}`)}`
147
+ : parked.length || unknown.length || tipOk
148
+ ? `${fail("RUN ENDED — NOT GREEN")} ${mute(`${[...parked, ...unknown.map((k) => `${k} bucket unknown`), ...(owed ? [owed] : [])].join(" · ")} · ${tally}`)}`
146
149
  : `${warn("RUN ENDED — TIP NOT VERIFIED")} ${mute(`tip ${s.currentTip} · ${tally}`)}`;
147
150
  top.push(` ${accent("▌")} ${bold("NOW")} ${now}`);
148
151
  top.push("");
@@ -152,8 +155,11 @@ export function boardFrame(input, width) {
152
155
  top.push("");
153
156
  // ── aligned gate header: each label truncated to the cell it owns (prototype :382-394) ──
154
157
  const elastic = Math.max(30, W - BOARD_FIXED - BOARD_NOTE_RESERVE);
155
- const LBL = Math.min(46, Math.max(18, Math.round(elastic * 0.62)));
156
- const CHAN = Math.min(26, Math.max(12, elastic - LBL));
158
+ const compactLabel = Math.min(46, Math.max(18, Math.round(elastic * 0.62)));
159
+ const stacked = boardBand(W) === "stacked";
160
+ const LBL = stacked ? Math.max(0, W - 4 - 5 - BOARD_AREA_W - BOARD_DEPS_W)
161
+ : compactLabel;
162
+ const CHAN = Math.min(26, Math.max(12, elastic - compactLabel));
157
163
  const gateHead = BOARD_GATES.map((g) => faint(pad(g.slice(0, BOARD_CELL - 1), BOARD_CELL))).join("");
158
164
  const gateHeader = {
159
165
  head: ` ${pad("", 5)}${pad(faint("area"), BOARD_AREA_W)}${pad(faint("deps"), BOARD_DEPS_W)}${pad(faint("task"), LBL)}`,
@@ -200,7 +206,7 @@ export function boardFrame(input, width) {
200
206
  ? depText
201
207
  : depIds.length > 1 ? `${depIds[0]}+${depIds.length - 1}` : `${depIds[0].slice(0, BOARD_DEPS_W - 3)}…`;
202
208
  const deps = pad(faint(depLabel), BOARD_DEPS_W);
203
- const titleText = sliceCells(g.title ?? "", LBL - 2).head;
209
+ const titleText = sliceCells(g.title ?? "", stacked ? LBL : LBL - 2).head;
204
210
  const title = hot ? body(pad(titleText, LBL)) : faint(pad(titleText, LBL));
205
211
  const note = [
206
212
  parks >= 3 ? park(`◍ parked ×${parks}`) : "",
@@ -259,6 +259,10 @@ export declare const SHELL_BINDINGS: readonly [{
259
259
  readonly key: "5";
260
260
  readonly label: "Evidence";
261
261
  readonly action: "evidence";
262
+ }, {
263
+ readonly key: "7";
264
+ readonly label: "Log";
265
+ readonly action: "log";
262
266
  }, {
263
267
  readonly key: "Tab";
264
268
  readonly label: "Focus";
@@ -322,6 +326,10 @@ export declare function shellBindings(context: {
322
326
  readonly key: "5";
323
327
  readonly label: "Evidence";
324
328
  readonly action: "evidence";
329
+ } | {
330
+ readonly key: "7";
331
+ readonly label: "Log";
332
+ readonly action: "log";
325
333
  } | {
326
334
  readonly key: "Tab";
327
335
  readonly label: "Focus";
@@ -637,6 +637,7 @@ export const SHELL_BINDINGS = [
637
637
  { key: "1", label: "Home", action: "home" },
638
638
  { key: "4", label: "Run", action: "run" },
639
639
  { key: "5", label: "Evidence", action: "evidence" },
640
+ { key: "7", label: "Log", action: "log" },
640
641
  { key: "Tab", label: "Focus", action: "focus" },
641
642
  { key: "Enter", label: "Open", action: "open" },
642
643
  { key: "a", label: "Actions", action: "actions" },
@@ -658,7 +659,9 @@ export function shellBindings(context) {
658
659
  return context.canResize === true;
659
660
  if (binding.action === "export")
660
661
  return context.view === "evidence";
661
- if (binding.action === "filter" || binding.action === "follow")
662
+ if (binding.action === "follow")
663
+ return context.view === "log" || context.view === "evidence" && !context.evidenceSection;
664
+ if (binding.action === "filter")
662
665
  return context.view === "evidence" && !context.evidenceSection;
663
666
  return true;
664
667
  });
@@ -306,8 +306,8 @@ export declare function planFrame(size: MeasuredSize, view?: FrameView, state?:
306
306
  export declare function resolveCockpitLayout(columns: number, rows: number): CockpitLayout;
307
307
  /**
308
308
  * FINAL §3.1: one geometry for paint, focus and pointer hit testing. A ZERO shortcut budget is the
309
- * rail-less mount (BD-1, RULING-231-19 §2): the daemon-placed board is a one-view observer, so it
310
- * plans no rail and no shortcuts at any width — the budget is honoured, never floored back to 20.
309
+ * explicit compact mount: it plans no rail and no shortcuts at any width. Daemon-owned and manual
310
+ * boards both use the default budget; an explicit zero is honoured, never floored back to 20.
311
311
  */
312
312
  export declare function planShell(columns: number, rows: number, shortcutColumns?: number): {
313
313
  columns: number;
@@ -391,8 +391,8 @@ export function resolveCockpitLayout(columns, rows) {
391
391
  }
392
392
  /**
393
393
  * FINAL §3.1: one geometry for paint, focus and pointer hit testing. A ZERO shortcut budget is the
394
- * rail-less mount (BD-1, RULING-231-19 §2): the daemon-placed board is a one-view observer, so it
395
- * plans no rail and no shortcuts at any width — the budget is honoured, never floored back to 20.
394
+ * explicit compact mount: it plans no rail and no shortcuts at any width. Daemon-owned and manual
395
+ * boards both use the default budget; an explicit zero is honoured, never floored back to 20.
396
396
  */
397
397
  export function planShell(columns, rows, shortcutColumns = 22) {
398
398
  columns = Math.max(0, Math.floor(columns));
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { existsSync, 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";
@@ -7,13 +7,14 @@ import { estimateCosts } from "../../report/cost.js";
7
7
  import { PerformanceObserver, performance } from "node:perf_hooks";
8
8
  import { render, useInput, useStdin } from "ink";
9
9
  import { useLayoutEffect, useSyncExternalStore } from "react";
10
- import { createLiveStore } from "./live-store.js";
10
+ import { createLiveStore, journalBasis, readObservedJournal } from "./live-store.js";
11
11
  import { HomeView, deriveHomeView, selectNeedsYouTarget } from "./home-view.js";
12
12
  import { RunView, applyRunViewKey, initialRunViewSession, runGateCells, evidenceLookup } from "./run-view.js";
13
13
  import { renderBoardLines, stripBoardAnsi } from "./board.js";
14
14
  import { EvidenceView, deriveEvidenceView, defaultExportPath, planEvidenceExport, writeEvidenceExport } from "./evidence-view.js";
15
15
  import { ShellGridFlow, ShellJournalSelection } from "./components.js";
16
16
  import { CockpitShell, initialShellState } from "./shell.js";
17
+ import { LogView, readLogPage, moveLogEnd } from "./log-view.js";
17
18
  import { planShell } from "./layout.js";
18
19
  import { shellBindings } from "./keys.js";
19
20
  import { createPointerReportReader, POINTER_TRACKING_ON, POINTER_TRACKING_OFF } from "./pointer.js";
@@ -21,7 +22,7 @@ import { deriveRunDecisions, previewDecision, executeDecision, withDecisionPrevi
21
22
  import { approvalRunOwner } from "../../cli/commands/approve.js";
22
23
  import { GLYPHS } from "../../brand.js";
23
24
  import { effectiveEvents, Journal, parseJournalText, physicalLine } from "../../run/journal.js";
24
- import { observeNamedRun, WATCH_OWNER_ENV } from "../../run/supervision.js";
25
+ import { observeNamedRun } from "../../run/supervision.js";
25
26
  import { formatOwnedName } from "../../drivers/types.js";
26
27
  import { readOperatorState } from "../../run/operator-state.js";
27
28
  import { cellWidth, wrapCells } from "./width.js";
@@ -59,6 +60,10 @@ export function borrowRuntimeTimeline() {
59
60
  }
60
61
  export const SHELL_TERMINAL_ENTER = "\x1b[?1049h\x1b[22;0t\x1b]0;tickmarkr cockpit\x07" + POINTER_TRACKING_ON;
61
62
  export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25h\x1b[?1049l";
63
+ /** Rows not bound to the store's observation: only the store's own observation is served, with debt unknown
64
+ * (the fold's formula then gives PARTIAL for a headline that was green — green implies ended and unapproved). */
65
+ const unbound = (snap) => ({ ...snap, owed: undefined, operator: { ...snap.operator, debt: "unknown", outstanding: [],
66
+ green: false, ...(snap.operator.green ? { lifecycle: "PARTIAL", label: "PARTIAL" } : {}) } });
62
67
  /**
63
68
  * OBS-1178: the store's incremental fold reads every task-approved row as a release. A refused or unsound
64
69
  * decision released nothing, so whenever the rows the store has read hold one — whatever state the raw
@@ -70,36 +75,48 @@ export const SHELL_TERMINAL_RESTORE = POINTER_TRACKING_OFF + "\x1b[23;0t\x1b[?25
70
75
  */
71
76
  export function decidedLiveStore(store) {
72
77
  let key;
73
- let operator;
78
+ let events;
79
+ // I2: the refold is keyed by the store's owed authority too, so a debt the store re-folded (an artifact
80
+ // removed or altered under unchanged journal bytes) is never served from an earlier observation's refold.
81
+ let folded;
74
82
  const decided = (snap) => {
75
83
  const { journal, graph } = snap;
76
- const observed = `${journal.source}:${journal.generation}:${journal.offset}:${graph.identity}:${graph.status}`;
84
+ const observed = `${journalBasis(journal)}:${graph.identity}:${graph.status}:${snap.lock.state}`;
77
85
  if (observed !== key) {
78
86
  key = observed;
79
- operator = undefined;
87
+ events = undefined;
88
+ folded = undefined;
80
89
  let bytes;
90
+ // I2 (D-857): the refold reads the very journal version the store observed — the one its owed authority
91
+ // and basis name. A changed or failed read is never cached (un-keyed, re-read next observation) and
92
+ // decides nothing meanwhile: debt unknown, never green.
81
93
  try {
82
- bytes = readFileSync(journal.source).subarray(0, journal.offset);
94
+ bytes = readObservedJournal(journal);
83
95
  }
84
96
  catch {
85
- return snap;
97
+ key = undefined;
98
+ return unbound(snap);
86
99
  }
87
100
  if (bytes.includes("task-approved")) {
88
101
  const rows = parseJournalText(bytes.subarray(0, bytes.lastIndexOf(10) + 1).toString("utf8"))
89
102
  .filter(e => e !== null && typeof e === "object" && typeof e.event === "string" && typeof e.data === "object" && e.data !== null);
90
103
  const kept = effectiveEvents(rows);
91
104
  if (kept.length !== rows.length) {
92
- const events = kept.map((event, i) => {
105
+ events = kept.map((event, i) => {
93
106
  const line = physicalLine(kept, i);
94
107
  return { source: journal.source, line, id: `${journal.source}#L${line}`, generation: journal.generation, event };
95
108
  });
96
- operator = readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
97
- readable: journal.status === "readable" && journal.backlogBytes === 0 });
98
109
  }
99
110
  }
100
111
  }
101
- if (!operator)
112
+ if (!events)
102
113
  return snap;
114
+ if (!folded || folded.owed !== snap.owed) {
115
+ folded = { owed: snap.owed, operator: readOperatorState({ events, graph: graph.status === "readable" ? graph.value : undefined,
116
+ readable: journal.status === "readable" && journal.backlogBytes === 0, daemonDead: snap.lock.state === "dead",
117
+ owed: snap.owed, basis: journalBasis(journal) }) };
118
+ }
119
+ const operator = folded.operator;
103
120
  const { sequence, observedAt, graphAvailability } = snap.operator;
104
121
  return { ...snap, operator: { ...operator, sequence, observedAt, graphAvailability } };
105
122
  };
@@ -147,10 +164,8 @@ export async function runConsolidatedCockpit(options) {
147
164
  const source = decidedLiveStore(store);
148
165
  if (options.observeRun !== false)
149
166
  observation = observeNamedRun(cwd, runId, options.environment);
150
- // BD-1 (RULING-231-19 §2): the daemon-placed board carries the watch owner token and mounts
151
- // rail-less — a zero shortcut budget the plan honours at every width. The manual cockpit keeps its rail.
152
- const railless = Boolean((options.environment ?? process.env)[WATCH_OWNER_ENV]);
153
- let state = { ...initialShellState(), view: options.initialView ?? "home", ...(railless ? { shortcutColumns: 0 } : {}) };
167
+ // Daemon-placed and manual boards share the same rails and navigation.
168
+ let state = { ...initialShellState(), view: options.initialView ?? "home" };
154
169
  let runSession = initialRunViewSession();
155
170
  if (options.initialParks) {
156
171
  runSession = { ...runSession, selection: Math.max(0, source.snapshot().operator.tasks.findIndex(t => t.state === "human" || t.state === "blocked")) };
@@ -202,6 +217,9 @@ export async function runConsolidatedCockpit(options) {
202
217
  snap.journal.generation, snap.journal.offset, snap.journal.status, snap.journal.malformedCount, snap.journal.backlogBytes, snap.journal.pending, snap.journal.error,
203
218
  snap.graph.identity, snap.graph.status, snap.config.identity, snap.config.status, snap.cache.identity, snap.cache.status,
204
219
  snap.lock.value, snap.lock.status, snap.lock.state, snap.lock.alive,
220
+ // I2: owed-check debt moves under unchanged journal bytes (an artifact removed or altered) and
221
+ // decides the drawn lifecycle and board headline.
222
+ snap.operator.lifecycle, snap.operator.debt, snap.operator.outstanding,
205
223
  // D-383: supervision tier state is not drawn by any view; an ARMED→STALE transition earns no
206
224
  // frame. Its UNREADABLE case already surfaces through snap.errors above.
207
225
  ]);
@@ -418,7 +436,7 @@ export async function runConsolidatedCockpit(options) {
418
436
  }
419
437
  else {
420
438
  const action = shellBindings(state).find(b => b.key === text)?.action;
421
- if (action === "home" || action === "run" || action === "evidence")
439
+ if (action === "home" || action === "run" || action === "evidence" || action === "log")
422
440
  state = { ...state, view: action, scroll: 0, evidenceSection: action === state.view ? state.evidenceSection : 0 };
423
441
  else if (action === "widen" || action === "narrow")
424
442
  state = { ...state, shortcutColumns: planShell(geometry().columns, geometry().rows, geometry().shortcuts + (action === "widen" ? 1 : -1)).shortcuts };
@@ -432,6 +450,13 @@ export async function runConsolidatedCockpit(options) {
432
450
  }
433
451
  else if (state.view === "evidence" && (k.leftArrow || k.rightArrow))
434
452
  state = { ...state, evidenceSection: ((state.evidenceSection ?? 0) + (k.rightArrow ? 1 : 4)) % 5 };
453
+ else if (state.view === "log" && (k.pageDown || k.pageUp || k.upArrow || k.downArrow || action === "follow")) {
454
+ const snap = source.snapshot();
455
+ const sourceLog = { journal: snap.journal, page: source.page, runId, columns: geometry().bodyColumns };
456
+ const delta = (k.pageDown || k.downArrow ? 1 : -1) * (k.pageDown || k.pageUp ? geometry().bodyRows : 1);
457
+ state = { ...state, logEnd: action === "follow" ? state.logEnd === undefined ? snap.journal.lines : undefined
458
+ : moveLogEnd(sourceLog, state.logEnd, delta), scroll: 0 };
459
+ }
435
460
  else if ((k.pageDown || k.pageUp) && state.view !== "run")
436
461
  state = { ...state, scroll: Math.max(0, state.scroll + (k.pageDown ? 1 : -1) * geometry().bodyRows) };
437
462
  else if (k.return || action === "open") {
@@ -471,7 +496,7 @@ export async function runConsolidatedCockpit(options) {
471
496
  else if (text === "a")
472
497
  state = { ...state, overlay: ["Existing CLI actions", "tickmarkr fleet", "tickmarkr plan", "tickmarkr doctor"], overlayOffset: 0 };
473
498
  }
474
- if (!terminal && state.view !== "run" && !state.overlay && !state.help && state.editor === undefined) {
499
+ if (!terminal && state.view !== "run" && state.view !== "log" && !state.overlay && !state.help && state.editor === undefined) {
475
500
  const bytes = k.upArrow ? "\x1b[A" : k.downArrow ? "\x1b[B" : k.rightArrow ? "\x1b[C" : k.leftArrow ? "\x1b[D" : k.pageUp ? "\x1b[5~" : k.pageDown ? "\x1b[6~" : text === "f" ? "f" : undefined;
476
501
  if (bytes)
477
502
  forwardLeaf(bytes);
@@ -700,7 +725,7 @@ export async function runConsolidatedCockpit(options) {
700
725
  const usedTasks = new Set();
701
726
  for (const row of plan.paintedRows) {
702
727
  if (row.row < plan.bodyRow && (row.text.startsWith("1 Home") || row.text.startsWith("1H"))) {
703
- for (const binding of bindings.filter(binding => ["home", "run", "evidence"].includes(binding.action))) {
728
+ for (const binding of bindings.filter(binding => ["home", "run", "evidence", "log"].includes(binding.action))) {
704
729
  const caption = plan.columns < 60 ? `${binding.key}${binding.label[0]}` : `${binding.key} ${binding.label}`;
705
730
  const index = row.text.indexOf(caption);
706
731
  if (index >= 0)
@@ -758,7 +783,8 @@ export async function runConsolidatedCockpit(options) {
758
783
  committedTargets = targets;
759
784
  }, children: state.view === "home" ? _jsx(ShellGridFlow, { width: p.bodyColumns, columns: p.columns < 120 ? 2 : 3, rowHeight: p.rows < 20 ? 1 : p.columns < 120 ? 2 : 3, children: _jsx(HomeView, { width: p.bodyColumns, model: homeModel, focused: focused, onOpenPark: id => { runSession = { ...runSession, selection: Math.max(0, snap.operator.tasks.findIndex(t => t.id === id)) }; state = { ...state, view: "run", scroll: 0 }; followSelection(); publish(); }, onOpenDiagnostic: id => { state = { ...state, overlay: [id], overlayOffset: 0 }; publish(); }, onOpenEvidence: openEvidence, onSelectNeedsYou: reportHomeNeedsYouSelection }, String(homeModel.needsYou.length > 0)) }) :
760
785
  state.view === "run" ? _jsx(RunView, { snapshot: snap.operator, rows: snap.journal.history, page: source.page, graph: snap.graph.value, decisions: visibleDecisions, session: runSession, columns: p.bodyColumns, run: approvalRunOwner(cwd, runId), now: options.now }) :
761
- _jsx(ShellJournalSelection.Provider, { value: reportEvidenceSelection, children: _jsx(EvidenceView, { width: p.bodyColumns, model: { ...model, journal: evidenceRows }, focusEvidence: selectedEvidence, focused: focused, onExport: beginExport, onSelect: id => { selectedEvidence = id; openDetail(); publish(); } }, `${state.query}:${evidenceNavigation}`) }) });
786
+ state.view === "log" ? _jsx(LogView, { page: readLogPage({ journal: snap.journal, page: source.page, runId, columns: p.bodyColumns }, p.bodyRows, state.logEnd) }) :
787
+ _jsx(ShellJournalSelection.Provider, { value: reportEvidenceSelection, children: _jsx(EvidenceView, { width: p.bodyColumns, model: { ...model, journal: evidenceRows }, focusEvidence: selectedEvidence, focused: focused, onExport: beginExport, onSelect: id => { selectedEvidence = id; openDetail(); publish(); } }, `${state.query}:${evidenceNavigation}`) }) });
762
788
  }
763
789
  app = render(_jsx(App, {}), { stdin, stdout: output, debug: options.debug, exitOnCtrlC: false, patchConsole: false });
764
790
  void app.waitUntilExit().then(() => stop(), stop);