moflo 4.12.5 → 4.12.7
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-agent-rules.md +15 -0
- package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +10 -2
- package/.claude/helpers/gate-hook.mjs +29 -1
- package/.claude/helpers/gate.cjs +143 -18
- package/.claude/skills/fl/SKILL.md +6 -4
- package/.claude/skills/fl/phases.md +59 -7
- package/README.md +4 -1
- package/bin/gate-hook.mjs +29 -1
- package/bin/gate.cjs +143 -18
- package/dist/src/cli/commands/doctor-fixes.js +119 -20
- package/dist/src/cli/init/helpers-generator.js +186 -5
- package/dist/src/cli/init/moflo-yaml-template.js +1 -0
- package/dist/src/cli/services/hook-wiring.js +6 -2
- package/dist/src/cli/services/project-root.js +17 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -208,7 +208,7 @@ var os = require('os');
|
|
|
208
208
|
var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\\/([a-z])\\//i, '$1:/');
|
|
209
209
|
var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
210
210
|
|
|
211
|
-
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, verifyRun: false, verifyOutcome: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {}, flMode: null, swarmInitialized: false, hiveInitialized: false, sddMode: false, activeSddSlug: null };
|
|
211
|
+
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, tasksAcknowledged: false, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, verifyRun: false, verifyOutcome: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {}, flMode: null, swarmInitialized: false, hiveInitialized: false, sddMode: false, activeSddSlug: null };
|
|
212
212
|
|
|
213
213
|
function readState() {
|
|
214
214
|
try {
|
|
@@ -256,9 +256,17 @@ function writeState(s) {
|
|
|
256
256
|
|
|
257
257
|
// Load moflo.yaml gate config (defaults: all enabled)
|
|
258
258
|
function loadGateConfig() {
|
|
259
|
-
|
|
259
|
+
// #1435 — task_status_gate is a MODE ('block' | 'warn' | 'off'), not a boolean;
|
|
260
|
+
// boolean forms are accepted so it reads like its neighbours. Unrecognised
|
|
261
|
+
// values keep the default: a typo must not become a stealth opt-out.
|
|
262
|
+
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' };
|
|
260
263
|
var content = MOFLO_YAML;
|
|
261
264
|
if (content) {
|
|
265
|
+
var tsg = /task_status_gate:\\s*['"]?(block|warn|off|false|true)['"]?/i.exec(content);
|
|
266
|
+
if (tsg) {
|
|
267
|
+
var tsgMode = tsg[1].toLowerCase();
|
|
268
|
+
defaults.task_status_gate = tsgMode === 'false' ? 'off' : tsgMode === 'true' ? 'block' : tsgMode;
|
|
269
|
+
}
|
|
262
270
|
if (/memory_first:\\s*false/i.test(content)) defaults.memory_first = false;
|
|
263
271
|
if (/task_create_first:\\s*false/i.test(content)) defaults.task_create_first = false;
|
|
264
272
|
if (/context_tracking:\\s*false/i.test(content)) defaults.context_tracking = false;
|
|
@@ -345,6 +353,18 @@ var COORD_FALLBACK_NOTE = 'If mcp__moflo__* tools are unavailable this session (
|
|
|
345
353
|
// #1348 — the pre-PR gates are order-dependent; naming only the missing one left
|
|
346
354
|
// callers to rediscover the sequence by trial. SYNC: mirrors bin/gate.cjs.
|
|
347
355
|
var ORDER_HINT = 'Order that satisfies all of them: tests green -> /flo-simplify (re-run tests if it edits) -> /verify -> its memory_store verdict -> gh pr create\\n';
|
|
356
|
+
// #1434 — the old text named the mechanism but no quality bar, so the cheapest
|
|
357
|
+
// way past the gate was a summary of the run. The escape command is built from
|
|
358
|
+
// __filename: the caller is the model typing into Bash, where $CLAUDE_PROJECT_DIR
|
|
359
|
+
// is unset and a relative path breaks from any cwd but the project root.
|
|
360
|
+
// SYNC: mirrors bin/gate.cjs.
|
|
361
|
+
var LEARNINGS_MISSING =
|
|
362
|
+
'no durable lesson recorded. A lesson qualifies only if it would help a future session ' +
|
|
363
|
+
'working on a DIFFERENT task — a reusable pattern, a trap, a decision + rationale. ' +
|
|
364
|
+
'Store one with mcp__moflo__memory_store (namespace "learnings"; use "patterns" for a ' +
|
|
365
|
+
'reusable code shape). What THIS run changed is git history — it belongs in the PR body, ' +
|
|
366
|
+
'not in memory. If this run taught nothing new, say so instead of inventing one: ' +
|
|
367
|
+
'node "' + __filename + '" record-no-durable-lesson';
|
|
348
368
|
// #1294 Finding 3 — exempt ephemeral reads/scans under the OS temp dir
|
|
349
369
|
// (background-task output, scratchpads) from the memory-first gate. Mirrors
|
|
350
370
|
// bin/gate.cjs isEphemeralPath. Cross-platform via os.tmpdir(); normalizes a
|
|
@@ -691,6 +711,71 @@ var EDIT_RESET_SKIP_PATH_RE = /(?:^|[\\\\\\/])\\.github[\\\\\\/](?:workflows|ISS
|
|
|
691
711
|
// new untested surface for code review.
|
|
692
712
|
var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\\\\/])(__tests__|__mocks__|tests?|spec|specs|cypress|e2e|fixtures?)[\\\\\\/]|\\.(test|spec)\\.[mc]?[jt]sx?$|\\.fixture\\.[mc]?[jt]sx?$/i;
|
|
693
713
|
|
|
714
|
+
// #1374/#1435 — count TaskCreate calls against terminal TaskUpdate calls in the
|
|
715
|
+
// session transcript. SYNC: mirrors bin/gate.cjs readTaskLedger (see there for
|
|
716
|
+
// why the transcript, and not a TaskUpdate observer or Claude Code's task store).
|
|
717
|
+
//
|
|
718
|
+
// This template variant omits RELAXATIONS the synced bin/gate.cjs carries (the
|
|
719
|
+
// docs-only exemption, fingerprint expiry) — omitting those only makes the
|
|
720
|
+
// fallback stricter. An ENFORCEMENT gate is the opposite: leaving it out would
|
|
721
|
+
// make the fallback silently permissive, which is the exact failure #1435 is
|
|
722
|
+
// about. So it is mirrored in full.
|
|
723
|
+
var TRANSCRIPT_MAX_BYTES = 16 * 1024 * 1024;
|
|
724
|
+
function readTaskLedger() {
|
|
725
|
+
var tp = process.env.HOOK_TRANSCRIPT_PATH || '';
|
|
726
|
+
if (!tp) return null;
|
|
727
|
+
var raw;
|
|
728
|
+
try {
|
|
729
|
+
var tst = fs.statSync(tp);
|
|
730
|
+
if (!tst.isFile() || tst.size > TRANSCRIPT_MAX_BYTES) return null;
|
|
731
|
+
raw = fs.readFileSync(tp, 'utf-8');
|
|
732
|
+
} catch (e) { return null; }
|
|
733
|
+
var created = 0, createdIdCount = 0;
|
|
734
|
+
var pendingCreates = {}, createdIds = {}, latest = {};
|
|
735
|
+
var pos = 0;
|
|
736
|
+
while (pos <= raw.length) {
|
|
737
|
+
var nl = raw.indexOf('\\n', pos);
|
|
738
|
+
var line = nl < 0 ? raw.slice(pos) : raw.slice(pos, nl);
|
|
739
|
+
pos = nl < 0 ? raw.length + 1 : nl + 1;
|
|
740
|
+
if (line.indexOf('TaskCreate') < 0 && line.indexOf('TaskUpdate') < 0
|
|
741
|
+
&& line.indexOf('created successfully') < 0) continue;
|
|
742
|
+
var entry;
|
|
743
|
+
try { entry = JSON.parse(line); } catch (e) { continue; }
|
|
744
|
+
var content = entry && entry.message && entry.message.content;
|
|
745
|
+
if (!Array.isArray(content)) continue;
|
|
746
|
+
for (var ci = 0; ci < content.length; ci++) {
|
|
747
|
+
var block = content[ci];
|
|
748
|
+
if (!block) continue;
|
|
749
|
+
if (block.type === 'tool_result') {
|
|
750
|
+
if (!pendingCreates[block.tool_use_id]) continue;
|
|
751
|
+
delete pendingCreates[block.tool_use_id];
|
|
752
|
+
var text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
753
|
+
var m = /Task #(\\S+) created successfully/.exec(text || '');
|
|
754
|
+
if (m && !createdIds[m[1]]) { createdIds[m[1]] = true; createdIdCount++; }
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (block.type !== 'tool_use') continue;
|
|
758
|
+
if (block.name === 'TaskCreate') {
|
|
759
|
+
created++;
|
|
760
|
+
if (block.id) pendingCreates[block.id] = true;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (block.name !== 'TaskUpdate') continue;
|
|
764
|
+
var tinput = block.input || {};
|
|
765
|
+
var tid = tinput.taskId != null ? tinput.taskId : tinput.task_id;
|
|
766
|
+
if (tid == null || typeof tinput.status !== 'string' || !tinput.status) continue;
|
|
767
|
+
latest[String(tid)] = tinput.status;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (created === 0) return null;
|
|
771
|
+
var open = created - createdIdCount;
|
|
772
|
+
Object.keys(createdIds).forEach(function(id) {
|
|
773
|
+
if (latest[id] !== 'completed' && latest[id] !== 'deleted') open++;
|
|
774
|
+
});
|
|
775
|
+
if (open > created) open = created;
|
|
776
|
+
return { created: created, closed: created - open, open: open };
|
|
777
|
+
}
|
|
778
|
+
|
|
694
779
|
switch (command) {
|
|
695
780
|
case 'check-before-agent': {
|
|
696
781
|
// Mostly advisory. The TaskCreate + memory reminders below go to stdout and
|
|
@@ -807,6 +892,28 @@ switch (command) {
|
|
|
807
892
|
writeState(s);
|
|
808
893
|
break;
|
|
809
894
|
}
|
|
895
|
+
// #1435 — the escape from the task-status gate, for work deliberately left
|
|
896
|
+
// open. Session-scoped via STATE_DEFAULTS; no prompt or edit reset touches it.
|
|
897
|
+
case 'record-tasks-acknowledged': {
|
|
898
|
+
var s = readState();
|
|
899
|
+
if (!s.tasksAcknowledged) {
|
|
900
|
+
s.tasksAcknowledged = true;
|
|
901
|
+
writeState(s);
|
|
902
|
+
}
|
|
903
|
+
// writeState swallows its own errors so a gate never crashes its hook. This
|
|
904
|
+
// is the ONLY escape from a BLOCKING gate, so an unconfirmed write would
|
|
905
|
+
// report "satisfied" and block the next 'gh pr create' anyway — confirm it.
|
|
906
|
+
if (!readState().tasksAcknowledged) {
|
|
907
|
+
process.stderr.write('Task-status gate NOT satisfied: the acknowledgement could not be persisted to\\n' +
|
|
908
|
+
STATE_FILE + '\\n' +
|
|
909
|
+
'Check that the file and its directory are writable, then run this again.\\n' +
|
|
910
|
+
'To proceed without it: set gates: task_status_gate: off in moflo.yaml.\\n');
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
process.stdout.write('Task-status gate satisfied: open tasks acknowledged as deliberately deferred.\\n' +
|
|
914
|
+
'They stay visible in the task list — this records the decision, it does not close them.\\n');
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
810
917
|
case 'record-memory-searched': {
|
|
811
918
|
var s = readState();
|
|
812
919
|
if (markMemorySearched(s)) writeState(s);
|
|
@@ -847,12 +954,35 @@ switch (command) {
|
|
|
847
954
|
// rather than per-task-transition.
|
|
848
955
|
break;
|
|
849
956
|
}
|
|
850
|
-
|
|
957
|
+
// #1434 — a mandatory write with nothing to say produces filler that displaces
|
|
958
|
+
// reusable lessons from every future bounded search. Both credits set the same
|
|
959
|
+
// flag and differ only in whether the run has something to say, so they share
|
|
960
|
+
// one case body. SYNC: mirrors bin/gate.cjs.
|
|
961
|
+
case 'record-learnings-stored':
|
|
962
|
+
case 'record-no-durable-lesson': {
|
|
851
963
|
var s = readState();
|
|
852
964
|
if (!s.learningsStored) {
|
|
853
965
|
s.learningsStored = true;
|
|
854
966
|
writeState(s);
|
|
855
967
|
}
|
|
968
|
+
if (command === 'record-no-durable-lesson') {
|
|
969
|
+
// Same reasoning as record-tasks-acknowledged: this is the ONLY escape
|
|
970
|
+
// from the BLOCKING learnings gate that needs no memory_store, so an
|
|
971
|
+
// unconfirmed write would report "satisfied" and block anyway. Verified
|
|
972
|
+
// only on this arm — record-learnings-stored fires automatically on every
|
|
973
|
+
// memory_store, where a lost write still leaves the ordinary way through.
|
|
974
|
+
if (!readState().learningsStored) {
|
|
975
|
+
process.stderr.write('Learnings gate NOT satisfied: the declaration could not be persisted to\\n' +
|
|
976
|
+
STATE_FILE + '\\n' +
|
|
977
|
+
'Check that the file and its directory are writable, then run this again.\\n' +
|
|
978
|
+
'To proceed without it: set gates: learnings_gate: false in moflo.yaml.\\n');
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
process.stdout.write(
|
|
982
|
+
'Learnings gate satisfied: no durable lesson declared for this run.\\n' +
|
|
983
|
+
'What this run did belongs in the PR body, not in memory.\\n',
|
|
984
|
+
);
|
|
985
|
+
}
|
|
856
986
|
break;
|
|
857
987
|
}
|
|
858
988
|
case 'record-test-run': {
|
|
@@ -981,11 +1111,34 @@ switch (command) {
|
|
|
981
1111
|
case 'check-before-pr': {
|
|
982
1112
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
983
1113
|
if (!/(?:^|&&\\s*|\\|\\|\\s*|;\\s*)\\s*(?:[A-Z_][A-Z0-9_]*=\\S+\\s+)*gh\\s+pr\\s+create\\b/.test(cmd)) break;
|
|
1114
|
+
// #1435 — task-status gate. Subordinate to task_create_first so both halves
|
|
1115
|
+
// of the task nag are on or off together; fail-open when the ledger is null.
|
|
1116
|
+
// State is read once for the whole case and reused below; reading it before
|
|
1117
|
+
// the ledger keeps an acknowledged run off the transcript scan entirely.
|
|
984
1118
|
var s = readState();
|
|
1119
|
+
if (config.task_create_first && config.task_status_gate !== 'off' && !s.tasksAcknowledged) {
|
|
1120
|
+
var ledger = readTaskLedger();
|
|
1121
|
+
if (ledger && ledger.open > 0) {
|
|
1122
|
+
var tally = ledger.created + ' task' + (ledger.created === 1 ? '' : 's') +
|
|
1123
|
+
' created this session, ' + ledger.open + ' still open.';
|
|
1124
|
+
var closeIt = 'Close them with TaskUpdate (status: completed), or delete the ones ' +
|
|
1125
|
+
'that no longer apply, so the run does not report done over an unfinished list.\\n';
|
|
1126
|
+
if (config.task_status_gate === 'warn') {
|
|
1127
|
+
process.stdout.write('REMINDER: ' + tally + ' ' + closeIt);
|
|
1128
|
+
} else {
|
|
1129
|
+
process.stderr.write('BLOCKED: ' + tally + '\\n' + closeIt +
|
|
1130
|
+
'Deferring them on purpose is a legitimate outcome — declare it instead of\\n' +
|
|
1131
|
+
'closing tasks that are not done: node "' + __filename + '" record-tasks-acknowledged\\n' +
|
|
1132
|
+
GATE_ORIGIN_NOTE + '\\n' +
|
|
1133
|
+
'Report instead of blocking via moflo.yaml: gates: task_status_gate: warn (or: off)\\n');
|
|
1134
|
+
process.exit(2);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
985
1138
|
var missing = [];
|
|
986
1139
|
if (config.testing_gate && !s.testsRun) missing.push('tests have not run green since the last code edit (run npm test, vitest, jest, pytest, or similar — a run whose output reports failures does not count)');
|
|
987
1140
|
if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
|
|
988
|
-
if (config.learnings_gate && !s.learningsStored) missing.push(
|
|
1141
|
+
if (config.learnings_gate && !s.learningsStored) missing.push(LEARNINGS_MISSING);
|
|
989
1142
|
if (missing.length === 0) break;
|
|
990
1143
|
process.stderr.write('BLOCKED: gh pr create requires the following before opening a PR:\\n');
|
|
991
1144
|
for (var i = 0; i < missing.length; i++) {
|
|
@@ -1251,6 +1404,34 @@ if (hookContext.tool_response && typeof hookContext.tool_response === 'object')
|
|
|
1251
1404
|
}
|
|
1252
1405
|
}
|
|
1253
1406
|
|
|
1407
|
+
// #1435 — deliver a PASSING gate's advisory to Claude, not only to the transcript.
|
|
1408
|
+
//
|
|
1409
|
+
// Claude Code shows a PreToolUse/PostToolUse hook's stdout to the user in
|
|
1410
|
+
// transcript mode and stops there; the model never sees it. So every advisory
|
|
1411
|
+
// the gates emit on the exit-0 path was invisible on exactly the runs it was
|
|
1412
|
+
// written for: #1374's open-task count, the pre-Agent TaskCreate reminder, the
|
|
1413
|
+
// namespace hint, the docs-only and simplify-auto-pass notes. They surfaced only
|
|
1414
|
+
// when some OTHER gate blocked, because the catch arm below re-routes err.stdout
|
|
1415
|
+
// to stderr — i.e. only once the PR had already been stopped for another reason.
|
|
1416
|
+
// A consumer shipped a PR over four untouched tasks with that reminder "working".
|
|
1417
|
+
//
|
|
1418
|
+
// \`hookSpecificOutput.additionalContext\` is the documented channel from a passing
|
|
1419
|
+
// tool hook into the model's context. Wrap there and nowhere else: SessionStart
|
|
1420
|
+
// and UserPromptSubmit already inject their stdout as context, so wrapping those
|
|
1421
|
+
// would rewrite a working path for nothing. An unknown or absent hook_event_name
|
|
1422
|
+
// falls back to raw stdout — byte-identical to the previous behaviour.
|
|
1423
|
+
var ADVISORY_EVENTS = { PreToolUse: true, PostToolUse: true };
|
|
1424
|
+
function emitAdvisory(text) {
|
|
1425
|
+
var event = hookContext.hook_event_name;
|
|
1426
|
+
if (!ADVISORY_EVENTS[event]) {
|
|
1427
|
+
process.stdout.write(text);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
process.stdout.write(JSON.stringify({
|
|
1431
|
+
hookSpecificOutput: { hookEventName: event, additionalContext: text },
|
|
1432
|
+
}) + '\\n');
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1254
1435
|
// Run gate.cjs with the enriched environment
|
|
1255
1436
|
var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\\/([a-z])\\//i, '$1:/');
|
|
1256
1437
|
var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
|
|
@@ -1258,7 +1439,7 @@ try {
|
|
|
1258
1439
|
var output = execFileSync('node', [gateScript, command], {
|
|
1259
1440
|
env: env, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true
|
|
1260
1441
|
});
|
|
1261
|
-
if (output.trim())
|
|
1442
|
+
if (output.trim()) emitAdvisory(output);
|
|
1262
1443
|
process.exit(0);
|
|
1263
1444
|
} catch (err) {
|
|
1264
1445
|
// gate.cjs exit(2) = block, exit(1) = also block attempt — translate both to exit(2)
|
|
@@ -215,6 +215,7 @@ gates:
|
|
|
215
215
|
task_create_first: ${gates}
|
|
216
216
|
context_tracking: ${gates}
|
|
217
217
|
verify_before_done: true # Run /verify before 'gh pr create'. On by default; opt out with false or per-run --no-verify
|
|
218
|
+
task_status_gate: block # Open tasks stop 'gh pr create'. warn → report only; off → silent
|
|
218
219
|
|
|
219
220
|
# Auto-index on session start
|
|
220
221
|
auto_index:
|
|
@@ -77,7 +77,9 @@ export const REQUIRED_HOOK_WIRING = [
|
|
|
77
77
|
// listing it here made repairHookWiring() graft the hook back into every
|
|
78
78
|
// consumer on session start, undoing the removal.
|
|
79
79
|
{ event: 'PostToolUse', pattern: 'record-learnings-stored' },
|
|
80
|
-
|
|
80
|
+
// PreToolUse, not PostToolUse: #1132 moved it so its process.exit(2) actually
|
|
81
|
+
// prevents the read instead of reporting one that already happened.
|
|
82
|
+
{ event: 'PreToolUse', pattern: 'check-bash-memory' },
|
|
81
83
|
{ event: 'PostToolUse', pattern: 'record-test-run' },
|
|
82
84
|
// #1338 follow-up — CLI half of the #952 swarm/hive init recorders. Listed
|
|
83
85
|
// here so an existing consumer picks it up via repairHookWiring on session
|
|
@@ -157,7 +159,9 @@ export const HOOK_ENTRY_MAP = {
|
|
|
157
159
|
'record-learnings-stored': { event: 'PostToolUse', matcher: '^mcp__moflo__memory_store$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-learnings-stored', timeout: 2000 } },
|
|
158
160
|
// #1171 — widened to ^(Bash|PowerShell)$ so PS reads / PS-invoked tests credit
|
|
159
161
|
// the same gates as Bash. Name kept as `check-bash-memory` for backwards compat.
|
|
160
|
-
|
|
162
|
+
// PreToolUse since #1132: grafting it under PostToolUse — as this entry did —
|
|
163
|
+
// would repair a consumer into a gate that reports a read it can no longer stop.
|
|
164
|
+
'check-bash-memory': { event: 'PreToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory', timeout: 2000 } },
|
|
161
165
|
'record-test-run': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-test-run', timeout: 2000 } },
|
|
162
166
|
// #1338 follow-up — same Bash/PowerShell PostToolUse block as record-test-run.
|
|
163
167
|
'record-bash-swarm-init': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-bash-swarm-init', timeout: 2000 } },
|
|
@@ -60,6 +60,22 @@ import { resolve, dirname, parse, join, basename } from 'node:path';
|
|
|
60
60
|
*
|
|
61
61
|
* Algorithmic twin of `bin/lib/moflo-paths.mjs:findAncestorMofloRoot()`.
|
|
62
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Does `dir` carry moflo state that makes it a project root?
|
|
65
|
+
*
|
|
66
|
+
* The single source of truth for Pass A's marker set, exported so callers that
|
|
67
|
+
* must agree with the resolver cannot drift from it. #1431 is exactly that
|
|
68
|
+
* drift: `doctor-fixes.ts` guarded its daemon reaps by looking for
|
|
69
|
+
* `.moflo/moflo.db` alone, went blind to a nested checkout carrying only the
|
|
70
|
+
* legacy `.swarm/memory.db`, and allowed the parent project's daemons to be
|
|
71
|
+
* killed. Any new marker belongs here and nowhere else.
|
|
72
|
+
*
|
|
73
|
+
* (`bin/lib/moflo-paths.mjs` holds a plain-JS twin for the launcher, which
|
|
74
|
+
* cannot import TypeScript — keep the two in step.)
|
|
75
|
+
*/
|
|
76
|
+
export function hasMofloStateMarker(dir) {
|
|
77
|
+
return existsSync(join(dir, '.moflo', 'moflo.db')) || existsSync(join(dir, '.swarm', 'memory.db'));
|
|
78
|
+
}
|
|
63
79
|
export function findAncestorMofloRoot(dir) {
|
|
64
80
|
const start = resolve(dir);
|
|
65
81
|
const fsRoot = parse(start).root;
|
|
@@ -201,7 +217,7 @@ export function findProjectRoot(opts) {
|
|
|
201
217
|
dir = dirname(dir);
|
|
202
218
|
continue;
|
|
203
219
|
}
|
|
204
|
-
if (
|
|
220
|
+
if (hasMofloStateMarker(dir)) {
|
|
205
221
|
topmostMemoryMarker = dir;
|
|
206
222
|
}
|
|
207
223
|
const parent = dirname(dir);
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.7",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"@typescript-eslint/parser": "^8.65.0",
|
|
99
99
|
"eslint": "^10.8.0",
|
|
100
100
|
"glob": "^11.1.0",
|
|
101
|
-
"moflo": "^4.12.
|
|
101
|
+
"moflo": "^4.12.6",
|
|
102
102
|
"tsx": "^4.21.0",
|
|
103
103
|
"typescript": "^5.9.3",
|
|
104
104
|
"vitest": "^4.0.0"
|