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.
@@ -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
- 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 };
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
- case 'record-learnings-stored': {
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('learnings have not been stored (call mcp__moflo__memory_store)');
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()) process.stdout.write(output);
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
- { event: 'PostToolUse', pattern: 'check-bash-memory' },
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
- 'check-bash-memory': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory', timeout: 2000 } },
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 (existsSync(join(dir, '.moflo', 'moflo.db')) || existsSync(join(dir, '.swarm', 'memory.db'))) {
220
+ if (hasMofloStateMarker(dir)) {
205
221
  topmostMemoryMarker = dir;
206
222
  }
207
223
  const parent = dirname(dir);
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.12.5';
5
+ export const VERSION = '4.12.7';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.12.5",
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.4",
101
+ "moflo": "^4.12.6",
102
102
  "tsx": "^4.21.0",
103
103
  "typescript": "^5.9.3",
104
104
  "vitest": "^4.0.0"