moflo 4.13.1 → 4.13.2
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 +4 -2
- package/.claude/helpers/gate.cjs +252 -8
- package/.claude/helpers/prompt-hook.mjs +10 -0
- package/README.md +5 -2
- package/bin/gate.cjs +252 -8
- package/bin/prompt-hook.mjs +10 -0
- package/bin/session-start-launcher.mjs +31 -8
- package/dist/src/cli/config/moflo-config.js +2 -1
- package/dist/src/cli/init/embedded-helpers.js +2 -2
- package/dist/src/cli/services/spell-gate.js +24 -4
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -38,7 +38,8 @@ code_map:
|
|
|
38
38
|
gates:
|
|
39
39
|
memory_first: true # Block Glob/Grep/Read until memory is searched
|
|
40
40
|
task_create_first: true # Advisory reminder before Agent tool (not blocking)
|
|
41
|
-
context_tracking: true #
|
|
41
|
+
context_tracking: true # Report measured context usage, once per band crossing
|
|
42
|
+
# context_limit: 200k # Optional window size (200000 / 200k / 1m); unset = raw token counts
|
|
42
43
|
|
|
43
44
|
# Auto-index on session start
|
|
44
45
|
auto_index:
|
|
@@ -230,7 +231,8 @@ skills:
|
|
|
230
231
|
| `session_continuity.inject: false` | Keep capturing digests but never auto-inject them at session start |
|
|
231
232
|
| `gates.memory_first: true` | Block Glob/Grep/Read until memory is searched first |
|
|
232
233
|
| `gates.task_create_first: true` | Advisory reminder before Agent tool (not blocking) |
|
|
233
|
-
| `gates.context_tracking: true` |
|
|
234
|
+
| `gates.context_tracking: true` | Report measured context usage, once per band crossing |
|
|
235
|
+
| `gates.context_limit: 200k` | Your model's context window, so the notice quotes a percentage. Unset by default — a hook cannot observe the window, and moflo will not guess one |
|
|
234
236
|
| `hooks.pre_edit: false` | Disable file-edit tracking (skips pre-edit hook) |
|
|
235
237
|
| `hooks.post_edit: false` | Disable edit outcome recording and neural training |
|
|
236
238
|
| `hooks.pre_task: false` | Disable agent routing recommendations before spawn |
|
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -13,7 +13,7 @@ var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
|
13
13
|
// the code it describes, so a change made outside Write/Edit/MultiEdit (a Bash
|
|
14
14
|
// write, a branch switch, the next issue in the same session) invalidates it.
|
|
15
15
|
// See creditFingerprint() for why the boolean flags alone cannot.
|
|
16
|
-
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, tasksAcknowledged: false, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, testsFingerprint: null, simplifyRun: false, simplifySnapshotSha: null, simplifyFingerprint: null, verifyRun: false, verifyOutcome: null, verifyFingerprint: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {}, flMode: null, swarmInitialized: false, hiveInitialized: false, sddMode: false, activeSddSlug: null };
|
|
16
|
+
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, tasksAcknowledged: false, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, testsFingerprint: null, simplifyRun: false, simplifySnapshotSha: null, simplifyFingerprint: null, verifyRun: false, verifyOutcome: null, verifyFingerprint: null, interactionCount: 0, contextBand: null, compactedAt: null, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {}, flMode: null, swarmInitialized: false, hiveInitialized: false, sddMode: false, activeSddSlug: null };
|
|
17
17
|
|
|
18
18
|
// Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
|
|
19
19
|
// is session-wide, so once the parent searches memory, every spawned subagent
|
|
@@ -76,7 +76,7 @@ function loadGateConfig() {
|
|
|
76
76
|
// consecutive ignores in one session is not a control. Blocking is the default
|
|
77
77
|
// because the honest "these stay open on purpose" outcome is one command away
|
|
78
78
|
// (record-tasks-acknowledged), so nothing here can deadlock a run.
|
|
79
|
-
var defaults = { memory_first: true, task_create_first: true, context_tracking: true, testing_gate: true, simplify_gate: true, learnings_gate: true, swarm_invocation_gate: true, verify_before_done: true, sdd_gate: true, task_status_gate: 'block' };
|
|
79
|
+
var defaults = { memory_first: true, task_create_first: true, context_tracking: true, context_limit: null, testing_gate: true, simplify_gate: true, learnings_gate: true, swarm_invocation_gate: true, verify_before_done: true, sdd_gate: true, task_status_gate: 'block' };
|
|
80
80
|
var content = MOFLO_YAML;
|
|
81
81
|
if (content) {
|
|
82
82
|
// Boolean forms are accepted so this key reads like every other gate in the
|
|
@@ -91,6 +91,17 @@ function loadGateConfig() {
|
|
|
91
91
|
if (/memory_first:\s*false/i.test(content)) defaults.memory_first = false;
|
|
92
92
|
if (/task_create_first:\s*false/i.test(content)) defaults.task_create_first = false;
|
|
93
93
|
if (/context_tracking:\s*false/i.test(content)) defaults.context_tracking = false;
|
|
94
|
+
// Optional and OFF by default (#1487). Only an explicit value lets the
|
|
95
|
+
// context notice quote a percentage — see contextLimit() for why nothing
|
|
96
|
+
// infers one.
|
|
97
|
+
// Anchored to the start of a line, unlike the boolean keys above, because
|
|
98
|
+
// this one is DOCUMENTED as a commented-out example (`# context_limit: 200k`
|
|
99
|
+
// in the README and the yaml reference). An unanchored match would read that
|
|
100
|
+
// comment as configuration the moment anyone pasted the block, quietly
|
|
101
|
+
// pinning a 200k denominator onto a 1M session — this issue's exact failure,
|
|
102
|
+
// redelivered through the docs.
|
|
103
|
+
var climit = /^[ \t]*context_limit:\s*['"]?([0-9.]+\s*[km]?)['"]?/im.exec(content);
|
|
104
|
+
if (climit) defaults.context_limit = parseTokenCount(climit[1]);
|
|
94
105
|
if (/testing_gate:\s*false/i.test(content)) defaults.testing_gate = false;
|
|
95
106
|
if (/simplify_gate:\s*false/i.test(content)) defaults.simplify_gate = false;
|
|
96
107
|
if (/learnings_gate:\s*false/i.test(content)) defaults.learnings_gate = false;
|
|
@@ -863,6 +874,226 @@ function classifyBashNamespaceHint(cmd) {
|
|
|
863
874
|
return '';
|
|
864
875
|
}
|
|
865
876
|
|
|
877
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
878
|
+
// Context usage (#1487)
|
|
879
|
+
//
|
|
880
|
+
// The banner this feeds used to be driven by `interactionCount` — a monotonic
|
|
881
|
+
// per-prompt counter with no reset anywhere, including on compaction. Past 30
|
|
882
|
+
// prompts it emitted "Context: CRITICAL. Commit, store learnings, suggest new
|
|
883
|
+
// session." on EVERY subsequent turn for the life of the session state, while
|
|
884
|
+
// real usage sat around 12%. Because the text was shaped as an instruction
|
|
885
|
+
// rather than a metric, the model complied: truncated investigations, handed
|
|
886
|
+
// back partial findings, and recommended a fresh session over a nearly empty
|
|
887
|
+
// window. A wrong instruction-shaped hook message is worse than none — the
|
|
888
|
+
// model cannot check it and will generally obey.
|
|
889
|
+
//
|
|
890
|
+
// So: measure instead of guess. Claude Code already writes the per-turn token
|
|
891
|
+
// usage into the session transcript it hands every hook, and that number drops
|
|
892
|
+
// the moment a compaction lands, which makes the whole class of "stuck after
|
|
893
|
+
// compact" impossible rather than merely patched.
|
|
894
|
+
|
|
895
|
+
// Accepts `200000`, `200k`, `1m`, `1.5m`. One parser for BOTH the yaml key and
|
|
896
|
+
// the env override, because they were two: `parseInt('200k')` is 200, so an
|
|
897
|
+
// operator who wrote the documented `MOFLO_CONTEXT_LIMIT=200k` got a 200-token
|
|
898
|
+
// window and `Context: 61000% used` every single turn — #1487 reproduced by the
|
|
899
|
+
// fix for #1487. Anything below 1000 tokens is not a context window; treat it as
|
|
900
|
+
// a typo and decline rather than quote arithmetic off it.
|
|
901
|
+
function parseTokenCount(raw) {
|
|
902
|
+
var m = /^\s*(\d+(?:\.\d+)?)\s*([km])?\s*$/i.exec(String(raw || ''));
|
|
903
|
+
if (!m) return null;
|
|
904
|
+
var unit = (m[2] || '').toLowerCase();
|
|
905
|
+
var n = Math.round(parseFloat(m[1]) * (unit === 'm' ? 1000000 : unit === 'k' ? 1000 : 1));
|
|
906
|
+
return n >= 1000 ? n : null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// The context LIMIT is not observable from a hook, and this is the one place it
|
|
910
|
+
// would be tempting to guess. Claude Code's transcript records `claude-opus-5`
|
|
911
|
+
// for a 1M-context session and for a 200k one alike — there is no marker, no
|
|
912
|
+
// limit field, and nothing in the UserPromptSubmit payload. Guessing 200k would
|
|
913
|
+
// have reported a 122k 1M-window session as "61% used", which is #1487's
|
|
914
|
+
// original failure re-created with better arithmetic: a confident, wrong number
|
|
915
|
+
// the model cannot check.
|
|
916
|
+
//
|
|
917
|
+
// So a percentage is quoted ONLY when the operator states the window, via
|
|
918
|
+
// `gates.context_limit` in moflo.yaml or MOFLO_CONTEXT_LIMIT in the environment.
|
|
919
|
+
// Otherwise the notice reports the raw token count, which cannot be wrong.
|
|
920
|
+
function contextLimit(config) {
|
|
921
|
+
var override = parseTokenCount(process.env.MOFLO_CONTEXT_LIMIT);
|
|
922
|
+
if (override) return override;
|
|
923
|
+
return config && config.context_limit > 0 ? config.context_limit : null;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Absolute-token milestones for the no-limit case, descending. Crossing one is a
|
|
927
|
+
// real event about a real number, and it keeps the notice rare without implying
|
|
928
|
+
// anything about how full the window is.
|
|
929
|
+
var CONTEXT_MILESTONES = [800000, 400000, 200000, 100000];
|
|
930
|
+
|
|
931
|
+
// Ordinal for "is this worse than the last thing we announced". Only an UPWARD
|
|
932
|
+
// move is announced: emitting "checkpointing progress is worth considering" on
|
|
933
|
+
// the way DOWN — right after a compaction freed the window — is the same
|
|
934
|
+
// backwards advice #1487 is about. A downward move still updates the memo, which
|
|
935
|
+
// is what re-arms the next genuine crossing.
|
|
936
|
+
function contextBandRank(band) {
|
|
937
|
+
if (!band || band === 'FRESH') return 0;
|
|
938
|
+
if (band.indexOf('tokens:') === 0) {
|
|
939
|
+
var n = parseInt(band.slice(7), 10);
|
|
940
|
+
for (var i = CONTEXT_MILESTONES.length - 1, rank = 1; i >= 0; i--, rank++) {
|
|
941
|
+
if (CONTEXT_MILESTONES[i] === n) return rank;
|
|
942
|
+
}
|
|
943
|
+
return 1;
|
|
944
|
+
}
|
|
945
|
+
return band === 'CRITICAL' ? 3 : band === 'DEPLETED' ? 2 : 1;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// First read. Sized for the common case: the newest assistant record is within a
|
|
949
|
+
// few KB of EOF.
|
|
950
|
+
var CONTEXT_TAIL_BYTES = 64 * 1024;
|
|
951
|
+
// Escalation, used only when the first window held no assistant usage record at
|
|
952
|
+
// all. One assistant turn carrying a large tool result can exceed the first
|
|
953
|
+
// window entirely, leaving a tail with no newline in it — and without this the
|
|
954
|
+
// gate would fall silent permanently, which is the failure this whole change
|
|
955
|
+
// exists to remove.
|
|
956
|
+
var CONTEXT_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
957
|
+
|
|
958
|
+
// Scan the last `tailBytes` of the transcript backwards for the newest main-loop
|
|
959
|
+
// assistant turn's context size. Returns { used, sawUsage }: `used` is null when
|
|
960
|
+
// nothing qualified, and `sawUsage` distinguishes "no record in this window"
|
|
961
|
+
// (escalate) from "records found but all older than minTs" (do not escalate —
|
|
962
|
+
// a wider window can only find even older ones).
|
|
963
|
+
//
|
|
964
|
+
// Cross-platform (Rule #1): fs primitives and an env-supplied absolute path —
|
|
965
|
+
// no shell, no `tail`, no path separators of our own.
|
|
966
|
+
function scanTranscriptTail(transcript, tailBytes, minTs) {
|
|
967
|
+
var fd = null;
|
|
968
|
+
var result = { used: null, sawUsage: false };
|
|
969
|
+
try {
|
|
970
|
+
var st = fs.statSync(transcript);
|
|
971
|
+
if (!st.isFile() || !st.size) return result;
|
|
972
|
+
var start = st.size > tailBytes ? st.size - tailBytes : 0;
|
|
973
|
+
var length = st.size - start;
|
|
974
|
+
// allocUnsafe, not alloc: `filled` bounds every read below, so the
|
|
975
|
+
// uninitialised tail is never observed — and zero-filling this on every
|
|
976
|
+
// prompt in every consumer session buys nothing.
|
|
977
|
+
var buf = Buffer.allocUnsafe(length);
|
|
978
|
+
fd = fs.openSync(transcript, 'r');
|
|
979
|
+
// Loop: readSync is permitted to return a short read, and a partial fill
|
|
980
|
+
// would leave the tail of the buffer as garbage — which lands on the NEWEST
|
|
981
|
+
// record, the one record this function exists to find.
|
|
982
|
+
var filled = 0;
|
|
983
|
+
while (filled < length) {
|
|
984
|
+
var n = fs.readSync(fd, buf, filled, length - filled, start + filled);
|
|
985
|
+
if (n <= 0) break;
|
|
986
|
+
filled += n;
|
|
987
|
+
}
|
|
988
|
+
var text = buf.toString('utf-8', 0, filled);
|
|
989
|
+
// Walked backwards by newline index rather than split(): the split would
|
|
990
|
+
// allocate a substring per line of a window whose last few records are all
|
|
991
|
+
// we want, and this loop normally exits after one or two of them.
|
|
992
|
+
var end = text.length;
|
|
993
|
+
while (end > 0) {
|
|
994
|
+
var nl = text.lastIndexOf('\n', end - 1);
|
|
995
|
+
// A window that starts mid-file cuts its first record in half. Stop rather
|
|
996
|
+
// than parse the fragment — and note that a window holding NO newline at
|
|
997
|
+
// all leaves sawUsage false, which is what triggers escalation.
|
|
998
|
+
if (nl < 0) { if (start > 0) break; nl = -1; }
|
|
999
|
+
var line = text.slice(nl + 1, end).trim();
|
|
1000
|
+
end = nl;
|
|
1001
|
+
// Cheap pre-filter, same shape as readTaskLedger's. A transcript tail is
|
|
1002
|
+
// dominated by large tool_result and user records; JSON.parse on every one
|
|
1003
|
+
// of them is the whole cost of this function.
|
|
1004
|
+
if (!line || line.indexOf('"input_tokens"') < 0) continue;
|
|
1005
|
+
var entry;
|
|
1006
|
+
try { entry = JSON.parse(line); } catch (e) { continue; }
|
|
1007
|
+
if (!entry || entry.type !== 'assistant') continue;
|
|
1008
|
+
// Subagent turns carry their OWN small context. Counting one would report
|
|
1009
|
+
// a freshly-spawned agent's window as the main loop's.
|
|
1010
|
+
if (entry.isSidechain === true) continue;
|
|
1011
|
+
var usage = entry.message && entry.message.usage;
|
|
1012
|
+
if (!usage || typeof usage.input_tokens !== 'number') continue;
|
|
1013
|
+
// Cached and uncached input are both resident context; the split is a
|
|
1014
|
+
// billing detail. Output is excluded — it is this turn's reply, not the
|
|
1015
|
+
// window it was produced from, and it lands in the next turn's input.
|
|
1016
|
+
var used = usage.input_tokens +
|
|
1017
|
+
(usage.cache_read_input_tokens || 0) +
|
|
1018
|
+
(usage.cache_creation_input_tokens || 0);
|
|
1019
|
+
if (!(used > 0)) continue;
|
|
1020
|
+
result.sawUsage = true;
|
|
1021
|
+
// Older than the last compaction: this record describes a window that no
|
|
1022
|
+
// longer exists. See readTranscriptUsage for why that matters.
|
|
1023
|
+
if (minTs && !(String(entry.timestamp || '') > minTs)) continue;
|
|
1024
|
+
result.used = used;
|
|
1025
|
+
return result;
|
|
1026
|
+
}
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
// Unreadable transcript — the caller stays silent rather than guessing.
|
|
1029
|
+
} finally {
|
|
1030
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch (e) { /* already gone */ } }
|
|
1031
|
+
}
|
|
1032
|
+
return result;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Newest main-loop assistant turn's context size in tokens, or null when the
|
|
1036
|
+
// transcript is absent, unreadable, or carries nothing newer than `minTs`.
|
|
1037
|
+
//
|
|
1038
|
+
// `minTs` is the last compaction's timestamp, and skipping past it is essential
|
|
1039
|
+
// rather than tidy: UserPromptSubmit fires BEFORE the first post-compaction
|
|
1040
|
+
// assistant turn exists, so the newest record on disk is still the pre-compaction
|
|
1041
|
+
// one. Without this the very first prompt after a `/compact` would report the
|
|
1042
|
+
// window the user just emptied — the reported bug, one turn wide.
|
|
1043
|
+
function readTranscriptUsage(minTs) {
|
|
1044
|
+
var transcript = process.env.HOOK_TRANSCRIPT_PATH || '';
|
|
1045
|
+
if (!transcript) return null;
|
|
1046
|
+
var r = scanTranscriptTail(transcript, CONTEXT_TAIL_BYTES, minTs);
|
|
1047
|
+
if (r.used !== null || r.sawUsage) return r.used;
|
|
1048
|
+
// Nothing in the first window even looked like a usage record — a single
|
|
1049
|
+
// oversized turn can be wider than the window itself. Widen once.
|
|
1050
|
+
return scanTranscriptTail(transcript, CONTEXT_TAIL_MAX_BYTES, minTs).used;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function contextBandForPct(pct) {
|
|
1054
|
+
if (pct >= 90) return 'CRITICAL';
|
|
1055
|
+
if (pct >= 75) return 'DEPLETED';
|
|
1056
|
+
if (pct >= 50) return 'MODERATE';
|
|
1057
|
+
return 'FRESH';
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function formatTokens(n) {
|
|
1061
|
+
return n >= 1000 ? Math.round(n / 1000) + 'k' : String(n);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// { band, line } for the current context, or null when it cannot be measured.
|
|
1065
|
+
//
|
|
1066
|
+
// There is deliberately NO fallback here. The obvious one — fall back to the
|
|
1067
|
+
// turn counter when the transcript is unreadable — reintroduces the bug twice
|
|
1068
|
+
// over: a turn count is not context usage (that IS #1487), and mixing the two
|
|
1069
|
+
// sources makes the memo oscillate, so a transcript that reads intermittently
|
|
1070
|
+
// would emit, fall silent, and emit the same line again forever. A hook with
|
|
1071
|
+
// nothing to measure has nothing to say.
|
|
1072
|
+
function describeContext(config, minTs) {
|
|
1073
|
+
var used = readTranscriptUsage(minTs);
|
|
1074
|
+
if (used === null) return null;
|
|
1075
|
+
var limit = contextLimit(config);
|
|
1076
|
+
if (limit) {
|
|
1077
|
+
var pct = Math.round((used / limit) * 100);
|
|
1078
|
+
var band = contextBandForPct(pct);
|
|
1079
|
+
var line = 'Context: ' + pct + '% used (' + formatTokens(used) + ' of ' +
|
|
1080
|
+
formatTokens(limit) + ' tokens).';
|
|
1081
|
+
if (band === 'DEPLETED') line += ' Checkpointing progress is worth considering.';
|
|
1082
|
+
else if (band === 'CRITICAL') line += ' /compact or a fresh session is worth considering.';
|
|
1083
|
+
return { band: band, line: line };
|
|
1084
|
+
}
|
|
1085
|
+
var milestone = 0;
|
|
1086
|
+
for (var i = 0; i < CONTEXT_MILESTONES.length; i++) {
|
|
1087
|
+
if (used >= CONTEXT_MILESTONES[i]) { milestone = CONTEXT_MILESTONES[i]; break; }
|
|
1088
|
+
}
|
|
1089
|
+
// No advice attached on purpose: without a window size there is nothing
|
|
1090
|
+
// honest to advise, and the model already knows its own limit.
|
|
1091
|
+
return {
|
|
1092
|
+
band: milestone ? 'tokens:' + milestone : 'FRESH',
|
|
1093
|
+
line: 'Context: ' + formatTokens(used) + ' tokens in the window.',
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
866
1097
|
// Apply per-prompt state reset shared by `prompt-reminder` (full) and
|
|
867
1098
|
// `prompt-state-reset` (defensive safety-net, no emission). Idempotent — both
|
|
868
1099
|
// UserPromptSubmit hooks can run it without compounding any field. Caller
|
|
@@ -2266,6 +2497,24 @@ switch (command) {
|
|
|
2266
2497
|
// written after it; refreshed every prompt, so a /clear that mints a new id
|
|
2267
2498
|
// is picked up on the next turn rather than going stale.
|
|
2268
2499
|
if (process.env.HOOK_SESSION_ID) s.sessionId = process.env.HOOK_SESSION_ID;
|
|
2500
|
+
// #1487 — resolved BEFORE the write so the edge-trigger memo rides the same
|
|
2501
|
+
// one, and emitted after the /flo modifiers below so the authoritative run
|
|
2502
|
+
// modes stay at the top of the hook's output.
|
|
2503
|
+
var contextNotice = '';
|
|
2504
|
+
if (config.context_tracking) {
|
|
2505
|
+
var ctx = describeContext(config, s.compactedAt);
|
|
2506
|
+
// Nothing measurable → nothing said, and the memo is left exactly as it
|
|
2507
|
+
// was so a transient read failure cannot re-arm an already-announced band.
|
|
2508
|
+
if (ctx) {
|
|
2509
|
+
// Edge-triggered, not level-triggered, and only upward. A banner
|
|
2510
|
+
// repeated verbatim for dozens of turns carries no information after the
|
|
2511
|
+
// first one and trains the model to treat it as a standing instruction.
|
|
2512
|
+
// A downward move still updates the memo — that is what re-arms the next
|
|
2513
|
+
// genuine crossing after a compaction.
|
|
2514
|
+
if (contextBandRank(ctx.band) > contextBandRank(s.contextBand)) contextNotice = ctx.line;
|
|
2515
|
+
s.contextBand = ctx.band;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2269
2518
|
writeState(s);
|
|
2270
2519
|
// Announce the resolved /flo run modifiers. The gate already parsed
|
|
2271
2520
|
// moflo.yaml in THIS process (fresh per prompt — a git pull or a mid-session
|
|
@@ -2294,12 +2543,7 @@ switch (command) {
|
|
|
2294
2543
|
console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
|
|
2295
2544
|
}
|
|
2296
2545
|
}
|
|
2297
|
-
if (
|
|
2298
|
-
var ic = s.interactionCount;
|
|
2299
|
-
if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
|
|
2300
|
-
else if (ic > 20) console.log('Context: DEPLETED. Checkpoint progress. Recommend /compact or fresh session.');
|
|
2301
|
-
else if (ic > 10) console.log('Context: MODERATE. Re-state goal before architectural decisions. Use agents for >300 LOC.');
|
|
2302
|
-
}
|
|
2546
|
+
if (contextNotice) console.log(contextNotice);
|
|
2303
2547
|
break;
|
|
2304
2548
|
}
|
|
2305
2549
|
case 'prompt-state-reset': {
|
|
@@ -32,6 +32,16 @@ if (typeof hookContext.session_id === 'string' && hookContext.session_id) {
|
|
|
32
32
|
env.HOOK_SESSION_ID = hookContext.session_id;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// #1487 — forward the transcript path so `prompt-reminder` can report REAL
|
|
36
|
+
// context usage instead of a turn counter. Claude Code writes per-turn token
|
|
37
|
+
// usage into this file, and it is the only place a UserPromptSubmit hook can
|
|
38
|
+
// read it: the payload carries no usage field of its own. Absent or non-string
|
|
39
|
+
// (an older host, a test harness) simply leaves the var unset, and gate.cjs
|
|
40
|
+
// falls back to the turn count.
|
|
41
|
+
if (typeof hookContext.transcript_path === 'string' && hookContext.transcript_path) {
|
|
42
|
+
env.HOOK_TRANSCRIPT_PATH = hookContext.transcript_path;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
// Run prompt-reminder via gate.cjs
|
|
36
46
|
var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
37
47
|
var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
|
package/README.md
CHANGED
|
@@ -324,7 +324,9 @@ gates:
|
|
|
324
324
|
memory_first: true # Set to false to disable memory-first enforcement
|
|
325
325
|
task_create_first: true # Set to false to disable TaskCreate enforcement (disables task_status_gate too)
|
|
326
326
|
task_status_gate: block # Open tasks block `gh pr create`; warn → report only; off → silent
|
|
327
|
-
context_tracking: true # Set to false to disable context
|
|
327
|
+
context_tracking: true # Set to false to disable the context-usage notice
|
|
328
|
+
# context_limit: 200k # Optional. Your model's window, so the notice can quote a %.
|
|
329
|
+
# Unset (default) = report raw tokens; moflo never guesses a window.
|
|
328
330
|
verify_before_done: true # On by default (#1294); set false to skip /verify before `gh pr create`
|
|
329
331
|
```
|
|
330
332
|
|
|
@@ -1013,7 +1015,8 @@ gates:
|
|
|
1013
1015
|
memory_first: true # Must search memory before file exploration
|
|
1014
1016
|
task_create_first: true # Must TaskCreate before Agent tool
|
|
1015
1017
|
task_status_gate: block # Open tasks block `gh pr create`; warn → report only; off → silent
|
|
1016
|
-
context_tracking: true #
|
|
1018
|
+
context_tracking: true # Report measured context-window usage once per band crossing
|
|
1019
|
+
# context_limit: 200k # Optional window size (200000 / 200k / 1m) to get a % instead of raw tokens
|
|
1017
1020
|
verify_before_done: true # On by default (#1294); /verify before `gh pr create` (unless --no-verify). false to disable
|
|
1018
1021
|
|
|
1019
1022
|
sdd:
|