tickmarkr 1.96.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/brand.d.ts CHANGED
@@ -26,6 +26,8 @@ export declare function paneDispatchCommand(scriptPath: string): string;
26
26
  export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
27
27
  /** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
28
28
  export declare const brand: (s: string) => string;
29
+ /** Compact product chip — black ink on the terminal theme's ANSI green. */
30
+ export declare const brandChip: (s: string) => string;
29
31
  /** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
30
32
  export declare const ok: (s: string) => string;
31
33
  /** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
@@ -39,12 +41,41 @@ export declare const bold: (s: string) => string;
39
41
  /** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
40
42
  export declare const TOKENS: {
41
43
  readonly brand: (s: string) => string;
44
+ readonly brandChip: (s: string) => string;
42
45
  readonly ok: (s: string) => string;
43
46
  readonly fail: (s: string) => string;
44
47
  readonly warn: (s: string) => string;
45
48
  readonly dim: (s: string) => string;
46
49
  readonly bold: (s: string) => string;
47
50
  };
51
+ /** Exact operator-approved live colours. Values stay hex so the authority is inspectable. */
52
+ export declare const LIVE_PALETTE: {
53
+ /** muted teal — brand identity and pass */
54
+ readonly brand: "#90C4A4";
55
+ /** cornflower blue — running state and information */
56
+ readonly running: "#5A76AE";
57
+ /** ice — primary text */
58
+ readonly text: "#E6FDFF";
59
+ /** cloud — chrome and secondary text */
60
+ readonly chrome: "#D9D7DD";
61
+ /** amethyst — attention and failure */
62
+ readonly attention: "#B07BAC";
63
+ };
64
+ export type LiveRole = keyof typeof LIVE_PALETTE;
65
+ /** Semantic live tokens. Aliases are intentional; no token introduces a sixth colour. */
66
+ export declare const LIVE: {
67
+ readonly brand: (s: string) => string;
68
+ readonly pass: (s: string) => string;
69
+ readonly running: (s: string) => string;
70
+ readonly information: (s: string) => string;
71
+ readonly text: (s: string) => string;
72
+ readonly primaryText: (s: string) => string;
73
+ readonly chrome: (s: string) => string;
74
+ readonly secondaryText: (s: string) => string;
75
+ readonly attention: (s: string) => string;
76
+ readonly failure: (s: string) => string;
77
+ readonly chip: (s: string) => string;
78
+ };
48
79
  /**
49
80
  * The glyph vocabulary — plain characters only; color layers on via tokens so
50
81
  * shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
package/dist/brand.js CHANGED
@@ -108,6 +108,8 @@ const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === u
108
108
  const sgr = (code) => (s) => visual() ? `\x1b[${code}m${s}${R}` : s;
109
109
  /** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
110
110
  export const brand = sgr(`38;5;${BRAND_RAMP[2]}`);
111
+ /** Compact product chip — black ink on the terminal theme's ANSI green. */
112
+ export const brandChip = sgr("30;42");
111
113
  /** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
112
114
  export const ok = brand;
113
115
  /** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
@@ -119,7 +121,48 @@ export const dim = sgr("2");
119
121
  /** Emphasis (titles, selection, the product name) — bold. */
120
122
  export const bold = sgr("1");
121
123
  /** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
122
- export const TOKENS = { brand, ok, fail, warn, dim, bold };
124
+ export const TOKENS = { brand, brandChip, ok, fail, warn, dim, bold };
125
+ // ── operator live-surface palette (v1.99) ───────────────────────────────────
126
+ // The tokens above remain the global design system for one-shot surfaces. LIVE is the closed
127
+ // five-colour system for long-running operator surfaces. Semantic aliases below deliberately share
128
+ // renderers: glyphs and words retain the distinction between pass/brand, running/information,
129
+ // chrome/secondary text, and attention/failure when colour is unavailable.
130
+ /** Exact operator-approved live colours. Values stay hex so the authority is inspectable. */
131
+ export const LIVE_PALETTE = {
132
+ /** muted teal — brand identity and pass */
133
+ brand: "#90C4A4",
134
+ /** cornflower blue — running state and information */
135
+ running: "#5A76AE",
136
+ /** ice — primary text */
137
+ text: "#E6FDFF",
138
+ /** cloud — chrome and secondary text */
139
+ chrome: "#D9D7DD",
140
+ /** amethyst — attention and failure */
141
+ attention: "#B07BAC",
142
+ };
143
+ const liveSgr = (hex) => {
144
+ const channels = hex.slice(1).match(/.{2}/gu).map((channel) => Number.parseInt(channel, 16));
145
+ return sgr(`38;2;${channels.join(";")}`);
146
+ };
147
+ const liveBrand = liveSgr(LIVE_PALETTE.brand);
148
+ const liveRunning = liveSgr(LIVE_PALETTE.running);
149
+ const liveText = liveSgr(LIVE_PALETTE.text);
150
+ const liveChrome = liveSgr(LIVE_PALETTE.chrome);
151
+ const liveAttention = liveSgr(LIVE_PALETTE.attention);
152
+ /** Semantic live tokens. Aliases are intentional; no token introduces a sixth colour. */
153
+ export const LIVE = {
154
+ brand: liveBrand,
155
+ pass: liveBrand,
156
+ running: liveRunning,
157
+ information: liveRunning,
158
+ text: liveText,
159
+ primaryText: liveText,
160
+ chrome: liveChrome,
161
+ secondaryText: liveChrome,
162
+ attention: liveAttention,
163
+ failure: liveAttention,
164
+ chip: liveBrand,
165
+ };
123
166
  /**
124
167
  * The glyph vocabulary — plain characters only; color layers on via tokens so
125
168
  * shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
@@ -3,8 +3,28 @@ import { parseArgs } from "node:util";
3
3
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
4
4
  import { compileSource } from "../../compile/index.js";
5
5
  import { saveGraph, stateDirName } from "../../graph/graph.js";
6
+ import { formatPriorFindingEvidence, readPriorRunEvidence } from "../../run/journal.js";
7
+ import { shGit } from "../../run/git.js";
6
8
  import { acquireRunLock, releaseRunLock } from "../../run/lock.js";
7
9
  import { harnessLine, resolveHarness } from "../harness.js";
10
+ async function mergedPendingDiagnostics(cwd, pending, merges) {
11
+ const head = await shGit("git rev-parse HEAD", cwd);
12
+ const base = head.code === 0 ? head.stdout.trim() : "";
13
+ if (!/^[0-9a-f]{40}$/i.test(base))
14
+ return [];
15
+ const lines = [];
16
+ for (const taskId of pending) {
17
+ const candidates = merges.filter((merge) => merge.taskId === taskId).reverse();
18
+ for (const merge of candidates) {
19
+ const ancestor = await shGit(`git merge-base --is-ancestor ${merge.commit} ${base}`, cwd);
20
+ if (ancestor.code !== 0)
21
+ continue;
22
+ lines.push(`${taskId}: merged in run ${merge.runId}; compiles as pending (plan not marked done) — this dispatch rebuilds it`);
23
+ break; // one pure-information line per pending task, newest reachable merge wins
24
+ }
25
+ }
26
+ return lines;
27
+ }
8
28
  // v1.89 T4: harnessFrom is the resolver's INPUT (see plan.ts); the default is the INVOKED entrypoint
9
29
  // (`process.argv[1]`, the bin symlink), never this module's own url — that names an internal module.
10
30
  export async function compile(argv, cwd = process.cwd(), harnessFrom = process.argv[1]) {
@@ -22,6 +42,11 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
22
42
  // resolve against the target repo, not the process cwd (the CLI test passes a tmp repo)
23
43
  // Both modes reach the same pure compiler; --dry-run only removes the lock/write side effect below.
24
44
  const g = compileSource(isAbsolute(src) ? src : join(cwd, src), values.type, cwd);
45
+ // One bounded read supplies both cross-run surfaces: unresolved findings below and merge facts for
46
+ // the ancestry check. Neither fact mutates the compiled graph; status and every readiness predicate
47
+ // remain the source compiler's answer.
48
+ const prior = readPriorRunEvidence(cwd, g.tasks);
49
+ const mergedPending = await mergedPendingDiagnostics(cwd, new Set(g.tasks.filter((task) => task.status === "pending").map((task) => task.id)), prior.merges);
25
50
  const stateDir = stateDirName(cwd);
26
51
  if (!values["dry-run"]) {
27
52
  // HARD-01 / Sol #3: hold the same link(2) run lock as the daemon around saveGraph so compile
@@ -41,5 +66,11 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
41
66
  const diagnostics = scopeLints.length
42
67
  ? `\nscope lints:\n${scopeLints.map((lint) => ` ! ${lint}`).join("\n")}`
43
68
  : "";
44
- return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}`;
69
+ const priorFindings = prior.findings.length
70
+ ? `\nprior-run evidence:\n${prior.findings.map((finding) => ` ${formatPriorFindingEvidence(finding)}`).join("\n")}`
71
+ : "";
72
+ const mergeHistory = mergedPending.length
73
+ ? `\nmerge history:\n${mergedPending.map((line) => ` ${line}`).join("\n")}`
74
+ : "";
75
+ return `${harnessLine(resolveHarness(harnessFrom))}\n${summary}${diagnostics}${priorFindings}${mergeHistory}`;
45
76
  }
@@ -2,8 +2,8 @@ import { loadConfig } from "../../config/config.js";
2
2
  import { pickDriver } from "../../drivers/index.js";
3
3
  import { loadGraph } from "../../graph/graph.js";
4
4
  import { formatSummary, runDaemon } from "../../run/daemon.js";
5
- import { formatJournalNarration } from "../../run/journal.js";
6
5
  import { denyPreferCollisionLine, denyPreferCollisions } from "../../route/preference.js";
6
+ import { narrationSink, bindNarration } from "./run.js";
7
7
  const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
8
8
  && s.tipVerify !== "failed";
9
9
  export async function resume(argv, cwd = process.cwd()) {
@@ -25,13 +25,19 @@ export async function resume(argv, cwd = process.cwd()) {
25
25
  if (collisions.length) {
26
26
  throw new Error(collisions.map(denyPreferCollisionLine).join("; "));
27
27
  }
28
+ const narrate = narrationSink(runId);
28
29
  const s = await runDaemon(cwd, {
29
30
  runId,
30
31
  resume: true,
31
32
  graphChanged,
32
33
  retryFailed,
33
- driver: pickDriver(cfg),
34
- narrate: (event) => console.log(formatJournalNarration(event)),
34
+ // bound to the same sink the daemon gets, so a driver-journaled recovery reaches this rail too
35
+ driver: bindNarration(pickDriver(cfg), narrate),
36
+ // v1.99 T2: the ONE narration sink — the quiet rail on a TTY, the raw journal formatter on a
37
+ // pipe. A resumed run meets the same surface a fresh one does; printing the raw formatter here
38
+ // would leave `resume` as the last place the old unfiltered dump survives. Bound to the run id
39
+ // the operator named, so a resumed run's lifecycle rows name THIS run and not a generic word.
40
+ narrate,
35
41
  });
36
42
  const out = `resumed ${s.runId} — ${formatSummary(s)}`;
37
43
  return { out, code: summaryGreen(s) ? 0 : 2 };
@@ -1,6 +1,66 @@
1
+ import type { ExecutorDriver } from "../../drivers/types.js";
1
2
  import { type JournalEvent } from "../../run/journal.js";
2
- export declare const narrationLine: (event: JournalEvent) => string;
3
+ /** Closed repetitive set the rail suppresses on a TTY. An ungated `phase-start` joins them below —
4
+ * it is a phase counter, while a phase-start CARRYING a gate is the gate start the rail draws. */
5
+ export declare const TTY_NOISE_EVENTS: readonly ["worker-contact", "worker-status"];
6
+ type RailTone = "pass" | "fail" | "attention" | "active" | "neutral";
7
+ /** Closed retained set: the short operator label and the row's default tone. Labels are the rail's
8
+ * own vocabulary — a raw journal event name is what this surface exists to stop printing. A `pass`
9
+ * or `ok` datum on the event overrides the default tone, so one gate row can read either way.
10
+ *
11
+ * MEMBERSHIP RULE — the daemon journals far more than this, and an allowlist built from whatever
12
+ * the tests happened to cover masks real events. An event earns a row when it changes what the run
13
+ * will DO next or reports an OUTCOME of it: a routing decision, a worker result, a gate start or
14
+ * verdict, a repair, an escalation, a merge, a run lifecycle step. Everything else — how the daemon
15
+ * got there (worktree setup, launch mechanics, baseline and routing lints) and every poll-time
16
+ * observation (contact reads, quota banners, held dead-verdicts, context samples) — stays off the
17
+ * rail and on the pipe, where it is byte-identical to the raw journal.
18
+ *
19
+ * Applying that rule is what put `graph-rehash` and the worker-nudge family here: a rehash is the
20
+ * operator's audited `--graph-changed` release, journaled by the resumed run through THIS sink, and
21
+ * a nudge is a decision the daemon takes on the operator's behalf (it contacts the worker and arms a
22
+ * grace deadline that force-concludes the wait), whose answered/failed/expired rows are that
23
+ * decision's outcome. Neither is mechanics, and both reach this process's narrate callback. */
24
+ export declare const RAIL_ROWS: Record<string, {
25
+ label: string;
26
+ tone: RailTone;
27
+ }>;
28
+ /**
29
+ * One rail row, or null when the TTY rail suppresses this event. Never wider than `columns`: the
30
+ * identity and the label are clipped SEPARATELY and the detail takes only what they leave, so a row
31
+ * can never wrap into a second line and can never lose its meaning to a long identity.
32
+ */
33
+ export declare function narrationRow(event: JournalEvent, runId: string, columns?: number): string | null;
34
+ /** The narration line for one event of the run named by `runId`: the raw journal formatter on a pipe
35
+ * (byte-identical, every event), the quiet rail on a TTY. Null means the rail suppressed it — the
36
+ * caller prints nothing. */
37
+ export declare const narrationLine: (event: JournalEvent, runId: string) => string | null;
38
+ /**
39
+ * The daemon's narration sink, BOUND TO THE RUN IT NARRATES: the rail on a TTY, the raw journal
40
+ * formatter on a pipe, and nothing at all for an event the rail suppressed. EVERY command that drives
41
+ * a daemon owes its narration to this sink - a second call site that prints `formatJournalNarration`
42
+ * itself is a surface where the rail does not exist, and the operator meets the old unfiltered dump
43
+ * under the newly stacked board.
44
+ *
45
+ * The binding is what puts a real identity on the run-scoped rows: the daemon's `narrate` callback is
46
+ * handed one event and nothing else, and the lifecycle events carry no run id of their own, so the
47
+ * run id can only come from the command that owns the run. Both daemon-driving commands supply it:
48
+ * `run` below mints the id it then passes to the daemon, and `resume` (src/cli/commands/resume.ts)
49
+ * carries the id the operator named.
50
+ */
51
+ export declare const narrationSink: (runId: string) => (event: JournalEvent) => void;
52
+ /**
53
+ * The run's driver, BOUND to the run's narration sink.
54
+ *
55
+ * A driver journals events of its own that the daemon never sees: `dispatch-retry` is appended by
56
+ * HerdrDriver from inside a pane recovery, through a Journal it opens itself (src/drivers/herdr.ts).
57
+ * Unbound, that event lands in the file and on the pipe while the rail — the operator's only live
58
+ * surface — stays silent about a redispatch that already happened. Both daemon-driving commands
59
+ * wrap their driver here so neither can forget the binding.
60
+ */
61
+ export declare function bindNarration<D extends ExecutorDriver>(driver: D, narrate: (event: JournalEvent) => void): D;
3
62
  export declare function run(argv: string[], cwd?: string): Promise<{
4
63
  out: string;
5
64
  code: number;
6
65
  }>;
66
+ export {};