moflo 4.12.8 → 4.12.10
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/guidance/shipped/moflo-yaml-reference.md +1 -0
- package/.claude/helpers/gate-hook.mjs +16 -0
- package/.claude/helpers/gate.cjs +286 -27
- package/.claude/helpers/simplify-classify.cjs +194 -12
- package/.claude/helpers/statusline.cjs +56 -7
- package/.claude/skills/flfl/SKILL.md +50 -0
- package/bin/gate-hook.mjs +16 -0
- package/bin/gate.cjs +286 -27
- package/bin/lib/internal-skills.mjs +5 -3
- package/bin/lib/session-continuity.mjs +109 -0
- package/bin/session-continuity.mjs +1 -22
- package/bin/simplify-classify.cjs +194 -12
- package/dist/src/cli/commands/hooks.js +8 -2
- package/dist/src/cli/config/moflo-config.js +3 -0
- package/dist/src/cli/hooks/statusline/index.js +15 -9
- package/dist/src/cli/init/claudemd-generator.js +9 -4
- package/dist/src/cli/init/embedded-helpers.js +2 -2
- package/dist/src/cli/init/executor.js +3 -0
- package/dist/src/cli/init/moflo-yaml-template.js +1 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
* "stats": { added, deleted, fileCount, declAdded, declRemoved, tsjsLOC, tsjsNetDecls, otherNetAdded, ... }
|
|
32
32
|
* }
|
|
33
33
|
*
|
|
34
|
+
* The diff it measures spans committed-since-base, working-tree, AND untracked
|
|
35
|
+
* non-ignored files — an untracked file is a change the branch will carry, and
|
|
36
|
+
* omitting it undercounts the diff exactly as a swallowed read error does.
|
|
37
|
+
* When a read it needed did not happen, `stats.diffUnavailable` is set and the
|
|
38
|
+
* decision routes to a review tier: the classifier cannot distinguish "no
|
|
39
|
+
* changes" from "I could not read the changes", and only the first is safe to
|
|
40
|
+
* call TRIVIAL (#1451).
|
|
41
|
+
*
|
|
34
42
|
* Usage:
|
|
35
43
|
* node bin/simplify-classify.cjs # auto-detects default branch
|
|
36
44
|
* node bin/simplify-classify.cjs --base develop # explicit override
|
|
@@ -42,6 +50,27 @@
|
|
|
42
50
|
'use strict';
|
|
43
51
|
|
|
44
52
|
const { execSync } = require('child_process');
|
|
53
|
+
const fs = require('fs');
|
|
54
|
+
const path = require('path');
|
|
55
|
+
|
|
56
|
+
// execSync defaults to a 1 MiB stdout buffer and a real branch diff clears that
|
|
57
|
+
// routinely (#1451 measured 2,113,712 bytes). Overflow throws ENOBUFS, which
|
|
58
|
+
// used to be swallowed into an empty diff — "TRIVIAL, nothing to review" on a
|
|
59
|
+
// branch with plenty to review. 64 MiB puts the cliff well past any diff a
|
|
60
|
+
// human opens a PR for; past it, the classifier now says so instead of
|
|
61
|
+
// reporting zero.
|
|
62
|
+
const EXEC_MAX_BUFFER = 64 * 1024 * 1024;
|
|
63
|
+
|
|
64
|
+
// Cap on how much of an untracked file is slurped to synthesize its new-file
|
|
65
|
+
// diff. Well past any hand-written source file; anything larger is treated like
|
|
66
|
+
// a binary (counted as a new file with no added lines) rather than read.
|
|
67
|
+
const UNTRACKED_MAX_BYTES = 8 * 1024 * 1024;
|
|
68
|
+
|
|
69
|
+
// Total budget for synthesized untracked-file content. A repo with a huge
|
|
70
|
+
// un-ignored directory must not be slurped into memory wholesale — past this
|
|
71
|
+
// the remaining files are recorded as new files and the result is flagged
|
|
72
|
+
// unmeasurable, which forces review rather than quietly undercounting.
|
|
73
|
+
const UNTRACKED_TOTAL_BUDGET = 32 * 1024 * 1024;
|
|
45
74
|
|
|
46
75
|
// Paths where new logic warrants the 3-agent fan-out.
|
|
47
76
|
// Mechanical edits inside these paths are still SMALL; only adding/removing
|
|
@@ -98,14 +127,43 @@ function noEscalate() {
|
|
|
98
127
|
return { suggested: false, target: null, reason: null };
|
|
99
128
|
}
|
|
100
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Run a git command, returning its stdout — or `null` if the command failed.
|
|
132
|
+
*
|
|
133
|
+
* `null` rather than `''` is load-bearing: a caller that cannot tell "git said
|
|
134
|
+
* nothing" from "git never answered" will report an unreadable diff as an empty
|
|
135
|
+
* one, and an empty diff is the one thing it is safe to call TRIVIAL (#1451).
|
|
136
|
+
*/
|
|
101
137
|
function safeExec(cmd, opts) {
|
|
102
138
|
try {
|
|
103
139
|
return execSync(cmd, {
|
|
104
140
|
encoding: 'utf-8',
|
|
105
141
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
142
|
+
maxBuffer: EXEC_MAX_BUFFER,
|
|
106
143
|
...(opts && opts.cwd ? { cwd: opts.cwd } : {}),
|
|
107
144
|
});
|
|
108
|
-
} catch { return
|
|
145
|
+
} catch { return null; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Is there a repo with at least one commit here? A git failure only means
|
|
150
|
+
* "there was something we could not measure" when there is history to read —
|
|
151
|
+
* outside a repo, or in a fresh `git init` before the first commit, there is
|
|
152
|
+
* genuinely no diff to miss, and forcing a review fan-out over nothing would be
|
|
153
|
+
* its own defect.
|
|
154
|
+
*/
|
|
155
|
+
function hasGitHistory(cwd) {
|
|
156
|
+
return safeExec('git rev-parse --verify HEAD', cwd ? { cwd } : undefined) !== null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* One `git rev-parse` per classification, shared by both diff readers. They
|
|
161
|
+
* consult it only on their failure paths — and a directory that is not a repo
|
|
162
|
+
* makes every read fail, so without sharing, the cheapest case pays twice.
|
|
163
|
+
*/
|
|
164
|
+
function makeHistoryProbe(cwd) {
|
|
165
|
+
let answer;
|
|
166
|
+
return () => (answer === undefined ? (answer = hasGitHistory(cwd)) : answer);
|
|
109
167
|
}
|
|
110
168
|
|
|
111
169
|
// Detect the consumer's default branch. Hardcoding 'main' silently miscalibrates
|
|
@@ -119,7 +177,7 @@ function detectDefaultBranch(cwd) {
|
|
|
119
177
|
const opts = cwd ? { cwd } : undefined;
|
|
120
178
|
|
|
121
179
|
// Preferred: origin/HEAD points to whatever the remote considers default.
|
|
122
|
-
const symbolic = safeExec('git symbolic-ref --short refs/remotes/origin/HEAD', opts).trim();
|
|
180
|
+
const symbolic = (safeExec('git symbolic-ref --short refs/remotes/origin/HEAD', opts) || '').trim();
|
|
123
181
|
if (symbolic.startsWith('origin/')) {
|
|
124
182
|
const v = symbolic.slice('origin/'.length);
|
|
125
183
|
if (cwd === undefined) _cachedDefaultBranch = v;
|
|
@@ -127,7 +185,7 @@ function detectDefaultBranch(cwd) {
|
|
|
127
185
|
}
|
|
128
186
|
|
|
129
187
|
// Fallback: local init.defaultBranch (set by `git init -b <name>` or config).
|
|
130
|
-
const configured = safeExec('git config --get init.defaultBranch', opts).trim();
|
|
188
|
+
const configured = (safeExec('git config --get init.defaultBranch', opts) || '').trim();
|
|
131
189
|
if (configured) {
|
|
132
190
|
if (cwd === undefined) _cachedDefaultBranch = configured;
|
|
133
191
|
return configured;
|
|
@@ -142,12 +200,95 @@ function _resetCacheForTest() {
|
|
|
142
200
|
_cachedDefaultBranch = null;
|
|
143
201
|
}
|
|
144
202
|
|
|
145
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Read the tracked half of the diff: committed-since-base + working-tree.
|
|
205
|
+
* Returns `{ text, unreadable, reason }` — `unreadable` means a read we needed
|
|
206
|
+
* did not happen, so `text` is an undercount and must not be trusted as zero.
|
|
207
|
+
*/
|
|
208
|
+
function readDiffFromGit(base, cwd, historyProbe) {
|
|
146
209
|
const opts = cwd ? { cwd } : undefined;
|
|
147
|
-
// Combined diff: committed-since-base + working-tree
|
|
148
210
|
const committed = safeExec(`git diff ${base}...HEAD`, opts);
|
|
149
211
|
const working = safeExec('git diff HEAD', opts);
|
|
150
|
-
|
|
212
|
+
const text = (committed || '') + (working ? '\n' + working : '');
|
|
213
|
+
|
|
214
|
+
if (committed !== null && working !== null) return { text, unreadable: false };
|
|
215
|
+
if (!(historyProbe ? historyProbe() : hasGitHistory(cwd))) return { text, unreadable: false };
|
|
216
|
+
|
|
217
|
+
const failed = [];
|
|
218
|
+
if (committed === null) failed.push(`git diff ${base}...HEAD`);
|
|
219
|
+
if (working === null) failed.push('git diff HEAD');
|
|
220
|
+
return { text, unreadable: true, reason: `${failed.join(' and ')} failed` };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read the untracked half of the diff.
|
|
225
|
+
*
|
|
226
|
+
* Untracked files appear in no `git diff` output at all, so a branch of
|
|
227
|
+
* brand-new files reads as a far smaller change than it is — 12 new CRUD files
|
|
228
|
+
* classified SMALL until someone staged them (#1451). Synthesize a new-file
|
|
229
|
+
* entry per untracked, non-ignored file so `parseDiff` counts it exactly as it
|
|
230
|
+
* would once staged.
|
|
231
|
+
*
|
|
232
|
+
* Built from Node file reads rather than `git diff --no-index` against a null
|
|
233
|
+
* device, which would need `/dev/null` vs `NUL` branching (Rule #1). The index
|
|
234
|
+
* is never touched.
|
|
235
|
+
*/
|
|
236
|
+
function readUntrackedDiff(cwd, historyProbe) {
|
|
237
|
+
const root = cwd || process.cwd();
|
|
238
|
+
const out = safeExec('git ls-files --others --exclude-standard -z', cwd ? { cwd } : undefined);
|
|
239
|
+
if (out === null) {
|
|
240
|
+
return (historyProbe ? historyProbe() : hasGitHistory(cwd))
|
|
241
|
+
? { text: '', unreadable: true, reason: 'git ls-files --others failed' }
|
|
242
|
+
: { text: '', unreadable: false };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parts = [];
|
|
246
|
+
let budgetSpent = 0;
|
|
247
|
+
let overBudget = false;
|
|
248
|
+
// -z keeps paths raw (no shell quoting of unusual characters). git emits them
|
|
249
|
+
// POSIX-separated on every platform, so they need no separator translation —
|
|
250
|
+
// only path.resolve to reach the file on disk.
|
|
251
|
+
for (const rel of out.split('\0')) {
|
|
252
|
+
if (!rel) continue;
|
|
253
|
+
const header = `diff --git a/${rel} b/${rel}\nnew file mode 100644\n`;
|
|
254
|
+
|
|
255
|
+
let body = null;
|
|
256
|
+
try {
|
|
257
|
+
const abs = path.resolve(root, rel);
|
|
258
|
+
// lstat, not stat: following an untracked symlink would read and count
|
|
259
|
+
// the TARGET's content — mismeasuring the diff, and pulling bytes from
|
|
260
|
+
// wherever the link points, which may be outside the working tree.
|
|
261
|
+
const stat = fs.lstatSync(abs);
|
|
262
|
+
if (stat.isFile() && stat.size <= UNTRACKED_MAX_BYTES && budgetSpent + stat.size <= UNTRACKED_TOTAL_BUDGET) {
|
|
263
|
+
const buf = fs.readFileSync(abs);
|
|
264
|
+
budgetSpent += stat.size;
|
|
265
|
+
if (!buf.includes(0)) body = buf.toString('utf-8');
|
|
266
|
+
} else if (stat.isFile()) {
|
|
267
|
+
overBudget = overBudget || budgetSpent + stat.size > UNTRACKED_TOTAL_BUDGET;
|
|
268
|
+
}
|
|
269
|
+
} catch { /* vanished or unreadable — header only */ }
|
|
270
|
+
|
|
271
|
+
if (body === null) {
|
|
272
|
+
// Binary, symlink, oversized, or unreadable. git emits no `+` lines for
|
|
273
|
+
// these either, so the file still counts toward fileCount/newFiles with
|
|
274
|
+
// zero added lines — the honest measurement, not a swallowed one.
|
|
275
|
+
parts.push(`${header}Binary files /dev/null and b/${rel} differ\n`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Split on \n and drop the trailing empty element from a final newline;
|
|
280
|
+
// CRLF files keep their \r on each line, which parseDiff trims before
|
|
281
|
+
// testing for declarations.
|
|
282
|
+
const lines = body.split('\n');
|
|
283
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
284
|
+
parts.push(`${header}--- /dev/null\n+++ b/${rel}\n@@ -0,0 +1,${lines.length} @@\n`);
|
|
285
|
+
for (const ln of lines) parts.push(`+${ln}\n`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const text = parts.join('');
|
|
289
|
+
return overBudget
|
|
290
|
+
? { text, unreadable: true, reason: 'untracked files exceeded the diff-synthesis budget' }
|
|
291
|
+
: { text, unreadable: false };
|
|
151
292
|
}
|
|
152
293
|
|
|
153
294
|
/**
|
|
@@ -240,14 +381,19 @@ function parseDiff(diff) {
|
|
|
240
381
|
}
|
|
241
382
|
|
|
242
383
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
384
|
+
* Route a diff we actually managed to measure. Pure — no I/O. Callers reach
|
|
385
|
+
* this through `decide`, which first handles the case where the measurement
|
|
386
|
+
* itself failed.
|
|
245
387
|
*/
|
|
246
|
-
function
|
|
388
|
+
function decideMeasured(stats) {
|
|
247
389
|
const reasoning = [];
|
|
248
390
|
const totalChange = stats.added + stats.deleted;
|
|
249
391
|
|
|
250
|
-
|
|
392
|
+
// Only a diff with no files at all is genuinely empty. A diff carrying files
|
|
393
|
+
// but no +/- lines — binary assets, pure renames, mode changes, an untracked
|
|
394
|
+
// binary — is a real change git simply does not express as lines, and calling
|
|
395
|
+
// it "nothing to review" is the same undercount as swallowing a read error.
|
|
396
|
+
if (totalChange === 0 && stats.fileCount === 0) {
|
|
251
397
|
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, escalate: noEscalate(), reasoning: ['empty diff — nothing to review'], stats };
|
|
252
398
|
}
|
|
253
399
|
|
|
@@ -345,13 +491,46 @@ function decide(stats) {
|
|
|
345
491
|
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, escalate: noEscalate(), reasoning, stats };
|
|
346
492
|
}
|
|
347
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Pure decision function. Takes parsed stats, returns dispatch decision.
|
|
496
|
+
* No I/O. Easy to unit-test with synthetic stats.
|
|
497
|
+
*
|
|
498
|
+
* `stats.diffUnavailable` marks a diff the reader could not fully measure. The
|
|
499
|
+
* classifier cannot tell "no changes" from "I could not read the changes", and
|
|
500
|
+
* only the first is safe to call TRIVIAL — so an unmeasurable diff routes to a
|
|
501
|
+
* review tier and says why, rather than stamping the gate clean (#1451).
|
|
502
|
+
*/
|
|
503
|
+
function decide(stats) {
|
|
504
|
+
if (!stats.diffUnavailable) return decideMeasured(stats);
|
|
505
|
+
|
|
506
|
+
const note = `diff could not be fully read (${stats.diffUnavailableReason || 'git read failed'})`
|
|
507
|
+
+ ' — a diff the classifier cannot measure is never TRIVIAL';
|
|
508
|
+
// Whatever DID parse may already warrant more than the forced NORMAL floor;
|
|
509
|
+
// an architectural diff whose working-tree half went missing stays DEEP.
|
|
510
|
+
const measured = decideMeasured(stats);
|
|
511
|
+
if (measured.agentCount >= 3) {
|
|
512
|
+
return { ...measured, reasoning: [note].concat(measured.reasoning) };
|
|
513
|
+
}
|
|
514
|
+
return { tier: 'NORMAL', model: 'sonnet', agentCount: 3, escalate: noEscalate(), reasoning: [note], stats };
|
|
515
|
+
}
|
|
516
|
+
|
|
348
517
|
function classifyDiff(diffText) {
|
|
349
518
|
return decide(parseDiff(diffText));
|
|
350
519
|
}
|
|
351
520
|
|
|
352
521
|
function classifyFromGit(base, cwd) {
|
|
353
522
|
const resolved = base || detectDefaultBranch(cwd);
|
|
354
|
-
|
|
523
|
+
const historyProbe = makeHistoryProbe(cwd);
|
|
524
|
+
const tracked = readDiffFromGit(resolved, cwd, historyProbe);
|
|
525
|
+
const untracked = readUntrackedDiff(cwd, historyProbe);
|
|
526
|
+
const stats = parseDiff(tracked.text + (untracked.text ? '\n' + untracked.text : ''));
|
|
527
|
+
if (tracked.unreadable || untracked.unreadable) {
|
|
528
|
+
stats.diffUnavailable = true;
|
|
529
|
+
// Both halves can fail independently; surface every reason, not just the
|
|
530
|
+
// first, so the printed decision explains the whole gap.
|
|
531
|
+
stats.diffUnavailableReason = [tracked.reason, untracked.reason].filter(Boolean).join('; ');
|
|
532
|
+
}
|
|
533
|
+
return decide(stats);
|
|
355
534
|
}
|
|
356
535
|
|
|
357
536
|
if (require.main === module) {
|
|
@@ -375,4 +554,7 @@ if (require.main === module) {
|
|
|
375
554
|
}
|
|
376
555
|
}
|
|
377
556
|
|
|
378
|
-
module.exports = {
|
|
557
|
+
module.exports = {
|
|
558
|
+
parseDiff, decide, classifyDiff, classifyFromGit,
|
|
559
|
+
readUntrackedDiff, detectDefaultBranch, EXEC_MAX_BUFFER, _resetCacheForTest,
|
|
560
|
+
};
|
|
@@ -45,6 +45,7 @@ function loadStatusLineConfig() {
|
|
|
45
45
|
show_model: true,
|
|
46
46
|
show_session: true,
|
|
47
47
|
show_intelligence: true,
|
|
48
|
+
show_context: true,
|
|
48
49
|
show_swarm: true,
|
|
49
50
|
show_hooks: true,
|
|
50
51
|
show_mcp: true,
|
|
@@ -153,6 +154,23 @@ function readJSON(filePath) {
|
|
|
153
154
|
return null;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// Normalize Claude Code's `context_window.used_percentage` into a 0-100 integer.
|
|
158
|
+
// Returns null (never a stand-in number) for anything that isn't a finite number,
|
|
159
|
+
// so every renderer can self-hide rather than publish a value it can't stand
|
|
160
|
+
// behind (#1453).
|
|
161
|
+
function normalizeContextPct(raw) {
|
|
162
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) return null;
|
|
163
|
+
return Math.max(0, Math.min(100, Math.round(raw)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Colour for a context gauge; lower is better. Matches the thresholds already
|
|
167
|
+
// used by the TypeScript statusline generator (src/cli/hooks/statusline/index.ts).
|
|
168
|
+
function contextColor(pct) {
|
|
169
|
+
if (pct >= 75) return c.brightRed;
|
|
170
|
+
if (pct >= 50) return c.brightYellow;
|
|
171
|
+
return c.brightGreen;
|
|
172
|
+
}
|
|
173
|
+
|
|
156
174
|
// Safe file stat (returns null on failure)
|
|
157
175
|
function safeStat(filePath) {
|
|
158
176
|
try {
|
|
@@ -386,7 +404,6 @@ function getSystemMetrics() {
|
|
|
386
404
|
// Intelligence from learning.json
|
|
387
405
|
const learningData = readJSON(path.join(CWD, '.moflo', 'metrics', 'learning.json'));
|
|
388
406
|
let intelligencePct = 0;
|
|
389
|
-
let contextPct = 0;
|
|
390
407
|
|
|
391
408
|
if (learningData?.intelligence?.score !== undefined) {
|
|
392
409
|
intelligencePct = Math.min(100, Math.floor(learningData.intelligence.score));
|
|
@@ -409,11 +426,20 @@ function getSystemMetrics() {
|
|
|
409
426
|
intelligencePct = Math.min(100, score);
|
|
410
427
|
}
|
|
411
428
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
429
|
+
// Context %: the real value, piped in by Claude Code on stdin (#1453).
|
|
430
|
+
//
|
|
431
|
+
// `context_window.used_percentage` is pre-calculated by Claude Code from INPUT
|
|
432
|
+
// tokens only (input_tokens + cache_creation_input_tokens + cache_read_input_tokens);
|
|
433
|
+
// it deliberately excludes output_tokens. It already accounts for
|
|
434
|
+
// `context_window_size`, so it stays correct on a 1M-context model. Do NOT
|
|
435
|
+
// "correct" this to `exceeds_200k_tokens`, which is a fixed 200k threshold and
|
|
436
|
+
// is meaningless on anything larger.
|
|
437
|
+
//
|
|
438
|
+
// It is null early in a session (before the first API response). Report null,
|
|
439
|
+
// never a substitute: this field used to be derived from the stored session
|
|
440
|
+
// count (`sessions * 5`), which pinned at 100% after 20 sessions and had nothing
|
|
441
|
+
// to do with the window. A blank beats a wrong number.
|
|
442
|
+
const contextPct = normalizeContextPct(STDIN_PAYLOAD?.context_window?.used_percentage);
|
|
417
443
|
|
|
418
444
|
// Sub-agents from file metrics (no ps aux)
|
|
419
445
|
let subAgents = 0;
|
|
@@ -797,6 +823,12 @@ function generateStatusline() {
|
|
|
797
823
|
parts.push(`${c.cyan}\u23F1 ${session.duration}${c.reset}`);
|
|
798
824
|
}
|
|
799
825
|
|
|
826
|
+
// Context % (#1453). Self-hides when Claude Code hasn't reported a window yet,
|
|
827
|
+
// rather than rendering a placeholder that reads as a real measurement.
|
|
828
|
+
if (SL_CONFIG.show_context && system.contextPct !== null) {
|
|
829
|
+
parts.push(`${contextColor(system.contextPct)}\uD83D\uDCC2 ${system.contextPct}%${c.reset}`);
|
|
830
|
+
}
|
|
831
|
+
|
|
800
832
|
// Intelligence %
|
|
801
833
|
if (SL_CONFIG.show_intelligence) {
|
|
802
834
|
const intellColor = system.intelligencePct >= 80 ? c.brightGreen : system.intelligencePct >= 40 ? c.brightYellow : c.dim;
|
|
@@ -875,6 +907,14 @@ function generateDashboard() {
|
|
|
875
907
|
);
|
|
876
908
|
}
|
|
877
909
|
|
|
910
|
+
// Context % (#1453). Self-hides when the value is unknown.
|
|
911
|
+
if (SL_CONFIG.show_context && system.contextPct !== null) {
|
|
912
|
+
lines.push(
|
|
913
|
+
`${c.brightCyan}\uD83D\uDCC2 Context${c.reset} ${contextColor(system.contextPct)}${system.contextPct}%${c.reset} ` +
|
|
914
|
+
`${c.dim}used${c.reset}`
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
878
918
|
// Embeddings line \u2014 vector store stats from .moflo/vector-stats.json.
|
|
879
919
|
// Reuses `system.embeddings` (already computed by getSystemMetrics()) instead
|
|
880
920
|
// of re-probing the cache file on every render.
|
|
@@ -952,8 +992,17 @@ function generateCompactDashboard() {
|
|
|
952
992
|
pushUpgradeNoticeSegment(lines);
|
|
953
993
|
lines.push(header);
|
|
954
994
|
|
|
955
|
-
// Combined swarm + embeddings + mcp line
|
|
995
|
+
// Combined context + swarm + embeddings + mcp line
|
|
956
996
|
const segments = [];
|
|
997
|
+
// Context % (#1453). Read straight off the stdin payload rather than via
|
|
998
|
+
// getSystemMetrics() — compact mode deliberately avoids that call so it stays
|
|
999
|
+
// probe-free, and this value costs nothing to derive.
|
|
1000
|
+
{
|
|
1001
|
+
const pct = normalizeContextPct(STDIN_PAYLOAD?.context_window?.used_percentage);
|
|
1002
|
+
if (SL_CONFIG.show_context && pct !== null) {
|
|
1003
|
+
segments.push(`${contextColor(pct)}\uD83D\uDCC2 ${pct}%${c.reset}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
957
1006
|
if (SL_CONFIG.show_swarm) {
|
|
958
1007
|
const swarm = getSwarmStatus();
|
|
959
1008
|
const swarmInd = swarm.coordinationActive ? `${c.brightGreen}\u25C9${c.reset}` : `${c.dim}\u25CB${c.reset}`;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flfl
|
|
3
|
+
description: Run /fl on a ticket with moflo's three standing considerations loaded first — cross-platform (Rule #1), consumer blast radius, and dogfooding. Use in the moflo repo itself instead of bare /fl. moflo-internal; never installed into consumer projects.
|
|
4
|
+
arguments: "[options] <issue-number | title>"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
$ARGUMENTS
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
# /flfl — /fl with moflo's standing considerations loaded first
|
|
12
|
+
|
|
13
|
+
Purpose: run the normal `/fl` ticket workflow, but seat the three things that break moflo changes **before** any research, code, or review happens — not after a reviewer catches them.
|
|
14
|
+
|
|
15
|
+
These are not a preamble to acknowledge and move past. Hold all three for the **whole** run: research, implementation, tests, `/flo-simplify`, `/verify`, and the PR body.
|
|
16
|
+
|
|
17
|
+
## The three considerations
|
|
18
|
+
|
|
19
|
+
| # | Consideration | What it changes about the work you are about to do |
|
|
20
|
+
|---|---------------|----------------------------------------------------|
|
|
21
|
+
| 1 | **Rule #1 — everything ships cross-platform** | Linux, macOS **and** Windows, identically. Audit every edit for: `path.join`/`path.sep` over hardcoded separators; `fs.realpathSync` on **both** sides of any path comparison; no `Foo.ts` beside `foo.ts`; platform EOL; no `bash`/`grep`/`sed`/`cat`/`find` shell-outs (use Node `fs`/`spawn`); `tasklist` vs `/proc` for process checks; `shell: true` on Windows vs `detached` on POSIX when spawning; `os.tmpdir()` and test ports in 40000–44999. Verify against CI's macOS **and** Ubuntu runs, not just your own OS. |
|
|
22
|
+
| 2 | **moflo is installed into a destination project** | This is a library, not an app. Before writing code, name (a) the **consumer surface** touched — `bin/`, `src/cli/`, `.claude/scripts/`, hooks, `init/`, settings/CLAUDE.md generators, anything synced into `node_modules/moflo/`; (b) the **failure mode** for someone already on the current version who upgrades — does their `.moflo/` state still parse, do their hooks still wire, is a migration needed; (c) the **round-trip cost** — does this need publish-then-reinstall to take effect. If you cannot name all three, re-scope before writing code. |
|
|
23
|
+
| 3 | **moflo dogfoods itself** | The daemon, hooks, statusline, MCP server and indexer all run from `node_modules/moflo/…`, **not** the source tree. A source edit changes nothing for those layers until publish + reinstall + Claude Code restart. Before diagnosing any "X is broken" symptom, establish **which copy is actually running** — diff `bin/` against `.claude/` against `node_modules/moflo/` first. Expect local flapping: the session-start launcher re-syncs `.claude/helpers/` from the **installed** package, so a local fix to a synced file reverts until published. |
|
|
24
|
+
|
|
25
|
+
## How to run
|
|
26
|
+
|
|
27
|
+
1. Restate the three considerations in one line each, mapped to **this specific ticket** — which surface it touches, which platform risks it carries, whether it needs a publish round-trip. Generic restatement is worthless; if a consideration genuinely does not apply, say so and why.
|
|
28
|
+
2. Invoke the real workflow with the arguments above, unchanged and in full — including every flag (`-sd`, `-s`, `-w`, `-m`, …):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Skill({ skill: "fl", args: "<the $ARGUMENTS block above, verbatim>" })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. Follow `/fl` from there. `/flfl` adds nothing to the workflow itself — same phases, same gates, same run-mode resolution. Re-check the three at each gate: they most often fail at `/flo-simplify` (a cross-platform miss) and at the PR body (an unnamed consumer failure mode).
|
|
35
|
+
|
|
36
|
+
## Anti-patterns
|
|
37
|
+
|
|
38
|
+
| Don't | Do |
|
|
39
|
+
|-------|-----|
|
|
40
|
+
| Acknowledge the three, then run `/fl` and never revisit them | Re-check them at implementation, simplify, and PR |
|
|
41
|
+
| Restate them verbatim from this file | Map each to the ticket's actual surface and risk |
|
|
42
|
+
| Drop or reorder `$ARGUMENTS` when calling `/fl` | Pass the argument string through untouched |
|
|
43
|
+
| Verify only on your own OS | Read the macOS and Ubuntu CI runs before claiming green |
|
|
44
|
+
| Debug a runtime symptom against the source tree | Confirm which copy is running first |
|
|
45
|
+
|
|
46
|
+
## See Also
|
|
47
|
+
|
|
48
|
+
- `.claude/skills/fl/SKILL.md` — the workflow this wraps
|
|
49
|
+
- `CLAUDE.md` — Rule #1, Rule #2, and the dogfooding section these three condense
|
|
50
|
+
- `.claude/guidance/internal/dogfooding.md` — required reading before diagnosing runtime symptoms or adding files under `bin/`
|
package/bin/gate-hook.mjs
CHANGED
|
@@ -41,6 +41,22 @@ if (typeof hookContext.session_id === 'string' && hookContext.session_id) {
|
|
|
41
41
|
if (typeof hookContext.transcript_path === 'string' && hookContext.transcript_path) {
|
|
42
42
|
env.HOOK_TRANSCRIPT_PATH = hookContext.transcript_path;
|
|
43
43
|
}
|
|
44
|
+
// #1447 — forward the user prompt. `gate.cjs` reads CLAUDE_USER_PROMPT to decide
|
|
45
|
+
// whether a prompt needs a memory search, and this bridge never set it, so the
|
|
46
|
+
// `prompt-state-reset` safety-net hook classified the EMPTY STRING on every
|
|
47
|
+
// prompt, concluded "no memory required", and wrote that over the correct value
|
|
48
|
+
// prompt-hook.mjs had just computed. A safety net that disarmed the gate it
|
|
49
|
+
// exists to protect — intermittently, since the surviving value depended on
|
|
50
|
+
// which of the two UserPromptSubmit hooks wrote last.
|
|
51
|
+
//
|
|
52
|
+
// Same field precedence as prompt-hook.mjs (`user_prompt` then `prompt`) so both
|
|
53
|
+
// UserPromptSubmit paths classify identical text and the reset is genuinely
|
|
54
|
+
// idempotent, which is the only thing that makes a safety-net hook safe.
|
|
55
|
+
if (typeof hookContext.user_prompt === 'string' && hookContext.user_prompt) {
|
|
56
|
+
env.CLAUDE_USER_PROMPT = hookContext.user_prompt;
|
|
57
|
+
} else if (typeof hookContext.prompt === 'string' && hookContext.prompt) {
|
|
58
|
+
env.CLAUDE_USER_PROMPT = hookContext.prompt;
|
|
59
|
+
}
|
|
44
60
|
// #1332: structured tool inputs are forwarded as JSON, not dropped.
|
|
45
61
|
//
|
|
46
62
|
// This previously forwarded ONLY string values, so any object-valued input was
|