mirror-mind 0.38.0 → 0.38.1
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/docs/releases/index.md +1 -0
- package/docs/releases/v0.33.3.md +9 -1
- package/docs/releases/v0.38.1.md +152 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/plugin.json +1 -1
- package/ts/src/frontDoor/cli.ts +32 -103
- package/ts/src/frontDoor/consultRoute.ts +4 -2
- package/ts/src/frontDoor/dbPath.ts +80 -7
- package/ts/src/frontDoor/initRoute.ts +190 -0
- package/ts/src/frontDoor/mirrorHomes.ts +95 -0
- package/ts/src/frontDoor/namedRefusal.ts +29 -1
- package/ts/src/frontDoor/runtimeRoute.ts +9 -5
- package/ts/src/frontDoor/searchRoute.ts +31 -3
- package/ts/src/frontDoor/usage.ts +17 -3
- package/ts/src/init/init.ts +2 -1
- package/ts/src/providers/config.ts +88 -1
- package/ts/src/providers/embedding.ts +26 -3
- package/ts/src/providers/familyProviders.ts +43 -0
- package/ts/src/runtime/diagnose.ts +28 -1
- package/ts/src/runtime/git.ts +153 -42
- package/ts/src/runtime/status.ts +27 -17
- package/ts/src/runtime/strategies/clone.ts +76 -55
- package/ts/src/runtime/update.ts +2 -1
- package/ts/src/search/memorySearch.ts +4 -2
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,7 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.38.1 — The Refusal Names the Way Out](v0.38.1.md) — patch release of three Change Requests, CR185, CR186, and CR187: a package with no home configured names the homes it finds and the `mirror init <name>` that selects one, and `init` adopts a home that already exists; a missing or refused OpenRouter key is one line that names the file, before anything is touched; a `stable` clone ahead only by `main`'s commits is told that `stable` is behind it, not to push or reset. No migration.
|
|
15
16
|
- [v0.38.0 — The Model Behind the Memories, Chosen by Measurement](v0.38.0.md) — minor release closing CV9.DS9: the model behind memories, tasks, titles, summaries, tags, reception, journal, shadow, and consolidation is Claude Haiku 5.5 at reasoning off, chosen on the eval suite with reasoning on and off and on 24 of the owner's own conversations ranked blind, where it came first on 16 of 26 surfaces. Every extraction-pin call can carry a reasoning effort, and a pin carries its own default. With CR180, summaries open with their subject, every lifecycle prompt sees the owner's name, and a framed opening is written once more. Release gate 70 of 70 probes. Rollback is one env line. No migration.
|
|
16
17
|
- [v0.37.0 — The Mirror Stays in the Conversation](v0.37.0.md) — minor release closing CV9.DS8 and, with CV21.DS11, CV21: on Pi, Claude Code, Codex, and Antigravity one doorman in the core puts Mirror's context before the model on every turn of Mirror Mode, all of it on the turn Mirror Mode starts and then the persona and memories each question calls for; an explicit turn loads once; one signature, `◇ persona-name`; Pi reaches L4, measured live. One parity matrix in the runtime interface contract, sourced and held by a test. Codex, Antigravity, and Grok Build need their plugin copy brought level. Migration 018, applied on its own.
|
|
17
18
|
- [v0.36.3 — A Skill You Invoke Is Not Something You Said](v0.36.3.md) — patch release of one Change Request, CR160, with CR159's docs: on Pi, a skill picked from the menu or typed is no longer stored as the person's words, and neither is its reply; a question asked with `/mm-mirror` is stored as the question. Pi's turns go through the rule the other runtimes have. `mirror conversations skill-turns` repairs what earlier versions stored, only on `--apply` and after a verified backup. No migration.
|
package/docs/releases/v0.33.3.md
CHANGED
|
@@ -111,7 +111,8 @@ hand:
|
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
# a clone: in the checkout
|
|
114
|
-
git
|
|
114
|
+
git fetch origin
|
|
115
|
+
git merge --ff-only origin/stable # origin/main if the clone follows main
|
|
115
116
|
# a package
|
|
116
117
|
npm install -g mirror-mind@latest
|
|
117
118
|
```
|
|
@@ -124,6 +125,13 @@ mirror runtime update # or: npm install -g mirror-mind
|
|
|
124
125
|
|
|
125
126
|
There is no database migration and no runtime wiring change.
|
|
126
127
|
|
|
128
|
+
*Corrected on 2026-10-29:* the clone line first read `git pull --ff-only`. A clone follows
|
|
129
|
+
`stable` unless you chose `main`. On `main`, that command pulls past the release, so the
|
|
130
|
+
next `runtime update` refused with `plan: local commits present` (CR186). If that happened
|
|
131
|
+
to you, your checkout is fine. Every later release contains the commits it pulled, so
|
|
132
|
+
`runtime update` works again from the next release on. `mirror runtime channel main`
|
|
133
|
+
follows `main` instead.
|
|
134
|
+
|
|
127
135
|
## Conscious Exclusions
|
|
128
136
|
|
|
129
137
|
- **A Windows product.** REFERENCE.md's platform envelope stands: POSIX. The Windows
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.38.1 is a patch release of three Change Requests, CR185, CR186, and CR187, two of
|
|
4
|
+
them from Leandro Garcia's report on PR #42. Where Mirror refused to go on, it now
|
|
5
|
+
says what to do next. A package with no home configured names the homes it finds and
|
|
6
|
+
the `mirror init <name>` that selects one, and `init` adopts a home that already
|
|
7
|
+
exists. A missing or refused OpenRouter key is one line that names the file the key
|
|
8
|
+
goes in, said before anything is touched. A `stable` clone that took `main`'s commits
|
|
9
|
+
early is told that `stable` is behind it, not to push or reset. No migration.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# v0.38.1 — The Refusal Names the Way Out
|
|
13
|
+
|
|
14
|
+
**Date:** 2026-10-30
|
|
15
|
+
|
|
16
|
+
## Highlights
|
|
17
|
+
|
|
18
|
+
- **A package with no home configured says where it looks and what to run**
|
|
19
|
+
([CR185](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr185-a-package-with-no-home-configured-does-not-say-where-it-looks.md)).
|
|
20
|
+
The error names the homes it finds under `~/.mirror-minds`, the `mirror init <name>`
|
|
21
|
+
that selects one, and the file that command writes. `mirror init <name>` now adopts a
|
|
22
|
+
home that already exists, changing nothing in it, so moving from a clone to the
|
|
23
|
+
package is one command. The bare `mirror` names the home too.
|
|
24
|
+
- **A missing OpenRouter key stops the command before it touches anything**
|
|
25
|
+
([CR187](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr187-a-missing-openrouter-key-crashes-three-commands-with-a-stack-trace-that-names-no-file.md)).
|
|
26
|
+
The six commands that cannot work without it (`consult`, `journal`, `week plan`,
|
|
27
|
+
`descriptor generate`, `consolidate scan`, and `shadow scan`) refuse in one line that
|
|
28
|
+
says what was not done and where the key goes, and exit 2. `journal` says the entry
|
|
29
|
+
was not saved, instead of crashing after it had copied the database into a snapshot.
|
|
30
|
+
A key that OpenRouter refuses is one line too. `runtime status`, `runtime diagnose`,
|
|
31
|
+
and a degraded search say when the key is missing.
|
|
32
|
+
- **A `stable` clone ahead only by `main`'s commits is told that `stable` is behind it**
|
|
33
|
+
([CR186](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr186-runtime-update-reads-main-s-commits-as-local-work.md)).
|
|
34
|
+
The ways out are `runtime channel main` or the next release, in the words a package
|
|
35
|
+
install already used. It is no longer told to push commits the remote already has, or
|
|
36
|
+
to reset its code below a database that may be newer than `stable`'s. Real local work
|
|
37
|
+
keeps the push-or-reset refusal.
|
|
38
|
+
- No migration, and no model or prompt change.
|
|
39
|
+
|
|
40
|
+
## Where We Started
|
|
41
|
+
|
|
42
|
+
v0.33.3 fixed installs under a non-ASCII path, which Leandro Garcia reproduced on PR #42.
|
|
43
|
+
On 2026-10-17 he came back with two bumps he met while following the update
|
|
44
|
+
instructions:
|
|
45
|
+
|
|
46
|
+
- Moving to the package, `runtime update` stopped with "Mirror home is not configured"
|
|
47
|
+
and did not say where the package looks.
|
|
48
|
+
- On a clone, the release notes' manual step, `git pull --ff-only`, pulled `main` past
|
|
49
|
+
the release. The next `runtime update` refused with `local commits present` and
|
|
50
|
+
"push or reset", for commits that were not his.
|
|
51
|
+
|
|
52
|
+
The answer came on 2026-10-29, twelve days late. The v0.33.3 notes were corrected to
|
|
53
|
+
`git fetch origin` and `git merge --ff-only origin/stable`. CR185's review turned up a
|
|
54
|
+
third refusal of the same kind: with no `OPENROUTER_API_KEY`, `consult`, `journal`, and
|
|
55
|
+
`week plan` crashed with a stack trace that named no file, and `journal` lost the entry.
|
|
56
|
+
|
|
57
|
+
## What Changed
|
|
58
|
+
|
|
59
|
+
**A home the error can name (CR185).** One module now owns the homes root,
|
|
60
|
+
`~/.mirror-minds`, and decides what counts as a home there: a directory with a non-empty
|
|
61
|
+
`identity/` or a `memory*.db` file. The not-configured error is one line everywhere it
|
|
62
|
+
appears. It ends with the command that finishes the job:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
Mirror home is not configured. To use the home at <root>/<name>, run: mirror init <name> (it writes MIRROR_USER=<name> to <config>).
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
There are forms for several homes, for a home whose name `init` would refuse, and for
|
|
69
|
+
none. `init <name>` on an existing home copies nothing and changes nothing in it. It
|
|
70
|
+
writes `MIRROR_USER` to an owner-only config file when no home is selected, and prints
|
|
71
|
+
only what is left to do: the key, `mirror seed` when there is no database, and the
|
|
72
|
+
runtime wiring. Selecting the only home automatically was considered and refused. A
|
|
73
|
+
silent fallback once caused the wrong-session export incident, so one explicit command,
|
|
74
|
+
whose effect is kept in a file, is the honest minimum.
|
|
75
|
+
|
|
76
|
+
**A key checked before anything is touched (CR187).** The key used to be read at the
|
|
77
|
+
first model call, so a failing command could already have done work. A keyless
|
|
78
|
+
`journal` copied the whole database into its pre-write snapshot, then crashed. Now the
|
|
79
|
+
six commands that cannot work without the key check it before the database opens. Any
|
|
80
|
+
other command that meets a missing key is answered by the front door's handler: one
|
|
81
|
+
line, no stack trace. All the sentences come from one place and name the file the
|
|
82
|
+
install reads its configuration from. When that file does not exist yet, the line says
|
|
83
|
+
to create it readable only by its owner. A key that OpenRouter refuses (HTTP 401 or 403)
|
|
84
|
+
gets its own line, whether a chat call or an embedding call met it. `consolidate apply`
|
|
85
|
+
is not checked up front, because only a merge proposal embeds.
|
|
86
|
+
|
|
87
|
+
**The channel behind the clone (CR186).** The plan stage of `runtime update` counted
|
|
88
|
+
every commit in `origin/stable..HEAD` as the person's own. Releases only fast-forward
|
|
89
|
+
`stable` to `main`, so one more local count tells the two cases apart. When every commit
|
|
90
|
+
past `stable` is already on `origin/main`, the clone's channel is behind it. The update
|
|
91
|
+
then refuses the way the package lane refuses a channel behind the installed version,
|
|
92
|
+
and in its words: "A downgrade is refused: migrations do not run backwards.", then
|
|
93
|
+
`runtime channel main`, or staying until the next release. The captured Change Request
|
|
94
|
+
also offered a reset to `origin/stable`; it was dropped. `main`'s code can migrate the
|
|
95
|
+
database when it opens it, and `stable`'s core refuses a database newer than itself. One
|
|
96
|
+
function now reads where a clone stands, for both the update plan and the welcome card,
|
|
97
|
+
which had counted two different ways. A count that cannot run reads as local work, so
|
|
98
|
+
real work is never mistaken for published work.
|
|
99
|
+
|
|
100
|
+
**Reviewed before code, validated by hand.** Each plan was reviewed through persona
|
|
101
|
+
lenses before any code was written: five for CR185, four each for CR186 and CR187. The
|
|
102
|
+
Navigator validated each change on a natural route: CR185 and CR187 as a package user,
|
|
103
|
+
the lane Leandro was on, and CR186 in a scratch clone ahead of `stable`. CR186's reviews
|
|
104
|
+
turned its plan around. It had proposed reporting the clone as up to date, until the
|
|
105
|
+
engineer found the package lane already refusing the same state.
|
|
106
|
+
|
|
107
|
+
## Upgrading
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
mirror runtime update # or: npm install -g mirror-mind@latest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No migration, and nothing to configure. Moving from a clone to the package with a home
|
|
114
|
+
that already exists: `mirror init <your-name>` now selects it without changing anything
|
|
115
|
+
in it, and says what is left, such as the key. No skill, plugin, or extension changed in
|
|
116
|
+
this release, so a reinstall line the updater prints for Codex, Antigravity, or Grok
|
|
117
|
+
Build only moves the version number.
|
|
118
|
+
|
|
119
|
+
## Conscious Exclusions
|
|
120
|
+
|
|
121
|
+
- Transport failures other than a refused key (a timeout, an outage, a rate limit, a
|
|
122
|
+
server error) still crash the commands that call a model with a stack trace, and
|
|
123
|
+
`journal` still loses the entry
|
|
124
|
+
([CR188](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr188-a-provider-outage-crashes-the-commands-that-call-a-model-with-a-stack-trace.md)).
|
|
125
|
+
- Extraction at session end with no key is unchanged, and a configuration error still
|
|
126
|
+
counts as an attempt, which can quarantine a conversation
|
|
127
|
+
([CR178](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr178-a-configuration-error-counts-as-an-extraction-attempt-and-nothing-releases-a-quarantine.md)).
|
|
128
|
+
- With a wrong key, `descriptor generate` still prints "skipped (empty response)" for each
|
|
129
|
+
persona. That line is golden-graded.
|
|
130
|
+
- `runtime update --check` still reports `local_ahead` for a clone ahead of `stable` by
|
|
131
|
+
`main`'s commits, where a package reports `channel_behind`.
|
|
132
|
+
- A `main`-channel clone ahead by commits on another pushed branch keeps the
|
|
133
|
+
push-or-reset refusal.
|
|
134
|
+
|
|
135
|
+
## What We Learned
|
|
136
|
+
|
|
137
|
+
A validation route is code too, and it has to be run before it is handed over. CR186's
|
|
138
|
+
route, run first by the agent, never reached the plan: a `node_modules` symlink made the
|
|
139
|
+
scratch tree dirty, because `.gitignore`'s `node_modules/` matches only a directory.
|
|
140
|
+
CR187's first route could not prove that no snapshot was taken, because `ls -l` shows
|
|
141
|
+
minutes. Both were caught before the Navigator ran them.
|
|
142
|
+
|
|
143
|
+
CI runs the suite under `MEMORY_ENV=test`. CR185's first push failed a test that had
|
|
144
|
+
assumed `memory.db`. The suite is now run that way before every push.
|
|
145
|
+
|
|
146
|
+
The same state deserves the same words. CR186's answer was already written, in the
|
|
147
|
+
package lane's refusal of a channel behind the installed version.
|
|
148
|
+
|
|
149
|
+
## Next Horizon
|
|
150
|
+
|
|
151
|
+
CR188, the transport failures, is the next refusal of this kind. CV9, Mirror Mind 1.0,
|
|
152
|
+
continues, and when its done condition is met the release is `v1.0.0`.
|
package/package.json
CHANGED
package/ts/src/frontDoor/cli.ts
CHANGED
|
@@ -44,12 +44,11 @@ import { type Database, openDatabaseReadOnly, type WritableDatabase } from "#db/
|
|
|
44
44
|
import { ensureDatabaseReady } from "#db/readyOnOpen.ts";
|
|
45
45
|
import { assertSchemaState, SchemaStateError } from "#db/schemaState.ts";
|
|
46
46
|
import { allDescriptors, descriptorsByLayer } from "#descriptor/descriptorRead.ts";
|
|
47
|
-
import { newIdentityKeyProblem
|
|
47
|
+
import { newIdentityKeyProblem } from "#identity/identityKey.ts";
|
|
48
48
|
import { identityRowExists, listIdentityByLayer } from "#identity/identityRead.ts";
|
|
49
49
|
import { listJourneysForListCommand } from "#identity/journeyListing.ts";
|
|
50
50
|
import { listPersonas } from "#identity/personaListing.ts";
|
|
51
51
|
import { setIdentity } from "#identity/setIdentity.ts";
|
|
52
|
-
import { IdentityRootExistsError, initUserHome, TemplatesNotFoundError } from "#init/init.ts";
|
|
53
52
|
import { JOURNEY_PATH_LAYER } from "#journey/journeyStatus.ts";
|
|
54
53
|
import { JourneyNotFoundError } from "#journey/journeyWrite.ts";
|
|
55
54
|
import { environmentSessionId } from "#mirror/runtimeSession.ts";
|
|
@@ -61,9 +60,14 @@ import {
|
|
|
61
60
|
import { chatLedgerHook } from "#observability/ledgerHooks.ts";
|
|
62
61
|
import { runWeekSave } from "#planning/weekSave.ts";
|
|
63
62
|
import type { EmbeddingProvider } from "#providers/embedding.ts";
|
|
64
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
requireFamilyProviders,
|
|
65
|
+
requireKeyForFamily,
|
|
66
|
+
resolveFamilyProviders,
|
|
67
|
+
} from "#providers/familyProviders.ts";
|
|
65
68
|
import type { LlmProvider } from "#providers/llm.ts";
|
|
66
69
|
import {
|
|
70
|
+
CONSULT_ASK_TRANSPORT,
|
|
67
71
|
CULTIVATION_APPLY_TRANSPORT,
|
|
68
72
|
CULTIVATION_SCAN_TRANSPORT,
|
|
69
73
|
DESCRIPTOR_TRANSPORT,
|
|
@@ -72,18 +76,10 @@ import {
|
|
|
72
76
|
} from "#providers/transport.ts";
|
|
73
77
|
import { verifyBackupArchive } from "#runtime/backup.ts";
|
|
74
78
|
import { exitQuietlyOnBrokenPipe } from "#runtime/brokenPipe.ts";
|
|
75
|
-
import {
|
|
76
|
-
configFilePath,
|
|
77
|
-
loadConfiguration,
|
|
78
|
-
silenceExperimentalWarnings,
|
|
79
|
-
writeConfigValue,
|
|
80
|
-
} from "#runtime/config.ts";
|
|
81
|
-
import { runningTreeRoot } from "#runtime/treeRoot.ts";
|
|
82
|
-
import { detectRuntimes, runtimeWiringLines } from "#runtime/wiring.ts";
|
|
79
|
+
import { configFilePath, loadConfiguration, silenceExperimentalWarnings } from "#runtime/config.ts";
|
|
83
80
|
import { runSeed } from "#seed/seed.ts";
|
|
84
81
|
import { getTasksForWeek, listTasks } from "#tasks/taskStore.ts";
|
|
85
82
|
import { computeWeekRange } from "#tasks/weekView.ts";
|
|
86
|
-
import { isExecutable } from "#util/paths.ts";
|
|
87
83
|
import { PROGRAM } from "#util/program.ts";
|
|
88
84
|
import { newId, nowIso } from "#util/pyGenerators.ts";
|
|
89
85
|
import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
|
|
@@ -105,7 +101,7 @@ import {
|
|
|
105
101
|
runReject,
|
|
106
102
|
runShadowApply as runShadowApplyRoute,
|
|
107
103
|
} from "./cultivationRoute.ts";
|
|
108
|
-
import { MirrorHomeNotConfiguredError, resolveDbPath } from "./dbPath.ts";
|
|
104
|
+
import { initCommandFor, MirrorHomeNotConfiguredError, resolveDbPath } from "./dbPath.ts";
|
|
109
105
|
import { runBackupRoute, runRepairEncodingRoute } from "./dbSafetyToolsRoute.ts";
|
|
110
106
|
import { defaultExploreRouteDeps, runExploreRoute } from "./exploreRoute.ts";
|
|
111
107
|
import { frontDoorLogPath, logFrontDoor } from "./frontDoorLog.ts";
|
|
@@ -114,8 +110,10 @@ import {
|
|
|
114
110
|
isImportedConversationsCommand,
|
|
115
111
|
runImportedConversationsRoute,
|
|
116
112
|
} from "./importedConversationsRoute.ts";
|
|
113
|
+
import { runInit } from "./initRoute.ts";
|
|
117
114
|
import { applyJourneySetPath } from "./journeyWriteRoute.ts";
|
|
118
115
|
import { openLiveWriteDatabase } from "./liveBackup.ts";
|
|
116
|
+
import { findMirrorHomes, homesRoot } from "./mirrorHomes.ts";
|
|
119
117
|
import {
|
|
120
118
|
isMirrorDoorman,
|
|
121
119
|
isMirrorWrite,
|
|
@@ -454,7 +452,10 @@ function runWeekSaveWrite(argv: readonly string[]): number {
|
|
|
454
452
|
* in depth.
|
|
455
453
|
*/
|
|
456
454
|
async function runJournalContentRoute(argv: readonly string[]): Promise<number> {
|
|
457
|
-
|
|
455
|
+
// CR187: refused before the database opens, saying what was not kept.
|
|
456
|
+
const family = await requireFamilyProviders(process.env, JOURNAL_TRANSPORT, {
|
|
457
|
+
consequence: "the journal entry was not saved",
|
|
458
|
+
});
|
|
458
459
|
if (!family?.llm || !family.embedding) {
|
|
459
460
|
throw new Error("journal requires an LLM and an embedding provider");
|
|
460
461
|
}
|
|
@@ -467,14 +468,15 @@ async function runJournalContentRoute(argv: readonly string[]): Promise<number>
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
async function runWeekPlanContentRoute(argv: readonly string[]): Promise<number> {
|
|
470
|
-
const family = await
|
|
471
|
+
const family = await requireFamilyProviders(process.env, WEEK_PLAN_TRANSPORT);
|
|
471
472
|
if (!family?.llm) throw new Error("week plan requires an LLM provider");
|
|
472
473
|
const llm = family.llm;
|
|
473
474
|
return withLiveWriteDbAsync(argv, (db) => runWeekPlanRoute(db, argv.slice(2), llm));
|
|
474
475
|
}
|
|
475
476
|
|
|
476
477
|
async function runDescriptorContentRoute(argv: readonly string[]): Promise<number> {
|
|
477
|
-
|
|
478
|
+
// CR187: without the key every persona would read "skipped (empty response)".
|
|
479
|
+
const family = await requireFamilyProviders(process.env, DESCRIPTOR_TRANSPORT);
|
|
478
480
|
if (!family?.llm) throw new Error("descriptor generate requires an LLM provider");
|
|
479
481
|
const llm = family.llm;
|
|
480
482
|
const reporter = outcomeReporter(argv, "descriptor");
|
|
@@ -884,90 +886,6 @@ function isInit(argv: readonly string[]): boolean {
|
|
|
884
886
|
return argv[0] === "init";
|
|
885
887
|
}
|
|
886
888
|
|
|
887
|
-
/**
|
|
888
|
-
* Serve `init <user>` (DS7.US1 Slice B). Filesystem-only — no database is
|
|
889
|
-
* opened. Python's argparse defines only the `user` positional (no
|
|
890
|
-
* --mirror-home/--db-path), so none are stripped here.
|
|
891
|
-
*
|
|
892
|
-
* Deliberate divergence: an already-populated identity root or a missing
|
|
893
|
-
* templates root are uncaught exceptions in Python (a raw traceback, exit 1) --
|
|
894
|
-
* not a designed message. This preserves the contract (exit 1, a stderr
|
|
895
|
-
* message naming the failure) without fabricating a fake traceback.
|
|
896
|
-
*
|
|
897
|
-
* CV22.DS10.US3 (D3): when no source configured a user, `init` writes
|
|
898
|
-
* `MIRROR_USER=<user>` into `~/.config/mirror/env` (0600 in 0700) and says
|
|
899
|
-
* where to add `OPENROUTER_API_KEY` by hand. The key is never read from argv.
|
|
900
|
-
* When a user IS configured -- the checkout's `.env`, or a shell running a
|
|
901
|
-
* second mirror -- the file is left alone and the person is told how to
|
|
902
|
-
* select the new home.
|
|
903
|
-
*
|
|
904
|
-
* Plateau 3: it ends with the wiring step for each runtime found on the PATH
|
|
905
|
-
* (`#runtime/wiring.ts`), with this tree's path in it, so the person is handed
|
|
906
|
-
* the next line rather than a page. Printing is not automating.
|
|
907
|
-
*/
|
|
908
|
-
function runInit(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): number {
|
|
909
|
-
const user = argv[1];
|
|
910
|
-
// A word that starts with `-` is a flag this command does not have, not a
|
|
911
|
-
// user: `init --help` once created a home named `--help`.
|
|
912
|
-
if (!user || user.startsWith("-")) {
|
|
913
|
-
console.error("Usage: init <user>");
|
|
914
|
-
return 2;
|
|
915
|
-
}
|
|
916
|
-
// And any other word is a key: the home's directory name and the value of
|
|
917
|
-
// MIRROR_USER in the wiring commands printed below. CR104's rule for slugs
|
|
918
|
-
// and persona ids, asked before anything is created (CR141).
|
|
919
|
-
const problem = newUserNameProblem(user);
|
|
920
|
-
if (problem !== null) {
|
|
921
|
-
console.error(`Error: ${problem}`);
|
|
922
|
-
return 2;
|
|
923
|
-
}
|
|
924
|
-
try {
|
|
925
|
-
const identityRoot = initUserHome(user);
|
|
926
|
-
const mirrorHome = dirname(identityRoot);
|
|
927
|
-
const configured = Boolean(env.MIRROR_USER || env.MIRROR_HOME);
|
|
928
|
-
const configuration = configured
|
|
929
|
-
? [
|
|
930
|
-
` 1. This machine already selects a user; to use this one, run with MIRROR_USER=${user}`,
|
|
931
|
-
` or set it in ${configFilePath(env)}`,
|
|
932
|
-
]
|
|
933
|
-
: [
|
|
934
|
-
`Configuration: ${writeConfigValue("MIRROR_USER", user, { env })} (MIRROR_USER=${user})`,
|
|
935
|
-
"",
|
|
936
|
-
"Next steps:",
|
|
937
|
-
" 1. Add OPENROUTER_API_KEY=<your key> to that file (it is created readable by you only)",
|
|
938
|
-
];
|
|
939
|
-
const prints = [
|
|
940
|
-
`Created user home: ${mirrorHome}`,
|
|
941
|
-
`Identity ready at: ${identityRoot}`,
|
|
942
|
-
...(configured ? ["", "Next steps:"] : []),
|
|
943
|
-
...configuration,
|
|
944
|
-
` 2. Run: ${PROGRAM} seed`,
|
|
945
|
-
"\nYour identity is ready to use. Deepen it over time with:",
|
|
946
|
-
` ${PROGRAM} identity edit user identity`,
|
|
947
|
-
...runtimeWiringSection(env),
|
|
948
|
-
];
|
|
949
|
-
process.stdout.write(prints.map((line) => `${line}\n`).join(""));
|
|
950
|
-
return 0;
|
|
951
|
-
} catch (error) {
|
|
952
|
-
if (error instanceof IdentityRootExistsError || error instanceof TemplatesNotFoundError) {
|
|
953
|
-
console.error(error.message);
|
|
954
|
-
return 1;
|
|
955
|
-
}
|
|
956
|
-
throw error;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
/** The "Wire your runtime" tail of `init`, empty when no runtime is on the PATH. */
|
|
961
|
-
function runtimeWiringSection(env: NodeJS.ProcessEnv): string[] {
|
|
962
|
-
const detected = detectRuntimes(env, isExecutable);
|
|
963
|
-
if (detected.length === 0) return [];
|
|
964
|
-
const root = runningTreeRoot();
|
|
965
|
-
return [
|
|
966
|
-
"\nWire your runtime to this install (each step is one you run and can read first):",
|
|
967
|
-
...detected.flatMap((runtime) => ["", ...runtimeWiringLines(runtime, root)]),
|
|
968
|
-
];
|
|
969
|
-
}
|
|
970
|
-
|
|
971
889
|
function isSeed(argv: readonly string[]): boolean {
|
|
972
890
|
return argv[0] === "seed";
|
|
973
891
|
}
|
|
@@ -1408,7 +1326,7 @@ async function runConsolidateScanWrite(argv: readonly string[]): Promise<number>
|
|
|
1408
1326
|
const limit = limitRaw !== null ? Number(limitRaw) : DEFAULT_CONSOLIDATE_SCAN_LIMIT;
|
|
1409
1327
|
const thresholdRaw = optionValue(args, "--threshold");
|
|
1410
1328
|
const threshold = thresholdRaw !== null ? Number(thresholdRaw) : DEFAULT_CLUSTER_THRESHOLD;
|
|
1411
|
-
const family = await
|
|
1329
|
+
const family = await requireFamilyProviders(process.env, CULTIVATION_SCAN_TRANSPORT);
|
|
1412
1330
|
if (!family?.llm) throw new Error("consolidate scan requires an LLM provider");
|
|
1413
1331
|
const provider = family.llm;
|
|
1414
1332
|
const reporter = outcomeReporter(argv, "consolidation");
|
|
@@ -1435,7 +1353,7 @@ async function runShadowScanWrite(argv: readonly string[]): Promise<number> {
|
|
|
1435
1353
|
const args = argv.slice(2);
|
|
1436
1354
|
const limitRaw = optionValue(args, "--limit");
|
|
1437
1355
|
const limit = limitRaw !== null ? Number(limitRaw) : DEFAULT_SHADOW_SCAN_LIMIT;
|
|
1438
|
-
const family = await
|
|
1356
|
+
const family = await requireFamilyProviders(process.env, CULTIVATION_SCAN_TRANSPORT);
|
|
1439
1357
|
if (!family?.llm) throw new Error("shadow scan requires an LLM provider");
|
|
1440
1358
|
const provider = family.llm;
|
|
1441
1359
|
const reporter = outcomeReporter(argv, "shadow_scan");
|
|
@@ -1663,6 +1581,9 @@ function resolveLogPath(argv: readonly string[]): string | null {
|
|
|
1663
1581
|
* itself, extended to db resolution.
|
|
1664
1582
|
*/
|
|
1665
1583
|
async function runConsultAskWithDb(argv: readonly string[]): Promise<number> {
|
|
1584
|
+
// CR187: before the ledger database opens, because that open takes the
|
|
1585
|
+
// pre-write snapshot; the route checks again, for `consult credits`.
|
|
1586
|
+
requireKeyForFamily(process.env, CONSULT_ASK_TRANSPORT);
|
|
1666
1587
|
const db = tryOpenDbForConsultLogging(argv);
|
|
1667
1588
|
try {
|
|
1668
1589
|
process.stdout.write(await runConsultRoute(db, argv.slice(1)));
|
|
@@ -1904,9 +1825,17 @@ export async function main(rawArgv = process.argv.slice(2)): Promise<number> {
|
|
|
1904
1825
|
// name is whatever the caller typed, and it stays out of the log.
|
|
1905
1826
|
if (decision.engine === "usage") {
|
|
1906
1827
|
const env = process.env;
|
|
1828
|
+
const userConfigured = Boolean(
|
|
1829
|
+
env.MIRROR_USER || env.MIRROR_HOME || env.MEMORY_DIR || env.DB_PATH,
|
|
1830
|
+
);
|
|
1907
1831
|
const answer = answerUsage(decision.request, {
|
|
1908
|
-
userConfigured
|
|
1832
|
+
userConfigured,
|
|
1909
1833
|
configFile: configFilePath(env),
|
|
1834
|
+
// CR185: a home on disk that nothing selects is named, read only when no
|
|
1835
|
+
// user is configured -- the one case the orientation is rendered.
|
|
1836
|
+
useHome: userConfigured
|
|
1837
|
+
? null
|
|
1838
|
+
: (initCommandFor(findMirrorHomes(), homesRoot())?.sentence ?? null),
|
|
1910
1839
|
});
|
|
1911
1840
|
(answer.stream === "stdout" ? process.stdout : process.stderr).write(answer.text);
|
|
1912
1841
|
logFrontDoor(logPath, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseConsultArgs } from "#consult/args.ts";
|
|
2
2
|
import { runConsult } from "#consult/core.ts";
|
|
3
3
|
import type { WritableDatabase } from "#db/database.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { requireFamilyProviders } from "#providers/familyProviders.ts";
|
|
5
5
|
import { CONSULT_ASK_TRANSPORT, CONSULT_CREDITS_TRANSPORT } from "#providers/transport.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -17,7 +17,9 @@ export async function runConsultRoute(
|
|
|
17
17
|
// Per leaf, from the same specs `routing.ts` decided with: `credits` needs
|
|
18
18
|
// only a credit provider, `ask` needs a chat provider too (CV22.DS8.US3).
|
|
19
19
|
const spec = command.command === "credits" ? CONSULT_CREDITS_TRANSPORT : CONSULT_ASK_TRANSPORT;
|
|
20
|
-
|
|
20
|
+
// CR187: no provider call without the key; `ask` checked before its ledger
|
|
21
|
+
// database opened, `credits` has none to open.
|
|
22
|
+
const family = await requireFamilyProviders(process.env, spec);
|
|
21
23
|
if (!family?.credits) {
|
|
22
24
|
// `routing.ts` sends a reverted or half-configured family to Python before
|
|
23
25
|
// the route is reached, so this is defense in depth.
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
// modern `~/.mirror-minds/<user>` does not exist (one-line stderr warning,
|
|
15
15
|
// permanent support — see docs/project/decisions.md).
|
|
16
16
|
// - Nothing configured fails loudly (CV9.E2.S6: unconfigured resolution must
|
|
17
|
-
// not write to the homes root), unlike the pre-CR024 silent fallback.
|
|
17
|
+
// not write to the homes root), unlike the pre-CR024 silent fallback. The
|
|
18
|
+
// sentence says what to run and which file that writes, naming the homes it
|
|
19
|
+
// finds (CR185): a move from a clone to the package leaves a home that no
|
|
20
|
+
// configuration points at, and the person needs the command, not the cause.
|
|
18
21
|
//
|
|
19
22
|
// Front-door-only extension: the CLI flags `--db-path` and `--mirror-home`
|
|
20
23
|
// are explicit per-invocation intent and take precedence over all env vars.
|
|
@@ -22,8 +25,13 @@
|
|
|
22
25
|
import { existsSync } from "node:fs";
|
|
23
26
|
import { homedir } from "node:os";
|
|
24
27
|
import { basename, join } from "node:path";
|
|
28
|
+
import { configFilePath } from "#runtime/config.ts";
|
|
25
29
|
import { expandHome } from "#util/paths.ts";
|
|
30
|
+
import { PROGRAM } from "#util/program.ts";
|
|
31
|
+
import { pyRepr } from "#util/pythonText.ts";
|
|
32
|
+
import { shellWord } from "#util/shellWord.ts";
|
|
26
33
|
import { optionValue } from "./args.ts";
|
|
34
|
+
import { type FoundHome, findMirrorHomes, homesRoot, userHomePath } from "./mirrorHomes.ts";
|
|
27
35
|
|
|
28
36
|
/** Raised when no database location can be resolved — maps to CLI exit 2. */
|
|
29
37
|
export class MirrorHomeNotConfiguredError extends Error {}
|
|
@@ -34,7 +42,6 @@ const DB_NAMES: Record<string, string> = {
|
|
|
34
42
|
test: "memory_test.db",
|
|
35
43
|
};
|
|
36
44
|
|
|
37
|
-
const DEFAULT_USER_HOMES_DIR = ".mirror-minds";
|
|
38
45
|
const LEGACY_USER_HOMES_DIR = ".mirror";
|
|
39
46
|
|
|
40
47
|
/** Port of Python `db_name_for_env`: one (environment) → one database name. */
|
|
@@ -50,6 +57,8 @@ export interface DbPathEnv {
|
|
|
50
57
|
MEMORY_DIR?: string;
|
|
51
58
|
MIRROR_HOME?: string;
|
|
52
59
|
MIRROR_USER?: string;
|
|
60
|
+
/** Read only to name the config file in the not-configured sentence. */
|
|
61
|
+
XDG_CONFIG_HOME?: string;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/** Injection points so tests can run without touching the real filesystem. */
|
|
@@ -57,6 +66,8 @@ export interface DbPathIo {
|
|
|
57
66
|
home?: string;
|
|
58
67
|
exists?: (path: string) => boolean;
|
|
59
68
|
warn?: (message: string) => void;
|
|
69
|
+
/** The homes under `<home>/.mirror-minds`, for the not-configured sentence. */
|
|
70
|
+
homes?: (home: string) => readonly FoundHome[];
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
/**
|
|
@@ -72,7 +83,7 @@ export function resolveMirrorHome(env: DbPathEnv, io: DbPathIo = {}): string {
|
|
|
72
83
|
const explicitHome = env.MIRROR_HOME ?? "";
|
|
73
84
|
const explicitUser = env.MIRROR_USER ?? "";
|
|
74
85
|
const resolvedHome = explicitHome ? expandHome(explicitHome) : null;
|
|
75
|
-
const derivedHome = explicitUser ?
|
|
86
|
+
const derivedHome = explicitUser ? userHomePath(explicitUser, home) : null;
|
|
76
87
|
|
|
77
88
|
if (resolvedHome && explicitUser && basename(resolvedHome) !== explicitUser) {
|
|
78
89
|
throw new MirrorHomeNotConfiguredError(
|
|
@@ -94,10 +105,72 @@ export function resolveMirrorHome(env: DbPathEnv, io: DbPathIo = {}): string {
|
|
|
94
105
|
}
|
|
95
106
|
return derivedHome;
|
|
96
107
|
}
|
|
97
|
-
throw new MirrorHomeNotConfiguredError(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
throw new MirrorHomeNotConfiguredError(homeNotConfiguredMessage(env, io));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The not-configured sentence (CR185), one line: what to run, and which file
|
|
113
|
+
* that command writes.
|
|
114
|
+
*
|
|
115
|
+
* Every surface prints it: `runtime update`, `migrate`, and `backup` bare, the
|
|
116
|
+
* other commands after `Mirror TS front door:`, and `runtime status` as its
|
|
117
|
+
* `Mirror home note:`. Built only when nothing resolves, so a hook on an
|
|
118
|
+
* unconfigured machine pays one directory read for a message it discards.
|
|
119
|
+
*
|
|
120
|
+
* A home name reaches the line bare only when `init` accepts it, so the line
|
|
121
|
+
* cannot be broken, or carry an escape sequence, by a directory name. Any other
|
|
122
|
+
* home is offered `MIRROR_HOME`, its path printed as a repr.
|
|
123
|
+
*/
|
|
124
|
+
export function homeNotConfiguredMessage(env: DbPathEnv, io: DbPathIo = {}): string {
|
|
125
|
+
const home = io.home ?? homedir();
|
|
126
|
+
const config = configFilePath({ XDG_CONFIG_HOME: env.XDG_CONFIG_HOME }, home);
|
|
127
|
+
const homes = (io.homes ?? findMirrorHomes)(home);
|
|
128
|
+
const sentences: string[] = [];
|
|
129
|
+
const command = initCommandFor(homes, homesRoot(home));
|
|
130
|
+
if (command !== null) {
|
|
131
|
+
sentences.push(`${command.sentence} (it writes MIRROR_USER=${command.name} to ${config}).`);
|
|
132
|
+
}
|
|
133
|
+
for (const other of homes.filter((found) => !found.nameable)) {
|
|
134
|
+
const path = pyRepr(other.path);
|
|
135
|
+
sentences.push(`To use the home at ${path}, add MIRROR_HOME=${path} to ${config}.`);
|
|
136
|
+
}
|
|
137
|
+
if (sentences.length === 0) {
|
|
138
|
+
const create = `${PROGRAM} init <your-name>`;
|
|
139
|
+
sentences.push(
|
|
140
|
+
`To create one, run: ${create} (it writes MIRROR_USER=<your-name> to ${config}).`,
|
|
141
|
+
`If yours lives elsewhere, add MIRROR_HOME=<path> to ${config}.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return ["Mirror home is not configured.", ...sentences].join(" ");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* "To use the home at <path>, run: mirror init <name>", or the form for
|
|
149
|
+
* several, for the homes `init` can name; null when there are none. `name` is
|
|
150
|
+
* what the command takes: the one home's own name, or `<name>`. Bare `mirror`
|
|
151
|
+
* prints the same sentence.
|
|
152
|
+
*
|
|
153
|
+
* The command is built on a line of its own, its value through `shellWord`
|
|
154
|
+
* (CR104): an agent runs it as printed. A name `init` accepts is a plain token,
|
|
155
|
+
* so it prints unchanged. The path and the names around the command are prose.
|
|
156
|
+
*/
|
|
157
|
+
export function initCommandFor(
|
|
158
|
+
homes: readonly FoundHome[],
|
|
159
|
+
root: string,
|
|
160
|
+
): { sentence: string; name: string } | null {
|
|
161
|
+
const nameable = homes.filter((found) => found.nameable);
|
|
162
|
+
const [only] = nameable;
|
|
163
|
+
if (only === undefined) return null;
|
|
164
|
+
if (nameable.length === 1) {
|
|
165
|
+
const command = `${PROGRAM} init ${shellWord(only.name)}`;
|
|
166
|
+
return { sentence: `To use the home at ${only.path}, run: ${command}`, name: only.name };
|
|
167
|
+
}
|
|
168
|
+
const names = nameable.map((found) => found.name).join(", ");
|
|
169
|
+
const command = `${PROGRAM} init <name>`;
|
|
170
|
+
return {
|
|
171
|
+
sentence: `To use one of the homes in ${root} (${names}), run: ${command}`,
|
|
172
|
+
name: "<name>",
|
|
173
|
+
};
|
|
101
174
|
}
|
|
102
175
|
|
|
103
176
|
/**
|