@bli-cockpit/telemetry-core 0.1.27 → 0.1.29
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/collector-heartbeat.d.ts +89 -0
- package/dist/collector-heartbeat.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/memory-install-receipt.d.ts +120 -0
- package/dist/memory-install-receipt.js +133 -0
- package/dist/setup-receipt.d.ts +146 -0
- package/dist/setup-receipt.js +197 -0
- package/package.json +1 -1
|
@@ -56,6 +56,95 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
|
|
|
56
56
|
last_sync_reason: z.ZodOptional<z.ZodString>;
|
|
57
57
|
sessions_observed: z.ZodOptional<z.ZodNumber>;
|
|
58
58
|
sessions_outside_root: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
sessions_new_this_tick: z.ZodOptional<z.ZodNumber>;
|
|
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>>;
|
|
59
148
|
}, z.core.$strict>;
|
|
60
149
|
export type CollectorHeartbeat = z.infer<typeof CollectorHeartbeatSchema>;
|
|
61
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;
|
|
@@ -89,6 +91,45 @@ export const CollectorHeartbeatSchema = z
|
|
|
89
91
|
.nonnegative()
|
|
90
92
|
.max(1_000_000)
|
|
91
93
|
.optional(),
|
|
94
|
+
/**
|
|
95
|
+
* BLI-3645. Two honest counters beside the raw scan count above, both
|
|
96
|
+
* optional so a collector too old to compute them keeps posting:
|
|
97
|
+
*
|
|
98
|
+
* - `sessions_new_this_tick` — sessions first seen since this source's
|
|
99
|
+
* cursor as it stood before this tick, a FLOW, not the re-scanned
|
|
100
|
+
* STOCK `sessions_observed` is.
|
|
101
|
+
* - `sessions_pending_upload` — sessions inside an approved root with
|
|
102
|
+
* no durable pointer yet, i.e. the "only copy" set. A FLOOR at the
|
|
103
|
+
* moment this tick observed it, same as every other counter here.
|
|
104
|
+
*/
|
|
105
|
+
sessions_new_this_tick: z
|
|
106
|
+
.number()
|
|
107
|
+
.int()
|
|
108
|
+
.nonnegative()
|
|
109
|
+
.max(1_000_000)
|
|
110
|
+
.optional(),
|
|
111
|
+
sessions_pending_upload: z
|
|
112
|
+
.number()
|
|
113
|
+
.int()
|
|
114
|
+
.nonnegative()
|
|
115
|
+
.max(1_000_000)
|
|
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(),
|
|
92
133
|
})
|
|
93
134
|
.strict();
|
|
94
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";
|
|
@@ -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
|
+
}
|