@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.19
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 +26 -0
- package/commands/act.md +1 -0
- package/commands/admin.md +1 -0
- package/commands/analyze-needs.md +2 -0
- package/commands/analyze-systems.md +2 -0
- package/commands/analyze-timing.md +2 -0
- package/commands/auto-explore.md +2 -0
- package/commands/beautiful-question.md +2 -0
- package/commands/brain-derive.md +2 -0
- package/commands/build-knowledge.md +2 -0
- package/commands/build-thesis.md +2 -0
- package/commands/causal.md +2 -0
- package/commands/challenge-assumptions.md +2 -0
- package/commands/compare-ventures.md +2 -0
- package/commands/dashboard.md +2 -1
- package/commands/deep-grade.md +2 -0
- package/commands/diagnose.md +21 -1
- package/commands/diagnostics.md +14 -3
- package/commands/doctor.md +4 -1
- package/commands/dogfood-flush.md +92 -0
- package/commands/dominant-designs.md +2 -0
- package/commands/explain-decision.md +2 -0
- package/commands/explore-domains.md +2 -0
- package/commands/explore-futures.md +2 -0
- package/commands/explore-trends.md +2 -0
- package/commands/export.md +1 -0
- package/commands/feynman-timeline-refresh.md +2 -0
- package/commands/file-meeting.md +2 -0
- package/commands/find-analogies.md +1 -0
- package/commands/find-bottlenecks.md +2 -0
- package/commands/find-connections.md +2 -0
- package/commands/funding.md +1 -0
- package/commands/grade.md +2 -0
- package/commands/graph.md +1 -0
- package/commands/hat-briefing.md +1 -0
- package/commands/heal.md +22 -170
- package/commands/help.md +54 -334
- package/commands/hmi-status.md +23 -144
- package/commands/jtbd.md +1 -0
- package/commands/leadership.md +2 -0
- package/commands/lean-canvas.md +2 -0
- package/commands/macro-trends.md +2 -0
- package/commands/map-unknowns.md +2 -0
- package/commands/memory.md +1 -0
- package/commands/models.md +1 -0
- package/commands/mos-reason.md +2 -0
- package/commands/mos.md +139 -0
- package/commands/mullins.md +2 -0
- package/commands/mva-brief.md +2 -0
- package/commands/mva-option.md +2 -0
- package/commands/new-project.md +2 -0
- package/commands/onboard.md +20 -7
- package/commands/operator.md +1 -0
- package/commands/opportunities.md +1 -0
- package/commands/organize.md +22 -469
- package/commands/persona.md +1 -0
- package/commands/pipeline.md +2 -0
- package/commands/present.md +1 -0
- package/commands/publish.md +2 -0
- package/commands/query.md +24 -102
- package/commands/radar.md +2 -0
- package/commands/reanalyze.md +1 -0
- package/commands/research.md +2 -0
- package/commands/room.md +2 -0
- package/commands/rooms.md +1 -0
- package/commands/root-cause.md +2 -0
- package/commands/rs-experts.md +1 -0
- package/commands/rs-explain.md +1 -0
- package/commands/rs-fetch.md +1 -0
- package/commands/rs-thesis.md +1 -0
- package/commands/scenario-plan.md +2 -0
- package/commands/scheduled-tasks.md +1 -0
- package/commands/score-innovation.md +2 -0
- package/commands/scout.md +1 -0
- package/commands/setup.md +2 -0
- package/commands/snapshot.md +2 -0
- package/commands/speakers.md +1 -0
- package/commands/splash.md +5 -2
- package/commands/status.md +1 -0
- package/commands/structure-argument.md +2 -0
- package/commands/suggest-next.md +2 -0
- package/commands/systems-thinking.md +2 -0
- package/commands/think-hats.md +2 -0
- package/commands/update.md +2 -0
- package/commands/user-needs.md +2 -0
- package/commands/validate.md +2 -0
- package/commands/value-proposition.md +2 -0
- package/commands/vault.md +2 -0
- package/commands/visualize.md +24 -29
- package/commands/whitespace.md +2 -1
- package/commands/wiki.md +1 -0
- package/hooks/hooks.json +22 -88
- package/lib/agents/auto-explore-agent.cjs +82 -0
- package/lib/core/breakthrough/canary.cjs +134 -0
- package/lib/core/breakthrough/canary.test.cjs +136 -0
- package/lib/core/breakthrough/detectors.cjs +359 -0
- package/lib/core/breakthrough/detectors.test.cjs +333 -0
- package/lib/core/breakthrough/ethics-fence.cjs +127 -0
- package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
- package/lib/core/breakthrough/resurfacing.cjs +150 -0
- package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
- package/lib/core/breakthrough/review-queue.cjs +154 -0
- package/lib/core/breakthrough/review-queue.test.cjs +160 -0
- package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
- package/lib/core/breakthrough/scanner.cjs +426 -0
- package/lib/core/breakthrough/scanner.test.cjs +267 -0
- package/lib/core/breakthrough/schema.cjs +164 -0
- package/lib/core/breakthrough/schema.test.cjs +256 -0
- package/lib/core/breakthrough/scoring.cjs +293 -0
- package/lib/core/breakthrough/scoring.test.cjs +423 -0
- package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
- package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
- package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
- package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
- package/lib/core/first-touch-version-stamper.cjs +113 -0
- package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
- package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
- package/lib/core/llm-name-suggester.cjs +194 -0
- package/lib/core/llm-name-suggester.test.cjs +132 -0
- package/lib/core/mva-orchestrator.cjs +41 -0
- package/lib/core/mva-telemetry.cjs +31 -143
- package/lib/core/navigation/edges.cjs +35 -0
- package/lib/core/navigation/memory-events.cjs +126 -0
- package/lib/core/room-auto-create.cjs +318 -0
- package/lib/core/room-auto-create.test.cjs +198 -0
- package/lib/core/room-discard-cascade.cjs +225 -0
- package/lib/core/room-discard-cascade.test.cjs +135 -0
- package/lib/core/room-name-validator.cjs +132 -0
- package/lib/core/room-name-validator.test.cjs +156 -0
- package/lib/core/room-naming-selector.cjs +357 -0
- package/lib/core/room-naming-selector.test.cjs +277 -0
- package/lib/core/room-receipt-emit.cjs +63 -0
- package/lib/core/room-skeleton-scaffold.cjs +315 -0
- package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
- package/lib/core/stale-copy-scanner.cjs +190 -0
- package/lib/core/state-aware-router.cjs +78 -0
- package/lib/core/telemetry/schema.cjs +168 -0
- package/lib/core/telemetry/schema.test.cjs +124 -0
- package/lib/core/telemetry/validator.cjs +197 -0
- package/lib/core/telemetry/validator.test.cjs +188 -0
- package/lib/core/telemetry/writer.cjs +141 -0
- package/lib/core/telemetry/writer.test.cjs +331 -0
- package/lib/core/terminal-capability.cjs +88 -0
- package/lib/core/venture-shape-nudge.cjs +163 -0
- package/lib/core/venture-shape-nudge.test.cjs +161 -0
- package/lib/core/visual-ops.cjs +70 -2
- package/lib/hmi/selector-dispatcher.cjs +90 -1
- package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
- package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
- package/lib/memory/body-shape-coverage.test.cjs +268 -0
- package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
- package/lib/memory/first-touch-version.test.cjs +198 -0
- package/lib/memory/help-coverage.test.cjs +108 -0
- package/lib/memory/help-renderer.test.cjs +145 -0
- package/lib/memory/palette-consistency.test.cjs +127 -0
- package/lib/memory/pending-tension-store.cjs +80 -0
- package/lib/memory/render-v2-disposition.test.cjs +199 -0
- package/lib/memory/run-feynman-tests.cjs +213 -0
- package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
- package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
- package/lib/memory/soft-alias.test.cjs +144 -0
- package/lib/memory/stale-copy-scanner.test.cjs +291 -0
- package/lib/memory/state-aware-router.test.cjs +90 -0
- package/lib/memory/statusline-two-row.test.cjs +338 -0
- package/lib/memory/terminal-capability.test.cjs +155 -0
- package/lib/render/ROOM.md +74 -22
- package/lib/sessionstart/budget-compressor.cjs +130 -0
- package/lib/sessionstart/contributor-interface.cjs +134 -0
- package/lib/sessionstart/contributor-isolator.cjs +128 -0
- package/lib/sessionstart/precedence-ladder.cjs +47 -0
- package/lib/statusline/governing-thought-truncator.cjs +45 -0
- package/lib/statusline/two-row-renderer.cjs +186 -0
- package/lib/statusline/version-resolver.cjs +81 -0
- package/package.json +1 -1
- package/references/visual/ROOM.md +55 -0
- package/references/visual/palette.json +54 -0
- package/skills/larry-personality/SKILL.md +34 -0
- package/skills/ui-system/SKILL.md +109 -1
- package/skills/ui-system/rules/dual-palette.md +156 -0
- package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
- package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Phase 119-01 -- Room naming selector orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* Fires AFTER Phase 118's mva-orchestrator emits the terminal mva_brief_rendered
|
|
6
|
+
* telemetry event. Renders an F.1 selector with the four LOCKED option labels
|
|
7
|
+
* from CONTEXT.md D-06; resolves the user's choice across CLI / Desktop / Cowork
|
|
8
|
+
* surfaces per CLAUDE.md tri-polar rule; on resolution, runs the rename ceremony
|
|
9
|
+
* OR the discard cascade OR no-op (keep-untitled); emits room_naming_decided
|
|
10
|
+
* memory_event via the navigation.cjs chokepoint.
|
|
11
|
+
*
|
|
12
|
+
* Canon Part 3: F.1 selector is the tri-context Decision Gate primitive.
|
|
13
|
+
* Canon Part 4: every choice (including [keep as untitled] and [discard room])
|
|
14
|
+
* becomes a typed graph event.
|
|
15
|
+
* Canon Part 8: LOCAL only; no Brain MCP coupling.
|
|
16
|
+
* Canon Part 9: ALL writes through navigation.cjs::logMemoryEvent.
|
|
17
|
+
* Canon Part 10 sub-claim 3: the retroactive naming IS the receipt being completed.
|
|
18
|
+
*
|
|
19
|
+
* Em-dash discipline: uses `--` never the U+2014 character per HARD RULE.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const child_process = require('node:child_process');
|
|
25
|
+
|
|
26
|
+
const { suggestRoomName, FALLBACK_SUGGESTION } = require('./llm-name-suggester.cjs');
|
|
27
|
+
const { validateRoomName } = require('./room-name-validator.cjs');
|
|
28
|
+
const { discardPlaceholderRoom } = require('./room-discard-cascade.cjs');
|
|
29
|
+
|
|
30
|
+
const DECISION_PATHS = Object.freeze(['llm-suggested', 'user-typed', 'kept-untitled', 'discarded']);
|
|
31
|
+
|
|
32
|
+
// Per CONTEXT.md D-06 -- LOCKED verbatim labels. The {{SUGGESTED}} placeholder
|
|
33
|
+
// is interpolated at render time via interpolateLlmSuggested.
|
|
34
|
+
// Verbatim option labels present in this module body (grep target for scaffold harness Gate 6):
|
|
35
|
+
// [name this room: {{SUGGESTED}}]
|
|
36
|
+
// [type your own name]
|
|
37
|
+
// [keep as untitled]
|
|
38
|
+
// [discard room]
|
|
39
|
+
const F1_OPTION_LABELS = Object.freeze({
|
|
40
|
+
LLM_SUGGESTED: '[name this room: {{SUGGESTED}}]',
|
|
41
|
+
USER_TYPED: '[type your own name]',
|
|
42
|
+
KEEP_UNTITLED: '[keep as untitled]',
|
|
43
|
+
DISCARD: '[discard room]',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function interpolateLlmSuggested(suggestedName) {
|
|
47
|
+
return F1_OPTION_LABELS.LLM_SUGGESTED.replace('{{SUGGESTED}}', suggestedName || FALLBACK_SUGGESTION);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readThinnessFromState(roomDir) {
|
|
51
|
+
try {
|
|
52
|
+
const statePath = path.join(roomDir, 'STATE.md');
|
|
53
|
+
if (!fs.existsSync(statePath)) return false;
|
|
54
|
+
const raw = fs.readFileSync(statePath, 'utf8');
|
|
55
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
56
|
+
if (!m) return false;
|
|
57
|
+
return /^\s*auto_explore_thin:\s*true\s*$/m.test(m[1]);
|
|
58
|
+
} catch (_e) { return false; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function maybeLoadThinnessVoiceLine() {
|
|
62
|
+
try {
|
|
63
|
+
const t = require('./larry-thinness-acknowledgment.cjs');
|
|
64
|
+
return (t && typeof t.voiceLine === 'function') ? t.voiceLine() : null;
|
|
65
|
+
} catch (_e) { return null; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* fireNamingSelector(args) -> Promise<{decision_path, new_slug?, room_dir?, decision_envelope}>
|
|
70
|
+
*
|
|
71
|
+
* @param {object} args
|
|
72
|
+
* @param {string} args.roomDir absolute path to the placeholder room
|
|
73
|
+
* @param {object} args.mvaCompletionPayload the {sentence_sha256, total_duration_ms, ...} from
|
|
74
|
+
* mva_brief_rendered (LOCAL only, no Brain content)
|
|
75
|
+
* @param {string} args.surface 'cli' | 'desktop' | 'cowork' (default 'cli')
|
|
76
|
+
* @param {string} args.decidedBy user identity for the memory_event payload
|
|
77
|
+
* @param {string} [args.userPick] when present (REVISION 2026-05-16 IN-path of
|
|
78
|
+
* directive-file duplex), skip dispatcher + channel
|
|
79
|
+
* and resolve directly from the verb-string
|
|
80
|
+
* @param {object} [args.dispatcher] injectable for tests
|
|
81
|
+
* @param {object} [args.userInputChannel] injectable for tests
|
|
82
|
+
*/
|
|
83
|
+
async function fireNamingSelector(args) {
|
|
84
|
+
const opts = args || {};
|
|
85
|
+
const roomDir = opts.roomDir;
|
|
86
|
+
if (typeof roomDir !== 'string' || !fs.existsSync(roomDir)) {
|
|
87
|
+
return { decision_path: null, new_slug: null, room_dir: null, decision_envelope: { shape: 'F.1', surface: false, reason: 'room_dir_not_found' } };
|
|
88
|
+
}
|
|
89
|
+
const roomsHome = path.dirname(roomDir);
|
|
90
|
+
const previousSlug = path.basename(roomDir);
|
|
91
|
+
|
|
92
|
+
// REVISION 2026-05-16 IN-path: when userPick is provided, bypass dispatcher
|
|
93
|
+
// + channel and resolve directly to the appropriate branch. Larry calls this
|
|
94
|
+
// form from her conversational layer after AskUserQuestion returns.
|
|
95
|
+
const decidedBy = opts.decidedBy || process.env.USER || 'anonymous';
|
|
96
|
+
if (typeof opts.userPick === 'string' && opts.userPick.length > 0) {
|
|
97
|
+
return await _resolveFromUserPick(roomsHome, previousSlug, opts.userPick, decidedBy, opts.suggestedName);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const dispatcher = opts.dispatcher || require('../hmi/selector-dispatcher.cjs');
|
|
101
|
+
|
|
102
|
+
// Build the LLM-suggested name FIRST (we need it for the option-0 label).
|
|
103
|
+
let suggestion;
|
|
104
|
+
try {
|
|
105
|
+
const sha8 = (opts.mvaCompletionPayload && opts.mvaCompletionPayload.sentence_sha256)
|
|
106
|
+
? opts.mvaCompletionPayload.sentence_sha256.slice(0, 8)
|
|
107
|
+
: 'unknown';
|
|
108
|
+
const autoExplorePath = path.join(roomDir, '.mindrian', 'auto-explore-' + sha8 + '.json');
|
|
109
|
+
const autoExploreFinding = fs.existsSync(autoExplorePath) ? JSON.parse(fs.readFileSync(autoExplorePath, 'utf8')) : null;
|
|
110
|
+
suggestion = await suggestRoomName({
|
|
111
|
+
auto_explore_finding: autoExploreFinding,
|
|
112
|
+
mva_brief_sentence: (opts.mvaCompletionPayload && opts.mvaCompletionPayload.mva_brief_sentence) || '',
|
|
113
|
+
llmClient: opts.llmClient || null,
|
|
114
|
+
});
|
|
115
|
+
} catch (_e) {
|
|
116
|
+
suggestion = { ok: false, suggested_name: FALLBACK_SUGGESTION };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const verbs = [
|
|
120
|
+
interpolateLlmSuggested(suggestion.suggested_name),
|
|
121
|
+
F1_OPTION_LABELS.USER_TYPED,
|
|
122
|
+
F1_OPTION_LABELS.KEEP_UNTITLED,
|
|
123
|
+
F1_OPTION_LABELS.DISCARD,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// Prepend thinness voice line if D-05 condition is met.
|
|
127
|
+
const thin = readThinnessFromState(roomDir);
|
|
128
|
+
const voiceLine = thin ? maybeLoadThinnessVoiceLine() : null;
|
|
129
|
+
|
|
130
|
+
// First-round F.1 dispatch.
|
|
131
|
+
const envelope = dispatcher.pickShape({
|
|
132
|
+
requestedShape: 'F.1',
|
|
133
|
+
roomDir: roomDir,
|
|
134
|
+
tier: 1,
|
|
135
|
+
payload: {
|
|
136
|
+
header: voiceLine || undefined,
|
|
137
|
+
previous_slug: previousSlug,
|
|
138
|
+
verbs: verbs,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Resolve user choice via the surface-specific channel.
|
|
143
|
+
const userInputChannel = opts.userInputChannel || _defaultUserInputChannel;
|
|
144
|
+
const choice = await userInputChannel(envelope, { surface: opts.surface || 'cli' });
|
|
145
|
+
|
|
146
|
+
if (choice.choice_index === 0) {
|
|
147
|
+
return await _resolveRename(roomsHome, previousSlug, suggestion.suggested_name, 'llm-suggested', decidedBy, envelope, dispatcher, userInputChannel, opts.surface);
|
|
148
|
+
} else if (choice.choice_index === 1) {
|
|
149
|
+
return await _resolveUserTyped(roomsHome, previousSlug, choice.free_text, decidedBy, envelope, dispatcher, userInputChannel, opts.surface);
|
|
150
|
+
} else if (choice.choice_index === 2) {
|
|
151
|
+
await _emitNamingDecided(roomDir, previousSlug, previousSlug, 'kept-untitled', decidedBy);
|
|
152
|
+
return { decision_path: 'kept-untitled', new_slug: previousSlug, room_dir: roomDir, decision_envelope: envelope };
|
|
153
|
+
} else if (choice.choice_index === 3) {
|
|
154
|
+
const cascadeResult = discardPlaceholderRoom(roomsHome, previousSlug, { decided_by: decidedBy });
|
|
155
|
+
await _emitNamingDecidedToMetaDb(roomsHome, previousSlug, null, 'discarded', decidedBy);
|
|
156
|
+
return { decision_path: 'discarded', new_slug: null, room_dir: null, decision_envelope: envelope, cascade_result: cascadeResult };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { decision_path: null, new_slug: previousSlug, room_dir: roomDir, decision_envelope: envelope, reason: 'unknown_choice_index' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// REVISION 2026-05-16 IN-path: resolve from a verb-string the user picked via
|
|
163
|
+
// Larry's AskUserQuestion render. Larry passes the verbatim verb-string;
|
|
164
|
+
// we map it back to the canonical decision_path.
|
|
165
|
+
async function _resolveFromUserPick(roomsHome, previousSlug, userPick, decidedBy, suggestedName) {
|
|
166
|
+
const roomDir = path.join(roomsHome, previousSlug);
|
|
167
|
+
// The verb-string the user picked. We compare against the locked labels.
|
|
168
|
+
const llmLabel = interpolateLlmSuggested(suggestedName || FALLBACK_SUGGESTION);
|
|
169
|
+
if (userPick === llmLabel || userPick.indexOf('[name this room:') === 0) {
|
|
170
|
+
return await _resolveRename(roomsHome, previousSlug, suggestedName || FALLBACK_SUGGESTION, 'llm-suggested', decidedBy, null, null, null, null);
|
|
171
|
+
}
|
|
172
|
+
if (userPick === F1_OPTION_LABELS.USER_TYPED) {
|
|
173
|
+
// User-typed path requires a free_text payload; in the IN-path duplex,
|
|
174
|
+
// Larry would call again with userPick = the free-text slug.
|
|
175
|
+
return { decision_path: null, new_slug: previousSlug, room_dir: roomDir, decision_envelope: null, reason: 'user_typed_requires_free_text' };
|
|
176
|
+
}
|
|
177
|
+
if (userPick === F1_OPTION_LABELS.KEEP_UNTITLED) {
|
|
178
|
+
await _emitNamingDecided(roomDir, previousSlug, previousSlug, 'kept-untitled', decidedBy);
|
|
179
|
+
return { decision_path: 'kept-untitled', new_slug: previousSlug, room_dir: roomDir, decision_envelope: null };
|
|
180
|
+
}
|
|
181
|
+
if (userPick === F1_OPTION_LABELS.DISCARD) {
|
|
182
|
+
const cascadeResult = discardPlaceholderRoom(roomsHome, previousSlug, { decided_by: decidedBy });
|
|
183
|
+
await _emitNamingDecidedToMetaDb(roomsHome, previousSlug, null, 'discarded', decidedBy);
|
|
184
|
+
return { decision_path: 'discarded', new_slug: null, room_dir: null, decision_envelope: null, cascade_result: cascadeResult };
|
|
185
|
+
}
|
|
186
|
+
// Otherwise treat as user-typed free-text input.
|
|
187
|
+
return await _resolveRename(roomsHome, previousSlug, userPick, 'user-typed', decidedBy, null, null, null, null);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function _resolveRename(roomsHome, previousSlug, candidate, decisionPath, decidedBy, envelope, dispatcher, userInputChannel, surface) {
|
|
191
|
+
const validation = validateRoomName(candidate, { roomsHome });
|
|
192
|
+
if (!validation.ok) {
|
|
193
|
+
// Re-prompt inline with the rejection reason -- only if dispatcher + channel
|
|
194
|
+
// are available (live F.1 flow). When called from the IN-path (userPick), we
|
|
195
|
+
// return the failure so Larry can re-prompt at the conversational layer.
|
|
196
|
+
if (!dispatcher || !userInputChannel) {
|
|
197
|
+
return {
|
|
198
|
+
decision_path: null,
|
|
199
|
+
new_slug: null,
|
|
200
|
+
room_dir: path.join(roomsHome, previousSlug),
|
|
201
|
+
decision_envelope: null,
|
|
202
|
+
reason: 'validation_failed:' + validation.reasons.join(','),
|
|
203
|
+
validation_failure: validation,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const repromptVerbs = [
|
|
207
|
+
F1_OPTION_LABELS.USER_TYPED,
|
|
208
|
+
F1_OPTION_LABELS.KEEP_UNTITLED,
|
|
209
|
+
F1_OPTION_LABELS.DISCARD,
|
|
210
|
+
];
|
|
211
|
+
const reprompt = dispatcher.pickShape({
|
|
212
|
+
requestedShape: 'F.1',
|
|
213
|
+
roomDir: path.join(roomsHome, previousSlug),
|
|
214
|
+
tier: 1,
|
|
215
|
+
payload: {
|
|
216
|
+
header: 'That name was rejected: ' + validation.reasons.join(', ') + '. Try again or pick another option.',
|
|
217
|
+
previous_slug: previousSlug,
|
|
218
|
+
validation_failure: validation,
|
|
219
|
+
verbs: repromptVerbs,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
const choice = await userInputChannel(reprompt, { surface });
|
|
223
|
+
if (choice.choice_index === 0) {
|
|
224
|
+
return await _resolveUserTyped(roomsHome, previousSlug, choice.free_text, decidedBy, reprompt, dispatcher, userInputChannel, surface);
|
|
225
|
+
} else if (choice.choice_index === 1) {
|
|
226
|
+
await _emitNamingDecided(path.join(roomsHome, previousSlug), previousSlug, previousSlug, 'kept-untitled', decidedBy);
|
|
227
|
+
return { decision_path: 'kept-untitled', new_slug: previousSlug, room_dir: path.join(roomsHome, previousSlug), decision_envelope: reprompt };
|
|
228
|
+
} else {
|
|
229
|
+
const cascadeResult = discardPlaceholderRoom(roomsHome, previousSlug, { decided_by: decidedBy });
|
|
230
|
+
await _emitNamingDecidedToMetaDb(roomsHome, previousSlug, null, 'discarded', decidedBy);
|
|
231
|
+
return { decision_path: 'discarded', new_slug: null, room_dir: null, decision_envelope: reprompt, cascade_result: cascadeResult };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Validation OK -- proceed with rename ceremony.
|
|
235
|
+
const newSlug = validation.normalized_slug;
|
|
236
|
+
const newRoomDir = path.join(roomsHome, newSlug);
|
|
237
|
+
|
|
238
|
+
// Registry update via room-registry CLI (venture_name field) followed by
|
|
239
|
+
// direct registry-key rename in the JSON.
|
|
240
|
+
try {
|
|
241
|
+
const registryScript = path.join(__dirname, '..', '..', 'scripts', 'room-registry');
|
|
242
|
+
if (fs.existsSync(registryScript)) {
|
|
243
|
+
try {
|
|
244
|
+
child_process.execFileSync('bash', [registryScript, 'update', previousSlug, 'venture_name', newSlug], {
|
|
245
|
+
cwd: process.cwd(),
|
|
246
|
+
env: Object.assign({}, process.env, { MINDRIAN_ROOMS_HOME: roomsHome }),
|
|
247
|
+
stdio: 'pipe',
|
|
248
|
+
timeout: 5000,
|
|
249
|
+
});
|
|
250
|
+
} catch (_e) { /* registry update failure non-fatal; direct mutation below covers it */ }
|
|
251
|
+
}
|
|
252
|
+
} catch (_e) { /* swallow */ }
|
|
253
|
+
|
|
254
|
+
// Direct registry mutation: rename the key.
|
|
255
|
+
try {
|
|
256
|
+
const registryPath = path.join(roomsHome, '.rooms', 'registry.json');
|
|
257
|
+
if (fs.existsSync(registryPath)) {
|
|
258
|
+
const raw = fs.readFileSync(registryPath, 'utf8');
|
|
259
|
+
const reg = JSON.parse(raw);
|
|
260
|
+
if (reg && reg.rooms && reg.rooms[previousSlug]) {
|
|
261
|
+
reg.rooms[newSlug] = reg.rooms[previousSlug];
|
|
262
|
+
reg.rooms[newSlug].venture_name = newSlug;
|
|
263
|
+
reg.rooms[newSlug].path = newRoomDir;
|
|
264
|
+
delete reg.rooms[previousSlug];
|
|
265
|
+
if (reg.active === previousSlug) reg.active = newSlug;
|
|
266
|
+
const tmp = registryPath + '.tmp.' + process.pid + '.' + Math.random().toString(36).slice(2, 8);
|
|
267
|
+
fs.writeFileSync(tmp, JSON.stringify(reg, null, 2), 'utf8');
|
|
268
|
+
fs.renameSync(tmp, registryPath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (_e) { /* surface via the rename failure below */ }
|
|
272
|
+
|
|
273
|
+
// fs.renameSync the directory itself. Preserves room.db row IDs (atomic
|
|
274
|
+
// intra-filesystem rename).
|
|
275
|
+
try {
|
|
276
|
+
fs.renameSync(path.join(roomsHome, previousSlug), newRoomDir);
|
|
277
|
+
} catch (renameErr) {
|
|
278
|
+
return {
|
|
279
|
+
decision_path: null,
|
|
280
|
+
new_slug: null,
|
|
281
|
+
room_dir: path.join(roomsHome, previousSlug),
|
|
282
|
+
decision_envelope: envelope,
|
|
283
|
+
reason: 'rename_failed:' + String(renameErr.message || renameErr).slice(0, 60),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Emit the memory_event AFTER the rename (so we open the NEW room.db location).
|
|
288
|
+
await _emitNamingDecided(newRoomDir, previousSlug, newSlug, decisionPath, decidedBy);
|
|
289
|
+
|
|
290
|
+
return { decision_path: decisionPath, new_slug: newSlug, room_dir: newRoomDir, decision_envelope: envelope };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function _resolveUserTyped(roomsHome, previousSlug, freeText, decidedBy, envelope, dispatcher, userInputChannel, surface) {
|
|
294
|
+
return await _resolveRename(roomsHome, previousSlug, freeText, 'user-typed', decidedBy, envelope, dispatcher, userInputChannel, surface);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function _emitNamingDecided(roomDir, previousSlug, newSlug, decisionPath, decidedBy) {
|
|
298
|
+
try {
|
|
299
|
+
const dbPath = path.join(roomDir, '.mindrian', 'room.db');
|
|
300
|
+
if (!fs.existsSync(dbPath)) return;
|
|
301
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
302
|
+
const handle = openRoomDb(roomDir);
|
|
303
|
+
if (!handle) return;
|
|
304
|
+
try {
|
|
305
|
+
const nav = require('./navigation.cjs');
|
|
306
|
+
nav.logMemoryEvent(handle, 'room_naming_decided', {
|
|
307
|
+
previous_slug: previousSlug,
|
|
308
|
+
new_slug: newSlug,
|
|
309
|
+
decision_path: decisionPath,
|
|
310
|
+
decided_by: decidedBy,
|
|
311
|
+
source_path: 'system:room-naming-selector',
|
|
312
|
+
created_by: 'system',
|
|
313
|
+
});
|
|
314
|
+
} finally {
|
|
315
|
+
try { closeRoomDb(handle); } catch (_e) {}
|
|
316
|
+
}
|
|
317
|
+
} catch (_e) { /* memory_event emission is best-effort */ }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function _emitNamingDecidedToMetaDb(roomsHome, previousSlug, newSlug, decisionPath, decidedBy) {
|
|
321
|
+
try {
|
|
322
|
+
const metaRoomDir = path.join(roomsHome, '.rooms', '_meta');
|
|
323
|
+
fs.mkdirSync(path.join(metaRoomDir, '.mindrian'), { recursive: true, mode: 0o755 });
|
|
324
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
325
|
+
const handle = openRoomDb(metaRoomDir);
|
|
326
|
+
if (!handle) return;
|
|
327
|
+
try {
|
|
328
|
+
const nav = require('./navigation.cjs');
|
|
329
|
+
nav.logMemoryEvent(handle, 'room_naming_decided', {
|
|
330
|
+
previous_slug: previousSlug,
|
|
331
|
+
new_slug: newSlug,
|
|
332
|
+
decision_path: decisionPath,
|
|
333
|
+
decided_by: decidedBy,
|
|
334
|
+
source_path: 'system:room-naming-selector',
|
|
335
|
+
created_by: 'system',
|
|
336
|
+
});
|
|
337
|
+
} finally {
|
|
338
|
+
try { closeRoomDb(handle); } catch (_e) {}
|
|
339
|
+
}
|
|
340
|
+
} catch (_e) { /* best-effort */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Default user-input channel: production resolves to AskUserQuestion via the
|
|
344
|
+
// directive-file handoff (scripts/room-naming-selector.cjs writes a directive
|
|
345
|
+
// at <roomDir>/.context/pending-naming-decision.md; Larry dispatches on the
|
|
346
|
+
// next conversational turn). Tests inject a stub.
|
|
347
|
+
async function _defaultUserInputChannel(envelope, opts) {
|
|
348
|
+
throw new Error('user_input_channel_required:inject_via_opts');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
fireNamingSelector: fireNamingSelector,
|
|
353
|
+
DECISION_PATHS: DECISION_PATHS,
|
|
354
|
+
F1_OPTION_LABELS: F1_OPTION_LABELS,
|
|
355
|
+
interpolateLlmSuggested: interpolateLlmSuggested,
|
|
356
|
+
readThinnessFromState: readThinnessFromState,
|
|
357
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 119-01 Task 3 tests for lib/core/room-naming-selector.cjs.
|
|
3
|
+
// Validates F1_OPTION_LABELS verbatim values, DECISION_PATHS enum, the four
|
|
4
|
+
// decision branches (llm-suggested / user-typed / kept-untitled / discarded),
|
|
5
|
+
// the rename ceremony preserving room.db row IDs, the thinness prepend,
|
|
6
|
+
// and the Canon Part 8/9/10 invariants.
|
|
7
|
+
|
|
8
|
+
const test = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const cp = require('node:child_process');
|
|
14
|
+
|
|
15
|
+
const selector = require('./room-naming-selector.cjs');
|
|
16
|
+
const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
|
|
17
|
+
|
|
18
|
+
function _mkPlaceholderRoom(slug, opts) {
|
|
19
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'naming-selector-test-'));
|
|
20
|
+
fs.mkdirSync(path.join(tmp, '.rooms'), { recursive: true });
|
|
21
|
+
const roomDir = path.join(tmp, slug);
|
|
22
|
+
fs.mkdirSync(path.join(roomDir, '.mindrian'), { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(roomDir, '.room-root'), '');
|
|
24
|
+
const thinness = (opts && opts.thinness) ? 'true' : 'false';
|
|
25
|
+
fs.writeFileSync(path.join(roomDir, 'STATE.md'),
|
|
26
|
+
'---\nphase: scoping\nauto_explore_thin: ' + thinness + '\n---\n');
|
|
27
|
+
const db = openRoomDb(roomDir);
|
|
28
|
+
closeRoomDb(db);
|
|
29
|
+
const reg = {
|
|
30
|
+
version: 1,
|
|
31
|
+
active: slug,
|
|
32
|
+
rooms: {
|
|
33
|
+
[slug]: {
|
|
34
|
+
path: roomDir,
|
|
35
|
+
venture_name: 'untitled',
|
|
36
|
+
venture_stage: 'Pre-Opportunity',
|
|
37
|
+
status: 'active',
|
|
38
|
+
last_opened: Date.now(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
fs.writeFileSync(path.join(tmp, '.rooms', 'registry.json'), JSON.stringify(reg, null, 2), 'utf8');
|
|
43
|
+
return { roomsHome: tmp, roomDir, slug };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _cleanup(roomsHome) {
|
|
47
|
+
try { fs.rmSync(roomsHome, { recursive: true, force: true }); } catch (_e) {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Static surface tests
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
test('Test 1: F1_OPTION_LABELS exposed verbatim per CONTEXT.md D-06', function () {
|
|
55
|
+
assert.strictEqual(selector.F1_OPTION_LABELS.LLM_SUGGESTED, '[name this room: {{SUGGESTED}}]');
|
|
56
|
+
assert.strictEqual(selector.F1_OPTION_LABELS.USER_TYPED, '[type your own name]');
|
|
57
|
+
assert.strictEqual(selector.F1_OPTION_LABELS.KEEP_UNTITLED, '[keep as untitled]');
|
|
58
|
+
assert.strictEqual(selector.F1_OPTION_LABELS.DISCARD, '[discard room]');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Test 2: DECISION_PATHS frozen enum -- four canonical values', function () {
|
|
62
|
+
assert.deepStrictEqual(selector.DECISION_PATHS, ['llm-suggested', 'user-typed', 'kept-untitled', 'discarded']);
|
|
63
|
+
assert.ok(Object.isFrozen(selector.DECISION_PATHS));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('Test 2b: interpolateLlmSuggested produces verbatim label with interpolated suggestion', function () {
|
|
67
|
+
assert.strictEqual(selector.interpolateLlmSuggested('acme-robotics'), '[name this room: acme-robotics]');
|
|
68
|
+
assert.strictEqual(selector.interpolateLlmSuggested(null), '[name this room: untitled]');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Decision-branch tests (via injected dispatcher + userInputChannel stubs)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function _stubDispatcher() {
|
|
76
|
+
return {
|
|
77
|
+
pickShape: function (args) {
|
|
78
|
+
return { shape: 'F.1', rendered: { options: (args.payload && args.payload.verbs) || [] }, payload: args.payload };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _stubChannel(choiceIndex, freeText) {
|
|
84
|
+
return async function () {
|
|
85
|
+
return { choice_index: choiceIndex, free_text: freeText };
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test('Test 3: llm-suggested branch -- rename + memory_event emission', async function () {
|
|
90
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1845');
|
|
91
|
+
try {
|
|
92
|
+
const result = await selector.fireNamingSelector({
|
|
93
|
+
roomDir: roomDir,
|
|
94
|
+
mvaCompletionPayload: { sentence_sha256: 'deadbeef0123' },
|
|
95
|
+
surface: 'cli',
|
|
96
|
+
decidedBy: 'jsagi',
|
|
97
|
+
dispatcher: _stubDispatcher(),
|
|
98
|
+
userInputChannel: _stubChannel(0),
|
|
99
|
+
llmClient: { complete: async function () { return { content: 'acme-robotics' }; } },
|
|
100
|
+
});
|
|
101
|
+
assert.strictEqual(result.decision_path, 'llm-suggested');
|
|
102
|
+
assert.strictEqual(result.new_slug, 'acme-robotics');
|
|
103
|
+
assert.ok(result.room_dir.endsWith('acme-robotics'));
|
|
104
|
+
assert.ok(fs.existsSync(result.room_dir), 'renamed room dir must exist on disk');
|
|
105
|
+
assert.ok(!fs.existsSync(roomDir), 'old placeholder dir must be gone');
|
|
106
|
+
} finally { _cleanup(roomsHome); }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('Test 4: user-typed branch with reserved-prefix rejection -> re-prompt', async function () {
|
|
110
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1846');
|
|
111
|
+
try {
|
|
112
|
+
// First channel response: option 1 = user-typed with reserved-prefix free_text
|
|
113
|
+
// Second channel response (after re-prompt): option 1 = user-typed with valid free_text
|
|
114
|
+
let callCount = 0;
|
|
115
|
+
const channel = async function () {
|
|
116
|
+
callCount++;
|
|
117
|
+
if (callCount === 1) return { choice_index: 1, free_text: 'untitled-mine' };
|
|
118
|
+
return { choice_index: 0, free_text: 'acme-robotics' };
|
|
119
|
+
};
|
|
120
|
+
const result = await selector.fireNamingSelector({
|
|
121
|
+
roomDir: roomDir,
|
|
122
|
+
mvaCompletionPayload: { sentence_sha256: 'deadbeef0123' },
|
|
123
|
+
surface: 'cli',
|
|
124
|
+
decidedBy: 'jsagi',
|
|
125
|
+
dispatcher: _stubDispatcher(),
|
|
126
|
+
userInputChannel: channel,
|
|
127
|
+
llmClient: { complete: async function () { return { content: 'biotech' }; } },
|
|
128
|
+
});
|
|
129
|
+
assert.ok(callCount >= 2, 'expected the channel to be called >= 2 times (re-prompt path); got ' + callCount);
|
|
130
|
+
assert.strictEqual(result.decision_path, 'user-typed');
|
|
131
|
+
assert.strictEqual(result.new_slug, 'acme-robotics');
|
|
132
|
+
} finally { _cleanup(roomsHome); }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('Test 5: kept-untitled branch -- no rename, memory_event with previous_slug == new_slug', async function () {
|
|
136
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1847');
|
|
137
|
+
try {
|
|
138
|
+
const result = await selector.fireNamingSelector({
|
|
139
|
+
roomDir: roomDir,
|
|
140
|
+
mvaCompletionPayload: { sentence_sha256: 'deadbeef0123' },
|
|
141
|
+
surface: 'cli',
|
|
142
|
+
decidedBy: 'jsagi',
|
|
143
|
+
dispatcher: _stubDispatcher(),
|
|
144
|
+
userInputChannel: _stubChannel(2),
|
|
145
|
+
llmClient: { complete: async function () { return { content: 'foo' }; } },
|
|
146
|
+
});
|
|
147
|
+
assert.strictEqual(result.decision_path, 'kept-untitled');
|
|
148
|
+
assert.strictEqual(result.new_slug, slug);
|
|
149
|
+
assert.strictEqual(result.room_dir, roomDir);
|
|
150
|
+
assert.ok(fs.existsSync(roomDir), 'roomDir must remain unchanged');
|
|
151
|
+
} finally { _cleanup(roomsHome); }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('Test 6: discarded branch -- cascade fires + decision_envelope returned', async function () {
|
|
155
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1848');
|
|
156
|
+
try {
|
|
157
|
+
const result = await selector.fireNamingSelector({
|
|
158
|
+
roomDir: roomDir,
|
|
159
|
+
mvaCompletionPayload: { sentence_sha256: 'deadbeef0123' },
|
|
160
|
+
surface: 'cli',
|
|
161
|
+
decidedBy: 'jsagi',
|
|
162
|
+
dispatcher: _stubDispatcher(),
|
|
163
|
+
userInputChannel: _stubChannel(3),
|
|
164
|
+
llmClient: { complete: async function () { return { content: 'foo' }; } },
|
|
165
|
+
});
|
|
166
|
+
assert.strictEqual(result.decision_path, 'discarded');
|
|
167
|
+
assert.strictEqual(result.new_slug, null);
|
|
168
|
+
assert.strictEqual(result.room_dir, null);
|
|
169
|
+
assert.ok(!fs.existsSync(roomDir), 'placeholder room must be removed by cascade');
|
|
170
|
+
} finally { _cleanup(roomsHome); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('Test 7: rename ceremony preserves room.db existence (row-IDs preserved by atomic rename)', async function () {
|
|
174
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1849');
|
|
175
|
+
try {
|
|
176
|
+
// Insert a marker memory_event via navigation.cjs (the chokepoint).
|
|
177
|
+
const dbBefore = openRoomDb(roomDir);
|
|
178
|
+
try {
|
|
179
|
+
const nav = require('./navigation.cjs');
|
|
180
|
+
nav.logMemoryEvent(dbBefore, 'room_auto_created', {
|
|
181
|
+
slug: slug,
|
|
182
|
+
source_path: 'system:test',
|
|
183
|
+
created_by: 'system',
|
|
184
|
+
});
|
|
185
|
+
} finally {
|
|
186
|
+
closeRoomDb(dbBefore);
|
|
187
|
+
}
|
|
188
|
+
const result = await selector.fireNamingSelector({
|
|
189
|
+
roomDir: roomDir,
|
|
190
|
+
mvaCompletionPayload: { sentence_sha256: 'deadbeef0123' },
|
|
191
|
+
surface: 'cli',
|
|
192
|
+
decidedBy: 'jsagi',
|
|
193
|
+
dispatcher: _stubDispatcher(),
|
|
194
|
+
userInputChannel: _stubChannel(0),
|
|
195
|
+
llmClient: { complete: async function () { return { content: 'preserved-name' }; } },
|
|
196
|
+
});
|
|
197
|
+
assert.strictEqual(result.decision_path, 'llm-suggested');
|
|
198
|
+
// The renamed db file must exist; the row count must include both the original
|
|
199
|
+
// room_auto_created AND the newly emitted room_naming_decided.
|
|
200
|
+
const dbAfter = openRoomDb(result.room_dir);
|
|
201
|
+
try {
|
|
202
|
+
const rows = dbAfter.prepare("SELECT COUNT(*) AS c FROM nodes WHERE type='memory_event'").get();
|
|
203
|
+
assert.ok(rows.c >= 2, 'expected >= 2 memory_events; got ' + rows.c);
|
|
204
|
+
} finally {
|
|
205
|
+
closeRoomDb(dbAfter);
|
|
206
|
+
}
|
|
207
|
+
} finally { _cleanup(roomsHome); }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('Test 8: rename ceremony updates registry -- new key + active flips', async function () {
|
|
211
|
+
const { roomsHome, roomDir, slug } = _mkPlaceholderRoom('untitled-2026-05-16-1850');
|
|
212
|
+
try {
|
|
213
|
+
await selector.fireNamingSelector({
|
|
214
|
+
roomDir: roomDir,
|
|
215
|
+
mvaCompletionPayload: { sentence_sha256: 'deadbeef0123' },
|
|
216
|
+
surface: 'cli',
|
|
217
|
+
decidedBy: 'jsagi',
|
|
218
|
+
dispatcher: _stubDispatcher(),
|
|
219
|
+
userInputChannel: _stubChannel(0),
|
|
220
|
+
llmClient: { complete: async function () { return { content: 'new-venture' }; } },
|
|
221
|
+
});
|
|
222
|
+
const reg = JSON.parse(fs.readFileSync(path.join(roomsHome, '.rooms', 'registry.json'), 'utf8'));
|
|
223
|
+
assert.ok(reg.rooms['new-venture'], 'new-venture key must exist after rename');
|
|
224
|
+
assert.ok(!reg.rooms[slug], 'old placeholder key must be removed');
|
|
225
|
+
assert.strictEqual(reg.active, 'new-venture');
|
|
226
|
+
} finally { _cleanup(roomsHome); }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('Test 9: thinness voice line is prepended when auto_explore_thin: true', function () {
|
|
230
|
+
const { roomsHome, roomDir } = _mkPlaceholderRoom('untitled-2026-05-16-1851', { thinness: true });
|
|
231
|
+
try {
|
|
232
|
+
assert.strictEqual(selector.readThinnessFromState(roomDir), true);
|
|
233
|
+
const { voiceLine } = require('./larry-thinness-acknowledgment.cjs');
|
|
234
|
+
assert.ok(typeof voiceLine === 'function');
|
|
235
|
+
const line = voiceLine();
|
|
236
|
+
assert.ok(typeof line === 'string' && line.length > 0, 'thinness voice line must be a non-empty string');
|
|
237
|
+
} finally { _cleanup(roomsHome); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('Test 10: Canon Part 8 LOCAL invariant -- no Brain coupling in module + shim', function () {
|
|
241
|
+
const srcModule = fs.readFileSync(require.resolve('./room-naming-selector.cjs'), 'utf8');
|
|
242
|
+
const srcShim = fs.readFileSync(path.resolve(__dirname, '..', '..', 'scripts', 'room-naming-selector.cjs'), 'utf8');
|
|
243
|
+
for (const src of [srcModule, srcShim]) {
|
|
244
|
+
assert.ok(src.indexOf('brain.mindrian') === -1, 'brain.mindrian substring present (Canon Part 8 breach)');
|
|
245
|
+
assert.ok(!/require\([^)]*brain-client[^)]*\)/.test(src), 'brain-client require (Canon Part 8 breach)');
|
|
246
|
+
assert.ok(!/fetch\([^)]*['\"][^'\"]*brain[^'\"]*['\"]/.test(src), 'fetch to brain.* host (Canon Part 8 breach)');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('Test 11: CLAUDE.md tri-polar header -- CLI / Desktop / Cowork mentioned in shim', function () {
|
|
251
|
+
const srcShim = fs.readFileSync(path.resolve(__dirname, '..', '..', 'scripts', 'room-naming-selector.cjs'), 'utf8');
|
|
252
|
+
assert.ok(srcShim.indexOf('CLI') !== -1, 'CLI must be acknowledged in shim header');
|
|
253
|
+
assert.ok(srcShim.indexOf('Desktop') !== -1, 'Desktop must be acknowledged in shim header');
|
|
254
|
+
assert.ok(srcShim.indexOf('Cowork') !== -1, 'Cowork must be acknowledged in shim header');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('Test 12: em-dash invariant across orchestrator + shim + test file', function () {
|
|
258
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
259
|
+
const srcModule = fs.readFileSync(require.resolve('./room-naming-selector.cjs'), 'utf8');
|
|
260
|
+
const srcShim = fs.readFileSync(path.resolve(__dirname, '..', '..', 'scripts', 'room-naming-selector.cjs'), 'utf8');
|
|
261
|
+
const srcTest = fs.readFileSync(__filename, 'utf8');
|
|
262
|
+
for (const [name, src] of [['module', srcModule], ['shim', srcShim], ['test', srcTest]]) {
|
|
263
|
+
assert.ok(src.indexOf(EMDASH) === -1, 'em-dash present in ' + name + ' (HARD RULE)');
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('Test 13: CLI shim exits 0 on missing room with structured JSON error', function () {
|
|
268
|
+
const shimPath = path.resolve(__dirname, '..', '..', 'scripts', 'room-naming-selector.cjs');
|
|
269
|
+
const out = cp.spawnSync('node', [shimPath, '--room-dir', '/tmp/nonexistent-119-01-test-' + Date.now()], {
|
|
270
|
+
encoding: 'utf8',
|
|
271
|
+
});
|
|
272
|
+
assert.strictEqual(out.status, 0);
|
|
273
|
+
const parsed = JSON.parse(out.stdout.trim().split('\n').filter(Boolean).pop());
|
|
274
|
+
assert.strictEqual(parsed.shape, 'F.1');
|
|
275
|
+
assert.strictEqual(parsed.surface, false);
|
|
276
|
+
assert.strictEqual(parsed.reason, 'room_dir_not_found');
|
|
277
|
+
});
|