@mindrian_os/cli 2.0.0-beta.35 → 2.0.0-beta.37
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -0
- package/commands/doctor.md +2 -1
- package/data/hooks-markers.json +25 -0
- package/hooks/hooks.json +0 -23
- package/lib/core/brain-client.cjs +36 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +22 -0
- package/lib/core/install-id.cjs +232 -0
- package/lib/mcp/hook-adapter-audit.cjs +13 -9
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/doctor.cjs +35 -0
- package/skills/doctor/SKILL.md +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mos",
|
|
3
3
|
"description": "MindrianOS -- Your AI innovation co-founder. Larry thinks with you through PWS methodology, builds your Data Room as you explore, and chains frameworks intelligently. Install and go.",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.37",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Sagir",
|
|
7
7
|
"url": "https://mindrian-os.com"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## [2.0.0-beta.37] - 2026-09-11
|
|
2
|
+
|
|
3
|
+
### Fixed - hooks.json no longer prints an unknown-keys warning at every session start (quick task 260911-juq)
|
|
4
|
+
|
|
5
|
+
- Claude Code 2.1.268 printed, on every session start on every surface that loads the
|
|
6
|
+
plugin manifest: `hooks.json: unknown keys "_mcpFirst198Migrated",
|
|
7
|
+
"_firstInstallRouterOrdering" ignored`. Both keys were our own build metadata, never
|
|
8
|
+
hook configuration, so the loader was correct to ignore them and correct to complain.
|
|
9
|
+
They now live in `data/hooks-markers.json`, and `hooks/hooks.json`'s top level is
|
|
10
|
+
exactly one key, `hooks`. No matcher, command, or timeout inside the hooks block
|
|
11
|
+
changed; the block is byte-identical to the pre-fix file.
|
|
12
|
+
- New guard test `tests/test-quick-260911-juq-hooks-json-top-level.cjs`, registered as
|
|
13
|
+
a leg of `bash tests/run-all-198.sh`, keeps both halves of this fix honest going
|
|
14
|
+
forward: the top level cannot silently grow a stray key again, and the sidecar path
|
|
15
|
+
cannot silently go dead and make the D-06 adapter budget vacuous.
|
|
16
|
+
|
|
17
|
+
### Added - a per-install opaque header lets Theo bucket without learning who you are (quick task 260911-iko)
|
|
18
|
+
|
|
19
|
+
- Every Brain call now carries `x-theo-install-id`: a 32-character lowercase hex value
|
|
20
|
+
minted once from `crypto.randomBytes(16)`, stored at `~/.mindrian/theo-install-id.json`
|
|
21
|
+
(mode 0600) beside the pre-warm marker. It rides both of `callTool`'s wire requests
|
|
22
|
+
(the session initialize and the tools/call), and is simply omitted, never an error,
|
|
23
|
+
when it cannot be minted or read. This lets Theo tell one install calling twice from
|
|
24
|
+
two installs calling once, with nothing else about the install attached to it.
|
|
25
|
+
- `node scripts/doctor.cjs --reset-install-id` rotates the id on demand, prints exactly
|
|
26
|
+
`install id rotated`, and never prints the value. The doctor's Brain-smoke layer 0
|
|
27
|
+
also grows an `install_id_present` boolean, reported the same way: presence only,
|
|
28
|
+
never the value.
|
|
29
|
+
- Canon Part 8 position, stated once and reproduced in full at
|
|
30
|
+
`docs/THEO-INSTALL-ID.md`: the id is an opaque CSPRNG handle, never derived from
|
|
31
|
+
identity, never logged, never printed in full.
|
|
32
|
+
|
|
1
33
|
## [2.0.0-beta.35] - 2026-09-11
|
|
2
34
|
|
|
3
35
|
### Added - doctor sees the Tier 0 shadow, Theo's node floor stops rubber-stamping (quick task 260911-axz)
|
package/commands/doctor.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: doctor
|
|
3
3
|
description: "Diagnose and optionally repair MindrianOS install: install-cache drift, sentinel gaps, active-room guard, surface-verification, ROOM.md drift, UI compliance, statusline visibility, Brain smoke, and a paste-ready command-registration bug report for Anthropic"
|
|
4
4
|
help_jtbd: "Diagnose and optionally repair an off-feeling install."
|
|
5
|
-
argument-hint: "[--fix] [--all] [--cascade-rooms] [--graph-derive-health] [--heal-room] [--verify-surface] [--room-md] [--ui-compliance] [--statusline-visibility] [--card-fire-health] [--install-state] [--stale-first-touch] [--deprecated-usage] [--brain-smoke] [--eureka-smoke] [--drift] [--report-registration-bug] [--acceptance] [--pre-tag] [--pre-flight] [--dogfood-acceptance] [--claims] [--check-rs-engine] [--post-update] [--bind-check] [--simulate-write] [--scan-commands] [--scan-scripts] [--light-npx] [--dry-run] [--json]"
|
|
5
|
+
argument-hint: "[--fix] [--all] [--cascade-rooms] [--graph-derive-health] [--heal-room] [--verify-surface] [--room-md] [--ui-compliance] [--statusline-visibility] [--card-fire-health] [--install-state] [--stale-first-touch] [--deprecated-usage] [--brain-smoke] [--eureka-smoke] [--drift] [--report-registration-bug] [--reset-install-id] [--acceptance] [--pre-tag] [--pre-flight] [--dogfood-acceptance] [--claims] [--check-rs-engine] [--post-update] [--bind-check] [--simulate-write] [--scan-commands] [--scan-scripts] [--light-npx] [--dry-run] [--json]"
|
|
6
6
|
body_shape: E (Action Report)
|
|
7
7
|
hitl_shape: "F.0"
|
|
8
8
|
hitl_why: "It surfaces one diagnosed repair for a single approve-or-defer decision."
|
|
@@ -125,6 +125,7 @@ These siblings own their own exit contracts and are not part of the class-flag d
|
|
|
125
125
|
- `--dogfood-acceptance` -> the Canon Part 6 dog-fooding acceptance pass.
|
|
126
126
|
- `--claims` -> the claims-verification leg.
|
|
127
127
|
- `--check-rs-engine` -> the reason-select engine probe (NOT part of `--all`).
|
|
128
|
+
- `--reset-install-id` -> rotates the opaque per-install bucket key the plugin sends to Theo, prints `install id rotated`, never prints the value, and always exits 0. Full contract: `docs/THEO-INSTALL-ID.md`.
|
|
128
129
|
- `--post-update` -> the post-`claude plugin update` confirmation pass.
|
|
129
130
|
- `--bind-check <roomDir>` -> a lightweight LOCAL room-health check run at BIND-TIME (never per-turn, never a Brain call). NEVER-BLOCK: an unhealthy room degrades to an advisory and STILL exits 0.
|
|
130
131
|
- `--light-npx` -> the lighter npx-roundtrip variant for `--acceptance`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_mcpFirst198Migrated": {
|
|
3
|
+
"_note": "Phase 198-08 (SPEC-5, D-05/D-06) migration markers, extended by Phase 198-09 (D-05 final wave). Each entry names a hook script that now dispatches to lib/mcp/adapter-client.cjs behind MINDRIAN_MCP_FIRST ('cli' or 'all') instead of running its business logic locally; flag OFF (unset/empty, the default) keeps the pre-migration legacy behavior byte-identical (SPEC-7). This is metadata for tests/test-198-adapter-budget.test.cjs's import-audit + line-count budget (D-06) to enumerate -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key in hooks/hooks.json. Migration order is D-05: statusline + SessionStart FIRST (198-08); Stop-gate enforcement LAST (198-09, this entry), only after server-side gate dedup + relevance existed (lib/mcp/gate-dedup.cjs, Task 1 of 198-09). scripts/check-card-fire.cjs is NOT listed here -- it stays a separate Stop hook entry that no-ops under the flag (deferring to on-stop's daemon-backed decision), never itself migrated to a thin adapter shape.",
|
|
4
|
+
"surfaces": [
|
|
5
|
+
{
|
|
6
|
+
"script": "scripts/statusline-mos-dispatch",
|
|
7
|
+
"tool": "status_read",
|
|
8
|
+
"note": "Statusline segment (spend/cap from day one). Flag ON wakes the daemon and queries status_read; flag OFF execs the legacy scripts/statusline-mos exactly as shipped."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"script": "scripts/sessionstart-coordinator.cjs",
|
|
12
|
+
"tool": "room_state_bound,status_read",
|
|
13
|
+
"note": "SessionStart binding + status segments. Flag ON wakes the daemon and queries room_state_bound + status_read (runThinAdapter); flag OFF runs the full 11-contributor runAll() exactly as shipped."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"script": "scripts/on-stop",
|
|
17
|
+
"tool": "stop_gate_check",
|
|
18
|
+
"note": "Stop-gate enforcement + business close-out (D-05 final wave). Flag ON wakes the daemon and queries stop_gate_check (lib/mcp/tools/stop-gate.cjs -> lib/mcp/stop-gate-handler.cjs -- gate-dedup + relevance gate the fire decision, the shipped check-card-fire.cjs predicate is the final authority, the daemon owns STATE.md/memory-lifecycle/minto-debouncer/folder-memory close-out); flag OFF runs the full legacy body exactly as shipped."
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"_firstInstallRouterOrdering": {
|
|
23
|
+
"_note": "Phase 267.2-06 (HOOK-08). scripts/first-install-router.cjs is registered in the UserPromptSubmit chain immediately AFTER scripts/mva-detect.cjs and before scripts/brain-derivation-drain.cjs. This is metadata for a human reader (and any future drift test) -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key in hooks/hooks.json. ORDERING IS LOAD BEARING: the router consumes the pending MVA state that mva-detect.cjs writes on the same turn, so running before it would read a stale or absent state. Do not reorder these two entries without re-checking this dependency."
|
|
24
|
+
}
|
|
25
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_mcpFirst198Migrated": {
|
|
3
|
-
"_note": "Phase 198-08 (SPEC-5, D-05/D-06) migration markers, extended by Phase 198-09 (D-05 final wave). Each entry names a hook script that now dispatches to lib/mcp/adapter-client.cjs behind MINDRIAN_MCP_FIRST ('cli' or 'all') instead of running its business logic locally; flag OFF (unset/empty, the default) keeps the pre-migration legacy behavior byte-identical (SPEC-7). This is metadata for tests/test-198-adapter-budget.test.cjs's import-audit + line-count budget (D-06) to enumerate -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key below. Migration order is D-05: statusline + SessionStart FIRST (198-08); Stop-gate enforcement LAST (198-09, this entry), only after server-side gate dedup + relevance existed (lib/mcp/gate-dedup.cjs, Task 1 of 198-09). scripts/check-card-fire.cjs is NOT listed here -- it stays a separate Stop hook entry that no-ops under the flag (deferring to on-stop's daemon-backed decision), never itself migrated to a thin adapter shape.",
|
|
4
|
-
"surfaces": [
|
|
5
|
-
{
|
|
6
|
-
"script": "scripts/statusline-mos-dispatch",
|
|
7
|
-
"tool": "status_read",
|
|
8
|
-
"note": "Statusline segment (spend/cap from day one). Flag ON wakes the daemon and queries status_read; flag OFF execs the legacy scripts/statusline-mos exactly as shipped."
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"script": "scripts/sessionstart-coordinator.cjs",
|
|
12
|
-
"tool": "room_state_bound,status_read",
|
|
13
|
-
"note": "SessionStart binding + status segments. Flag ON wakes the daemon and queries room_state_bound + status_read (runThinAdapter); flag OFF runs the full 11-contributor runAll() exactly as shipped."
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"script": "scripts/on-stop",
|
|
17
|
-
"tool": "stop_gate_check",
|
|
18
|
-
"note": "Stop-gate enforcement + business close-out (D-05 final wave). Flag ON wakes the daemon and queries stop_gate_check (lib/mcp/tools/stop-gate.cjs -> lib/mcp/stop-gate-handler.cjs -- gate-dedup + relevance gate the fire decision, the shipped check-card-fire.cjs predicate is the final authority, the daemon owns STATE.md/memory-lifecycle/minto-debouncer/folder-memory close-out); flag OFF runs the full legacy body exactly as shipped."
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"_firstInstallRouterOrdering": {
|
|
23
|
-
"_note": "Phase 267.2-06 (HOOK-08). scripts/first-install-router.cjs is registered in the UserPromptSubmit chain immediately AFTER scripts/mva-detect.cjs and before scripts/brain-derivation-drain.cjs. This is metadata for a human reader (and any future drift test) -- it is NOT consumed by Claude Code's hook loader, which only reads the 'hooks' key below. ORDERING IS LOAD BEARING: the router consumes the pending MVA state that mva-detect.cjs writes on the same turn, so running before it would read a stale or absent state. Do not reorder these two entries without re-checking this dependency."
|
|
24
|
-
},
|
|
25
2
|
"hooks": {
|
|
26
3
|
"SessionStart": [
|
|
27
4
|
{
|
|
@@ -305,6 +305,34 @@ function isAvailable() {
|
|
|
305
305
|
return !!getApiKey();
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Quick 260911-iko (D-04): the opaque per-install header, spread into both
|
|
309
|
+
// of callTool's wire requests. A per-process memo is correct here (mirrors
|
|
310
|
+
// the getApiKey() memo idiom above) and must be commented as such: the id
|
|
311
|
+
// only changes on reinstall or on a `doctor --reset-install-id` run, and
|
|
312
|
+
// both of those happen in a DIFFERENT process, so re-reading it on every
|
|
313
|
+
// call would buy nothing. Wrapped in its own try/catch so a missing or
|
|
314
|
+
// broken install-id module can never take a Brain call down -- the header
|
|
315
|
+
// is a bucket key, never a requirement (D-04: omitted, never an error).
|
|
316
|
+
//
|
|
317
|
+
// This helper never touches the Part 8 egress belt (classify() at
|
|
318
|
+
// callTool's egress-guard call below): that belt classifies `args`, and
|
|
319
|
+
// headers are not payload. The install id never enters `args` and must
|
|
320
|
+
// never be added to them.
|
|
321
|
+
let _memoizedInstallIdHeaders = null;
|
|
322
|
+
function _installIdHeaders() {
|
|
323
|
+
if (_memoizedInstallIdHeaders) return _memoizedInstallIdHeaders;
|
|
324
|
+
try {
|
|
325
|
+
const installIdMod = require('./install-id.cjs');
|
|
326
|
+
const id = installIdMod.getInstallId();
|
|
327
|
+
_memoizedInstallIdHeaders = (typeof id === 'string')
|
|
328
|
+
? { [installIdMod.installIdHeaderName]: id }
|
|
329
|
+
: {};
|
|
330
|
+
} catch (_e) {
|
|
331
|
+
_memoizedInstallIdHeaders = {};
|
|
332
|
+
}
|
|
333
|
+
return _memoizedInstallIdHeaders;
|
|
334
|
+
}
|
|
335
|
+
|
|
308
336
|
// ---------------------------------------------------------------------------
|
|
309
337
|
// Phase 250-04 (HONEST-03, SEED-011 Option A) -- per-install silent
|
|
310
338
|
// registration. Design doc: docs/BRAIN-IDENTITY-DESIGN.md.
|
|
@@ -350,6 +378,12 @@ async function _tryAutoRegister() {
|
|
|
350
378
|
|
|
351
379
|
try {
|
|
352
380
|
const installId = crypto.randomUUID();
|
|
381
|
+
// Quick 260911-iko: the x-theo-install-id header does NOT ride here.
|
|
382
|
+
// D-04 names callTool's two header blocks only; registration is a
|
|
383
|
+
// different endpoint with its own contract, and it runs before there is
|
|
384
|
+
// an established Brain session to bucket. Adding it here would be a
|
|
385
|
+
// change to Theo's registration surface, which D-06 says must be
|
|
386
|
+
// announced to Theo first -- a deliberate exclusion, not a miss.
|
|
353
387
|
const res = await fetch(`${BRAIN_URL}/register`, {
|
|
354
388
|
method: 'POST',
|
|
355
389
|
signal: AbortSignal.timeout(BRAIN_REQUEST_TIMEOUT_MS),
|
|
@@ -456,6 +490,7 @@ async function _ensureSession(apiKey) {
|
|
|
456
490
|
'Content-Type': 'application/json',
|
|
457
491
|
'Accept': 'application/json, text/event-stream',
|
|
458
492
|
'Authorization': `Bearer ${apiKey}`,
|
|
493
|
+
..._installIdHeaders(),
|
|
459
494
|
},
|
|
460
495
|
body: JSON.stringify({
|
|
461
496
|
jsonrpc: '2.0',
|
|
@@ -675,6 +710,7 @@ async function callTool(toolName, args) {
|
|
|
675
710
|
'Content-Type': 'application/json',
|
|
676
711
|
'Accept': 'application/json, text/event-stream',
|
|
677
712
|
'Authorization': `Bearer ${key}`,
|
|
713
|
+
..._installIdHeaders(),
|
|
678
714
|
},
|
|
679
715
|
body: JSON.stringify({
|
|
680
716
|
jsonrpc: '2.0',
|
|
@@ -202,6 +202,14 @@ async function _runLayer(_name, fn) {
|
|
|
202
202
|
// `instanceUri`, `quarantineCode`, and `serverAgent` are never copied
|
|
203
203
|
// (T-axz-02).
|
|
204
204
|
//
|
|
205
|
+
// (d) install_id_present (Quick 260911-iko, T-iko-02): whether the opaque
|
|
206
|
+
// per-install id (lib/core/install-id.cjs) can be read from the state
|
|
207
|
+
// dir. This row carries the BOOLEAN ONLY and NEVER the id value -- it
|
|
208
|
+
// reads through peekInstallId, which never mints, so running doctor
|
|
209
|
+
// can never create the id it is reporting on. A missing id is
|
|
210
|
+
// information, never a failure: it never changes the verdict below,
|
|
211
|
+
// exactly as the origin and health halves already do not.
|
|
212
|
+
//
|
|
205
213
|
// Verdict: ok is false if and only if at least one mindrian-brain shadow
|
|
206
214
|
// entry was found. The origin and health halves are information, never the
|
|
207
215
|
// verdict, exactly as L6 treats canon/override as information separate from
|
|
@@ -221,6 +229,19 @@ async function _layer0(opts) {
|
|
|
221
229
|
|
|
222
230
|
const payload = { resolved_origin: resolved_origin, is_theo: is_theo, override: override, shadows: shadows };
|
|
223
231
|
|
|
232
|
+
// Quick 260911-iko (D-03, T-iko-02): install_id_present, a BOOLEAN ONLY,
|
|
233
|
+
// never the value. Uses peekInstallId, NOT getInstallId, so a diagnostic
|
|
234
|
+
// run never mints the thing it is reporting on. A missing install id is
|
|
235
|
+
// information, never a failure -- this never changes the verdict below,
|
|
236
|
+
// exactly as the origin and health halves already are.
|
|
237
|
+
try {
|
|
238
|
+
const installIdFn = opts.mockInstallId
|
|
239
|
+
|| (() => require('../install-id.cjs').peekInstallId());
|
|
240
|
+
payload.install_id_present = typeof installIdFn() === 'string';
|
|
241
|
+
} catch (_e) {
|
|
242
|
+
payload.install_id_present = false;
|
|
243
|
+
}
|
|
244
|
+
|
|
224
245
|
const theoHealthFn = opts.mockTheoHealth
|
|
225
246
|
|| (async () => require('../brain-client.cjs').callTool('theo_health', {}));
|
|
226
247
|
try {
|
|
@@ -533,6 +554,7 @@ async function _layer6(opts) {
|
|
|
533
554
|
* mockQuery?: function,
|
|
534
555
|
* mockScopedServers?: function,
|
|
535
556
|
* mockTheoHealth?: function,
|
|
557
|
+
* mockInstallId?: function,
|
|
536
558
|
* claudeConfigPath?: string,
|
|
537
559
|
* projectDir?: string,
|
|
538
560
|
* }} [opts]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Quick 260911-iko (D-01 through D-08) -- the opaque per-install id Theo
|
|
8
|
+
* uses to bucket "one install calling twice" apart from "two installs
|
|
9
|
+
* calling once", without ever learning who the install belongs to.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT IS FOR: Theo needs a bucket key. Today it has none, and every
|
|
12
|
+
* substitute it could reach for instead (a key, a hostname, an account id)
|
|
13
|
+
* would be user data crossing the Brain boundary, which Canon Part 8
|
|
14
|
+
* forbids. A 128-bit coin flip minted locally and sent as a header is the
|
|
15
|
+
* one answer that gives Theo the bucketing and gives the user nothing to
|
|
16
|
+
* leak.
|
|
17
|
+
*
|
|
18
|
+
* WHY A CSPRNG VALUE IS A GENERIC HANDLE, NOT USER DATA (D-07): the value
|
|
19
|
+
* is `crypto.randomBytes(16)`, 16 bytes straight out of the platform CSPRNG
|
|
20
|
+
* with NO INPUT at all. There is no function from the user, the machine,
|
|
21
|
+
* the account, the room, the path, the hostname, or the Brain key to this
|
|
22
|
+
* value, so there is nothing to invert. A hash of an identifier would still
|
|
23
|
+
* BE that identifier wearing a hat: the same user on two installs would
|
|
24
|
+
* hash to the same bucket, and anyone holding the identifier could confirm
|
|
25
|
+
* a match. A random 128-bit value cannot do either. It carries exactly one
|
|
26
|
+
* bit of meaning: "the caller that sent this header before is the caller
|
|
27
|
+
* sending it now."
|
|
28
|
+
*
|
|
29
|
+
* THE EXPLICIT FORBIDDEN LIST -- this module must NEVER derive the id from:
|
|
30
|
+
* - os.hostname()
|
|
31
|
+
* - os.userInfo() / os.userInfo().username
|
|
32
|
+
* - process.env.USER / process.env.USERNAME / process.env.LOGNAME
|
|
33
|
+
* - process.cwd()
|
|
34
|
+
* - a home directory path
|
|
35
|
+
* - a MAC address or machine id
|
|
36
|
+
* - an account id or room name
|
|
37
|
+
* - the Brain key (process.env.MINDRIAN_BRAIN_KEY, resolve-brain-key.cjs)
|
|
38
|
+
* - a hash of any of the above (crypto's createHash)
|
|
39
|
+
* tests/test-339-install-id-header.cjs arm 9 scans THIS FILE with comments
|
|
40
|
+
* stripped and fails the suite if any of those tokens appears outside this
|
|
41
|
+
* prose -- so this comment is safe and LOAD-BEARING, and must stay.
|
|
42
|
+
*
|
|
43
|
+
* NEVER LOGGED, NEVER PRINTED IN FULL (D-03): doctor reports presence only,
|
|
44
|
+
* and on rotation the word "rotated" -- never the value. The id itself is
|
|
45
|
+
* the user's own to read from their own file.
|
|
46
|
+
*
|
|
47
|
+
* Posture copied deliberately from lib/core/brain-prewarm.cjs: this module
|
|
48
|
+
* NEVER throws to its caller and NEVER writes to stdout, because it can run
|
|
49
|
+
* inside an MCP stdio process where a stray stdout byte corrupts the
|
|
50
|
+
* JSON-RPC transport. A single stderr line, guarded by MINDRIAN_DEBUG, is
|
|
51
|
+
* permitted, and it prints the FILE PATH or an error message only, never
|
|
52
|
+
* the id value.
|
|
53
|
+
*
|
|
54
|
+
* CJS only, no new dependencies: require only fs, path, os, crypto.
|
|
55
|
+
* No em-dashes (hyphens only).
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const path = require('path');
|
|
60
|
+
const os = require('os');
|
|
61
|
+
const crypto = require('crypto');
|
|
62
|
+
|
|
63
|
+
const INSTALL_ID_HEADER_NAME = 'x-theo-install-id';
|
|
64
|
+
const ID_SHAPE_RE = /^[a-f0-9]{32}$/;
|
|
65
|
+
const FILE_NAME = 'theo-install-id.json';
|
|
66
|
+
|
|
67
|
+
const debugLog = (msg) => {
|
|
68
|
+
if (!process.env.MINDRIAN_DEBUG) return;
|
|
69
|
+
try {
|
|
70
|
+
process.stderr.write('[install-id] ' + msg + '\n');
|
|
71
|
+
} catch (_e) {
|
|
72
|
+
// swallow -- this function must never throw
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* installIdPath(homeDir) -- the on-disk location of the id file.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately duplicates the SAME resolution expression as
|
|
80
|
+
* lib/core/brain-prewarm.cjs::markerPath rather than extracting a shared
|
|
81
|
+
* helper -- two call sites is below the threshold where coupling two
|
|
82
|
+
* never-throws modules beats a duplicated two-line expression. Safety net:
|
|
83
|
+
* tests/test-339-install-id-header.cjs arm 10 asserts both modules resolve
|
|
84
|
+
* to the same directory (the drift guard). Extraction trigger: a THIRD
|
|
85
|
+
* call site.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} [homeDir] defaults to MINDRIAN_HOME or ~/.mindrian
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
function installIdPath(homeDir) {
|
|
91
|
+
const home = homeDir || process.env.MINDRIAN_HOME || path.join(os.homedir(), '.mindrian');
|
|
92
|
+
return path.join(home, FILE_NAME);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read the id file and return the id string, or null on any failure, any
|
|
97
|
+
* shape mismatch, or a fresh homeDir with no file yet. Never mints, never
|
|
98
|
+
* writes, never throws.
|
|
99
|
+
* @param {string} filePath
|
|
100
|
+
* @returns {string|null}
|
|
101
|
+
*/
|
|
102
|
+
function _readValidId(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
if (parsed && typeof parsed.id === 'string' && ID_SHAPE_RE.test(parsed.id)) {
|
|
107
|
+
return parsed.id;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
} catch (_e) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* peekInstallId({ homeDir }) -- read-only. Returns the id when the file
|
|
117
|
+
* holds a valid one, otherwise null. NEVER mints, NEVER writes, NEVER
|
|
118
|
+
* throws. This exists so the doctor can report presence without a
|
|
119
|
+
* diagnostic run silently creating the thing it is diagnosing.
|
|
120
|
+
* @param {{homeDir?: string}} [opts]
|
|
121
|
+
* @returns {string|null}
|
|
122
|
+
*/
|
|
123
|
+
function peekInstallId(opts) {
|
|
124
|
+
const o = opts || {};
|
|
125
|
+
try {
|
|
126
|
+
return _readValidId(installIdPath(o.homeDir));
|
|
127
|
+
} catch (_e) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Atomically write { id, minted_at } to installIdPath(homeDir), mode 0600.
|
|
134
|
+
* Every fs operation is wrapped; any failure returns false and leaves no
|
|
135
|
+
* temp file behind.
|
|
136
|
+
* @param {string} homeDir
|
|
137
|
+
* @param {string} id
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function _atomicWrite(homeDir, id) {
|
|
141
|
+
const finalPath = installIdPath(homeDir);
|
|
142
|
+
const dir = path.dirname(finalPath);
|
|
143
|
+
const tmpPath = path.join(
|
|
144
|
+
dir,
|
|
145
|
+
FILE_NAME + '.tmp-' + process.pid + '-' + crypto.randomBytes(3).toString('hex')
|
|
146
|
+
);
|
|
147
|
+
const body = { id: id, minted_at: new Date().toISOString() };
|
|
148
|
+
const json = JSON.stringify(body);
|
|
149
|
+
try {
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
151
|
+
fs.writeFileSync(tmpPath, json, { encoding: 'utf8', mode: 0o600 });
|
|
152
|
+
fs.renameSync(tmpPath, finalPath);
|
|
153
|
+
if (process.platform !== 'win32') {
|
|
154
|
+
try {
|
|
155
|
+
fs.chmodSync(finalPath, 0o600);
|
|
156
|
+
} catch (_e) {
|
|
157
|
+
// belt only -- the write-time mode already applied it.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
debugLog('id written: ' + finalPath);
|
|
161
|
+
return true;
|
|
162
|
+
} catch (e) {
|
|
163
|
+
debugLog('id write failed: ' + (e && e.message ? e.message : String(e)));
|
|
164
|
+
try {
|
|
165
|
+
fs.unlinkSync(tmpPath);
|
|
166
|
+
} catch (_e) {
|
|
167
|
+
// no temp file to clean up, or already gone.
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* getInstallId({ homeDir }) -- mint-once. peekInstallId first; on a hit
|
|
175
|
+
* return it. On a miss, mint a fresh 32-hex id from crypto.randomBytes(16),
|
|
176
|
+
* write it atomically, then RE-CHECK for a concurrent winner: two processes
|
|
177
|
+
* making their first Brain call at the same instant must converge on ONE
|
|
178
|
+
* bucket, not split it. Every failure returns null rather than throwing.
|
|
179
|
+
*
|
|
180
|
+
* Returning null rather than an unpersisted in-memory id on a write
|
|
181
|
+
* failure is deliberate: a volatile id would send a different value on
|
|
182
|
+
* every process and quietly break the one-install-one-id property the
|
|
183
|
+
* whole header exists to provide.
|
|
184
|
+
*
|
|
185
|
+
* @param {{homeDir?: string}} [opts]
|
|
186
|
+
* @returns {string|null}
|
|
187
|
+
*/
|
|
188
|
+
function getInstallId(opts) {
|
|
189
|
+
const o = opts || {};
|
|
190
|
+
const existing = peekInstallId(o);
|
|
191
|
+
if (existing) return existing;
|
|
192
|
+
|
|
193
|
+
const minted = crypto.randomBytes(16).toString('hex');
|
|
194
|
+
const wrote = _atomicWrite(o.homeDir, minted);
|
|
195
|
+
if (!wrote) {
|
|
196
|
+
// Concurrent-winner re-check even on our own write failure: another
|
|
197
|
+
// process may have won the race while we were failing.
|
|
198
|
+
return peekInstallId(o);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Concurrent-winner re-check: if another process's mint landed between
|
|
202
|
+
// our write and this read, defer to it so both processes converge on one
|
|
203
|
+
// bucket rather than splitting into two.
|
|
204
|
+
const afterWrite = peekInstallId(o);
|
|
205
|
+
if (afterWrite && afterWrite !== minted) {
|
|
206
|
+
return afterWrite;
|
|
207
|
+
}
|
|
208
|
+
return afterWrite || minted;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* resetInstallId({ homeDir }) -- mints unconditionally and REPLACES,
|
|
213
|
+
* skipping the concurrent-winner re-check (rotation must win over an
|
|
214
|
+
* existing file by definition). Same atomic write, same mode, same
|
|
215
|
+
* never-throws contract.
|
|
216
|
+
* @param {{homeDir?: string}} [opts]
|
|
217
|
+
* @returns {string|null}
|
|
218
|
+
*/
|
|
219
|
+
function resetInstallId(opts) {
|
|
220
|
+
const o = opts || {};
|
|
221
|
+
const minted = crypto.randomBytes(16).toString('hex');
|
|
222
|
+
const wrote = _atomicWrite(o.homeDir, minted);
|
|
223
|
+
return wrote ? minted : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
installIdHeaderName: INSTALL_ID_HEADER_NAME,
|
|
228
|
+
installIdPath: installIdPath,
|
|
229
|
+
peekInstallId: peekInstallId,
|
|
230
|
+
getInstallId: getInstallId,
|
|
231
|
+
resetInstallId: resetInstallId,
|
|
232
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// Phase 198-08 (SPEC-5, D-06) -- hook-adapter-audit: the measured
|
|
3
3
|
// "adapter-only" budget for MIGRATED hook scripts (enumerated from
|
|
4
|
-
//
|
|
4
|
+
// data/hooks-markers.json's _mcpFirst198Migrated marker, moved there by
|
|
5
|
+
// quick task 260911-juq because the Claude Code hook loader warns on
|
|
6
|
+
// unknown top-level keys in hooks/hooks.json). Two checks:
|
|
5
7
|
// 1. IMPORT AUDIT -- a migrated hook script's own require() calls (after
|
|
6
8
|
// comment-stripping, so a header/doc comment naming lib/core does not
|
|
7
9
|
// self-invalidate the gate) never reach into lib/core, lib/workflow, or
|
|
@@ -14,10 +16,10 @@
|
|
|
14
16
|
// cannot silently re-fatten a thin adapter back into a business-logic
|
|
15
17
|
// script without this test failing.
|
|
16
18
|
//
|
|
17
|
-
// Scope: ONLY the surfaces hooks.json's own migration marker
|
|
18
|
-
// statusline + SessionStart THIS plan; Stop-gate scripts are
|
|
19
|
-
// concern and are never enumerated here -- enumeration comes from
|
|
20
|
-
// marker, not a hand-maintained list, so Plan 09 adding its own marker
|
|
19
|
+
// Scope: ONLY the surfaces data/hooks-markers.json's own migration marker
|
|
20
|
+
// names (D-05: statusline + SessionStart THIS plan; Stop-gate scripts are
|
|
21
|
+
// Plan 09's concern and are never enumerated here -- enumeration comes from
|
|
22
|
+
// the marker, not a hand-maintained list, so Plan 09 adding its own marker
|
|
21
23
|
// entries later does not require touching this file).
|
|
22
24
|
//
|
|
23
25
|
// No em-dashes. CJS only.
|
|
@@ -26,7 +28,7 @@ const fs = require('node:fs');
|
|
|
26
28
|
const path = require('node:path');
|
|
27
29
|
|
|
28
30
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
29
|
-
const
|
|
31
|
+
const HOOKS_MARKERS_PATH = path.join(REPO_ROOT, 'data', 'hooks-markers.json');
|
|
30
32
|
|
|
31
33
|
// Measured baseline + a small margin (D-06: "set the budget from the thin
|
|
32
34
|
// post-migration line counts with a small margin; record the exact numbers
|
|
@@ -68,15 +70,17 @@ const LINE_BUDGETS = {
|
|
|
68
70
|
const FORBIDDEN_IMPORT_PATTERN = /require\(.*lib\/core|require\(.*lib\/workflow|require\(.*lib\/memory/;
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
|
-
* migratedSurfaces() -- read hooks.json's
|
|
72
|
-
* marker (Task 1
|
|
73
|
+
* migratedSurfaces() -- read data/hooks-markers.json's
|
|
74
|
+
* _mcpFirst198Migrated.surfaces marker (Task 1; moved out of hooks/hooks.json
|
|
75
|
+
* by quick task 260911-juq so the hook loader stops warning on unknown
|
|
76
|
+
* top-level keys) and return the list of migrated script paths (repo-root
|
|
73
77
|
* relative). Never throws; a missing/malformed marker returns [].
|
|
74
78
|
*
|
|
75
79
|
* @returns {string[]}
|
|
76
80
|
*/
|
|
77
81
|
function migratedSurfaces() {
|
|
78
82
|
try {
|
|
79
|
-
const raw = fs.readFileSync(
|
|
83
|
+
const raw = fs.readFileSync(HOOKS_MARKERS_PATH, 'utf8');
|
|
80
84
|
const parsed = JSON.parse(raw);
|
|
81
85
|
const marker = parsed && parsed._mcpFirst198Migrated;
|
|
82
86
|
const surfaces = marker && Array.isArray(marker.surfaces) ? marker.surfaces : [];
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindrian_os/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.37",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@mindrian_os/cli",
|
|
9
|
-
"version": "2.0.0-beta.
|
|
9
|
+
"version": "2.0.0-beta.37",
|
|
10
10
|
"license": "BSL-1.1",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@ig3/markdown-it-wikilinks": "^1.0.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindrian_os/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.37",
|
|
4
4
|
"description": "Install MindrianOS into Claude Code with one command -- `npx @mindrian_os/cli`. Ships the MindrianOS plugin (Larry + PWS methodology + Data Room) plus a setup/diagnostics CLI (install/doctor/update).",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"mcp": "node bin/mindrian-mcp-server.cjs",
|
package/scripts/doctor.cjs
CHANGED
|
@@ -255,6 +255,12 @@ function parseArgs(argv) {
|
|
|
255
255
|
// reads through the read-only navigation door + a LOCAL queue write; zero
|
|
256
256
|
// network surface (Canon Part 8).
|
|
257
257
|
graphDeriveHealth: false,
|
|
258
|
+
// Quick 260911-iko (D-05): --reset-install-id is a SIBLING flag, like
|
|
259
|
+
// --report-registration-bug, NOT a class flag: it is deliberately NOT
|
|
260
|
+
// added to the --all activation block below, and it carries its own
|
|
261
|
+
// always-exit-0 contract (rotation either succeeds or the state dir is
|
|
262
|
+
// not writable; neither case is a doctor-drift finding).
|
|
263
|
+
resetInstallId: false,
|
|
258
264
|
};
|
|
259
265
|
for (const arg of argv) {
|
|
260
266
|
if (arg === '--fix') flags.fix = true;
|
|
@@ -286,6 +292,10 @@ function parseArgs(argv) {
|
|
|
286
292
|
// the --all activation block below (same exclusion rationale as
|
|
287
293
|
// --check-rs-engine: --all is class A-M drift detection, not escalation).
|
|
288
294
|
else if (arg === '--report-registration-bug') flags.reportRegistrationBug = true;
|
|
295
|
+
// Quick 260911-iko (D-05): --reset-install-id rotates the opaque
|
|
296
|
+
// per-install bucket key. SIBLING flag, own exit-0 contract, see the
|
|
297
|
+
// parseArgs default comment above.
|
|
298
|
+
else if (arg === '--reset-install-id') flags.resetInstallId = true;
|
|
289
299
|
else if (arg === '--post-update') flags.postUpdate = true;
|
|
290
300
|
else if (arg === '--dogfood-acceptance') flags.dogfoodAcceptance = true;
|
|
291
301
|
else if (arg === '--claims') flags.claims = true;
|
|
@@ -461,6 +471,11 @@ Environment readiness probes (Phase 127.2 Plan 03 -- separate from class flags):
|
|
|
461
471
|
host-side core bug. stdout only; --json for the machine shape.
|
|
462
472
|
Exit 0 whenever the report assembles (even offline); non-zero
|
|
463
473
|
only if the assembler itself throws. NOT in --all.
|
|
474
|
+
--reset-install-id (Quick task 260911-iko) rotates the opaque per-install bucket
|
|
475
|
+
key the plugin sends to Theo as the x-theo-install-id header.
|
|
476
|
+
Mints a fresh id, replaces the state-dir file, prints exactly
|
|
477
|
+
"install id rotated", never prints the value, and always exits
|
|
478
|
+
0 (even when the state dir is not writable). NOT in --all.
|
|
464
479
|
--post-update (Phase 127.2 Plan 04 Instance #7) atomically activate
|
|
465
480
|
freshly-landed cache-staging bytes via scripts/post-update-
|
|
466
481
|
activation.cjs (delegates to --fix pipeline + writes the
|
|
@@ -3143,6 +3158,26 @@ function main() {
|
|
|
3143
3158
|
return;
|
|
3144
3159
|
}
|
|
3145
3160
|
|
|
3161
|
+
// Quick 260911-iko (D-05): --reset-install-id dispatch. Dispatched BEFORE
|
|
3162
|
+
// the class-flag block and BEFORE --acceptance, same position/shape as
|
|
3163
|
+
// --bind-check above: own dispatch, own always-exit-0 contract. Never
|
|
3164
|
+
// prints the id value, not even on the failure path (D-03).
|
|
3165
|
+
if (flags.resetInstallId) {
|
|
3166
|
+
let rotated = null;
|
|
3167
|
+
try {
|
|
3168
|
+
rotated = require(path.join(__dirname, '..', 'lib', 'core', 'install-id.cjs')).resetInstallId();
|
|
3169
|
+
} catch (_e) {
|
|
3170
|
+
rotated = null;
|
|
3171
|
+
}
|
|
3172
|
+
if (typeof rotated === 'string') {
|
|
3173
|
+
console.log('install id rotated');
|
|
3174
|
+
} else {
|
|
3175
|
+
console.log('install id rotation failed (state dir not writable)');
|
|
3176
|
+
}
|
|
3177
|
+
process.exit(0);
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3146
3181
|
// Phase 123 Plan-04: release-gate runner. --acceptance has its own exit-
|
|
3147
3182
|
// code contract (0 = all points passed; 1 = any point failed); HARD ABORT
|
|
3148
3183
|
// -- no --allow override (per CONTEXT D-16: release infra is the one gate
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: doctor
|
|
|
3
3
|
description: "Diagnose and optionally repair MindrianOS install: install-cache drift, sentinel gaps, active-room guard, surface-verification, ROOM.md drift, UI compliance, statusline visibility, Brain smoke, and a paste-ready command-registration bug report for Anthropic"
|
|
4
4
|
license: BSL-1.1. See LICENSE for complete terms (Business Source License 1.1, Change Date 2030-04-16 to Apache License 2.0).
|
|
5
5
|
help_jtbd: "Diagnose and optionally repair an off-feeling install."
|
|
6
|
-
argument-hint: "[--fix] [--all] [--cascade-rooms] [--graph-derive-health] [--heal-room] [--verify-surface] [--room-md] [--ui-compliance] [--statusline-visibility] [--card-fire-health] [--install-state] [--stale-first-touch] [--deprecated-usage] [--brain-smoke] [--eureka-smoke] [--drift] [--report-registration-bug] [--acceptance] [--pre-tag] [--pre-flight] [--dogfood-acceptance] [--claims] [--check-rs-engine] [--post-update] [--bind-check] [--simulate-write] [--scan-commands] [--scan-scripts] [--light-npx] [--dry-run] [--json]"
|
|
6
|
+
argument-hint: "[--fix] [--all] [--cascade-rooms] [--graph-derive-health] [--heal-room] [--verify-surface] [--room-md] [--ui-compliance] [--statusline-visibility] [--card-fire-health] [--install-state] [--stale-first-touch] [--deprecated-usage] [--brain-smoke] [--eureka-smoke] [--drift] [--report-registration-bug] [--reset-install-id] [--acceptance] [--pre-tag] [--pre-flight] [--dogfood-acceptance] [--claims] [--check-rs-engine] [--post-update] [--bind-check] [--simulate-write] [--scan-commands] [--scan-scripts] [--light-npx] [--dry-run] [--json]"
|
|
7
7
|
body_shape: E (Action Report)
|
|
8
8
|
hitl_shape: "F.0"
|
|
9
9
|
hitl_why: "It surfaces one diagnosed repair for a single approve-or-defer decision."
|
|
@@ -123,6 +123,7 @@ These siblings own their own exit contracts and are not part of the class-flag d
|
|
|
123
123
|
- `--dogfood-acceptance` -> the Canon Part 6 dog-fooding acceptance pass.
|
|
124
124
|
- `--claims` -> the claims-verification leg.
|
|
125
125
|
- `--check-rs-engine` -> the reason-select engine probe (NOT part of `--all`).
|
|
126
|
+
- `--reset-install-id` -> rotates the opaque per-install bucket key the plugin sends to Theo, prints `install id rotated`, never prints the value, and always exits 0. Full contract: `docs/THEO-INSTALL-ID.md`.
|
|
126
127
|
- `--post-update` -> the post-`claude plugin update` confirmation pass.
|
|
127
128
|
- `--bind-check <roomDir>` -> a lightweight LOCAL room-health check run at BIND-TIME (never per-turn, never a Brain call). NEVER-BLOCK: an unhealthy room degrades to an advisory and STILL exits 0.
|
|
128
129
|
- `--light-npx` -> the lighter npx-roundtrip variant for `--acceptance`.
|