@bli-cockpit/telemetry-core 0.1.28 → 0.1.30

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.
@@ -58,6 +58,93 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
58
58
  sessions_outside_root: z.ZodOptional<z.ZodNumber>;
59
59
  sessions_new_this_tick: z.ZodOptional<z.ZodNumber>;
60
60
  sessions_pending_upload: z.ZodOptional<z.ZodNumber>;
61
+ memory_install: z.ZodOptional<z.ZodObject<{
62
+ schema_version: z.ZodLiteral<"memory-install-receipt.v1">;
63
+ checked_at: z.ZodString;
64
+ claude: z.ZodObject<{
65
+ mcp: z.ZodEnum<{
66
+ unknown: "unknown";
67
+ ok: "ok";
68
+ stale: "stale";
69
+ missing: "missing";
70
+ }>;
71
+ hooks: z.ZodEnum<{
72
+ unknown: "unknown";
73
+ ok: "ok";
74
+ stale: "stale";
75
+ missing: "missing";
76
+ }>;
77
+ }, z.core.$strict>;
78
+ codex: z.ZodObject<{
79
+ mcp: z.ZodEnum<{
80
+ unknown: "unknown";
81
+ ok: "ok";
82
+ stale: "stale";
83
+ missing: "missing";
84
+ }>;
85
+ skill: z.ZodEnum<{
86
+ unknown: "unknown";
87
+ ok: "ok";
88
+ stale: "stale";
89
+ missing: "missing";
90
+ }>;
91
+ hooks: z.ZodEnum<{
92
+ unknown: "unknown";
93
+ ok: "ok";
94
+ stale: "stale";
95
+ missing: "missing";
96
+ needs_trust: "needs_trust";
97
+ unsupported: "unsupported";
98
+ }>;
99
+ }, z.core.$strict>;
100
+ reasons: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
101
+ bin_found: z.ZodOptional<z.ZodBoolean>;
102
+ }, z.core.$strict>>;
103
+ setup_receipt: z.ZodOptional<z.ZodObject<{
104
+ schema_version: z.ZodLiteral<"setup-receipt.v1">;
105
+ checked_at: z.ZodString;
106
+ browser: z.ZodObject<{
107
+ status: z.ZodEnum<{
108
+ unknown: "unknown";
109
+ failed: "failed";
110
+ skipped: "skipped";
111
+ ok: "ok";
112
+ stale: "stale";
113
+ missing: "missing";
114
+ needs_trust: "needs_trust";
115
+ unsupported: "unsupported";
116
+ }>;
117
+ reason: z.ZodOptional<z.ZodString>;
118
+ }, z.core.$strict>;
119
+ device: z.ZodObject<{
120
+ status: z.ZodEnum<{
121
+ unknown: "unknown";
122
+ failed: "failed";
123
+ skipped: "skipped";
124
+ ok: "ok";
125
+ stale: "stale";
126
+ missing: "missing";
127
+ needs_trust: "needs_trust";
128
+ unsupported: "unsupported";
129
+ }>;
130
+ reason: z.ZodOptional<z.ZodString>;
131
+ }, z.core.$strict>;
132
+ collector: z.ZodObject<{
133
+ autostart: z.ZodObject<{
134
+ status: z.ZodEnum<{
135
+ unknown: "unknown";
136
+ failed: "failed";
137
+ skipped: "skipped";
138
+ ok: "ok";
139
+ stale: "stale";
140
+ missing: "missing";
141
+ needs_trust: "needs_trust";
142
+ unsupported: "unsupported";
143
+ }>;
144
+ reason: z.ZodOptional<z.ZodString>;
145
+ }, z.core.$strict>;
146
+ }, z.core.$strict>;
147
+ }, z.core.$strict>>;
61
148
  }, z.core.$strict>;
62
149
  export type CollectorHeartbeat = z.infer<typeof CollectorHeartbeatSchema>;
63
150
  /**
@@ -24,6 +24,8 @@
24
24
  */
25
25
  import { z } from "zod";
26
26
  import { IsoDateTimeSchema, Sha256Schema, VersionStringSchema } from "./common.js";
27
+ import { MemoryInstallReceiptSchema } from "./memory-install-receipt.js";
28
+ import { SetupReceiptSchema } from "./setup-receipt.js";
27
29
  export const COLLECTOR_HEARTBEAT_SCHEMA_VERSION = "collector-heartbeat.v1";
28
30
  /** The most roots one machine may declare. Well above any real operator. */
29
31
  export const COLLECTOR_HEARTBEAT_MAX_ROOTS = 40;
@@ -112,6 +114,22 @@ export const CollectorHeartbeatSchema = z
112
114
  .nonnegative()
113
115
  .max(1_000_000)
114
116
  .optional(),
117
+ /**
118
+ * BLI-3729. Is BLI Memory actually switched on for both agent hosts on
119
+ * this machine? Six words computed by reading the host configs BACK, not
120
+ * by remembering what the last install wrote. Optional so a collector too
121
+ * old to compute it keeps posting — an absent receipt reads as "this
122
+ * collector predates the receipt", never as "nothing is installed".
123
+ */
124
+ memory_install: MemoryInstallReceiptSchema.optional(),
125
+ /**
126
+ * BLI-3731. "Is this machine actually connected?" in seven read-back
127
+ * words — the browser sign-in, the device session, both agent hosts and
128
+ * the scheduler. Optional so a collector too old to compute it keeps
129
+ * posting, and so a tick that sends none leaves whatever the device row
130
+ * already holds rather than blanking it.
131
+ */
132
+ setup_receipt: SetupReceiptSchema.optional(),
115
133
  })
116
134
  .strict();
117
135
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./agent-artifacts.js";
2
2
  export * from "./collector-heartbeat.js";
3
+ export * from "./setup-receipt.js";
3
4
  export * from "./common.js";
4
5
  export * from "./decision-event.js";
5
6
  export * from "./decision-event-id.js";
@@ -8,6 +9,7 @@ export * from "./evidence-reconcile.js";
8
9
  export * from "./evidence-upload.js";
9
10
  export * from "./ingest-dto.js";
10
11
  export * from "./local-config.js";
12
+ export * from "./memory-install-receipt.js";
11
13
  export * from "./paths.js";
12
14
  export * from "./privacy.js";
13
15
  export * from "./risk-flags.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./agent-artifacts.js";
2
2
  export * from "./collector-heartbeat.js";
3
+ export * from "./setup-receipt.js";
3
4
  export * from "./common.js";
4
5
  export * from "./decision-event.js";
5
6
  export * from "./decision-event-id.js";
@@ -8,6 +9,7 @@ export * from "./evidence-reconcile.js";
8
9
  export * from "./evidence-upload.js";
9
10
  export * from "./ingest-dto.js";
10
11
  export * from "./local-config.js";
12
+ export * from "./memory-install-receipt.js";
11
13
  export * from "./paths.js";
12
14
  export * from "./privacy.js";
13
15
  export * from "./risk-flags.js";
@@ -401,6 +401,8 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
401
401
  started_at: z.ZodString;
402
402
  updated_at: z.ZodOptional<z.ZodString>;
403
403
  active_ticket_id: z.ZodOptional<z.ZodString>;
404
+ tower_issue_id: z.ZodOptional<z.ZodString>;
405
+ linear_ticket_id: z.ZodOptional<z.ZodString>;
404
406
  ticket_binding_candidates: z.ZodDefault<z.ZodArray<z.ZodObject<{
405
407
  ticket_id: z.ZodString;
406
408
  binding_source: z.ZodEnum<{
@@ -0,0 +1,120 @@
1
+ /**
2
+ * THE MEMORY RECEIPT (BLI-3729) — is BLI Memory actually switched on, on this
3
+ * laptop, for both agent hosts?
4
+ *
5
+ * `cockpit memory install` already converged the two host configurations and
6
+ * already wrote a per-target `memory_install` install event. What nobody could
7
+ * answer was the fleet-wide question: "does Savina's machine have memory hooks
8
+ * at all?" The install event is a per-run receipt in an outbox; this is the
9
+ * per-MACHINE state, recomputed every sync tick by READING THE FILES BACK and
10
+ * carried on the heartbeat, so the ops board can name the machine that is
11
+ * missing a piece instead of somebody grepping logs.
12
+ *
13
+ * Six words, one per thing that can independently be absent:
14
+ *
15
+ * claude.mcp `~/.claude.json` → the `bli-memory` MCP server
16
+ * claude.hooks `~/.claude/settings.json` → SessionStart/UserPromptSubmit/Stop
17
+ * codex.mcp `~/.codex/config.toml` → `[mcp_servers.bli-memory]`
18
+ * codex.skill `~/.codex/skills/bli-memory/` → the skill files
19
+ * codex.hooks `~/.codex/hooks.json` → the same three events, USER scope
20
+ *
21
+ * `codex.hooks` is the only one with a fourth word, and it is the whole reason
22
+ * this receipt exists rather than a boolean: Codex requires a person to review
23
+ * and TRUST a non-managed command hook before it runs (`/hooks` in the CLI),
24
+ * and records that trust against the hook's current hash. So a Codex machine
25
+ * can be perfectly installed and still be running nothing. `needs_trust` is
26
+ * that state, said out loud, and it is what the intern checklist ticket is
27
+ * for. `unsupported` is the other honest word: a person who set
28
+ * `[features] hooks = false` has turned the mechanism off, which is a
29
+ * decision, not a fault.
30
+ *
31
+ * **Nothing here is a path.** A home directory names a person; this travels as
32
+ * six words and a timestamp, exactly like the root labels beside it.
33
+ */
34
+ import { z } from "zod";
35
+ export declare const MEMORY_INSTALL_RECEIPT_SCHEMA_VERSION = "memory-install-receipt.v1";
36
+ /**
37
+ * What one piece of the installation is.
38
+ *
39
+ * `ok` registered, and what is stored is what we would write
40
+ * `stale` something of ours is there, but not what we would write today
41
+ * (a hand edit, an older CLI's shape, a bin path that moved)
42
+ * `missing` nothing of ours is there
43
+ * `unknown` the file could not be read or parsed, so NEITHER of the above is
44
+ * claimed — an unreadable config is not an absent one
45
+ */
46
+ export declare const MemoryInstallPieceSchema: z.ZodEnum<{
47
+ unknown: "unknown";
48
+ ok: "ok";
49
+ stale: "stale";
50
+ missing: "missing";
51
+ }>;
52
+ export type MemoryInstallPiece = z.infer<typeof MemoryInstallPieceSchema>;
53
+ /**
54
+ * `codex.hooks` carries two more words than the others.
55
+ *
56
+ * `needs_trust` — the hooks file holds our three entries and Codex has not
57
+ * been told to trust them yet. The hooks are INSTALLED and NOT RUNNING, which
58
+ * is a state no other piece here has.
59
+ * `unsupported` — hooks are turned off in this person's `config.toml`
60
+ * (`[features] hooks = false`, or the deprecated `codex_hooks = false` alias).
61
+ */
62
+ export declare const MemoryInstallHooksPieceSchema: z.ZodEnum<{
63
+ unknown: "unknown";
64
+ ok: "ok";
65
+ stale: "stale";
66
+ missing: "missing";
67
+ needs_trust: "needs_trust";
68
+ unsupported: "unsupported";
69
+ }>;
70
+ export type MemoryInstallHooksPiece = z.infer<typeof MemoryInstallHooksPieceSchema>;
71
+ export declare const MemoryInstallReceiptSchema: z.ZodObject<{
72
+ schema_version: z.ZodLiteral<"memory-install-receipt.v1">;
73
+ checked_at: z.ZodString;
74
+ claude: z.ZodObject<{
75
+ mcp: z.ZodEnum<{
76
+ unknown: "unknown";
77
+ ok: "ok";
78
+ stale: "stale";
79
+ missing: "missing";
80
+ }>;
81
+ hooks: z.ZodEnum<{
82
+ unknown: "unknown";
83
+ ok: "ok";
84
+ stale: "stale";
85
+ missing: "missing";
86
+ }>;
87
+ }, z.core.$strict>;
88
+ codex: z.ZodObject<{
89
+ mcp: z.ZodEnum<{
90
+ unknown: "unknown";
91
+ ok: "ok";
92
+ stale: "stale";
93
+ missing: "missing";
94
+ }>;
95
+ skill: z.ZodEnum<{
96
+ unknown: "unknown";
97
+ ok: "ok";
98
+ stale: "stale";
99
+ missing: "missing";
100
+ }>;
101
+ hooks: z.ZodEnum<{
102
+ unknown: "unknown";
103
+ ok: "ok";
104
+ stale: "stale";
105
+ missing: "missing";
106
+ needs_trust: "needs_trust";
107
+ unsupported: "unsupported";
108
+ }>;
109
+ }, z.core.$strict>;
110
+ reasons: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
111
+ bin_found: z.ZodOptional<z.ZodBoolean>;
112
+ }, z.core.$strict>;
113
+ export type MemoryInstallReceipt = z.infer<typeof MemoryInstallReceiptSchema>;
114
+ /** Every piece is `ok`. The one question the board asks first. */
115
+ export declare function memoryInstallFullyOn(receipt: MemoryInstallReceipt): boolean;
116
+ /**
117
+ * The pieces that are NOT `ok`, as `host.piece=word` labels, in a fixed order
118
+ * so two machines' receipts read the same way. Empty when everything is on.
119
+ */
120
+ export declare function memoryInstallGaps(receipt: MemoryInstallReceipt): string[];
@@ -0,0 +1,133 @@
1
+ /**
2
+ * THE MEMORY RECEIPT (BLI-3729) — is BLI Memory actually switched on, on this
3
+ * laptop, for both agent hosts?
4
+ *
5
+ * `cockpit memory install` already converged the two host configurations and
6
+ * already wrote a per-target `memory_install` install event. What nobody could
7
+ * answer was the fleet-wide question: "does Savina's machine have memory hooks
8
+ * at all?" The install event is a per-run receipt in an outbox; this is the
9
+ * per-MACHINE state, recomputed every sync tick by READING THE FILES BACK and
10
+ * carried on the heartbeat, so the ops board can name the machine that is
11
+ * missing a piece instead of somebody grepping logs.
12
+ *
13
+ * Six words, one per thing that can independently be absent:
14
+ *
15
+ * claude.mcp `~/.claude.json` → the `bli-memory` MCP server
16
+ * claude.hooks `~/.claude/settings.json` → SessionStart/UserPromptSubmit/Stop
17
+ * codex.mcp `~/.codex/config.toml` → `[mcp_servers.bli-memory]`
18
+ * codex.skill `~/.codex/skills/bli-memory/` → the skill files
19
+ * codex.hooks `~/.codex/hooks.json` → the same three events, USER scope
20
+ *
21
+ * `codex.hooks` is the only one with a fourth word, and it is the whole reason
22
+ * this receipt exists rather than a boolean: Codex requires a person to review
23
+ * and TRUST a non-managed command hook before it runs (`/hooks` in the CLI),
24
+ * and records that trust against the hook's current hash. So a Codex machine
25
+ * can be perfectly installed and still be running nothing. `needs_trust` is
26
+ * that state, said out loud, and it is what the intern checklist ticket is
27
+ * for. `unsupported` is the other honest word: a person who set
28
+ * `[features] hooks = false` has turned the mechanism off, which is a
29
+ * decision, not a fault.
30
+ *
31
+ * **Nothing here is a path.** A home directory names a person; this travels as
32
+ * six words and a timestamp, exactly like the root labels beside it.
33
+ */
34
+ import { z } from "zod";
35
+ import { IsoDateTimeSchema } from "./common.js";
36
+ export const MEMORY_INSTALL_RECEIPT_SCHEMA_VERSION = "memory-install-receipt.v1";
37
+ /**
38
+ * What one piece of the installation is.
39
+ *
40
+ * `ok` registered, and what is stored is what we would write
41
+ * `stale` something of ours is there, but not what we would write today
42
+ * (a hand edit, an older CLI's shape, a bin path that moved)
43
+ * `missing` nothing of ours is there
44
+ * `unknown` the file could not be read or parsed, so NEITHER of the above is
45
+ * claimed — an unreadable config is not an absent one
46
+ */
47
+ export const MemoryInstallPieceSchema = z.enum(["ok", "stale", "missing", "unknown"]);
48
+ /**
49
+ * `codex.hooks` carries two more words than the others.
50
+ *
51
+ * `needs_trust` — the hooks file holds our three entries and Codex has not
52
+ * been told to trust them yet. The hooks are INSTALLED and NOT RUNNING, which
53
+ * is a state no other piece here has.
54
+ * `unsupported` — hooks are turned off in this person's `config.toml`
55
+ * (`[features] hooks = false`, or the deprecated `codex_hooks = false` alias).
56
+ */
57
+ export const MemoryInstallHooksPieceSchema = z.enum([
58
+ "ok",
59
+ "needs_trust",
60
+ "stale",
61
+ "missing",
62
+ "unknown",
63
+ "unsupported",
64
+ ]);
65
+ /**
66
+ * A closed reason label per piece, for the operator who asks "why `stale`?".
67
+ * Bounded and charset-restricted for the same reason `last_sync_reason` is:
68
+ * nothing content-bearing may ride in on it.
69
+ */
70
+ const ReasonLabelSchema = z
71
+ .string()
72
+ .trim()
73
+ .min(1)
74
+ .max(60)
75
+ .regex(/^[a-z0-9_:.-]+$/iu);
76
+ export const MemoryInstallReceiptSchema = z
77
+ .object({
78
+ schema_version: z.literal(MEMORY_INSTALL_RECEIPT_SCHEMA_VERSION),
79
+ checked_at: IsoDateTimeSchema,
80
+ claude: z
81
+ .object({
82
+ mcp: MemoryInstallPieceSchema,
83
+ hooks: MemoryInstallPieceSchema,
84
+ })
85
+ .strict(),
86
+ codex: z
87
+ .object({
88
+ mcp: MemoryInstallPieceSchema,
89
+ skill: MemoryInstallPieceSchema,
90
+ hooks: MemoryInstallHooksPieceSchema,
91
+ })
92
+ .strict(),
93
+ /**
94
+ * One label per piece, keyed by the same names. Optional so an older
95
+ * collector that only computes the words still validates.
96
+ */
97
+ reasons: z.record(z.string().max(24), ReasonLabelSchema).optional(),
98
+ /**
99
+ * Whether `bli-memory-mcp` resolved on this machine at all. A machine
100
+ * where every piece is `missing` because the SERVER is not installed yet
101
+ * is a different machine from one whose configs were wiped, and the
102
+ * install path deliberately writes nothing in the first case
103
+ * (`no_bin_no_write`).
104
+ */
105
+ bin_found: z.boolean().optional(),
106
+ })
107
+ .strict();
108
+ /** Every piece is `ok`. The one question the board asks first. */
109
+ export function memoryInstallFullyOn(receipt) {
110
+ return (receipt.claude.mcp === "ok" &&
111
+ receipt.claude.hooks === "ok" &&
112
+ receipt.codex.mcp === "ok" &&
113
+ receipt.codex.skill === "ok" &&
114
+ receipt.codex.hooks === "ok");
115
+ }
116
+ /**
117
+ * The pieces that are NOT `ok`, as `host.piece=word` labels, in a fixed order
118
+ * so two machines' receipts read the same way. Empty when everything is on.
119
+ */
120
+ export function memoryInstallGaps(receipt) {
121
+ const gaps = [];
122
+ if (receipt.claude.mcp !== "ok")
123
+ gaps.push(`claude.mcp=${receipt.claude.mcp}`);
124
+ if (receipt.claude.hooks !== "ok")
125
+ gaps.push(`claude.hooks=${receipt.claude.hooks}`);
126
+ if (receipt.codex.mcp !== "ok")
127
+ gaps.push(`codex.mcp=${receipt.codex.mcp}`);
128
+ if (receipt.codex.skill !== "ok")
129
+ gaps.push(`codex.skill=${receipt.codex.skill}`);
130
+ if (receipt.codex.hooks !== "ok")
131
+ gaps.push(`codex.hooks=${receipt.codex.hooks}`);
132
+ return gaps;
133
+ }
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ import type { MemoryInstallReceipt } from "./memory-install-receipt.js";
3
+ /**
4
+ * "Is this machine actually connected?" — one block, read back off the machine
5
+ * (BLI-3731).
6
+ *
7
+ * Installing was never the hard part. What nobody could answer was whether
8
+ * Savina's laptop has Codex hooks trusted, or whether Viet's autostart survived
9
+ * a reinstall. This is that answer, in seven words, computed where the truth
10
+ * is and carried to wherever a person reads it: the terminal
11
+ * (`cockpit status`, `cockpit doctor`), the heartbeat, the ops board and
12
+ * JARVIS's `readFleet`.
13
+ *
14
+ * browser ✓ · device ✓ · claude mcp ✓ hooks ✓ · codex mcp ✓ skill ✓
15
+ * hooks needs_trust · collector autostart ✓
16
+ *
17
+ * **The five middle words are BLI-3729's, not ours.** `MemoryInstallReceipt`
18
+ * already owns both agent hosts, it is already computed by reading the host
19
+ * configs back, and it already rides the heartbeat under `memory_install`.
20
+ * This schema carries only the three pieces that ticket had no way to see —
21
+ * whether a browser sign-in ever happened, whether the device session is still
22
+ * good, and whether the scheduler is registered — and the renderers below JOIN
23
+ * the two. One fact stored twice on one row is one fact that can disagree
24
+ * with itself.
25
+ *
26
+ * **Every word is a READ-BACK, never a memory of what we wrote** (BLI-2541:
27
+ * `cockpit autostart` validated a correct Windows task against the strings
28
+ * Cockpit had sent and rejected it on every machine for months). A piece this
29
+ * run did not actually inspect is `unknown`, never `missing` — "we did not
30
+ * look" and "it is not there" are different facts and only one of them is
31
+ * somebody's problem.
32
+ *
33
+ * **Metadata only.** Three words, three reason labels, one timestamp. No path
34
+ * — a home directory names a person — no token, no email.
35
+ */
36
+ export declare const SETUP_RECEIPT_SCHEMA_VERSION = "setup-receipt.v1";
37
+ /**
38
+ * What one piece of the setup can be. Deliberately small and closed: a word a
39
+ * person can read at a glance. The vocabulary is a superset of
40
+ * `MemoryInstallPiece`'s, so one renderer can print both halves.
41
+ */
42
+ export declare const SetupPieceStatusSchema: z.ZodEnum<{
43
+ unknown: "unknown";
44
+ failed: "failed";
45
+ skipped: "skipped";
46
+ ok: "ok";
47
+ stale: "stale";
48
+ missing: "missing";
49
+ needs_trust: "needs_trust";
50
+ unsupported: "unsupported";
51
+ }>;
52
+ export type SetupPieceStatus = z.infer<typeof SetupPieceStatusSchema>;
53
+ declare const PieceSchema: z.ZodObject<{
54
+ status: z.ZodEnum<{
55
+ unknown: "unknown";
56
+ failed: "failed";
57
+ skipped: "skipped";
58
+ ok: "ok";
59
+ stale: "stale";
60
+ missing: "missing";
61
+ needs_trust: "needs_trust";
62
+ unsupported: "unsupported";
63
+ }>;
64
+ reason: z.ZodOptional<z.ZodString>;
65
+ }, z.core.$strict>;
66
+ export type SetupPiece = z.infer<typeof PieceSchema>;
67
+ export declare const SetupReceiptSchema: z.ZodObject<{
68
+ schema_version: z.ZodLiteral<"setup-receipt.v1">;
69
+ checked_at: z.ZodString;
70
+ browser: z.ZodObject<{
71
+ status: z.ZodEnum<{
72
+ unknown: "unknown";
73
+ failed: "failed";
74
+ skipped: "skipped";
75
+ ok: "ok";
76
+ stale: "stale";
77
+ missing: "missing";
78
+ needs_trust: "needs_trust";
79
+ unsupported: "unsupported";
80
+ }>;
81
+ reason: z.ZodOptional<z.ZodString>;
82
+ }, z.core.$strict>;
83
+ device: z.ZodObject<{
84
+ status: z.ZodEnum<{
85
+ unknown: "unknown";
86
+ failed: "failed";
87
+ skipped: "skipped";
88
+ ok: "ok";
89
+ stale: "stale";
90
+ missing: "missing";
91
+ needs_trust: "needs_trust";
92
+ unsupported: "unsupported";
93
+ }>;
94
+ reason: z.ZodOptional<z.ZodString>;
95
+ }, z.core.$strict>;
96
+ collector: z.ZodObject<{
97
+ autostart: z.ZodObject<{
98
+ status: z.ZodEnum<{
99
+ unknown: "unknown";
100
+ failed: "failed";
101
+ skipped: "skipped";
102
+ ok: "ok";
103
+ stale: "stale";
104
+ missing: "missing";
105
+ needs_trust: "needs_trust";
106
+ unsupported: "unsupported";
107
+ }>;
108
+ reason: z.ZodOptional<z.ZodString>;
109
+ }, z.core.$strict>;
110
+ }, z.core.$strict>;
111
+ }, z.core.$strict>;
112
+ export type SetupReceipt = z.infer<typeof SetupReceiptSchema>;
113
+ export interface SetupReceiptPiece {
114
+ key: string;
115
+ label: string;
116
+ piece: SetupPiece;
117
+ }
118
+ /**
119
+ * Every piece in the order the one-line block prints them, with BLI-3729's
120
+ * five memory words spliced into the middle when there is a memory receipt to
121
+ * splice. Without one, the memory keys are still LISTED, as `unknown` — a
122
+ * machine whose memory state nobody knows must not read as a machine with no
123
+ * memory pieces at all.
124
+ */
125
+ export declare function setupReceiptPieces(receipt: SetupReceipt, memory?: MemoryInstallReceipt | null): SetupReceiptPiece[];
126
+ /**
127
+ * The keys of every piece that is not `ok` — what a manager wants named, and
128
+ * what `readFleet` says out loud ("Savina's machine has no Codex hooks
129
+ * trusted"). `unknown` counts as a gap on purpose: a machine nobody can read
130
+ * is not a machine anybody should call fine.
131
+ */
132
+ export declare function setupReceiptGaps(receipt: SetupReceipt, memory?: MemoryInstallReceipt | null): string[];
133
+ /**
134
+ * The one line, exactly as a terminal prints it and as the ops board renders
135
+ * it. Written here, in the shared package, so the CLI and the dashboard cannot
136
+ * word the same machine's state two different ways.
137
+ *
138
+ * Five groups, dot-separated, in the order somebody debugs them: the two
139
+ * sign-ins first, then each agent host, then the scheduler. The grouping is
140
+ * the whole readability of the line — `hooks` reads as Claude's or Codex's
141
+ * only because of which side of a dot it sits on.
142
+ */
143
+ export declare function setupReceiptLine(receipt: SetupReceipt, memory?: MemoryInstallReceipt | null): string;
144
+ /** `ok` is a tick, a broken piece is a cross, and everything else says its own word. */
145
+ export declare function setupPieceMark(status: SetupPieceStatus): string;
146
+ export {};
@@ -0,0 +1,197 @@
1
+ import { z } from "zod";
2
+ import { IsoDateTimeSchema } from "./common.js";
3
+ /**
4
+ * "Is this machine actually connected?" — one block, read back off the machine
5
+ * (BLI-3731).
6
+ *
7
+ * Installing was never the hard part. What nobody could answer was whether
8
+ * Savina's laptop has Codex hooks trusted, or whether Viet's autostart survived
9
+ * a reinstall. This is that answer, in seven words, computed where the truth
10
+ * is and carried to wherever a person reads it: the terminal
11
+ * (`cockpit status`, `cockpit doctor`), the heartbeat, the ops board and
12
+ * JARVIS's `readFleet`.
13
+ *
14
+ * browser ✓ · device ✓ · claude mcp ✓ hooks ✓ · codex mcp ✓ skill ✓
15
+ * hooks needs_trust · collector autostart ✓
16
+ *
17
+ * **The five middle words are BLI-3729's, not ours.** `MemoryInstallReceipt`
18
+ * already owns both agent hosts, it is already computed by reading the host
19
+ * configs back, and it already rides the heartbeat under `memory_install`.
20
+ * This schema carries only the three pieces that ticket had no way to see —
21
+ * whether a browser sign-in ever happened, whether the device session is still
22
+ * good, and whether the scheduler is registered — and the renderers below JOIN
23
+ * the two. One fact stored twice on one row is one fact that can disagree
24
+ * with itself.
25
+ *
26
+ * **Every word is a READ-BACK, never a memory of what we wrote** (BLI-2541:
27
+ * `cockpit autostart` validated a correct Windows task against the strings
28
+ * Cockpit had sent and rejected it on every machine for months). A piece this
29
+ * run did not actually inspect is `unknown`, never `missing` — "we did not
30
+ * look" and "it is not there" are different facts and only one of them is
31
+ * somebody's problem.
32
+ *
33
+ * **Metadata only.** Three words, three reason labels, one timestamp. No path
34
+ * — a home directory names a person — no token, no email.
35
+ */
36
+ export const SETUP_RECEIPT_SCHEMA_VERSION = "setup-receipt.v1";
37
+ /**
38
+ * What one piece of the setup can be. Deliberately small and closed: a word a
39
+ * person can read at a glance. The vocabulary is a superset of
40
+ * `MemoryInstallPiece`'s, so one renderer can print both halves.
41
+ */
42
+ export const SetupPieceStatusSchema = z.enum([
43
+ /** Read back, correct. */
44
+ "ok",
45
+ /** Read back, nothing of ours is there. */
46
+ "missing",
47
+ /** Read back, something of ours is there but not what we would write. */
48
+ "stale",
49
+ /**
50
+ * Installed correctly and waiting on ONE human action. Today only Codex
51
+ * hooks, which the host requires a person to trust once per machine. It is
52
+ * its own word rather than `missing` because nothing is broken and no
53
+ * command fixes it.
54
+ */
55
+ "needs_trust",
56
+ /** The mechanism is switched off by a person's own choice. */
57
+ "unsupported",
58
+ /** Deliberately not done, and no file was touched. Carries its reason. */
59
+ "skipped",
60
+ /** Tried, and did not work. Carries its reason. */
61
+ "failed",
62
+ /**
63
+ * This run did not look. NEVER a substitute for `missing`: a collector too
64
+ * old to compute a piece, a dry run, and a probe that could not run all land
65
+ * here, and every one of them means "ask again", not "fix it".
66
+ */
67
+ "unknown",
68
+ ]);
69
+ const PieceSchema = z
70
+ .object({
71
+ status: SetupPieceStatusSchema,
72
+ /**
73
+ * A reason label, never prose: `never_signed_in`, `session_expired`,
74
+ * `autostart_not_loaded`. Bounded and charset-restricted so nothing
75
+ * content-bearing can ride in on it.
76
+ */
77
+ reason: z
78
+ .string()
79
+ .trim()
80
+ .min(1)
81
+ .max(120)
82
+ .regex(/^[a-z0-9_:.-]+$/iu)
83
+ .optional(),
84
+ })
85
+ .strict();
86
+ export const SetupReceiptSchema = z
87
+ .object({
88
+ schema_version: z.literal(SETUP_RECEIPT_SCHEMA_VERSION),
89
+ /** When these words were read off this machine. Ages out; never trusted stale. */
90
+ checked_at: IsoDateTimeSchema,
91
+ /** Did a person ever sign a browser in for this machine? */
92
+ browser: PieceSchema,
93
+ /** Is the device session on disk still one the dashboard would honour? */
94
+ device: PieceSchema,
95
+ /** The 15-minute scheduler: launchd on macOS, Task Scheduler on Windows. */
96
+ collector: z.object({ autostart: PieceSchema }).strict(),
97
+ })
98
+ .strict();
99
+ /**
100
+ * Every piece in the order the one-line block prints them, with BLI-3729's
101
+ * five memory words spliced into the middle when there is a memory receipt to
102
+ * splice. Without one, the memory keys are still LISTED, as `unknown` — a
103
+ * machine whose memory state nobody knows must not read as a machine with no
104
+ * memory pieces at all.
105
+ */
106
+ export function setupReceiptPieces(receipt, memory) {
107
+ const memoryPiece = (word, key) => {
108
+ if (!word)
109
+ return { status: "unknown", reason: "memory_receipt_absent" };
110
+ const reason = memory?.reasons?.[key];
111
+ return {
112
+ status: word,
113
+ ...(word !== "ok" && reason ? { reason } : {}),
114
+ };
115
+ };
116
+ return [
117
+ { key: "browser", label: "browser", piece: receipt.browser },
118
+ { key: "device", label: "device", piece: receipt.device },
119
+ {
120
+ key: "claude.mcp",
121
+ label: "claude mcp",
122
+ piece: memoryPiece(memory?.claude.mcp, "claude.mcp"),
123
+ },
124
+ {
125
+ key: "claude.hooks",
126
+ label: "hooks",
127
+ piece: memoryPiece(memory?.claude.hooks, "claude.hooks"),
128
+ },
129
+ {
130
+ key: "codex.mcp",
131
+ label: "codex mcp",
132
+ piece: memoryPiece(memory?.codex.mcp, "codex.mcp"),
133
+ },
134
+ {
135
+ key: "codex.skill",
136
+ label: "skill",
137
+ piece: memoryPiece(memory?.codex.skill, "codex.skill"),
138
+ },
139
+ {
140
+ key: "codex.hooks",
141
+ label: "hooks",
142
+ piece: memoryPiece(memory?.codex.hooks, "codex.hooks"),
143
+ },
144
+ {
145
+ key: "collector.autostart",
146
+ label: "collector autostart",
147
+ piece: receipt.collector.autostart,
148
+ },
149
+ ];
150
+ }
151
+ /**
152
+ * The keys of every piece that is not `ok` — what a manager wants named, and
153
+ * what `readFleet` says out loud ("Savina's machine has no Codex hooks
154
+ * trusted"). `unknown` counts as a gap on purpose: a machine nobody can read
155
+ * is not a machine anybody should call fine.
156
+ */
157
+ export function setupReceiptGaps(receipt, memory) {
158
+ return setupReceiptPieces(receipt, memory)
159
+ .filter(({ piece }) => piece.status !== "ok")
160
+ .map(({ key }) => key);
161
+ }
162
+ /**
163
+ * The one line, exactly as a terminal prints it and as the ops board renders
164
+ * it. Written here, in the shared package, so the CLI and the dashboard cannot
165
+ * word the same machine's state two different ways.
166
+ *
167
+ * Five groups, dot-separated, in the order somebody debugs them: the two
168
+ * sign-ins first, then each agent host, then the scheduler. The grouping is
169
+ * the whole readability of the line — `hooks` reads as Claude's or Codex's
170
+ * only because of which side of a dot it sits on.
171
+ */
172
+ export function setupReceiptLine(receipt, memory) {
173
+ const groups = new Map([
174
+ ["browser", []],
175
+ ["device", []],
176
+ ["claude", []],
177
+ ["codex", []],
178
+ ["collector", []],
179
+ ]);
180
+ for (const { key, label, piece } of setupReceiptPieces(receipt, memory)) {
181
+ const group = key.includes(".") ? key.split(".")[0] : key;
182
+ groups.get(group)?.push(`${label} ${setupPieceMark(piece.status)}`);
183
+ }
184
+ return [...groups.values()]
185
+ .filter((group) => group.length > 0)
186
+ .map((group) => group.join(" "))
187
+ .join(" · ");
188
+ }
189
+ /** `ok` is a tick, a broken piece is a cross, and everything else says its own word. */
190
+ export function setupPieceMark(status) {
191
+ if (status === "ok")
192
+ return "✓";
193
+ if (status === "missing" || status === "failed" || status === "stale") {
194
+ return "✗";
195
+ }
196
+ return status;
197
+ }
@@ -1 +1,44 @@
1
+ /**
2
+ * WHICH TRACKER MINTED THIS ID (BLI-3779).
3
+ *
4
+ * Tower runs its own issue tracker beside Linear during the dual-run, and both
5
+ * spell an identifier the same way: a team key, a hyphen, a number. Two facts
6
+ * tell them apart, and neither is a guess:
7
+ *
8
+ * - **The number.** Tower's tracker mints from `BLI-10001` up;
9
+ * Linear's BLI team is in the 3,000s. An id at or above
10
+ * `TOWER_NATIVE_ISSUE_NUMBER_FLOOR` is one only Tower could have issued.
11
+ * The dashboard's `lib/jarvis/chat-v2/issue-word-boundary.ts` (BLI-3783)
12
+ * re-exports this constant rather than keeping a second copy of it.
13
+ * - **The title.** A Tower-native row that mirrors a Linear ticket ends its
14
+ * title with `[BLI-3779]` — Edward's dogfooding convention, 2026-09-05 —
15
+ * so the Linear id a Tower row stands for is READ, never inferred.
16
+ *
17
+ * Exact after upper-casing, never fuzzy: the `person-identity.ts` discipline,
18
+ * for the same reason. A wrong match binds a session's work to somebody
19
+ * else's ticket.
20
+ */
21
+ /**
22
+ * The first identifier Tower's own tracker mints. Linear's BLI team is in the
23
+ * 3,000s as of 2026-09-05 and would need six thousand more tickets to reach
24
+ * it; if it ever does, this constant moves and `work_issues.source` becomes
25
+ * the only honest discriminator.
26
+ */
27
+ export declare const TOWER_NATIVE_ISSUE_NUMBER_FLOOR = 10000;
1
28
  export declare function parseTicketIdFromText(value: string): string | null;
29
+ /** Is this the shape either tracker uses for an identifier? */
30
+ export declare function looksLikeIssueIdentifier(value: string): boolean;
31
+ /** The one spelling of an identifier: trimmed and upper-cased, or null. */
32
+ export declare function normalizeIssueIdentifier(value: string): string | null;
33
+ /**
34
+ * Is this an id only TOWER's tracker could have minted? A fact about the
35
+ * number, not a claim about any row: `BLI-10042` cannot be in Linear whether
36
+ * or not it is in Tower.
37
+ */
38
+ export declare function isTowerNativeIssueId(value: string): boolean;
39
+ /**
40
+ * The Linear id a Tower-native title mirrors: the LAST `[BLI-3779]` in the
41
+ * title, because a title may quote another ticket in its prose and the
42
+ * convention puts the mirrored id at the end.
43
+ */
44
+ export declare function mirroredTicketIdInTitle(title: string): string | null;
package/dist/ticket-id.js CHANGED
@@ -1,4 +1,61 @@
1
+ /**
2
+ * WHICH TRACKER MINTED THIS ID (BLI-3779).
3
+ *
4
+ * Tower runs its own issue tracker beside Linear during the dual-run, and both
5
+ * spell an identifier the same way: a team key, a hyphen, a number. Two facts
6
+ * tell them apart, and neither is a guess:
7
+ *
8
+ * - **The number.** Tower's tracker mints from `BLI-10001` up;
9
+ * Linear's BLI team is in the 3,000s. An id at or above
10
+ * `TOWER_NATIVE_ISSUE_NUMBER_FLOOR` is one only Tower could have issued.
11
+ * The dashboard's `lib/jarvis/chat-v2/issue-word-boundary.ts` (BLI-3783)
12
+ * re-exports this constant rather than keeping a second copy of it.
13
+ * - **The title.** A Tower-native row that mirrors a Linear ticket ends its
14
+ * title with `[BLI-3779]` — Edward's dogfooding convention, 2026-09-05 —
15
+ * so the Linear id a Tower row stands for is READ, never inferred.
16
+ *
17
+ * Exact after upper-casing, never fuzzy: the `person-identity.ts` discipline,
18
+ * for the same reason. A wrong match binds a session's work to somebody
19
+ * else's ticket.
20
+ */
21
+ /** `BLI-3654`, `TRI-12` — a team key, a hyphen, a number, and nothing else. */
22
+ const ISSUE_IDENTIFIER = /^[A-Z][A-Z0-9]{1,12}-\d+$/;
23
+ /**
24
+ * The first identifier Tower's own tracker mints. Linear's BLI team is in the
25
+ * 3,000s as of 2026-09-05 and would need six thousand more tickets to reach
26
+ * it; if it ever does, this constant moves and `work_issues.source` becomes
27
+ * the only honest discriminator.
28
+ */
29
+ export const TOWER_NATIVE_ISSUE_NUMBER_FLOOR = 10_000;
1
30
  export function parseTicketIdFromText(value) {
2
31
  const match = value.match(/\b[A-Z][A-Z0-9]{1,12}-\d+\b/);
3
32
  return match?.[0] ?? null;
4
33
  }
34
+ /** Is this the shape either tracker uses for an identifier? */
35
+ export function looksLikeIssueIdentifier(value) {
36
+ return ISSUE_IDENTIFIER.test(value.trim().toUpperCase());
37
+ }
38
+ /** The one spelling of an identifier: trimmed and upper-cased, or null. */
39
+ export function normalizeIssueIdentifier(value) {
40
+ const normalized = value.trim().toUpperCase();
41
+ return ISSUE_IDENTIFIER.test(normalized) ? normalized : null;
42
+ }
43
+ /**
44
+ * Is this an id only TOWER's tracker could have minted? A fact about the
45
+ * number, not a claim about any row: `BLI-10042` cannot be in Linear whether
46
+ * or not it is in Tower.
47
+ */
48
+ export function isTowerNativeIssueId(value) {
49
+ const match = /^[A-Z][A-Z0-9]{1,12}-(\d+)$/.exec(value.trim().toUpperCase());
50
+ return match ? Number(match[1]) >= TOWER_NATIVE_ISSUE_NUMBER_FLOOR : false;
51
+ }
52
+ /**
53
+ * The Linear id a Tower-native title mirrors: the LAST `[BLI-3779]` in the
54
+ * title, because a title may quote another ticket in its prose and the
55
+ * convention puts the mirrored id at the end.
56
+ */
57
+ export function mirroredTicketIdInTitle(title) {
58
+ const matches = [...title.matchAll(/\[([A-Z][A-Z0-9]{1,12}-\d+)\]/gi)];
59
+ const last = matches.at(-1);
60
+ return last ? last[1].toUpperCase() : null;
61
+ }
@@ -85,6 +85,8 @@ export declare const LocalWorkContextSchema: z.ZodObject<{
85
85
  started_at: z.ZodString;
86
86
  updated_at: z.ZodOptional<z.ZodString>;
87
87
  active_ticket_id: z.ZodOptional<z.ZodString>;
88
+ tower_issue_id: z.ZodOptional<z.ZodString>;
89
+ linear_ticket_id: z.ZodOptional<z.ZodString>;
88
90
  ticket_binding_candidates: z.ZodDefault<z.ZodArray<z.ZodObject<{
89
91
  ticket_id: z.ZodString;
90
92
  binding_source: z.ZodEnum<{
@@ -76,6 +76,20 @@ export const LocalWorkContextSchema = z
76
76
  started_at: IsoDateTimeSchema,
77
77
  updated_at: IsoDateTimeSchema.optional(),
78
78
  active_ticket_id: NonEmptyStringSchema.optional(),
79
+ /**
80
+ * BLI-3779, the dual-run: one piece of work has an id in TWO trackers, so
81
+ * the context carries both beside `active_ticket_id` (which stays exactly
82
+ * what the person typed — rewriting it would move every downstream
83
+ * attribution to an id nobody bound).
84
+ *
85
+ * `tower_issue_id` is a `work_issues.identifier` (`BLI-10019`);
86
+ * `linear_ticket_id` is the Linear one (`BLI-3779`). Both OPTIONAL and
87
+ * both absent when nothing was resolved: `cockpit start` never blocks a
88
+ * session on a tracker lookup (session-first commandment), so an
89
+ * unresolved ticket is a named gap, never a missing context.
90
+ */
91
+ tower_issue_id: NonEmptyStringSchema.optional(),
92
+ linear_ticket_id: NonEmptyStringSchema.optional(),
79
93
  ticket_binding_candidates: z
80
94
  .array(TicketBindingCandidateSchema)
81
95
  .default([]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/telemetry-core",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",