@10kdevs/matha 0.1.0

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.
@@ -0,0 +1,334 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { readJsonOrNull } from '../storage/reader.js';
4
+ import { writeAtomic, appendToArray } from '../storage/writer.js';
5
+ import { recordDecision, recordDangerZone } from '../brain/hippocampus.js';
6
+ import { checkSchemaVersion, getSchemaMessage } from '../utils/schema-version.js';
7
+ async function pathExists(filePath) {
8
+ try {
9
+ await fs.access(filePath);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ function generateSessionId(now) {
17
+ const year = now.getFullYear();
18
+ const month = String(now.getMonth() + 1).padStart(2, '0');
19
+ const day = String(now.getDate()).padStart(2, '0');
20
+ const hours = String(now.getHours()).padStart(2, '0');
21
+ const minutes = String(now.getMinutes()).padStart(2, '0');
22
+ const seconds = String(now.getSeconds()).padStart(2, '0');
23
+ const randomHex = Array.from({ length: 4 })
24
+ .map(() => Math.floor(Math.random() * 16).toString(16))
25
+ .join('');
26
+ return `${year}${month}${day}-${hours}${minutes}${seconds}-${randomHex}`;
27
+ }
28
+ async function findMostRecentPredictionFile(predictionsDir) {
29
+ try {
30
+ const files = await fs.readdir(predictionsDir);
31
+ if (files.length === 0)
32
+ return null;
33
+ // Sort filenames descending to get most recent
34
+ const sorted = files
35
+ .filter((f) => f.endsWith('.json'))
36
+ .map((f) => f.replace('.json', ''))
37
+ .sort()
38
+ .reverse();
39
+ return sorted.length > 0 ? sorted[0] : null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function getLinkedSessionBrief(mathaDir, sessionId) {
46
+ try {
47
+ const briefPath = path.join(mathaDir, `sessions/${sessionId}.brief`);
48
+ return await readJsonOrNull(briefPath);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ async function runAfter(projectRoot = process.cwd(), deps) {
55
+ const ask = deps?.ask ?? defaultAsk;
56
+ const log = deps?.log ?? console.log;
57
+ const now = deps?.now ?? (() => new Date());
58
+ const mathaDir = path.join(projectRoot, '.matha');
59
+ const configPath = path.join(mathaDir, 'config.json');
60
+ // GUARD: Check if .matha/config.json exists
61
+ const configExists = await pathExists(configPath);
62
+ if (!configExists) {
63
+ const message = 'MATHA is not initialised. Run `matha init` first.';
64
+ log(message);
65
+ return { exitCode: 1, message };
66
+ }
67
+ // SCHEMA VERSION CHECK
68
+ const schemaResult = await checkSchemaVersion(mathaDir);
69
+ const schemaMsg = getSchemaMessage(schemaResult);
70
+ if (schemaMsg)
71
+ log(schemaMsg);
72
+ if (schemaResult.status === 'newer') {
73
+ return { exitCode: 1, message: schemaMsg };
74
+ }
75
+ const timestamp = now().toISOString();
76
+ const predictionsDir = path.join(mathaDir, 'dopamine/predictions');
77
+ // Find most recent prediction file
78
+ const linkedSessionId = await findMostRecentPredictionFile(predictionsDir);
79
+ const sessionId = linkedSessionId ?? generateSessionId(now());
80
+ if (linkedSessionId) {
81
+ log(`\nLinking to session: ${linkedSessionId}\n`);
82
+ }
83
+ // Get scope and contract from linked session brief if available
84
+ const brief = await getLinkedSessionBrief(mathaDir, sessionId);
85
+ let scope = brief?.scope ?? 'unknown';
86
+ const assertions = brief?.contract && Array.isArray(brief.contract) ? brief.contract : [];
87
+ // PROMPT 01 — DISCOVERY: What assumption broke?
88
+ const assumptionInput = await ask('What assumption broke or needed correction? (press enter to skip)');
89
+ const assumption = assumptionInput.trim() || null;
90
+ // PROMPT 02 — CORRECTION: Only show if assumption provided
91
+ let correction = null;
92
+ if (assumption) {
93
+ const correctionInput = await ask('What was the correction? What is the right understanding?');
94
+ correction = correctionInput.trim() || null;
95
+ }
96
+ // PROMPT 03 — DANGER ZONE
97
+ const dangerPatternInput = await ask('Should this be recorded as a danger zone for future sessions?\n' +
98
+ 'Describe the pattern to watch for, or press enter to skip.');
99
+ const dangerPattern = dangerPatternInput.trim() || null;
100
+ // PROMPT 04 — CONTRACT RESULT
101
+ let contractResult = 'none';
102
+ const failedAssertions = [];
103
+ if (assertions.length > 0) {
104
+ log(`\nCONTRACT VALIDATION — ${assertions.length} assertion(s) to check:\n`);
105
+ let passCount = 0;
106
+ let failCount = 0;
107
+ let skipCount = 0;
108
+ for (let i = 0; i < assertions.length; i++) {
109
+ const assertionText = assertions[i];
110
+ log(` [${i + 1}/${assertions.length}] ${assertionText}`);
111
+ let valid = false;
112
+ while (!valid) {
113
+ let answer = await ask(' Did this pass? (y/n/skip)');
114
+ answer = answer.trim().toLowerCase();
115
+ if (answer === 'y' || answer === 'yes' || answer === 'pass') {
116
+ passCount++;
117
+ valid = true;
118
+ }
119
+ else if (answer === 'n' || answer === 'no' || answer === 'fail') {
120
+ failCount++;
121
+ failedAssertions.push(assertionText);
122
+ valid = true;
123
+ }
124
+ else if (answer === 'skip' || answer === 's') {
125
+ skipCount++;
126
+ valid = true;
127
+ }
128
+ else {
129
+ log(' Please answer y, n, or skip.');
130
+ }
131
+ }
132
+ }
133
+ if (failCount > 0) {
134
+ contractResult = 'violated';
135
+ }
136
+ else if (passCount === 0 && skipCount > 0) {
137
+ contractResult = 'none';
138
+ }
139
+ else if (passCount > 0 && skipCount > 0) {
140
+ contractResult = 'partial';
141
+ }
142
+ else if (passCount > 0) {
143
+ contractResult = 'passed';
144
+ }
145
+ else {
146
+ contractResult = 'none';
147
+ }
148
+ const marks = { passed: 'PASSED ✓', violated: 'VIOLATED ✗', partial: 'PARTIAL ~', none: 'NONE –' };
149
+ log(`\nContract result: ${marks[contractResult]}`);
150
+ }
151
+ else {
152
+ log('\nDid the behaviour contract pass?');
153
+ log(' 1. Yes — all assertions passed');
154
+ log(' 2. Partial — some assertions passed');
155
+ log(' 3. No — contract was violated');
156
+ log(' 4. No contract was written');
157
+ log('');
158
+ const contractChoice = await ask('Did the behaviour contract pass? (1-4):');
159
+ const contractResultMap = {
160
+ '1': 'passed',
161
+ '2': 'partial',
162
+ '3': 'violated',
163
+ '4': 'none',
164
+ };
165
+ contractResult = contractResultMap[contractChoice] || 'none';
166
+ }
167
+ // PROMPT 05 — ACTUALS: Files changed
168
+ const filesInput = await ask('Approximately how many files were changed in this session?');
169
+ const filesChanged = parseInt(filesInput, 10) || 0;
170
+ // PROMPT 05 — ACTUALS: Tokens used
171
+ const tokensInput = await ask('Roughly how many tokens did this session use? (press enter to skip)');
172
+ const tokensUsed = tokensInput.trim() ? parseInt(tokensInput, 10) : null;
173
+ // ──────────────────────────────────────────────────────────────
174
+ // WRITE-BACK OPERATIONS
175
+ // ──────────────────────────────────────────────────────────────
176
+ let decisionRecorded = false;
177
+ let dangerZoneRecorded = false;
178
+ // 1. DECISION ENTRY (if both assumption AND correction)
179
+ if (assumption && correction) {
180
+ try {
181
+ await recordDecision(mathaDir, {
182
+ id: `${sessionId}-decision`,
183
+ timestamp,
184
+ component: scope,
185
+ previous_assumption: assumption,
186
+ correction,
187
+ trigger: sessionId,
188
+ confidence: 'confirmed',
189
+ status: 'active',
190
+ supersedes: null,
191
+ session_id: sessionId,
192
+ });
193
+ decisionRecorded = true;
194
+ }
195
+ catch (err) {
196
+ // Gracefully handle duplicate decision (same sessionId)
197
+ // Decision is still recorded from the first run
198
+ if (linkedSessionId) {
199
+ decisionRecorded = false; // Don't claim it was recorded on duplicate
200
+ }
201
+ }
202
+ }
203
+ // 2. DANGER ZONE (if pattern provided)
204
+ if (dangerPattern) {
205
+ try {
206
+ const dangerZoneId = `${sessionId}-danger`;
207
+ await recordDangerZone(mathaDir, {
208
+ id: dangerZoneId,
209
+ component: scope,
210
+ pattern: 'Session-discovered pattern',
211
+ description: dangerPattern,
212
+ });
213
+ dangerZoneRecorded = true;
214
+ }
215
+ catch {
216
+ dangerZoneRecorded = false;
217
+ }
218
+ }
219
+ // 3. DOPAMINE ACTUAL (always written)
220
+ const actualRecord = {
221
+ session_id: sessionId,
222
+ timestamp,
223
+ contract_result: contractResult,
224
+ actual: {
225
+ files_changed: filesChanged,
226
+ tokens_used: tokensUsed,
227
+ },
228
+ };
229
+ const actualPath = path.join(mathaDir, `dopamine/actuals/${sessionId}.json`);
230
+ await writeAtomic(actualPath, actualRecord);
231
+ // 4. DOPAMINE DELTA
232
+ let predictionData = null;
233
+ try {
234
+ const predictionPath = path.join(mathaDir, `dopamine/predictions/${sessionId}.json`);
235
+ predictionData = await readJsonOrNull(predictionPath);
236
+ }
237
+ catch {
238
+ predictionData = null;
239
+ }
240
+ const tokenDelta = predictionData && tokensUsed ? tokensUsed - predictionData.predicted.token_budget : null;
241
+ const deltaEntry = {
242
+ session_id: sessionId,
243
+ timestamp,
244
+ operation_type: predictionData?.operation_type ?? 'unknown',
245
+ contract_result: contractResult,
246
+ token_delta: tokenDelta,
247
+ files_changed: filesChanged,
248
+ model_tier_used: predictionData?.predicted.model_tier ?? 'unknown',
249
+ };
250
+ const deltasPath = path.join(mathaDir, 'dopamine/deltas.json');
251
+ await appendToArray(deltasPath, deltaEntry);
252
+ // 5. CONTRACT VIOLATION LOG (only if violated or partial)
253
+ if (failedAssertions.length > 0) {
254
+ const violationPath = path.join(mathaDir, 'cerebellum/violation-log.json');
255
+ for (const assertionText of failedAssertions) {
256
+ const violationEntry = {
257
+ session_id: sessionId,
258
+ timestamp,
259
+ result: 'violated',
260
+ scope,
261
+ assertion: assertionText,
262
+ component: scope,
263
+ };
264
+ await appendToArray(violationPath, violationEntry);
265
+ await updateContractViolation(mathaDir, scope, assertionText, timestamp);
266
+ }
267
+ }
268
+ else if (contractResult === 'violated' || contractResult === 'partial') {
269
+ const violationEntry = {
270
+ session_id: sessionId,
271
+ timestamp,
272
+ result: contractResult,
273
+ scope,
274
+ };
275
+ const violationPath = path.join(mathaDir, 'cerebellum/violation-log.json');
276
+ await appendToArray(violationPath, violationEntry);
277
+ }
278
+ // ──────────────────────────────────────────────────────────────
279
+ // TERMINAL OUTPUT
280
+ // ──────────────────────────────────────────────────────────────
281
+ log('\n════════════════════════════════════════');
282
+ log(`MATHA WRITE-BACK COMPLETE — ${sessionId}`);
283
+ log('════════════════════════════════════════\n');
284
+ const decisionMark = decisionRecorded ? '✓' : '–';
285
+ const dangerMark = dangerZoneRecorded ? '✓' : '–';
286
+ const contractMark = '✓';
287
+ const dopamineMark = '✓';
288
+ log(`${decisionMark} Decision recorded` +
289
+ (decisionRecorded ? ` (${scope}: ${correction?.substring(0, 40)})` : ''));
290
+ log(`${dangerMark} Danger zone recorded` + (dangerZoneRecorded ? ` (${dangerPattern})` : ' (none)'));
291
+ log(`${contractMark} Contract result logged (${contractResult})`);
292
+ const tokenDeltaStr = tokenDelta !== null ? `${tokenDelta > 0 ? '+' : ''}${tokenDelta}` : 'N/A';
293
+ log(`${dopamineMark} Dopamine delta recorded (tokens: ${tokenDeltaStr} | files: ${filesChanged})`);
294
+ log('\nBrain updated. Next session starts warmer.');
295
+ log('════════════════════════════════════════\n');
296
+ return {
297
+ exitCode: 0,
298
+ sessionId,
299
+ scope,
300
+ decisionRecorded,
301
+ dangerZoneRecorded,
302
+ };
303
+ }
304
+ async function updateContractViolation(mathaDir, component, assertionText, timestamp) {
305
+ try {
306
+ const sanitizedComponent = component.replace(/[^a-zA-Z0-9_-]/g, '_');
307
+ const contractPath = path.join(mathaDir, `cerebellum/contracts/${sanitizedComponent}.json`);
308
+ const contract = await readJsonOrNull(contractPath);
309
+ if (!contract || !Array.isArray(contract.assertions)) {
310
+ return;
311
+ }
312
+ let modified = false;
313
+ const searchTarget = assertionText.trim().toLowerCase();
314
+ for (const assertion of contract.assertions) {
315
+ if (assertion.description && assertion.description.trim().toLowerCase() === searchTarget) {
316
+ assertion.violation_count = (assertion.violation_count || 0) + 1;
317
+ assertion.last_violated = timestamp;
318
+ modified = true;
319
+ }
320
+ }
321
+ if (modified) {
322
+ await writeAtomic(contractPath, contract, { overwrite: true });
323
+ }
324
+ }
325
+ catch {
326
+ // Fail silently
327
+ }
328
+ }
329
+ // Default ask implementation using @inquirer/prompts
330
+ async function defaultAsk(question) {
331
+ const { input } = await import('@inquirer/prompts');
332
+ return await input({ message: question });
333
+ }
334
+ export { runAfter };
@@ -0,0 +1,328 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { writeAtomic } from '../storage/writer.js';
4
+ import { getRules } from '../brain/hippocampus.js';
5
+ import { checkSchemaVersion, getSchemaMessage } from '../utils/schema-version.js';
6
+ import { getSnapshot, getStability } from '../brain/cortex.js';
7
+ import { matchAll } from '../analysis/contract-matcher.js';
8
+ import { getRecommendation } from '../brain/dopamine.js';
9
+ // Operation type mapping from menu choice to operation_type
10
+ const operationTypeMap = {
11
+ '1': 'rename',
12
+ '2': 'crud',
13
+ '3': 'business_logic',
14
+ '4': 'architecture',
15
+ '5': 'frozen_component',
16
+ };
17
+ // Model tier and token budget mapping
18
+ const modelTierBudget = {
19
+ rename: { modelTier: 'lightweight', tokenBudget: 2000 },
20
+ crud: { modelTier: 'lightweight', tokenBudget: 2000 },
21
+ business_logic: { modelTier: 'capable', tokenBudget: 8000 },
22
+ architecture: { modelTier: 'capable', tokenBudget: 16000 },
23
+ frozen_component: { modelTier: 'capable', tokenBudget: 16000 },
24
+ };
25
+ async function pathExists(filePath) {
26
+ try {
27
+ await fs.access(filePath);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function generateSessionId(now) {
35
+ const year = now.getFullYear();
36
+ const month = String(now.getMonth() + 1).padStart(2, '0');
37
+ const day = String(now.getDate()).padStart(2, '0');
38
+ const hours = String(now.getHours()).padStart(2, '0');
39
+ const minutes = String(now.getMinutes()).padStart(2, '0');
40
+ const seconds = String(now.getSeconds()).padStart(2, '0');
41
+ const randomHex = Array.from({ length: 4 })
42
+ .map(() => Math.floor(Math.random() * 16).toString(16))
43
+ .join('');
44
+ return `${year}${month}${day}-${hours}${minutes}${seconds}-${randomHex}`;
45
+ }
46
+ async function runBefore(projectRoot = process.cwd(), deps) {
47
+ const ask = deps?.ask ?? defaultAsk;
48
+ const log = deps?.log ?? console.log;
49
+ const now = deps?.now ?? (() => new Date());
50
+ const mathaDir = path.join(projectRoot, '.matha');
51
+ const configPath = path.join(mathaDir, 'config.json');
52
+ // GUARD: Check if .matha/config.json exists
53
+ const configExists = await pathExists(configPath);
54
+ if (!configExists) {
55
+ const message = 'MATHA is not initialised. Run `matha init` first.';
56
+ log(message);
57
+ return { exitCode: 1, message };
58
+ }
59
+ // SCHEMA VERSION CHECK
60
+ const schemaResult = await checkSchemaVersion(mathaDir);
61
+ const schemaMsg = getSchemaMessage(schemaResult);
62
+ if (schemaMsg)
63
+ log(schemaMsg);
64
+ if (schemaResult.status === 'newer') {
65
+ return { exitCode: 1, message: schemaMsg };
66
+ }
67
+ // Generate session ID
68
+ const sessionId = generateSessionId(now());
69
+ const timestamp = now().toISOString();
70
+ // GATE 01 — UNDERSTAND: What are you about to build or change?
71
+ const operationDescription = await ask('What are you about to build or change?');
72
+ // GATE 02 — BOUND: Which components or files will this affect?
73
+ const scopeInput = await ask('Which components or files will this affect? (comma separated)');
74
+ const scope = scopeInput;
75
+ // GATE 03 — ORIENT: Read cortex (via cortex module)
76
+ let cortexSnapshot = null;
77
+ let frozenFiles = [];
78
+ try {
79
+ cortexSnapshot = await getSnapshot(mathaDir);
80
+ }
81
+ catch {
82
+ cortexSnapshot = null;
83
+ }
84
+ if (cortexSnapshot && cortexSnapshot.stability && cortexSnapshot.stability.length > 0) {
85
+ const s = cortexSnapshot.summary;
86
+ log(`\nCORTEX (${cortexSnapshot.fileCount} files mapped):`);
87
+ log(` frozen: ${s.frozen} stable: ${s.stable} volatile: ${s.volatile} disposable: ${s.disposable}`);
88
+ // If scope was provided, check for frozen files in scope
89
+ if (scope) {
90
+ const scopeFiles = scope.split(',').map((f) => f.trim().replace(/\\/g, '/'));
91
+ try {
92
+ const stabilityMap = await getStability(mathaDir, scopeFiles);
93
+ for (const [fp, record] of Object.entries(stabilityMap)) {
94
+ if (record && record.stability === 'frozen') {
95
+ log(` ⚠ ${fp} — FROZEN (${record.reason})`);
96
+ frozenFiles.push(fp);
97
+ }
98
+ else if (record && record.stability === 'stable') {
99
+ log(` · ${fp} — STABLE`);
100
+ }
101
+ }
102
+ }
103
+ catch {
104
+ // Gracefully handle stability lookup errors
105
+ }
106
+ }
107
+ log('');
108
+ }
109
+ else {
110
+ log('\n Cortex empty — run matha init or commit some code\n');
111
+ }
112
+ // GATE 04 — SENSE PAST HISTORY: Run Contract Matcher
113
+ const matchContext = {
114
+ scope,
115
+ intent: operationDescription,
116
+ operationType: 'unknown',
117
+ filepaths: scope.split(',').map((s) => s.trim()).filter(Boolean),
118
+ };
119
+ let matchResults = [];
120
+ try {
121
+ matchResults = await matchAll(matchContext, mathaDir);
122
+ }
123
+ catch {
124
+ matchResults = [];
125
+ }
126
+ const criticals = matchResults.filter(r => r.severity === 'critical');
127
+ const warnings = matchResults.filter(r => r.severity === 'warning');
128
+ const infos = matchResults.filter(r => r.severity === 'info');
129
+ const hasCritical = criticals.length > 0;
130
+ if (matchResults.length === 0) {
131
+ log('\n✓ No issues detected for this scope.\n');
132
+ }
133
+ else {
134
+ log('');
135
+ if (criticals.length > 0) {
136
+ log(`🚨 CRITICAL — ${criticals.length} issue(s) require attention:`);
137
+ for (const res of criticals) {
138
+ log(` ✗ ${res.title}`);
139
+ log(` ${res.description}`);
140
+ log(` → ${res.recommendation}`);
141
+ }
142
+ }
143
+ if (warnings.length > 0) {
144
+ log(`⚠ WARNINGS — ${warnings.length} prior finding(s):`);
145
+ for (const res of warnings) {
146
+ log(` · ${res.title}`);
147
+ log(` ${res.description}`);
148
+ }
149
+ }
150
+ if (infos.length > 0) {
151
+ log(`ℹ CONTEXT — ${infos.length} relevant contract(s):`);
152
+ for (const res of infos) {
153
+ log(` · ${res.title} — ${res.description}`);
154
+ }
155
+ }
156
+ log('');
157
+ }
158
+ // GATE 05 — CONTRACT: Write the behaviour contract
159
+ const contractInput = await ask('Write the behaviour contract for this session.\nWhat must be true after your changes? (one assertion per line, empty line to finish)');
160
+ const assertions = contractInput
161
+ .split('\n')
162
+ .map((line) => line.trim())
163
+ .filter((line) => line.length > 0);
164
+ if (assertions.length === 0) {
165
+ log('⚠ No contract written — build gate will be advisory only.\n');
166
+ }
167
+ // GATE 06 — COST CHECK: What type of operation?
168
+ log('\nWhat type of operation is this?');
169
+ log(' 1. Rename / Format');
170
+ log(' 2. CRUD / Boilerplate');
171
+ log(' 3. Business Logic');
172
+ log(' 4. Architecture Change');
173
+ log(' 5. Frozen Component Change');
174
+ log('');
175
+ const operationTypeChoice = await ask('What type of operation is this? (1-5):');
176
+ const operationType = operationTypeMap[operationTypeChoice] || 'rename';
177
+ const rec = await getRecommendation(mathaDir, operationType);
178
+ const defaultDef = modelTierBudget[operationType];
179
+ const modelTier = rec.tier;
180
+ const tokenBudget = rec.budget;
181
+ if (rec.source === 'learned') {
182
+ log(`Model: ${modelTier} (budget: ${tokenBudget} tokens) — learned from ${rec.sample_size} sessions (${rec.confidence} confidence)`);
183
+ if (modelTier !== defaultDef.modelTier) {
184
+ const isUpgrade = (modelTier === 'capable' && (defaultDef.modelTier === 'mid' || defaultDef.modelTier === 'lightweight')) ||
185
+ (modelTier === 'mid' && defaultDef.modelTier === 'lightweight');
186
+ if (isUpgrade) {
187
+ log(` ↑ Upgraded from ${defaultDef.modelTier} based on history`);
188
+ }
189
+ else {
190
+ log(` ↓ Downgraded from ${defaultDef.modelTier} based on history`);
191
+ }
192
+ }
193
+ }
194
+ else {
195
+ log(`Model: ${modelTier} (budget: ${tokenBudget} tokens) — default (insufficient history for this operation type)`);
196
+ }
197
+ // Read hippocampus context
198
+ let businessRules = [];
199
+ try {
200
+ businessRules = await getRules(mathaDir);
201
+ }
202
+ catch {
203
+ businessRules = [];
204
+ }
205
+ // Build session brief directly
206
+ const readyToBuild = assertions.length > 0; // Only ready if contract provided
207
+ const brief = {
208
+ sessionId,
209
+ scope,
210
+ operationType,
211
+ timestamp,
212
+ operation_description: operationDescription,
213
+ why: '', // Not collected interactively - would be from Gate 01 in full flow
214
+ business_rules: businessRules,
215
+ matchResults,
216
+ hasCritical,
217
+ contract: assertions,
218
+ assertions,
219
+ modelTier,
220
+ tokenBudget,
221
+ routingSource: rec.source,
222
+ routingConfidence: rec.confidence,
223
+ gatesCompleted: [1, 2, 3, 4, 5, 6],
224
+ readyToBuild,
225
+ cortexSummary: cortexSnapshot?.summary ?? null,
226
+ frozenFiles,
227
+ };
228
+ // Write session brief to .matha/sessions/[sessionId].brief
229
+ const briefPath = path.join(mathaDir, `sessions/${sessionId}.brief`);
230
+ await writeAtomic(briefPath, brief);
231
+ // Write dopamine prediction to .matha/dopamine/predictions/[sessionId].json
232
+ const prediction = {
233
+ session_id: sessionId,
234
+ timestamp,
235
+ operation_type: operationType,
236
+ scope,
237
+ predicted: {
238
+ model_tier: modelTier,
239
+ token_budget: tokenBudget,
240
+ },
241
+ actual: null,
242
+ delta: null,
243
+ };
244
+ const predictionPath = path.join(mathaDir, `dopamine/predictions/${sessionId}.json`);
245
+ await writeAtomic(predictionPath, prediction);
246
+ // Print human-readable brief to terminal
247
+ log('\n════════════════════════════════════════');
248
+ log(`MATHA SESSION BRIEF — ${sessionId}`);
249
+ log('════════════════════════════════════════\n');
250
+ log(`SCOPE: ${scope}`);
251
+ log(`WHAT: ${operationDescription}`);
252
+ log(`TYPE: ${operationType}`);
253
+ const learnedSuffix = rec.source === 'learned' ? ` — learned from ${rec.sample_size} sessions (${rec.confidence} confidence)` : ' — default';
254
+ log(`MODEL: ${modelTier} (budget: ${tokenBudget} tokens)${learnedSuffix}\n`);
255
+ log('BUSINESS RULES:');
256
+ if (businessRules.length > 0) {
257
+ for (const rule of businessRules) {
258
+ log(` · ${rule}`);
259
+ }
260
+ }
261
+ else {
262
+ log(' (none defined)');
263
+ }
264
+ log('');
265
+ log('MATCH RESULTS:');
266
+ if (matchResults.length > 0) {
267
+ for (const res of matchResults) {
268
+ log(` · [${res.severity.toUpperCase()}] ${res.title}`);
269
+ }
270
+ }
271
+ else {
272
+ log(' None detected');
273
+ }
274
+ log('');
275
+ log('CONTRACT:');
276
+ if (assertions.length > 0) {
277
+ for (const assertion of assertions) {
278
+ log(` · ${assertion}`);
279
+ }
280
+ }
281
+ else {
282
+ log(' (no contract written — advisory only)');
283
+ }
284
+ log('');
285
+ log('════════════════════════════════════════');
286
+ log(`READY TO BUILD: ${brief.readyToBuild ? 'YES' : 'NO (advisory only)'}`);
287
+ if (hasCritical) {
288
+ log('⚠ Critical issues detected — proceed with caution');
289
+ }
290
+ log('════════════════════════════════════════\n');
291
+ log('Paste this brief into your AI agent before starting.\n');
292
+ return {
293
+ exitCode: 0,
294
+ sessionId,
295
+ brief,
296
+ readyToBuild: brief.readyToBuild,
297
+ };
298
+ }
299
+ // Default ask implementation using @inquirer/prompts
300
+ async function defaultAsk(question) {
301
+ // Dynamically import to avoid ESM issues
302
+ const { input } = await import('@inquirer/prompts');
303
+ // Handle multiline input
304
+ if (question.includes('behaviour contract')) {
305
+ let lines = [];
306
+ let lineCount = 1;
307
+ log(question);
308
+ while (true) {
309
+ const line = await input({
310
+ message: `Line ${lineCount}:`,
311
+ default: '',
312
+ });
313
+ if (line.trim() === '') {
314
+ break;
315
+ }
316
+ lines.push(line);
317
+ lineCount++;
318
+ }
319
+ return lines.join('\n');
320
+ }
321
+ // Regular single-line input
322
+ return await input({ message: question });
323
+ }
324
+ // Log helper
325
+ function log(msg) {
326
+ console.log(msg);
327
+ }
328
+ export { runBefore };