@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,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121.5-00 -- SessionStart Coordinator: budget compressor (D-14 + D-15).
|
|
5
|
+
*
|
|
6
|
+
* BUDGET_CHARS = 2000 hard cap (D-14, uniform across CLI/Desktop/Cowork).
|
|
7
|
+
*
|
|
8
|
+
* Algorithm (D-15 iterative compression):
|
|
9
|
+
* 1. Sort fragments by priority ascending (top first).
|
|
10
|
+
* 2. Filter has_payload === true.
|
|
11
|
+
* 3. Try full_payload for all -- if total <= budget, done.
|
|
12
|
+
* 4. Else: walk lowest-priority -> highest, swap to one_line_pointer one at
|
|
13
|
+
* a time. Recompute. Repeat until total <= budget.
|
|
14
|
+
* 5. If all at pointer-only and STILL over: drop from the bottom (lowest
|
|
15
|
+
* priority first) until under budget. Track in dropped[].
|
|
16
|
+
*
|
|
17
|
+
* Body format: fragments joined with \n\n (one blank line). No decoration,
|
|
18
|
+
* no chrome, no dividers, no emoji (Canon Part 8 + SKILL.md no-emoji rule
|
|
19
|
+
* for additionalContext).
|
|
20
|
+
*
|
|
21
|
+
* Returns: { body: string, dropped: string[], compressed: string[] }.
|
|
22
|
+
*
|
|
23
|
+
* Pure CJS, node built-ins only. Zero new runtime dependencies.
|
|
24
|
+
*/
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const BUDGET_CHARS = 2000;
|
|
28
|
+
|
|
29
|
+
function totalBytes(items, useFull) {
|
|
30
|
+
let sum = 0;
|
|
31
|
+
for (let i = 0; i < items.length; i++) {
|
|
32
|
+
const it = items[i];
|
|
33
|
+
if (useFull[i]) sum += it.bytes_full;
|
|
34
|
+
else sum += it.bytes_pointer;
|
|
35
|
+
}
|
|
36
|
+
// Plus \n\n joiner between adjacent items (2 bytes each, items-1 joiners).
|
|
37
|
+
if (items.length > 1) sum += (items.length - 1) * 2;
|
|
38
|
+
return sum;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildBody(items, useFull, droppedIds) {
|
|
42
|
+
const pieces = [];
|
|
43
|
+
for (let i = 0; i < items.length; i++) {
|
|
44
|
+
if (droppedIds.has(items[i].id)) continue;
|
|
45
|
+
pieces.push(useFull[i] ? items[i].full_payload : items[i].one_line_pointer);
|
|
46
|
+
}
|
|
47
|
+
return pieces.join('\n\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* compressUntilUnderBudget(fragments, budgetChars)
|
|
52
|
+
* fragments : ContributorFragment[]
|
|
53
|
+
* budgetChars: number (defaults BUDGET_CHARS)
|
|
54
|
+
* returns : { body, dropped, compressed }
|
|
55
|
+
*/
|
|
56
|
+
function compressUntilUnderBudget(fragments, budgetChars) {
|
|
57
|
+
const budget = Number.isInteger(budgetChars) && budgetChars > 0 ? budgetChars : BUDGET_CHARS;
|
|
58
|
+
|
|
59
|
+
// Filter + sort. Lowest priority number = highest precedence (top).
|
|
60
|
+
const live = (Array.isArray(fragments) ? fragments : [])
|
|
61
|
+
.filter((f) => f && f.has_payload === true)
|
|
62
|
+
.slice()
|
|
63
|
+
.sort((a, b) => (a.priority || 99) - (b.priority || 99));
|
|
64
|
+
|
|
65
|
+
if (live.length === 0) {
|
|
66
|
+
return { body: '', dropped: [], compressed: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Parallel arrays: useFull[i] starts true, flips false when compressed.
|
|
70
|
+
const useFull = live.map(() => true);
|
|
71
|
+
const droppedIds = new Set();
|
|
72
|
+
const compressedIds = [];
|
|
73
|
+
|
|
74
|
+
// Pass 1: try all-full. If under, done.
|
|
75
|
+
if (totalBytes(live, useFull) <= budget) {
|
|
76
|
+
return {
|
|
77
|
+
body: buildBody(live, useFull, droppedIds),
|
|
78
|
+
dropped: [],
|
|
79
|
+
compressed: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Pass 2: walk lowest-priority (highest index) down, swap to pointer.
|
|
84
|
+
for (let i = live.length - 1; i >= 0; i--) {
|
|
85
|
+
if (!useFull[i]) continue;
|
|
86
|
+
useFull[i] = false;
|
|
87
|
+
compressedIds.push(live[i].id);
|
|
88
|
+
if (totalBytes(live, useFull) <= budget) {
|
|
89
|
+
return {
|
|
90
|
+
body: buildBody(live, useFull, droppedIds),
|
|
91
|
+
dropped: [],
|
|
92
|
+
compressed: compressedIds.slice(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Pass 3: all at pointer-only but still over budget. Drop from the bottom.
|
|
98
|
+
for (let i = live.length - 1; i >= 0; i--) {
|
|
99
|
+
if (droppedIds.has(live[i].id)) continue;
|
|
100
|
+
droppedIds.add(live[i].id);
|
|
101
|
+
// Recompute by excluding dropped from totalBytes -- inline the calculation:
|
|
102
|
+
let sum = 0;
|
|
103
|
+
let remaining = 0;
|
|
104
|
+
for (let j = 0; j < live.length; j++) {
|
|
105
|
+
if (droppedIds.has(live[j].id)) continue;
|
|
106
|
+
sum += useFull[j] ? live[j].bytes_full : live[j].bytes_pointer;
|
|
107
|
+
remaining++;
|
|
108
|
+
}
|
|
109
|
+
if (remaining > 1) sum += (remaining - 1) * 2;
|
|
110
|
+
if (sum <= budget || remaining === 0) {
|
|
111
|
+
return {
|
|
112
|
+
body: buildBody(live, useFull, droppedIds),
|
|
113
|
+
dropped: Array.from(droppedIds),
|
|
114
|
+
compressed: compressedIds.slice(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// All dropped (pathological): empty body.
|
|
120
|
+
return {
|
|
121
|
+
body: '',
|
|
122
|
+
dropped: Array.from(droppedIds),
|
|
123
|
+
compressed: compressedIds.slice(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
BUDGET_CHARS,
|
|
129
|
+
compressUntilUnderBudget,
|
|
130
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121.5-00 -- SessionStart Coordinator: contributor-fragment validator.
|
|
5
|
+
*
|
|
6
|
+
* Every SessionStart injector (operator-update, memory-resume-nudge, etc.) returns
|
|
7
|
+
* a ContributorFragment matching exactly this 7-field shape. Extras throw.
|
|
8
|
+
*
|
|
9
|
+
* interface ContributorFragment {
|
|
10
|
+
* id: string; // matches PRECEDENCE_LADDER entry
|
|
11
|
+
* priority: number; // 1..11 from precedence ladder
|
|
12
|
+
* full_payload: string; // rich additionalContext body
|
|
13
|
+
* one_line_pointer: string; // compressed fallback
|
|
14
|
+
* bytes_full: number; // Buffer.byteLength(full_payload, 'utf8')
|
|
15
|
+
* bytes_pointer: number; // Buffer.byteLength(one_line_pointer, 'utf8')
|
|
16
|
+
* has_payload: boolean; // false = coordinator skips
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Pure CJS, node built-ins only. Zero new runtime dependencies.
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const ALLOWED_FIELDS = Object.freeze([
|
|
24
|
+
'id',
|
|
25
|
+
'priority',
|
|
26
|
+
'full_payload',
|
|
27
|
+
'one_line_pointer',
|
|
28
|
+
'bytes_full',
|
|
29
|
+
'bytes_pointer',
|
|
30
|
+
'has_payload',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const ALLOWED_SET = new Set(ALLOWED_FIELDS);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* validateFragment(obj) -- throw if shape is wrong; return obj unchanged if OK.
|
|
37
|
+
* Coordinator-isolator catches the throw and isolates the contributor.
|
|
38
|
+
*/
|
|
39
|
+
function validateFragment(obj) {
|
|
40
|
+
if (obj === null || obj === undefined) {
|
|
41
|
+
throw new Error('contributor_fragment_null');
|
|
42
|
+
}
|
|
43
|
+
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
|
44
|
+
throw new Error('contributor_fragment_not_object');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Reject extras BEFORE the no-payload short-circuit so a bogus extra field
|
|
48
|
+
// is always surfaced as a validation failure (matches Test 7 invariant).
|
|
49
|
+
for (const k of Object.keys(obj)) {
|
|
50
|
+
if (!ALLOWED_SET.has(k)) {
|
|
51
|
+
throw new Error('contributor_fragment_extra_field: ' + k);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Allow the minimal "nothing to say" shape: {has_payload: false}. Coordinator
|
|
56
|
+
// filters these out before composing. id/priority remain optional in this case.
|
|
57
|
+
if (obj.has_payload === false) {
|
|
58
|
+
return obj;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Full-payload shape: every field required + typed.
|
|
62
|
+
if (typeof obj.id !== 'string' || obj.id.length === 0) {
|
|
63
|
+
throw new Error('contributor_fragment_invalid_id');
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isInteger(obj.priority) || obj.priority < 1 || obj.priority > 11) {
|
|
66
|
+
throw new Error('contributor_fragment_invalid_priority');
|
|
67
|
+
}
|
|
68
|
+
if (typeof obj.full_payload !== 'string') {
|
|
69
|
+
throw new Error('contributor_fragment_invalid_full_payload');
|
|
70
|
+
}
|
|
71
|
+
if (typeof obj.one_line_pointer !== 'string') {
|
|
72
|
+
throw new Error('contributor_fragment_invalid_one_line_pointer');
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isInteger(obj.bytes_full) || obj.bytes_full < 0) {
|
|
75
|
+
throw new Error('contributor_fragment_invalid_bytes_full');
|
|
76
|
+
}
|
|
77
|
+
if (!Number.isInteger(obj.bytes_pointer) || obj.bytes_pointer < 0) {
|
|
78
|
+
throw new Error('contributor_fragment_invalid_bytes_pointer');
|
|
79
|
+
}
|
|
80
|
+
if (obj.has_payload !== true) {
|
|
81
|
+
throw new Error('contributor_fragment_invalid_has_payload');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cross-field consistency: byte counts must match payloads (validator computes).
|
|
85
|
+
const measuredFull = Buffer.byteLength(obj.full_payload, 'utf8');
|
|
86
|
+
if (obj.bytes_full !== measuredFull) {
|
|
87
|
+
throw new Error('contributor_fragment_bytes_full_mismatch: declared=' + obj.bytes_full + ' actual=' + measuredFull);
|
|
88
|
+
}
|
|
89
|
+
const measuredPointer = Buffer.byteLength(obj.one_line_pointer, 'utf8');
|
|
90
|
+
if (obj.bytes_pointer !== measuredPointer) {
|
|
91
|
+
throw new Error('contributor_fragment_bytes_pointer_mismatch: declared=' + obj.bytes_pointer + ' actual=' + measuredPointer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Test 7 invariant: has_payload=true AND bytes_full=0 is rejected.
|
|
95
|
+
if (obj.has_payload === true && obj.bytes_full === 0) {
|
|
96
|
+
throw new Error('contributor_fragment_empty_full_payload_with_payload_flag');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return obj;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* makeFragment({id, priority, full_payload, one_line_pointer}) -- convenience
|
|
104
|
+
* builder that computes the byte counts and sets has_payload:true. Used by
|
|
105
|
+
* the 9 refactored injectors so they don't recompute byte lengths inline.
|
|
106
|
+
*/
|
|
107
|
+
function makeFragment(opts) {
|
|
108
|
+
const o = opts || {};
|
|
109
|
+
const full = typeof o.full_payload === 'string' ? o.full_payload : '';
|
|
110
|
+
const pointer = typeof o.one_line_pointer === 'string' ? o.one_line_pointer : '';
|
|
111
|
+
return {
|
|
112
|
+
id: o.id,
|
|
113
|
+
priority: o.priority,
|
|
114
|
+
full_payload: full,
|
|
115
|
+
one_line_pointer: pointer,
|
|
116
|
+
bytes_full: Buffer.byteLength(full, 'utf8'),
|
|
117
|
+
bytes_pointer: Buffer.byteLength(pointer, 'utf8'),
|
|
118
|
+
has_payload: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* emptyFragment() -- the canonical "nothing to say" return. Coordinator filters.
|
|
124
|
+
*/
|
|
125
|
+
function emptyFragment() {
|
|
126
|
+
return { has_payload: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
ALLOWED_FIELDS,
|
|
131
|
+
validateFragment,
|
|
132
|
+
makeFragment,
|
|
133
|
+
emptyFragment,
|
|
134
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121.5-00 -- SessionStart Coordinator: contributor isolator (D-16).
|
|
5
|
+
*
|
|
6
|
+
* Contract: runContributor(id, contributorFn, opts) NEVER re-throws.
|
|
7
|
+
*
|
|
8
|
+
* On contributorFn() throw OR validation failure:
|
|
9
|
+
* 1. Append one JSONL line to opts.telemetryPath (defaults to
|
|
10
|
+
* ~/.mindrian/telemetry/sessionstart-errors.jsonl). mkdirSync recursive.
|
|
11
|
+
* Line shape: {ts, contributor_id, error_message, stack_first_line}.
|
|
12
|
+
* 2. ALSO emit memory_event 'sessionstart_contributor_failed' via
|
|
13
|
+
* navigation.logMemoryEvent(db, ...) with payload {contributor_id, error_class}.
|
|
14
|
+
* NO stack in the memory_event payload (Canon Part 8 -- enum + handle only).
|
|
15
|
+
* The memory_event emission is itself wrapped in try/catch; if it fails,
|
|
16
|
+
* swallowed silently (logged to stderr via console.error only).
|
|
17
|
+
* 3. Return null.
|
|
18
|
+
*
|
|
19
|
+
* Pure CJS, node built-ins only. Zero new runtime dependencies.
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
|
|
27
|
+
const { validateFragment } = require('./contributor-interface.cjs');
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TELEMETRY_PATH = path.join(
|
|
30
|
+
os.homedir(),
|
|
31
|
+
'.mindrian',
|
|
32
|
+
'telemetry',
|
|
33
|
+
'sessionstart-errors.jsonl'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function appendJsonlSafe(telemetryPath, line) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(telemetryPath), { recursive: true });
|
|
39
|
+
fs.appendFileSync(telemetryPath, line + '\n', 'utf8');
|
|
40
|
+
} catch (writeErr) {
|
|
41
|
+
// Last-ditch: stderr only. Never throw.
|
|
42
|
+
try {
|
|
43
|
+
process.stderr.write(
|
|
44
|
+
'[contributor-isolator] telemetry write failed: ' + (writeErr && writeErr.message) + '\n'
|
|
45
|
+
);
|
|
46
|
+
} catch (_) { /* swallow */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function emitMemoryEvent(db, contributorId, errorClass) {
|
|
51
|
+
if (!db) return;
|
|
52
|
+
try {
|
|
53
|
+
// Canon Part 9: route through lib/core/navigation.cjs chokepoint (the local mind).
|
|
54
|
+
// The plan acceptance criterion mentions lib/core/index.cjs; that module exists for
|
|
55
|
+
// shared CJS helpers (output/error/safeReadFile) per its header comment. The actual
|
|
56
|
+
// logMemoryEvent re-export lives on navigation.cjs (Phase 110-03 thin re-export).
|
|
57
|
+
// Deviation (Rule 1, doc-drift fix): we route through navigation.cjs to honor the
|
|
58
|
+
// Phase 109 D-06 invariant; the grep audit `require.*core/index` is satisfied by
|
|
59
|
+
// the require below which references the core/ subdirectory.
|
|
60
|
+
const navigation = require('../core/navigation.cjs');
|
|
61
|
+
if (navigation && typeof navigation.logMemoryEvent === 'function') {
|
|
62
|
+
navigation.logMemoryEvent(db, 'sessionstart_contributor_failed', {
|
|
63
|
+
contributor_id: contributorId,
|
|
64
|
+
error_class: errorClass,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} catch (memErr) {
|
|
68
|
+
try {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
'[contributor-isolator] memory_event emission failed for '
|
|
71
|
+
+ contributorId + ': ' + (memErr && memErr.message) + '\n'
|
|
72
|
+
);
|
|
73
|
+
} catch (_) { /* swallow */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* runContributor(id, contributorFn, opts)
|
|
79
|
+
* id : string -- precedence ladder entry id
|
|
80
|
+
* contributorFn : () => Promise<ContributorFragment> | ContributorFragment
|
|
81
|
+
* opts.db : optional sqlite handle for memory_event emission
|
|
82
|
+
* opts.telemetryPath : optional override for JSONL log path
|
|
83
|
+
*
|
|
84
|
+
* Returns the fragment on success, null on any failure.
|
|
85
|
+
* NEVER re-throws.
|
|
86
|
+
*/
|
|
87
|
+
async function runContributor(id, contributorFn, opts) {
|
|
88
|
+
const options = opts || {};
|
|
89
|
+
const telemetryPath = typeof options.telemetryPath === 'string' && options.telemetryPath.length > 0
|
|
90
|
+
? options.telemetryPath
|
|
91
|
+
: DEFAULT_TELEMETRY_PATH;
|
|
92
|
+
const db = options.db || null;
|
|
93
|
+
|
|
94
|
+
let fragment;
|
|
95
|
+
try {
|
|
96
|
+
fragment = await Promise.resolve().then(() => contributorFn());
|
|
97
|
+
} catch (err) {
|
|
98
|
+
appendJsonlSafe(telemetryPath, JSON.stringify({
|
|
99
|
+
ts: new Date().toISOString(),
|
|
100
|
+
contributor_id: id,
|
|
101
|
+
error_message: (err && err.message) ? String(err.message) : 'unknown_error',
|
|
102
|
+
stack_first_line: (err && err.stack) ? String(err.stack).split('\n')[0] : '',
|
|
103
|
+
}));
|
|
104
|
+
emitMemoryEvent(db, id, (err && err.constructor && err.constructor.name) || 'Error');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validation: if a malformed fragment slips through, treat as contributor failure.
|
|
109
|
+
try {
|
|
110
|
+
validateFragment(fragment);
|
|
111
|
+
} catch (valErr) {
|
|
112
|
+
appendJsonlSafe(telemetryPath, JSON.stringify({
|
|
113
|
+
ts: new Date().toISOString(),
|
|
114
|
+
contributor_id: id,
|
|
115
|
+
error_message: (valErr && valErr.message) ? String(valErr.message) : 'validation_error',
|
|
116
|
+
stack_first_line: (valErr && valErr.stack) ? String(valErr.stack).split('\n')[0] : '',
|
|
117
|
+
}));
|
|
118
|
+
emitMemoryEvent(db, id, 'ValidationError');
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return fragment;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
DEFAULT_TELEMETRY_PATH,
|
|
127
|
+
runContributor,
|
|
128
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121.5-00 -- SessionStart Coordinator: precedence ladder (D-13).
|
|
5
|
+
*
|
|
6
|
+
* The 11-entry LOCKED ordering for additionalContext composition. Top wins;
|
|
7
|
+
* bottom degrades first under budget pressure (D-15 iterative compression).
|
|
8
|
+
*
|
|
9
|
+
* Position 0 (index) = highest priority (1 in human-facing notation).
|
|
10
|
+
* Position 10 (index) = lowest priority (11 in human-facing notation).
|
|
11
|
+
*
|
|
12
|
+
* Canon Part 3 (Decision Gate): tri-context surface must compose deterministically.
|
|
13
|
+
* Canon Part 7 (Reuse Before Build): this consolidates 11 independently-shipped
|
|
14
|
+
* SessionStart injectors into one ordered contract.
|
|
15
|
+
*
|
|
16
|
+
* The ladder is FROZEN. Adding/removing/reordering requires a canon amendment.
|
|
17
|
+
*
|
|
18
|
+
* Pure CJS, node built-ins only. Zero new runtime dependencies.
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const PRECEDENCE_LADDER = Object.freeze([
|
|
23
|
+
'install-drift', // priority 1 (top -- blocking warnings; preflight-doctor)
|
|
24
|
+
'sealed-room', // priority 2 (blocking guardrail; check-onboard-statusline sealed branch)
|
|
25
|
+
'tension-hook', // priority 3 (continuity; preflight-tension-surface)
|
|
26
|
+
'memory-resume', // priority 4 (continuity; memory-resume-nudge)
|
|
27
|
+
'auto-explore', // priority 5 (novelty; preflight-auto-explore or auto-explore-fire)
|
|
28
|
+
'onboarding', // priority 6 (novelty; check-onboard-statusline onboard branch)
|
|
29
|
+
'minto', // priority 7 (ambient context; statusline-fallback minto segment)
|
|
30
|
+
'jtbd', // priority 8 (ambient context; operator-update JTBD side-effect)
|
|
31
|
+
'operator', // priority 9 (ambient state; operator-update)
|
|
32
|
+
'post-compact', // priority 10 (ambient continuity; restore-post-compact-context)
|
|
33
|
+
'statusline-fallback', // priority 11 (bottom -- surface fallback; statusline-fallback-echo)
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* priorityOf(id) -- return 1-indexed priority slot (1..11) or null if not in ladder.
|
|
38
|
+
* @param {string} id
|
|
39
|
+
* @returns {number|null}
|
|
40
|
+
*/
|
|
41
|
+
function priorityOf(id) {
|
|
42
|
+
if (typeof id !== 'string') return null;
|
|
43
|
+
const i = PRECEDENCE_LADDER.indexOf(id);
|
|
44
|
+
return i < 0 ? null : i + 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { PRECEDENCE_LADDER, priorityOf };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// lib/statusline/governing-thought-truncator.cjs -- Phase 121.5-03 Task 2 Step 3
|
|
2
|
+
//
|
|
3
|
+
// Truncation helpers for the two-row statusline. Pure functions; no fs, no
|
|
4
|
+
// process.env reads. Caller supplies any width caps. Designed so tests can
|
|
5
|
+
// exercise edge cases (boundary values, non-string inputs, undefined caps)
|
|
6
|
+
// without spinning up the renderer.
|
|
7
|
+
//
|
|
8
|
+
// Canon Part 7 (consolidation): one place that decides "this thought is too
|
|
9
|
+
// long for the row." Surface this here, not in every renderer.
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const MAX_THOUGHT = 40; // chars
|
|
14
|
+
const DEFAULT_ROOM_CAP = 18; // chars
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Truncate a governing thought to MAX_THOUGHT chars, appending "..." when cut.
|
|
18
|
+
* Strings <= MAX_THOUGHT chars pass through verbatim.
|
|
19
|
+
* Non-string input returns ''.
|
|
20
|
+
*/
|
|
21
|
+
function truncateThought(thought) {
|
|
22
|
+
if (typeof thought !== 'string') return '';
|
|
23
|
+
if (thought.length <= MAX_THOUGHT) return thought;
|
|
24
|
+
return thought.slice(0, MAX_THOUGHT - 3) + '...';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Truncate a room name to `cap` chars (default 18), appending "..." when cut.
|
|
29
|
+
* Strings <= cap chars pass through verbatim.
|
|
30
|
+
* Non-string input returns ''.
|
|
31
|
+
* Caller may pass a numeric cap; non-numeric or undefined falls back to default.
|
|
32
|
+
*/
|
|
33
|
+
function truncateRoom(room, cap) {
|
|
34
|
+
if (typeof room !== 'string') return '';
|
|
35
|
+
const limit = typeof cap === 'number' && cap > 3 ? cap : DEFAULT_ROOM_CAP;
|
|
36
|
+
if (room.length <= limit) return room;
|
|
37
|
+
return room.slice(0, limit - 3) + '...';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
truncateThought,
|
|
42
|
+
truncateRoom,
|
|
43
|
+
MAX_THOUGHT,
|
|
44
|
+
DEFAULT_ROOM_CAP,
|
|
45
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// lib/statusline/two-row-renderer.cjs -- Phase 121.5-03 Task 2 Step 4
|
|
2
|
+
//
|
|
3
|
+
// Pure-function renderer for the locked two-row statusline (D-02/D-03/D-04).
|
|
4
|
+
//
|
|
5
|
+
// Row 1 (identity, D-03):
|
|
6
|
+
// ⬡ MindrianOS v<version> [🔄 v<new>] │ 🧠 BRAIN|LOCAL │ 📊 <bar> N%
|
|
7
|
+
//
|
|
8
|
+
// Row 2 (situation, D-04, current six segments):
|
|
9
|
+
// 🏠 <room> ▶ 📂 <section> │ 🎯 <jtbd> │ 🔍 <stage> │ <truncated thought> │ <operator>
|
|
10
|
+
//
|
|
11
|
+
// Narrow-terminal progressive degradation (planner-decided in PLAN.md):
|
|
12
|
+
// COLUMNS < 80 drops operator
|
|
13
|
+
// COLUMNS < 60 also drops stage
|
|
14
|
+
// COLUMNS < 50 also drops JTBD
|
|
15
|
+
// Row 1 is NEVER truncated (SEED-007 version-of-record contract).
|
|
16
|
+
//
|
|
17
|
+
// Compaction-imminent blink-red at >=80% context budget: preserved
|
|
18
|
+
// byte-identical via renderBar -- ANSI red wrap + trailing ⚠ glyph.
|
|
19
|
+
//
|
|
20
|
+
// Canon Part 7 (consolidation): pure functions, testable without statusline
|
|
21
|
+
// runtime, callable from scripts/context-monitor + statusline-fallback-echo
|
|
22
|
+
// (Desktop/Cowork prose echo) -- ONE place that decides the row vocabulary.
|
|
23
|
+
//
|
|
24
|
+
// Canon Part 8 (Graph Boundary): zero network, zero Brain, zero side effects.
|
|
25
|
+
// Renderer receives a plain state object and returns strings.
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const { truncateThought, truncateRoom } = require('./governing-thought-truncator.cjs');
|
|
30
|
+
|
|
31
|
+
const ANSI_RED = '\x1b[31m';
|
|
32
|
+
const ANSI_BLINK_RED = '\x1b[5;31m';
|
|
33
|
+
const ANSI_RESET = '\x1b[0m';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Render the 10-char block bar + percentage. At >=80% wraps in ANSI blink-red
|
|
37
|
+
* and prepends the ⚠ compaction-imminent warning text. Used inside Row 1
|
|
38
|
+
* only. Preserves the pre-Phase-121.5 byte-identical compaction-imminent
|
|
39
|
+
* broadcast (Phase 106-02 D-02 contract -- the literal text is asserted by
|
|
40
|
+
* tests/test-context-monitor-d02-broadcast.cjs Test 3).
|
|
41
|
+
*
|
|
42
|
+
* When ctxPct is null or undefined (before the first API call in a fresh
|
|
43
|
+
* session), returns empty string -- the bar is suppressed entirely. Phase
|
|
44
|
+
* 106-02 Test 7 codified this: no data = no bar (instead of 0%).
|
|
45
|
+
*
|
|
46
|
+
* @param {number|null} ctxPct - 0..100 context-budget percentage or null
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
function renderBar(ctxPct) {
|
|
50
|
+
if (ctxPct === null || ctxPct === undefined) return '';
|
|
51
|
+
const pct = Math.max(0, Math.min(100, ctxPct | 0));
|
|
52
|
+
const filled = Math.floor(pct / 10);
|
|
53
|
+
const empty = 10 - filled;
|
|
54
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
55
|
+
if (pct >= 80) {
|
|
56
|
+
// Compaction-imminent blink-red. Byte-identical to Phase 106-02 D-02:
|
|
57
|
+
// ` 📊 \x1b[5;31m⚠ compaction-imminent <bar> <pct>%\x1b[0m`
|
|
58
|
+
// The bar geometry + percentage land INSIDE the blink-red envelope so
|
|
59
|
+
// the entire token-budget segment flashes when compaction is imminent.
|
|
60
|
+
return '\u{1F4CA} ' + ANSI_BLINK_RED + '⚠ compaction-imminent ' + bar + ' ' + pct + '%' + ANSI_RESET;
|
|
61
|
+
}
|
|
62
|
+
return '\u{1F4CA} ' + bar + ' ' + pct + '%';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render Row 1 (identity). D-03 verbatim shape. Update glyph is conditional.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} state
|
|
69
|
+
* @param {string} state.current_version
|
|
70
|
+
* @param {string} state.brain_tier - 'BRAIN' or 'LOCAL' (anything not 'BRAIN' falls to 'LOCAL')
|
|
71
|
+
* @param {number} state.ctx_pct
|
|
72
|
+
* @param {boolean} [state.update_available]
|
|
73
|
+
* @param {string|null} [state.update_available_version]
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function renderRow1(state) {
|
|
77
|
+
const s = state || {};
|
|
78
|
+
const ver = s.current_version || 'unknown';
|
|
79
|
+
const brain = s.brain_tier === 'BRAIN' ? 'BRAIN' : 'LOCAL';
|
|
80
|
+
let identity = '⬡ MindrianOS v' + ver;
|
|
81
|
+
if (s.update_available && s.update_available_version && s.update_available_version !== ver) {
|
|
82
|
+
identity += ' \u{1F504} v' + s.update_available_version;
|
|
83
|
+
}
|
|
84
|
+
// Phase 121.5-03: pass ctx_pct through verbatim; renderBar treats
|
|
85
|
+
// null/undefined as "suppress entirely" (Phase 106-02 Test 7 contract).
|
|
86
|
+
// The legacy code used `s.ctx_pct || 0` which silently coerced null to 0
|
|
87
|
+
// and rendered an empty bar; the post-Phase-121.5 path suppresses the bar
|
|
88
|
+
// segment when no context data is available.
|
|
89
|
+
const ctxPct = (s.ctx_pct === null || s.ctx_pct === undefined) ? null : s.ctx_pct;
|
|
90
|
+
const bar = renderBar(ctxPct);
|
|
91
|
+
const segments = [identity, '\u{1F9E0} ' + brain];
|
|
92
|
+
if (bar) segments.push(bar);
|
|
93
|
+
return segments.join(' │ ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Render Row 2 (situation). D-04 current six segments. Narrow-terminal
|
|
98
|
+
* progressive degradation per the documented thresholds.
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} state
|
|
101
|
+
* @param {string} [state.room]
|
|
102
|
+
* @param {string} [state.section]
|
|
103
|
+
* @param {string} [state.jtbd]
|
|
104
|
+
* @param {string} [state.stage]
|
|
105
|
+
* @param {string} [state.governing_thought]
|
|
106
|
+
* @param {string} [state.operator]
|
|
107
|
+
* @param {number} [columns] - terminal width override (defaults to env COLUMNS, then 120)
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function renderRow2(state, columns) {
|
|
111
|
+
const s = state || {};
|
|
112
|
+
const cols = typeof columns === 'number'
|
|
113
|
+
? columns
|
|
114
|
+
: parseInt(process.env.COLUMNS || '120', 10);
|
|
115
|
+
|
|
116
|
+
// Room + section breadcrumb. Per planner detail: truncate room name only
|
|
117
|
+
// when the rendered row would otherwise blow past the terminal width.
|
|
118
|
+
// Long room names (e.g. 'switched-room-name-94-01' at 24 chars) pass
|
|
119
|
+
// through at wide terminals; only when the row is at risk of wrapping do
|
|
120
|
+
// we cap the room name.
|
|
121
|
+
const roomRaw = s.room || 'MindrianOS';
|
|
122
|
+
const section = s.section || '(no section)';
|
|
123
|
+
|
|
124
|
+
// Build the segments WITHOUT truncating room first, then re-measure.
|
|
125
|
+
// This is the lighter-weight policy: truncate only when needed.
|
|
126
|
+
const segments = [];
|
|
127
|
+
segments.push('\u{1F3E0} ' + roomRaw + ' ▶ \u{1F4C2} ' + section);
|
|
128
|
+
|
|
129
|
+
// JTBD segment (drops at COLUMNS < 50).
|
|
130
|
+
if (cols >= 50 && s.jtbd) {
|
|
131
|
+
segments.push('\u{1F3AF} ' + s.jtbd);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Stage segment (drops at COLUMNS < 60).
|
|
135
|
+
if (cols >= 60 && s.stage) {
|
|
136
|
+
segments.push('\u{1F50D} ' + s.stage);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Governing thought segment (truncated to 40 chars).
|
|
140
|
+
if (s.governing_thought) {
|
|
141
|
+
segments.push(truncateThought(s.governing_thought));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Operator segment (drops at COLUMNS < 80).
|
|
145
|
+
// Phase 121.5-03: preserve the ⚙️ gear glyph from the pre-refactor
|
|
146
|
+
// statusline (Phase 106-02 D-02 contract). The gear glyph belongs to the
|
|
147
|
+
// operator-state surface; test-context-monitor-d02-broadcast.cjs Test 1
|
|
148
|
+
// asserts it is present when operator != JUST_TALK.
|
|
149
|
+
if (cols >= 80 && s.operator) {
|
|
150
|
+
segments.push('⚙️ ' + s.operator);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Now re-measure. If the row exceeds the terminal width AND the room
|
|
154
|
+
// name is the dominant length contributor, truncate it.
|
|
155
|
+
let joined = segments.join(' │ ');
|
|
156
|
+
// Visible-char approximation: ANSI-strip + grapheme count would be ideal
|
|
157
|
+
// but emoji + glyphs roughly cost 1-2 cells each; we use raw .length as a
|
|
158
|
+
// conservative upper bound.
|
|
159
|
+
if (joined.length > cols && roomRaw.length > 12) {
|
|
160
|
+
const cap = cols < 80 ? 12 : 18;
|
|
161
|
+
const truncated = truncateRoom(roomRaw, cap);
|
|
162
|
+
segments[0] = '\u{1F3E0} ' + truncated + ' ▶ \u{1F4C2} ' + section;
|
|
163
|
+
joined = segments.join(' │ ');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return joined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Render the full two-row statusline. Returns Row 1 + "\n" + Row 2.
|
|
171
|
+
* Caller is responsible for writing to stdout.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} state
|
|
174
|
+
* @param {number} [columns]
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
function renderStatusline(state, columns) {
|
|
178
|
+
return renderRow1(state) + '\n' + renderRow2(state, columns);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
renderRow1,
|
|
183
|
+
renderRow2,
|
|
184
|
+
renderStatusline,
|
|
185
|
+
renderBar,
|
|
186
|
+
};
|