infernoflow 0.33.0 → 0.34.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.
- package/README.md +208 -120
- package/dist/bin/infernoflow.mjs +271 -85
- package/dist/lib/adopters/angular.mjs +128 -1
- package/dist/lib/adopters/css.mjs +111 -1
- package/dist/lib/adopters/react.mjs +104 -1
- package/dist/lib/ai/ideDetection.mjs +31 -1
- package/dist/lib/ai/localProvider.mjs +88 -1
- package/dist/lib/ai/providerRouter.mjs +295 -2
- package/dist/lib/commands/adopt.mjs +869 -20
- package/dist/lib/commands/adoptWizard.mjs +320 -9
- package/dist/lib/commands/agent.mjs +191 -5
- package/dist/lib/commands/ai.mjs +407 -2
- package/dist/lib/commands/ask.mjs +299 -0
- package/dist/lib/commands/audit.mjs +300 -13
- package/dist/lib/commands/changelog.mjs +594 -26
- package/dist/lib/commands/check.mjs +184 -3
- package/dist/lib/commands/ci.mjs +208 -3
- package/dist/lib/commands/claudeMd.mjs +139 -28
- package/dist/lib/commands/cloud.mjs +521 -5
- package/dist/lib/commands/context.mjs +346 -34
- package/dist/lib/commands/coverage.mjs +282 -2
- package/dist/lib/commands/dashboard.mjs +635 -123
- package/dist/lib/commands/demo.mjs +465 -8
- package/dist/lib/commands/diff.mjs +274 -5
- package/dist/lib/commands/docGate.mjs +81 -2
- package/dist/lib/commands/doctor.mjs +321 -3
- package/dist/lib/commands/explain.mjs +438 -8
- package/dist/lib/commands/export.mjs +239 -10
- package/dist/lib/commands/generateSkills.mjs +163 -38
- package/dist/lib/commands/graph.mjs +378 -11
- package/dist/lib/commands/health.mjs +309 -2
- package/dist/lib/commands/impact.mjs +325 -2
- package/dist/lib/commands/implement.mjs +103 -7
- package/dist/lib/commands/init.mjs +545 -23
- package/dist/lib/commands/installCursorHooks.mjs +36 -1
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
- package/dist/lib/commands/link.mjs +342 -2
- package/dist/lib/commands/log.mjs +164 -16
- package/dist/lib/commands/monorepo.mjs +428 -4
- package/dist/lib/commands/notify.mjs +258 -4
- package/dist/lib/commands/onboard.mjs +296 -4
- package/dist/lib/commands/prComment.mjs +361 -2
- package/dist/lib/commands/prImpact.mjs +157 -2
- package/dist/lib/commands/publish.mjs +316 -15
- package/dist/lib/commands/recap.mjs +359 -0
- package/dist/lib/commands/report.mjs +272 -28
- package/dist/lib/commands/review.mjs +223 -9
- package/dist/lib/commands/run.mjs +336 -8
- package/dist/lib/commands/scaffold.mjs +419 -54
- package/dist/lib/commands/scan.mjs +1118 -5
- package/dist/lib/commands/scout.mjs +291 -2
- package/dist/lib/commands/setup.mjs +310 -5
- package/dist/lib/commands/share.mjs +196 -13
- package/dist/lib/commands/snapshot.mjs +383 -3
- package/dist/lib/commands/stability.mjs +293 -2
- package/dist/lib/commands/stats.mjs +402 -0
- package/dist/lib/commands/status.mjs +172 -4
- package/dist/lib/commands/suggest.mjs +563 -21
- package/dist/lib/commands/switch.mjs +310 -0
- package/dist/lib/commands/syncAuto.mjs +96 -1
- package/dist/lib/commands/synthesize.mjs +228 -10
- package/dist/lib/commands/teamSync.mjs +388 -2
- package/dist/lib/commands/test.mjs +363 -6
- package/dist/lib/commands/theme.mjs +195 -18
- package/dist/lib/commands/upgrade.mjs +153 -0
- package/dist/lib/commands/version.mjs +282 -2
- package/dist/lib/commands/vibe.mjs +357 -7
- package/dist/lib/commands/watch.mjs +203 -4
- package/dist/lib/commands/why.mjs +358 -4
- package/dist/lib/cursorHooksInstall.mjs +60 -1
- package/dist/lib/draftToolingInstall.mjs +68 -7
- package/dist/lib/git/detect-drift.mjs +208 -4
- package/dist/lib/learning/adapt.mjs +101 -6
- package/dist/lib/learning/observe.mjs +119 -1
- package/dist/lib/learning/patternDetector.mjs +298 -1
- package/dist/lib/learning/profile.mjs +279 -2
- package/dist/lib/learning/skillSynthesizer.mjs +145 -24
- package/dist/lib/templates/index.mjs +131 -1
- package/dist/lib/theme/scanner.mjs +343 -4
- package/dist/lib/ui/errors.mjs +142 -1
- package/dist/lib/ui/output.mjs +72 -6
- package/dist/lib/ui/prompts.mjs +147 -6
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
- package/package.json +1 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow recap
|
|
3
|
+
*
|
|
4
|
+
* End-of-session summary. Answers "what did infernoflow capture today?"
|
|
5
|
+
* and "what git changes might be worth logging?"
|
|
6
|
+
*
|
|
7
|
+
* The feedback loop that makes the habit stick:
|
|
8
|
+
* - You see what was captured this session
|
|
9
|
+
* - You see what git changes happened but weren't logged
|
|
10
|
+
* - You get a session health score
|
|
11
|
+
* - You get one-line nudges to log what's missing
|
|
12
|
+
*
|
|
13
|
+
* "Session" = entries since the last `handoff` entry, or the last 24h,
|
|
14
|
+
* whichever is more recent. Use --since to override.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* infernoflow recap Full session summary
|
|
18
|
+
* infernoflow recap --since 48h Look back 48 hours
|
|
19
|
+
* infernoflow recap --since 2026-04-20 Since a specific date
|
|
20
|
+
* infernoflow recap --json Machine-readable output
|
|
21
|
+
* infernoflow recap --brief One-line health score only
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import { execSync } from "node:child_process";
|
|
27
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
28
|
+
|
|
29
|
+
const INFERNO_DIR = "inferno";
|
|
30
|
+
const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
|
|
31
|
+
const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
|
|
32
|
+
|
|
33
|
+
function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
|
|
34
|
+
|
|
35
|
+
// ── Session boundary detection ────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the start of the "current session":
|
|
39
|
+
* - The timestamp of the last `handoff` entry (switching to a new agent = new session)
|
|
40
|
+
* - OR 24 hours ago — whichever is more recent
|
|
41
|
+
* - --since flag overrides both
|
|
42
|
+
*/
|
|
43
|
+
function findSessionStart(entries, sinceArg) {
|
|
44
|
+
if (sinceArg) {
|
|
45
|
+
// e.g. "48h", "7d", "2026-04-20"
|
|
46
|
+
const hoursMatch = sinceArg.match(/^(\d+)h$/i);
|
|
47
|
+
const daysMatch = sinceArg.match(/^(\d+)d$/i);
|
|
48
|
+
if (hoursMatch) return new Date(Date.now() - parseInt(hoursMatch[1]) * 3600000);
|
|
49
|
+
if (daysMatch) return new Date(Date.now() - parseInt(daysMatch[1]) * 86400000);
|
|
50
|
+
const parsed = new Date(sinceArg);
|
|
51
|
+
if (!isNaN(parsed)) return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find last handoff entry
|
|
55
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
56
|
+
if (entries[i].type === "handoff") {
|
|
57
|
+
const ts = new Date(entries[i].ts || 0);
|
|
58
|
+
const dayAgo = new Date(Date.now() - 86400000);
|
|
59
|
+
// Use whichever is more recent
|
|
60
|
+
return ts > dayAgo ? ts : dayAgo;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Default: last 24h
|
|
65
|
+
return new Date(Date.now() - 86400000);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Git helpers ───────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function gitChangedFiles(since) {
|
|
71
|
+
const cwd = process.cwd();
|
|
72
|
+
const run = (cmd) => {
|
|
73
|
+
try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 5000, stdio: ["pipe","pipe","pipe"] }).trim(); }
|
|
74
|
+
catch { return ""; }
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const sinceIso = since.toISOString().slice(0, 19);
|
|
78
|
+
const staged = run("git diff --cached --name-only");
|
|
79
|
+
const unstaged = run("git diff --name-only");
|
|
80
|
+
const committed = run(`git log --since="${sinceIso}" --name-only --pretty=format:""`);
|
|
81
|
+
|
|
82
|
+
const all = new Set([
|
|
83
|
+
...staged.split("\n"),
|
|
84
|
+
...unstaged.split("\n"),
|
|
85
|
+
...committed.split("\n"),
|
|
86
|
+
].map(f => f.trim()).filter(Boolean));
|
|
87
|
+
|
|
88
|
+
return [...all];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* From a list of changed files, infer what topics might need logging.
|
|
93
|
+
* Returns: [{ topic, files, suggestedType }]
|
|
94
|
+
*/
|
|
95
|
+
function inferUnloggedTopics(changedFiles, sessionEntries) {
|
|
96
|
+
const TOPIC_RULES = [
|
|
97
|
+
{ keywords: ["auth", "login", "logout", "session", "jwt", "token", "password"], topic: "authentication" },
|
|
98
|
+
{ keywords: ["stripe", "payment", "checkout", "billing", "subscription"], topic: "payments" },
|
|
99
|
+
{ keywords: ["upload", "file", "s3", "storage", "bucket", "cdn"], topic: "file handling" },
|
|
100
|
+
{ keywords: ["email", "sendgrid", "ses", "smtp", "nodemailer", "twilio"], topic: "notifications" },
|
|
101
|
+
{ keywords: ["db", "database", "prisma", "mongoose", "postgres", "mysql", "migration"], topic: "database" },
|
|
102
|
+
{ keywords: ["deploy", "docker", "ci", "workflow", "action", "kubernetes"], topic: "deployment" },
|
|
103
|
+
{ keywords: ["cache", "redis", "memcache"], topic: "caching" },
|
|
104
|
+
{ keywords: ["test", "spec", "jest", "vitest", "cypress", "playwright"], topic: "testing" },
|
|
105
|
+
{ keywords: ["config", "env", ".env", "environment", "secret"], topic: "configuration" },
|
|
106
|
+
{ keywords: ["api", "route", "endpoint", "controller", "handler"], topic: "API routes" },
|
|
107
|
+
{ keywords: ["ui", "component", "style", "css", "tailwind", "theme"], topic: "UI/styles" },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// Build set of topics already mentioned in session entries
|
|
111
|
+
const loggedText = sessionEntries.map(e => (e.summary || "").toLowerCase()).join(" ");
|
|
112
|
+
|
|
113
|
+
const candidates = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
|
|
116
|
+
for (const rule of TOPIC_RULES) {
|
|
117
|
+
if (seen.has(rule.topic)) continue;
|
|
118
|
+
|
|
119
|
+
const matchingFiles = changedFiles.filter(f =>
|
|
120
|
+
rule.keywords.some(kw => f.toLowerCase().includes(kw))
|
|
121
|
+
);
|
|
122
|
+
if (!matchingFiles.length) continue;
|
|
123
|
+
|
|
124
|
+
// Check if session already has entries about this topic
|
|
125
|
+
const alreadyLogged = rule.keywords.some(kw => loggedText.includes(kw));
|
|
126
|
+
if (alreadyLogged) continue;
|
|
127
|
+
|
|
128
|
+
seen.add(rule.topic);
|
|
129
|
+
candidates.push({
|
|
130
|
+
topic: rule.topic,
|
|
131
|
+
files: matchingFiles.slice(0, 3),
|
|
132
|
+
suggestedType: "gotcha", // prompt for the most valuable type
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return candidates;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Session health score ──────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Score 0-100 based on what types were logged this session.
|
|
143
|
+
* A "healthy" session has at least one gotcha or decision.
|
|
144
|
+
*/
|
|
145
|
+
function sessionHealth(entries) {
|
|
146
|
+
const types = new Set(entries.map(e => e.type));
|
|
147
|
+
let score = 0;
|
|
148
|
+
const checks = [];
|
|
149
|
+
|
|
150
|
+
if (entries.length > 0) {
|
|
151
|
+
score += 20;
|
|
152
|
+
checks.push({ ok: true, label: `${entries.length} entr${entries.length !== 1 ? "ies" : "y"} logged` });
|
|
153
|
+
} else {
|
|
154
|
+
checks.push({ ok: false, label: "nothing logged this session" });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (types.has("gotcha")) {
|
|
158
|
+
score += 35;
|
|
159
|
+
checks.push({ ok: true, label: "gotchas captured" });
|
|
160
|
+
} else {
|
|
161
|
+
checks.push({ ok: false, label: "no gotchas (most valuable — log landmines!)" });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (types.has("decision")) {
|
|
165
|
+
score += 25;
|
|
166
|
+
checks.push({ ok: true, label: "decisions recorded" });
|
|
167
|
+
} else {
|
|
168
|
+
checks.push({ ok: false, label: "no decisions recorded" });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (types.has("attempt")) {
|
|
172
|
+
score += 10;
|
|
173
|
+
checks.push({ ok: true, label: "attempts tracked" });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (types.has("preference")) {
|
|
177
|
+
score += 10;
|
|
178
|
+
checks.push({ ok: true, label: "preferences noted" });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { score: Math.min(score, 100), checks };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── formatters ────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function fmtRelDate(iso) {
|
|
187
|
+
if (!iso) return "";
|
|
188
|
+
const d = new Date(iso);
|
|
189
|
+
const diff = Date.now() - d.getTime();
|
|
190
|
+
const mins = Math.floor(diff / 60000);
|
|
191
|
+
if (mins < 60) return `${mins}m ago`;
|
|
192
|
+
const hours = Math.floor(diff / 3600000);
|
|
193
|
+
if (hours < 24) return `${hours}h ago`;
|
|
194
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const TYPE_ICONS = { gotcha:"⚠", decision:"✓", attempt:"↺", preference:"♦", theme:"◈", note:"·", error:"✗", handoff:"→" };
|
|
198
|
+
const TYPE_COLORS = { gotcha: yellow, decision: green, attempt: cyan, preference: cyan, theme: cyan, note: gray, error: red, handoff: gray };
|
|
199
|
+
|
|
200
|
+
function printEntry(e) {
|
|
201
|
+
const colorFn = TYPE_COLORS[e.type] || gray;
|
|
202
|
+
const icon = TYPE_ICONS[e.type] || "·";
|
|
203
|
+
const result = e.result ? gray(` [${e.result}]`) : "";
|
|
204
|
+
const date = gray(` (${fmtRelDate(e.ts)})`);
|
|
205
|
+
console.log(` ${colorFn(icon + " " + (e.type || "note").padEnd(11))}${result}${date}`);
|
|
206
|
+
console.log(` ${e.summary}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── entry point ───────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export async function recapCommand(rawArgs = []) {
|
|
212
|
+
const args = rawArgs;
|
|
213
|
+
const jsonMode = args.includes("--json");
|
|
214
|
+
const briefMode = args.includes("--brief");
|
|
215
|
+
const sinceIdx = args.indexOf("--since");
|
|
216
|
+
const sinceArg = sinceIdx !== -1 ? args[sinceIdx + 1] : null;
|
|
217
|
+
|
|
218
|
+
const cwd = process.cwd();
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(path.join(cwd, INFERNO_DIR))) {
|
|
221
|
+
if (!jsonMode) console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Load all sessions
|
|
226
|
+
const allEntries = [];
|
|
227
|
+
const sessPath = path.join(cwd, SESSIONS_FILE);
|
|
228
|
+
if (fs.existsSync(sessPath)) {
|
|
229
|
+
fs.readFileSync(sessPath, "utf8").split("\n").filter(Boolean).forEach(l => {
|
|
230
|
+
try { allEntries.push(JSON.parse(l)); } catch {}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find session start
|
|
235
|
+
const sessionStart = findSessionStart(allEntries, sinceArg);
|
|
236
|
+
const sessionEntries = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
|
|
237
|
+
|
|
238
|
+
// Git changes in this window
|
|
239
|
+
const changedFiles = gitChangedFiles(sessionStart);
|
|
240
|
+
const unloggedTopics = inferUnloggedTopics(changedFiles, sessionEntries);
|
|
241
|
+
|
|
242
|
+
// Health score
|
|
243
|
+
const { score, checks } = sessionHealth(sessionEntries);
|
|
244
|
+
|
|
245
|
+
const contract = readJSON(path.join(cwd, CONTRACT_FILE));
|
|
246
|
+
|
|
247
|
+
if (jsonMode) {
|
|
248
|
+
console.log(JSON.stringify({
|
|
249
|
+
sessionStart: sessionStart.toISOString(),
|
|
250
|
+
entries: sessionEntries,
|
|
251
|
+
changedFiles,
|
|
252
|
+
unloggedTopics,
|
|
253
|
+
health: { score, checks },
|
|
254
|
+
}, null, 2));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (briefMode) {
|
|
259
|
+
const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : "D";
|
|
260
|
+
const colorFn = score >= 60 ? green : score >= 40 ? yellow : red;
|
|
261
|
+
console.log(colorFn(`Session health: ${grade} (${score}/100)`) + gray(` — ${sessionEntries.length} entries logged`));
|
|
262
|
+
if (unloggedTopics.length) {
|
|
263
|
+
console.log(yellow(` ${unloggedTopics.length} topic${unloggedTopics.length !== 1 ? "s" : ""} changed but not logged: `) + unloggedTopics.map(t => t.topic).join(", "));
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Full dashboard ─────────────────────────────────────────────────────────
|
|
269
|
+
const SEP = gray(" " + "─".repeat(52));
|
|
270
|
+
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(" " + bold("🔥 infernoflow recap"));
|
|
273
|
+
if (contract?.policyId) console.log(gray(` Project: ${contract.policyId}`));
|
|
274
|
+
const sinceStr = sessionStart.toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
275
|
+
console.log(gray(` Session since: ${sinceStr}`));
|
|
276
|
+
console.log(SEP);
|
|
277
|
+
|
|
278
|
+
// ── This session's entries ────────────────────────────────────────────────
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(" " + bold("Captured this session"));
|
|
281
|
+
console.log();
|
|
282
|
+
|
|
283
|
+
if (sessionEntries.length === 0) {
|
|
284
|
+
console.log(gray(" Nothing logged yet this session."));
|
|
285
|
+
} else {
|
|
286
|
+
// Group by type, priority order
|
|
287
|
+
const typeOrder = ["gotcha", "decision", "attempt", "preference", "theme", "note", "error"];
|
|
288
|
+
const byType = new Map();
|
|
289
|
+
for (const e of sessionEntries) {
|
|
290
|
+
const t = e.type || "note";
|
|
291
|
+
if (!byType.has(t)) byType.set(t, []);
|
|
292
|
+
byType.get(t).push(e);
|
|
293
|
+
}
|
|
294
|
+
for (const t of typeOrder) {
|
|
295
|
+
const group = byType.get(t);
|
|
296
|
+
if (!group?.length) continue;
|
|
297
|
+
for (const e of group) { console.log(); printEntry(e); }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Unlogged changes ──────────────────────────────────────────────────────
|
|
302
|
+
if (unloggedTopics.length > 0) {
|
|
303
|
+
console.log();
|
|
304
|
+
console.log(SEP);
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(" " + bold("Changed but not logged") + gray(" (git diff since session start)"));
|
|
307
|
+
console.log();
|
|
308
|
+
|
|
309
|
+
for (const { topic, files } of unloggedTopics) {
|
|
310
|
+
console.log(yellow(` ? ${topic}`));
|
|
311
|
+
for (const f of files) console.log(gray(` ${f}`));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(gray(" Any gotchas or decisions from these areas worth capturing?"));
|
|
316
|
+
console.log(gray(" Run: ") + cyan(`infernoflow log "<what happened>" --type gotcha`));
|
|
317
|
+
} else if (changedFiles.length > 0) {
|
|
318
|
+
console.log();
|
|
319
|
+
console.log(SEP);
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(green(" ✔ ") + gray(`${changedFiles.length} changed files — all topics appear to be logged`));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Session health score ──────────────────────────────────────────────────
|
|
325
|
+
console.log();
|
|
326
|
+
console.log(SEP);
|
|
327
|
+
console.log();
|
|
328
|
+
console.log(" " + bold("Session health"));
|
|
329
|
+
console.log();
|
|
330
|
+
|
|
331
|
+
const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : "D";
|
|
332
|
+
const colorFn = score >= 60 ? green : score >= 40 ? yellow : red;
|
|
333
|
+
console.log(` ${colorFn(bold(`${grade}`))} ${colorFn(`${score}/100`)}`);
|
|
334
|
+
console.log();
|
|
335
|
+
|
|
336
|
+
for (const { ok, label } of checks) {
|
|
337
|
+
const icon = ok ? green(" ✔") : yellow(" ·");
|
|
338
|
+
console.log(`${icon} ${ok ? label : gray(label)}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Nudge if health is low
|
|
342
|
+
if (score < 60) {
|
|
343
|
+
console.log();
|
|
344
|
+
console.log(gray(" To improve: log at least one gotcha and one decision per session."));
|
|
345
|
+
console.log(gray(" Gotchas are the highest-value entries — they prevent repeated mistakes."));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Next session tip ──────────────────────────────────────────────────────
|
|
349
|
+
if (sessionEntries.length > 0 || unloggedTopics.length > 0) {
|
|
350
|
+
console.log();
|
|
351
|
+
console.log(SEP);
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(gray(" Before your next session:"));
|
|
354
|
+
console.log(gray(" ") + cyan("infernoflow switch") + gray(" — generate a handoff summary for the next AI agent"));
|
|
355
|
+
console.log(gray(" ") + cyan("infernoflow ask --recent") + gray(" — review what's in memory before starting"));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log();
|
|
359
|
+
}
|