moflo 4.12.6 → 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.
@@ -170,8 +170,23 @@ npx flo memory store --namespace patterns --key "brief-descriptive-key" --value
170
170
  | `patterns` | Solutions to tricky bugs, patterns that worked, gotchas, workarounds |
171
171
  | `learnings` | Architectural choices, user-stated decisions, post-mortem insights (`knowledge` is a deprecated alias — writes auto-redirect) |
172
172
 
173
+ **Every entry must clear one bar: would it help a future session working on a *different* task?**
174
+ If not, it does not go in memory. `memory_search` returns a **bounded** result set, so a non-durable
175
+ entry does not merely waste space — it permanently displaces a reusable lesson from every future
176
+ search.
177
+
178
+ **Never store a summary of the run that just happened.** What you changed, which files you touched,
179
+ which tests you ran, and what you decided for this one ticket are git history and belong in the
180
+ commit or PR body. That content is applicable exactly once, so it can only ever crowd out something
181
+ that is applicable repeatedly.
182
+
173
183
  **Skip** generic summaries of retrieved guidance, restated rules, and trivial file-location notes — those waste retrieval bandwidth on every future search.
174
184
 
185
+ **A gate never justifies a write.** When a gate demands a memory write and the run produced nothing
186
+ durable, declare that instead of inventing something — a mandatory write with nothing to say
187
+ produces filler by construction. `/flo`'s learnings gate takes
188
+ `node .claude/helpers/gate.cjs record-no-durable-lesson` for exactly this case.
189
+
175
190
  ---
176
191
 
177
192
  ## See Also
@@ -119,7 +119,15 @@ TaskList() // Shows what's now unblocked
119
119
  TaskUpdate({ taskId: "2", status: "in_progress" }) // Next agent starts
120
120
  ```
121
121
 
122
- Close every task you open. moflo's PR gate reads the session transcript on `gh pr create` and prints `N tasks created this session, M still open` an unclosed list reports as unfinished work on the PR. Mark tasks that no longer apply `status: "deleted"`; that closes the loop exactly like `completed`.
122
+ **Close every task you open — `gh pr create` is blocked until you do.** moflo's PR gate reads the session transcript, counts the tasks this session opened against the ones it closed, and stops the PR while any remain open. A stale list reports work as not-started that is about to merge.
123
+
124
+ | Situation | Action |
125
+ |-----------|--------|
126
+ | The work is done | `TaskUpdate({ taskId: "1", status: "completed" })` |
127
+ | The task no longer applies | `TaskUpdate({ taskId: "1", status: "deleted" })` — closes the loop exactly like `completed` |
128
+ | The work is deliberately deferred past this PR | Run the acknowledgement command the block message prints — it credits the gate and leaves the tasks open |
129
+
130
+ Never mark a task `completed` to clear the gate. The acknowledgement path exists so the honest outcome costs one command; a false `completed` costs the user their only accurate view of what shipped. Set `gates: task_status_gate: warn` in `moflo.yaml` to report instead of block, or `off` to silence it.
123
131
 
124
132
  ---
125
133
 
@@ -132,7 +140,7 @@ Close every task you open. moflo's PR gate reads the session transcript on `gh p
132
140
  | 3 | Mark `completed` when results return |
133
141
  | 4 | Use `TaskList` to monitor what's unblocked |
134
142
  | 5 | Synthesize all agent outputs before proceeding |
135
- | 6 | Store learnings in memory after completion |
143
+ | 6 | Store a learning after completion **only if** it would help a different future task — a run summary belongs in the PR body, not memory |
136
144
 
137
145
  ---
138
146
 
@@ -114,6 +114,34 @@ if (hookContext.tool_response && typeof hookContext.tool_response === 'object')
114
114
  }
115
115
  }
116
116
 
117
+ // #1435 — deliver a PASSING gate's advisory to Claude, not only to the transcript.
118
+ //
119
+ // Claude Code shows a PreToolUse/PostToolUse hook's stdout to the user in
120
+ // transcript mode and stops there; the model never sees it. So every advisory
121
+ // the gates emit on the exit-0 path was invisible on exactly the runs it was
122
+ // written for: #1374's open-task count, the pre-Agent TaskCreate reminder, the
123
+ // namespace hint, the docs-only and simplify-auto-pass notes. They surfaced only
124
+ // when some OTHER gate blocked, because the catch arm below re-routes err.stdout
125
+ // to stderr — i.e. only once the PR had already been stopped for another reason.
126
+ // A consumer shipped a PR over four untouched tasks with that reminder "working".
127
+ //
128
+ // `hookSpecificOutput.additionalContext` is the documented channel from a passing
129
+ // tool hook into the model's context. Wrap there and nowhere else: SessionStart
130
+ // and UserPromptSubmit already inject their stdout as context, so wrapping those
131
+ // would rewrite a working path for nothing. An unknown or absent hook_event_name
132
+ // falls back to raw stdout — byte-identical to the previous behaviour.
133
+ var ADVISORY_EVENTS = { PreToolUse: true, PostToolUse: true };
134
+ function emitAdvisory(text) {
135
+ var event = hookContext.hook_event_name;
136
+ if (!ADVISORY_EVENTS[event]) {
137
+ process.stdout.write(text);
138
+ return;
139
+ }
140
+ process.stdout.write(JSON.stringify({
141
+ hookSpecificOutput: { hookEventName: event, additionalContext: text },
142
+ }) + '\n');
143
+ }
144
+
117
145
  // Run gate.cjs with the enriched environment
118
146
  var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
119
147
  var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
@@ -121,7 +149,7 @@ try {
121
149
  var output = execFileSync('node', [gateScript, command], {
122
150
  env: env, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true
123
151
  });
124
- if (output.trim()) process.stdout.write(output);
152
+ if (output.trim()) emitAdvisory(output);
125
153
  process.exit(0);
126
154
  } catch (err) {
127
155
  // gate.cjs exit(2) = block, exit(1) = also block attempt — translate both to exit(2)
@@ -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, 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, 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
@@ -70,9 +70,24 @@ function loadGateConfig() {
70
70
  // ships a real /verify skill and has /flo delegate to it, so leaving it off by
71
71
  // default would make the default /flo run silently skip the acceptance check.
72
72
  // Disable per-project with `verify_before_done: false` or per-run `--no-verify`.
73
- 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 };
73
+ // task_status_gate is a MODE, not a boolean: 'block' | 'warn' | 'off' (#1435).
74
+ // #1374 shipped the open-task count as a warn-only stdout line, and a consumer
75
+ // still shipped a PR over four untouched tasks — a reminder that survived ten
76
+ // consecutive ignores in one session is not a control. Blocking is the default
77
+ // because the honest "these stay open on purpose" outcome is one command away
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' };
74
80
  var content = MOFLO_YAML;
75
81
  if (content) {
82
+ // Boolean forms are accepted so this key reads like every other gate in the
83
+ // block: `false` is the same opt-out `testing_gate: false` is, `true` means
84
+ // enforce. Anything unrecognised falls through to the default rather than
85
+ // silently disabling the gate — a typo must not be a stealth opt-out.
86
+ var tsg = /task_status_gate:\s*['"]?(block|warn|off|false|true)['"]?/i.exec(content);
87
+ if (tsg) {
88
+ var mode = tsg[1].toLowerCase();
89
+ defaults.task_status_gate = mode === 'false' ? 'off' : mode === 'true' ? 'block' : mode;
90
+ }
76
91
  if (/memory_first:\s*false/i.test(content)) defaults.memory_first = false;
77
92
  if (/task_create_first:\s*false/i.test(content)) defaults.task_create_first = false;
78
93
  if (/context_tracking:\s*false/i.test(content)) defaults.context_tracking = false;
@@ -172,6 +187,25 @@ var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule
172
187
  // /verify's Step 5 memory_store carries the verdict AND stamps learnings, which
173
188
  // is why learnings has no separate step here.
174
189
  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';
190
+ // #1434 — the old text ('learnings have not been stored (call memory_store)')
191
+ // named the mechanism but no quality bar, so the cheapest way past it was a
192
+ // summary of the run — audit exhaust that displaces reusable lessons from every
193
+ // future bounded search. Name the bar AND the no-write path here: an escape
194
+ // hatch nobody can find is not an escape hatch (see #1332's gate deadlock).
195
+ //
196
+ // The escape command is built from __filename, not written as a relative path.
197
+ // The caller is the model typing into a Bash tool, NOT a hook: $CLAUDE_PROJECT_DIR
198
+ // is unset there (so the settings.json form would expand to "/.claude/..."), and a
199
+ // bare `.claude/helpers/gate.cjs` breaks from any cwd but the project root. The
200
+ // running script's own absolute path is correct on every OS and from any cwd;
201
+ // double quotes carry Windows separators and spaces through the shell.
202
+ var LEARNINGS_MISSING =
203
+ 'no durable lesson recorded. A lesson qualifies only if it would help a future session ' +
204
+ 'working on a DIFFERENT task — a reusable pattern, a trap, a decision + rationale. ' +
205
+ 'Store one with mcp__moflo__memory_store (namespace "learnings"; use "patterns" for a ' +
206
+ 'reusable code shape). What THIS run changed is git history — it belongs in the PR body, ' +
207
+ 'not in memory. If this run taught nothing new, say so instead of inventing one: ' +
208
+ 'node "' + __filename + '" record-no-durable-lesson';
175
209
  var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
176
210
  // #1338 — Claude Code spawns stdio MCP servers once at session start and never
177
211
  // respawns them, so a session can outlive its moflo MCP connection. Naming only
@@ -1355,6 +1389,41 @@ switch (command) {
1355
1389
  writeState(s);
1356
1390
  break;
1357
1391
  }
1392
+ // #1435 — the escape from the task-status gate, for work deliberately left
1393
+ // open. Session-scoped like `learningsStored`: it lives in STATE_DEFAULTS, so
1394
+ // session-reset clears it, and neither applyPromptStateReset nor
1395
+ // reset-edit-gates touches it — a decision the user made about the task list
1396
+ // is not invalidated by the next prompt or the next source edit.
1397
+ //
1398
+ // A plain flag, not a count. The command is typed by the model into a Bash
1399
+ // tool, where HOOK_TRANSCRIPT_PATH is unset (it is forwarded by gate-hook.mjs
1400
+ // from the hook payload and exists only inside a hook), so this process cannot
1401
+ // read the ledger to record WHICH tasks were acknowledged even if it wanted to.
1402
+ case 'record-tasks-acknowledged': {
1403
+ var s = readState();
1404
+ if (!s.tasksAcknowledged) {
1405
+ s.tasksAcknowledged = true;
1406
+ writeState(s);
1407
+ }
1408
+ // writeState swallows its own errors by design — a gate must never crash the
1409
+ // hook it runs in. That was harmless while every recorder was advisory. This
1410
+ // one is the ONLY escape from a BLOCKING gate, so a lost write would report
1411
+ // "satisfied" and then block the very next `gh pr create` with nothing said
1412
+ // about why: #1332's deadlock shape exactly. Confirm it landed before
1413
+ // claiming it did, and name the file and the way out when it did not.
1414
+ if (!readState().tasksAcknowledged) {
1415
+ process.stderr.write(
1416
+ 'Task-status gate NOT satisfied: the acknowledgement could not be persisted to\n' +
1417
+ STATE_FILE + '\n' +
1418
+ 'Check that the file and its directory are writable, then run this again.\n' +
1419
+ 'To proceed without it: set gates: task_status_gate: off in moflo.yaml.\n');
1420
+ process.exit(1);
1421
+ }
1422
+ process.stdout.write(
1423
+ 'Task-status gate satisfied: open tasks acknowledged as deliberately deferred.\n' +
1424
+ 'They stay visible in the task list — this records the decision, it does not close them.\n');
1425
+ break;
1426
+ }
1358
1427
  case 'record-memory-searched': {
1359
1428
  var s = readState();
1360
1429
  if (markMemorySearched(s)) writeState(s);
@@ -1417,12 +1486,48 @@ switch (command) {
1417
1486
  // Why it does nothing: see applyPromptStateReset().
1418
1487
  break;
1419
1488
  }
1420
- case 'record-learnings-stored': {
1489
+ // #1434 — the gate demanded one memory_store per run whether or not the run
1490
+ // produced a reusable lesson, and a mandatory write with nothing to say
1491
+ // produces filler: a summary of this ticket, this commit, applicable never
1492
+ // again. memory_search returns a bounded set, so each of those displaces a
1493
+ // real lesson from every future search — the cost is retrieval quality, not
1494
+ // disk. Declaring "nothing durable here" is the honest outcome of a run that
1495
+ // learned nothing new, so it has to be reachable without a write; otherwise
1496
+ // the cheapest way past the gate stays the filler write.
1497
+ //
1498
+ // Both credits set the same flag; they differ only in whether the run has
1499
+ // something to say. Sharing the case body keeps that single write in one
1500
+ // place across all three copies of this file.
1501
+ case 'record-learnings-stored':
1502
+ case 'record-no-durable-lesson': {
1421
1503
  var s = readState();
1422
1504
  if (!s.learningsStored) {
1423
1505
  s.learningsStored = true;
1424
1506
  writeState(s);
1425
1507
  }
1508
+ if (command === 'record-no-durable-lesson') {
1509
+ // Same reasoning as record-tasks-acknowledged above: writeState swallows
1510
+ // its own errors so a gate never crashes the hook it runs in, and this is
1511
+ // the ONLY escape from the BLOCKING learnings gate that does not require a
1512
+ // memory_store. A lost write here would print "satisfied" and then block
1513
+ // the next `gh pr create` with nothing said about why — #1332's deadlock.
1514
+ // Verified only on this arm: record-learnings-stored is fired
1515
+ // automatically by the PostToolUse hook on every memory_store, where a
1516
+ // failed write leaves the gate closed but the run still has the ordinary
1517
+ // way through, and a diagnostic on every store would be noise.
1518
+ if (!readState().learningsStored) {
1519
+ process.stderr.write(
1520
+ 'Learnings gate NOT satisfied: the declaration could not be persisted to\n' +
1521
+ STATE_FILE + '\n' +
1522
+ 'Check that the file and its directory are writable, then run this again.\n' +
1523
+ 'To proceed without it: set gates: learnings_gate: false in moflo.yaml.\n');
1524
+ process.exit(1);
1525
+ }
1526
+ process.stdout.write(
1527
+ 'Learnings gate satisfied: no durable lesson declared for this run.\n' +
1528
+ 'What this run did belongs in the PR body, not in memory.\n',
1529
+ );
1530
+ }
1426
1531
  break;
1427
1532
  }
1428
1533
  case 'record-test-run': {
@@ -1645,25 +1750,46 @@ switch (command) {
1645
1750
  // chained, piped, parenthesised, and multi-line shapes (#1410).
1646
1751
  var cmd = process.env.TOOL_INPUT_command || '';
1647
1752
  if (!isPrCreateCommand(cmd)) break;
1648
- // #1374 close the loop the TaskCreate reminder opens. Advisory: stdout,
1649
- // no exit, and worded as a reminder, because a message may only claim to
1650
- // block when it blocks (#1326). An open task list is a reporting failure,
1651
- // not a quality failure — blocking the PR on it would be a new deadlock.
1753
+ // #1374 opened this loop; #1435 closes it. The count itself is unchanged —
1754
+ // what changed is that it now has teeth and, in warn mode, a delivery path.
1652
1755
  //
1653
1756
  // Deliberately ABOVE the no-source exemption below: a docs-only PR can
1654
1757
  // abandon a list exactly like a source PR can, and the exemption is about
1655
1758
  // testing/simplify/learnings, not about whether the run told the user what
1656
- // it did. Gated on the same `task_create_first` flag as the reminder itself
1657
- // so the two halves are always consistent a project that turned the nag
1658
- // off is not then nagged about the other end of it.
1659
- if (config.task_create_first) {
1759
+ // it did. It also exits on its own rather than joining `missing` below, for
1760
+ // the same reason `missing` is unreachable on an exempt diff.
1761
+ //
1762
+ // Gated on the same `task_create_first` flag as the reminder itself so the
1763
+ // two halves are always consistent: a project that turned the nag off is
1764
+ // not then blocked about the other end of it.
1765
+ //
1766
+ // Fail-open is load-bearing. readTaskLedger() returns null on a missing,
1767
+ // oversized, or unreadable transcript and on a session with no TaskCreate at
1768
+ // all, and null must never block — a gate that stops PRs because it could
1769
+ // not read a file is worse than the reporting gap it is closing.
1770
+ //
1771
+ // State is read ONCE for the whole case, here — the pre-PR gate logic below
1772
+ // reuses it and nothing writes in between. Reading it before the ledger also
1773
+ // means an already-acknowledged run never pays for the transcript scan.
1774
+ var s = readState();
1775
+ if (config.task_create_first && config.task_status_gate !== 'off' && !s.tasksAcknowledged) {
1660
1776
  var ledger = readTaskLedger();
1661
1777
  if (ledger && ledger.open > 0) {
1662
- process.stdout.write(
1663
- 'REMINDER: ' + ledger.created + ' task' + (ledger.created === 1 ? '' : 's') +
1664
- ' created this session, ' + ledger.open + ' still open. Close them with ' +
1665
- 'TaskUpdate (status: completed), or delete the ones that no longer apply, ' +
1666
- 'so the run does not report done over an unfinished list.\n');
1778
+ var tally = ledger.created + ' task' + (ledger.created === 1 ? '' : 's') +
1779
+ ' created this session, ' + ledger.open + ' still open.';
1780
+ var closeIt = 'Close them with TaskUpdate (status: completed), or delete the ones ' +
1781
+ 'that no longer apply, so the run does not report done over an unfinished list.\n';
1782
+ if (config.task_status_gate === 'warn') {
1783
+ process.stdout.write('REMINDER: ' + tally + ' ' + closeIt);
1784
+ } else {
1785
+ process.stderr.write(
1786
+ 'BLOCKED: ' + tally + '\n' + closeIt +
1787
+ 'Deferring them on purpose is a legitimate outcome — declare it instead of\n' +
1788
+ 'closing tasks that are not done: node "' + __filename + '" record-tasks-acknowledged\n' +
1789
+ GATE_ORIGIN_NOTE + '\n' +
1790
+ 'Report instead of blocking via moflo.yaml: gates: task_status_gate: warn (or: off)\n');
1791
+ process.exit(2);
1792
+ }
1667
1793
  }
1668
1794
  }
1669
1795
  // No-source-files exemption (#1176, supersedes the original docs-only path).
@@ -1688,7 +1814,6 @@ switch (command) {
1688
1814
  break;
1689
1815
  }
1690
1816
  }
1691
- var s = readState();
1692
1817
  // Expire any credit whose fingerprint no longer matches the code before
1693
1818
  // reading the flags. This is what catches the mutations reset-edit-gates
1694
1819
  // structurally cannot see — Bash writes, git checkout/pull/merge, and the
@@ -1721,7 +1846,7 @@ switch (command) {
1721
1846
  var missing = [];
1722
1847
  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)');
1723
1848
  if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
1724
- if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
1849
+ if (config.learnings_gate && !s.learningsStored) missing.push(LEARNINGS_MISSING);
1725
1850
  if (missing.length === 0) break;
1726
1851
  process.stderr.write('BLOCKED: gh pr create requires the following before opening a PR:\n');
1727
1852
  for (var i = 0; i < missing.length; i++) {
@@ -106,10 +106,12 @@ research → ticket → execute → tests → simplify → learnings → pr
106
106
  | Execute | Assign issue, create branch, implement |
107
107
  | Tests | Run unit + integration + E2E |
108
108
  | Simplify | Run `/flo-simplify` on changed code |
109
- | Learnings | Call `mcp__moflo__memory_store` with what was learned |
110
- | PR | Open the PR, update issue status |
109
+ | Learnings | Store a **durable** lesson one that helps a *different* future task — or declare there is none. A run summary belongs in the PR body, never in memory |
110
+ | PR | Close every task this run opened, then open the PR and update issue status |
111
111
 
112
- The tests, simplify, and learnings steps are enforced by hooks. `gh pr create` is blocked by `check-before-pr` until each has run in the current session. Skill text describes the flow; the gates handle compliance.
112
+ The tests, simplify, learnings, and task-closure steps are enforced by hooks. `gh pr create` is blocked by `check-before-pr` until each has run in the current session. Skill text describes the flow; the gates handle compliance.
113
+
114
+ **Close the task list before the PR, and close it honestly.** `TaskUpdate` each task `completed`, or `deleted` if it no longer applies. Work deliberately left open is a legitimate outcome — the block message prints a one-command acknowledgement for it. Never mark a task `completed` just to clear the gate: the list is the user's only view of what actually shipped.
113
115
 
114
116
  ## Companion files
115
117
 
@@ -270,6 +272,6 @@ Full mode runs end-to-end without further prompts.
270
272
  6. Run `/flo-simplify` on changed code; rerun tests if it edits — `./phases.md` Phase 4.5
271
273
  7. Commit — `./phases.md` Phase 5.1
272
274
  8. **Verify — default, unless `--no-verify`** (`verifyMode`, always on under `sddMode`): delegate to the `/verify` skill — `Skill({ skill: "verify" })`. It checks the change against the acceptance criteria, reusing Phase 4's tests (no double verify) and recording its own outcome; invoking it satisfies the verify-before-done gate. Mechanics live in `.claude/skills/verify/SKILL.md`; trigger/flow in `./phases.md` Phase 5.1b.
273
- 9. Store learnings via `mcp__moflo__memory_store` — `./phases.md` Phase 5.2
275
+ 9. Record a durable lesson via `mcp__moflo__memory_store`, or declare there is none — `./phases.md` Phase 5.2
274
276
  10. Open PR, update issue status — `./phases.md` Phases 5.3–5.4
275
277
  11. **If `mergeMode`:** await the PR's merge preconditions and merge it (native `--auto` preferred, else poll-then-merge) — `./phases.md` Phase 5.3b
@@ -173,21 +173,73 @@ repository and their git history is permanent — moflo does not sign their comm
173
173
 
174
174
  **When it runs:** by default (`verify_before_done` now defaults true) and always under `--sdd`; `--no-verify` skips it for one run. See `./sdd.md` for triggers and `.claude/skills/verify/SKILL.md` for how verification is performed.
175
175
 
176
- ### 5.2 Store learnings
177
- Before opening the PR, call `mcp__moflo__memory_store` with what was learned. The `check-before-pr` gate blocks `gh pr create` until this has run.
176
+ ### 5.2 Record a durable lesson — or declare there isn't one
177
+
178
+ **A run summary is not a learning.** What this run changed — files touched, the fix applied,
179
+ the decision taken for this ticket — is git history and belongs in the PR body. Writing it to
180
+ memory instead is audit exhaust: one ticket, one commit, applicable never again. `memory_search`
181
+ returns a **bounded** result set, so every such entry permanently displaces a reusable lesson
182
+ from every future search. That cost is retrieval quality, not disk.
183
+
184
+ Apply the **durability bar** — the same one `/meditate` uses:
185
+
186
+ > *Would this help a future session working on a **different** task?*
187
+
188
+ | Store it | Where | Skip it — it is not a lesson |
189
+ |----------|-------|------------------------------|
190
+ | A reusable pattern: "for X, do Y because Z" | `learnings` | "Fixed #<n> by editing `<file>`" → PR body |
191
+ | A trap: "W silently fails when V" | `learnings` | "Added tests for Z" → the test records itself |
192
+ | A decision + rationale future work must honor | `learnings` | "Ran tests, they passed" → the run records itself |
193
+ | A reusable code shape this repo should copy | `patterns` | Restating an existing CLAUDE.md / guidance rule |
194
+
195
+ Key the entry on the **symptom or rule**, not the ticket. A future session hits the symptom
196
+ without knowing the issue number, so a ticket-shaped key is unfindable exactly when it is needed.
197
+ (An incident narrative may carry the issue as provenance — `1145-daemon-port-collision-fix` — but
198
+ the searchable words must still be the symptom.) A real entry from this repo, abridged:
178
199
 
179
200
  ```
180
201
  mcp__moflo__memory_store:
181
- key: "pattern:<topic>"
182
- namespace: "patterns"
183
- value: "<files changed, patterns used, decisions made>"
184
- tags: ["<relevant-tags>"]
202
+ key: "wal-defeats-db-mtime-as-change-signal"
203
+ namespace: "learnings"
204
+ value: "In WAL mode SQLite writes land in the -wal sidecar, so the .db file's mtime
205
+ stops advancing on write. Any cache or staleness check keyed on db mtime
206
+ silently never invalidates. Hash the schema+row count, or stat the -wal too."
207
+ tags: ["sqlite", "caching", "gotcha"]
185
208
  ```
186
209
 
187
- This step happens before `gh pr create`, not after.
210
+ Note what makes it durable: it states a rule that holds beyond the ticket that discovered it,
211
+ and a future session hitting a stale-cache symptom finds it without knowing the issue existed.
212
+
213
+ **If this run taught nothing new, do not invent something.** Declare it and move on — the
214
+ `check-before-pr` gate accepts the declaration in place of a write:
215
+
216
+ ```bash
217
+ node .claude/helpers/gate.cjs record-no-durable-lesson # from the project root
218
+ ```
219
+
220
+ (When the gate has already blocked, it prints this command with an absolute path — use that
221
+ verbatim and the cwd stops mattering.)
222
+
223
+ Most runs land here, and that is the expected outcome: a routine fix in a well-understood area
224
+ produces no transferable lesson. One real lesson beats five manufactured ones.
225
+
226
+ Either path satisfies the gate, and either way it happens before `gh pr create`, not after.
227
+ Note that when `/verify` ran (5.1b, the default), its verdict write has **already** satisfied
228
+ this gate — so reach for `memory_store` here only when you genuinely have a lesson, never to
229
+ unblock the PR.
188
230
 
189
231
  ### 5.3 Create the PR
190
232
 
233
+ **Close the task list first.** `TaskUpdate` every task this run opened to `completed`, or to `deleted` where it no longer applies; `check-before-pr` blocks `gh pr create` while any stay open, because a list that still reads *pending* over merged work is the user's only view of what shipped.
234
+
235
+ | Task state at PR time | Action |
236
+ |-----------------------|--------|
237
+ | Work finished | `TaskUpdate({ taskId: "<id>", status: "completed" })` |
238
+ | No longer applies | `TaskUpdate({ taskId: "<id>", status: "deleted" })` |
239
+ | Deliberately deferred past this PR | Run the acknowledgement command printed in the block message — it credits the gate and leaves the tasks open |
240
+
241
+ Never mark a task `completed` to clear the gate. Declaring the deferral takes one command and keeps the list true.
242
+
191
243
  In epic mode (`--epic-branch` set): skip the PR. The commit from 5.1 (with `Closes #<issue-number>`) is enough; the epic orchestrator handles the consolidated PR and final push. Skip pushing too.
192
244
 
193
245
  Otherwise:
package/README.md CHANGED
@@ -291,6 +291,7 @@ MoFlo installs Claude Code hooks that run on every tool call. Together, these ga
291
291
  |------|-----------------|------------------|----------------|
292
292
  | **Memory-first** | Claude must search the memory database before using Glob, Grep, or Read on guidance files | Before every Glob/Grep call, and before Read calls targeting `.claude/guidance/` | Prevents the AI from re-exploring files it (or a previous session) already indexed. Forces it to check what it knows first, saving tokens and context window. |
293
293
  | **TaskCreate-first** | Claude must call TaskCreate before spawning sub-agents via the Task tool | Before every Task (agent spawn) call | Ensures every piece of delegated work is tracked. Prevents runaway agent proliferation where Claude spawns agents without a clear plan. |
294
+ | **Task-status** *(blocks by default)* | The task list Claude opened must reflect reality before the PR — every task closed, deleted, or explicitly acknowledged as deferred | Before `gh pr create`, whenever the session opened tasks and left some open | A stale task list is worse than none: it reports as not-started work that is about to merge. Every other step here is gated on an artifact; this is the one that gates what the *user is shown*. Deliberate deferrals stay legitimate — the block prints a one-command acknowledgement. Set `task_status_gate: warn` to report without blocking, or `off`; `task_create_first: false` disables both halves. |
294
295
  | **Context tracking** | Tracks conversation length and warns about context depletion | On every user prompt (UserPromptSubmit hook) | As conversations grow, AI quality degrades. MoFlo tracks interaction count and assigns a bracket (FRESH → MODERATE → DEPLETED → CRITICAL), advising Claude to checkpoint progress or start a fresh session before quality drops. |
295
296
  | **Routing** | Analyzes each prompt and recommends the optimal agent type and model tier | On every user prompt (UserPromptSubmit hook) | Saves cost by suggesting haiku for simple tasks, sonnet for moderate ones, opus for complex reasoning — without you having to think about model selection. |
296
297
  | **Verify-before-done** *(on by default)* | Claude must verify the change end-to-end (the `/verify` skill) before `gh pr create` | Before `gh pr create` (docs-only diffs exempt) | Enforces "prove it works before done." On by default since #1294 (`/flo` delegates to `/verify` and reuses its test run — no double verify). Opt out with `verify_before_done: false` or `--no-verify`. Pairs with the [Spec-Driven Development](#spec-driven-development-sdd) cycle, which gives verification its acceptance criteria. |
@@ -321,7 +322,8 @@ All gates are configurable in `moflo.yaml`:
321
322
  ```yaml
322
323
  gates:
323
324
  memory_first: true # Set to false to disable memory-first enforcement
324
- task_create_first: true # Set to false to disable TaskCreate enforcement
325
+ task_create_first: true # Set to false to disable TaskCreate enforcement (disables task_status_gate too)
326
+ task_status_gate: block # Open tasks block `gh pr create`; warn → report only; off → silent
325
327
  context_tracking: true # Set to false to disable context bracket warnings
326
328
  verify_before_done: true # On by default (#1294); set false to skip /verify before `gh pr create`
327
329
  ```
@@ -916,6 +918,7 @@ tests:
916
918
  gates:
917
919
  memory_first: true # Must search memory before file exploration
918
920
  task_create_first: true # Must TaskCreate before Agent tool
921
+ task_status_gate: block # Open tasks block `gh pr create`; warn → report only; off → silent
919
922
  context_tracking: true # Track context window depletion
920
923
  verify_before_done: true # On by default (#1294); /verify before `gh pr create` (unless --no-verify). false to disable
921
924
 
package/bin/gate-hook.mjs CHANGED
@@ -114,6 +114,34 @@ if (hookContext.tool_response && typeof hookContext.tool_response === 'object')
114
114
  }
115
115
  }
116
116
 
117
+ // #1435 — deliver a PASSING gate's advisory to Claude, not only to the transcript.
118
+ //
119
+ // Claude Code shows a PreToolUse/PostToolUse hook's stdout to the user in
120
+ // transcript mode and stops there; the model never sees it. So every advisory
121
+ // the gates emit on the exit-0 path was invisible on exactly the runs it was
122
+ // written for: #1374's open-task count, the pre-Agent TaskCreate reminder, the
123
+ // namespace hint, the docs-only and simplify-auto-pass notes. They surfaced only
124
+ // when some OTHER gate blocked, because the catch arm below re-routes err.stdout
125
+ // to stderr — i.e. only once the PR had already been stopped for another reason.
126
+ // A consumer shipped a PR over four untouched tasks with that reminder "working".
127
+ //
128
+ // `hookSpecificOutput.additionalContext` is the documented channel from a passing
129
+ // tool hook into the model's context. Wrap there and nowhere else: SessionStart
130
+ // and UserPromptSubmit already inject their stdout as context, so wrapping those
131
+ // would rewrite a working path for nothing. An unknown or absent hook_event_name
132
+ // falls back to raw stdout — byte-identical to the previous behaviour.
133
+ var ADVISORY_EVENTS = { PreToolUse: true, PostToolUse: true };
134
+ function emitAdvisory(text) {
135
+ var event = hookContext.hook_event_name;
136
+ if (!ADVISORY_EVENTS[event]) {
137
+ process.stdout.write(text);
138
+ return;
139
+ }
140
+ process.stdout.write(JSON.stringify({
141
+ hookSpecificOutput: { hookEventName: event, additionalContext: text },
142
+ }) + '\n');
143
+ }
144
+
117
145
  // Run gate.cjs with the enriched environment
118
146
  var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
119
147
  var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
@@ -121,7 +149,7 @@ try {
121
149
  var output = execFileSync('node', [gateScript, command], {
122
150
  env: env, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true
123
151
  });
124
- if (output.trim()) process.stdout.write(output);
152
+ if (output.trim()) emitAdvisory(output);
125
153
  process.exit(0);
126
154
  } catch (err) {
127
155
  // gate.cjs exit(2) = block, exit(1) = also block attempt — translate both to exit(2)
package/bin/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, 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, 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
@@ -70,9 +70,24 @@ function loadGateConfig() {
70
70
  // ships a real /verify skill and has /flo delegate to it, so leaving it off by
71
71
  // default would make the default /flo run silently skip the acceptance check.
72
72
  // Disable per-project with `verify_before_done: false` or per-run `--no-verify`.
73
- 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 };
73
+ // task_status_gate is a MODE, not a boolean: 'block' | 'warn' | 'off' (#1435).
74
+ // #1374 shipped the open-task count as a warn-only stdout line, and a consumer
75
+ // still shipped a PR over four untouched tasks — a reminder that survived ten
76
+ // consecutive ignores in one session is not a control. Blocking is the default
77
+ // because the honest "these stay open on purpose" outcome is one command away
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' };
74
80
  var content = MOFLO_YAML;
75
81
  if (content) {
82
+ // Boolean forms are accepted so this key reads like every other gate in the
83
+ // block: `false` is the same opt-out `testing_gate: false` is, `true` means
84
+ // enforce. Anything unrecognised falls through to the default rather than
85
+ // silently disabling the gate — a typo must not be a stealth opt-out.
86
+ var tsg = /task_status_gate:\s*['"]?(block|warn|off|false|true)['"]?/i.exec(content);
87
+ if (tsg) {
88
+ var mode = tsg[1].toLowerCase();
89
+ defaults.task_status_gate = mode === 'false' ? 'off' : mode === 'true' ? 'block' : mode;
90
+ }
76
91
  if (/memory_first:\s*false/i.test(content)) defaults.memory_first = false;
77
92
  if (/task_create_first:\s*false/i.test(content)) defaults.task_create_first = false;
78
93
  if (/context_tracking:\s*false/i.test(content)) defaults.context_tracking = false;
@@ -172,6 +187,25 @@ var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule
172
187
  // /verify's Step 5 memory_store carries the verdict AND stamps learnings, which
173
188
  // is why learnings has no separate step here.
174
189
  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';
190
+ // #1434 — the old text ('learnings have not been stored (call memory_store)')
191
+ // named the mechanism but no quality bar, so the cheapest way past it was a
192
+ // summary of the run — audit exhaust that displaces reusable lessons from every
193
+ // future bounded search. Name the bar AND the no-write path here: an escape
194
+ // hatch nobody can find is not an escape hatch (see #1332's gate deadlock).
195
+ //
196
+ // The escape command is built from __filename, not written as a relative path.
197
+ // The caller is the model typing into a Bash tool, NOT a hook: $CLAUDE_PROJECT_DIR
198
+ // is unset there (so the settings.json form would expand to "/.claude/..."), and a
199
+ // bare `.claude/helpers/gate.cjs` breaks from any cwd but the project root. The
200
+ // running script's own absolute path is correct on every OS and from any cwd;
201
+ // double quotes carry Windows separators and spaces through the shell.
202
+ var LEARNINGS_MISSING =
203
+ 'no durable lesson recorded. A lesson qualifies only if it would help a future session ' +
204
+ 'working on a DIFFERENT task — a reusable pattern, a trap, a decision + rationale. ' +
205
+ 'Store one with mcp__moflo__memory_store (namespace "learnings"; use "patterns" for a ' +
206
+ 'reusable code shape). What THIS run changed is git history — it belongs in the PR body, ' +
207
+ 'not in memory. If this run taught nothing new, say so instead of inventing one: ' +
208
+ 'node "' + __filename + '" record-no-durable-lesson';
175
209
  var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
176
210
  // #1338 — Claude Code spawns stdio MCP servers once at session start and never
177
211
  // respawns them, so a session can outlive its moflo MCP connection. Naming only
@@ -1355,6 +1389,41 @@ switch (command) {
1355
1389
  writeState(s);
1356
1390
  break;
1357
1391
  }
1392
+ // #1435 — the escape from the task-status gate, for work deliberately left
1393
+ // open. Session-scoped like `learningsStored`: it lives in STATE_DEFAULTS, so
1394
+ // session-reset clears it, and neither applyPromptStateReset nor
1395
+ // reset-edit-gates touches it — a decision the user made about the task list
1396
+ // is not invalidated by the next prompt or the next source edit.
1397
+ //
1398
+ // A plain flag, not a count. The command is typed by the model into a Bash
1399
+ // tool, where HOOK_TRANSCRIPT_PATH is unset (it is forwarded by gate-hook.mjs
1400
+ // from the hook payload and exists only inside a hook), so this process cannot
1401
+ // read the ledger to record WHICH tasks were acknowledged even if it wanted to.
1402
+ case 'record-tasks-acknowledged': {
1403
+ var s = readState();
1404
+ if (!s.tasksAcknowledged) {
1405
+ s.tasksAcknowledged = true;
1406
+ writeState(s);
1407
+ }
1408
+ // writeState swallows its own errors by design — a gate must never crash the
1409
+ // hook it runs in. That was harmless while every recorder was advisory. This
1410
+ // one is the ONLY escape from a BLOCKING gate, so a lost write would report
1411
+ // "satisfied" and then block the very next `gh pr create` with nothing said
1412
+ // about why: #1332's deadlock shape exactly. Confirm it landed before
1413
+ // claiming it did, and name the file and the way out when it did not.
1414
+ if (!readState().tasksAcknowledged) {
1415
+ process.stderr.write(
1416
+ 'Task-status gate NOT satisfied: the acknowledgement could not be persisted to\n' +
1417
+ STATE_FILE + '\n' +
1418
+ 'Check that the file and its directory are writable, then run this again.\n' +
1419
+ 'To proceed without it: set gates: task_status_gate: off in moflo.yaml.\n');
1420
+ process.exit(1);
1421
+ }
1422
+ process.stdout.write(
1423
+ 'Task-status gate satisfied: open tasks acknowledged as deliberately deferred.\n' +
1424
+ 'They stay visible in the task list — this records the decision, it does not close them.\n');
1425
+ break;
1426
+ }
1358
1427
  case 'record-memory-searched': {
1359
1428
  var s = readState();
1360
1429
  if (markMemorySearched(s)) writeState(s);
@@ -1417,12 +1486,48 @@ switch (command) {
1417
1486
  // Why it does nothing: see applyPromptStateReset().
1418
1487
  break;
1419
1488
  }
1420
- case 'record-learnings-stored': {
1489
+ // #1434 — the gate demanded one memory_store per run whether or not the run
1490
+ // produced a reusable lesson, and a mandatory write with nothing to say
1491
+ // produces filler: a summary of this ticket, this commit, applicable never
1492
+ // again. memory_search returns a bounded set, so each of those displaces a
1493
+ // real lesson from every future search — the cost is retrieval quality, not
1494
+ // disk. Declaring "nothing durable here" is the honest outcome of a run that
1495
+ // learned nothing new, so it has to be reachable without a write; otherwise
1496
+ // the cheapest way past the gate stays the filler write.
1497
+ //
1498
+ // Both credits set the same flag; they differ only in whether the run has
1499
+ // something to say. Sharing the case body keeps that single write in one
1500
+ // place across all three copies of this file.
1501
+ case 'record-learnings-stored':
1502
+ case 'record-no-durable-lesson': {
1421
1503
  var s = readState();
1422
1504
  if (!s.learningsStored) {
1423
1505
  s.learningsStored = true;
1424
1506
  writeState(s);
1425
1507
  }
1508
+ if (command === 'record-no-durable-lesson') {
1509
+ // Same reasoning as record-tasks-acknowledged above: writeState swallows
1510
+ // its own errors so a gate never crashes the hook it runs in, and this is
1511
+ // the ONLY escape from the BLOCKING learnings gate that does not require a
1512
+ // memory_store. A lost write here would print "satisfied" and then block
1513
+ // the next `gh pr create` with nothing said about why — #1332's deadlock.
1514
+ // Verified only on this arm: record-learnings-stored is fired
1515
+ // automatically by the PostToolUse hook on every memory_store, where a
1516
+ // failed write leaves the gate closed but the run still has the ordinary
1517
+ // way through, and a diagnostic on every store would be noise.
1518
+ if (!readState().learningsStored) {
1519
+ process.stderr.write(
1520
+ 'Learnings gate NOT satisfied: the declaration could not be persisted to\n' +
1521
+ STATE_FILE + '\n' +
1522
+ 'Check that the file and its directory are writable, then run this again.\n' +
1523
+ 'To proceed without it: set gates: learnings_gate: false in moflo.yaml.\n');
1524
+ process.exit(1);
1525
+ }
1526
+ process.stdout.write(
1527
+ 'Learnings gate satisfied: no durable lesson declared for this run.\n' +
1528
+ 'What this run did belongs in the PR body, not in memory.\n',
1529
+ );
1530
+ }
1426
1531
  break;
1427
1532
  }
1428
1533
  case 'record-test-run': {
@@ -1645,25 +1750,46 @@ switch (command) {
1645
1750
  // chained, piped, parenthesised, and multi-line shapes (#1410).
1646
1751
  var cmd = process.env.TOOL_INPUT_command || '';
1647
1752
  if (!isPrCreateCommand(cmd)) break;
1648
- // #1374 close the loop the TaskCreate reminder opens. Advisory: stdout,
1649
- // no exit, and worded as a reminder, because a message may only claim to
1650
- // block when it blocks (#1326). An open task list is a reporting failure,
1651
- // not a quality failure — blocking the PR on it would be a new deadlock.
1753
+ // #1374 opened this loop; #1435 closes it. The count itself is unchanged —
1754
+ // what changed is that it now has teeth and, in warn mode, a delivery path.
1652
1755
  //
1653
1756
  // Deliberately ABOVE the no-source exemption below: a docs-only PR can
1654
1757
  // abandon a list exactly like a source PR can, and the exemption is about
1655
1758
  // testing/simplify/learnings, not about whether the run told the user what
1656
- // it did. Gated on the same `task_create_first` flag as the reminder itself
1657
- // so the two halves are always consistent a project that turned the nag
1658
- // off is not then nagged about the other end of it.
1659
- if (config.task_create_first) {
1759
+ // it did. It also exits on its own rather than joining `missing` below, for
1760
+ // the same reason `missing` is unreachable on an exempt diff.
1761
+ //
1762
+ // Gated on the same `task_create_first` flag as the reminder itself so the
1763
+ // two halves are always consistent: a project that turned the nag off is
1764
+ // not then blocked about the other end of it.
1765
+ //
1766
+ // Fail-open is load-bearing. readTaskLedger() returns null on a missing,
1767
+ // oversized, or unreadable transcript and on a session with no TaskCreate at
1768
+ // all, and null must never block — a gate that stops PRs because it could
1769
+ // not read a file is worse than the reporting gap it is closing.
1770
+ //
1771
+ // State is read ONCE for the whole case, here — the pre-PR gate logic below
1772
+ // reuses it and nothing writes in between. Reading it before the ledger also
1773
+ // means an already-acknowledged run never pays for the transcript scan.
1774
+ var s = readState();
1775
+ if (config.task_create_first && config.task_status_gate !== 'off' && !s.tasksAcknowledged) {
1660
1776
  var ledger = readTaskLedger();
1661
1777
  if (ledger && ledger.open > 0) {
1662
- process.stdout.write(
1663
- 'REMINDER: ' + ledger.created + ' task' + (ledger.created === 1 ? '' : 's') +
1664
- ' created this session, ' + ledger.open + ' still open. Close them with ' +
1665
- 'TaskUpdate (status: completed), or delete the ones that no longer apply, ' +
1666
- 'so the run does not report done over an unfinished list.\n');
1778
+ var tally = ledger.created + ' task' + (ledger.created === 1 ? '' : 's') +
1779
+ ' created this session, ' + ledger.open + ' still open.';
1780
+ var closeIt = 'Close them with TaskUpdate (status: completed), or delete the ones ' +
1781
+ 'that no longer apply, so the run does not report done over an unfinished list.\n';
1782
+ if (config.task_status_gate === 'warn') {
1783
+ process.stdout.write('REMINDER: ' + tally + ' ' + closeIt);
1784
+ } else {
1785
+ process.stderr.write(
1786
+ 'BLOCKED: ' + tally + '\n' + closeIt +
1787
+ 'Deferring them on purpose is a legitimate outcome — declare it instead of\n' +
1788
+ 'closing tasks that are not done: node "' + __filename + '" record-tasks-acknowledged\n' +
1789
+ GATE_ORIGIN_NOTE + '\n' +
1790
+ 'Report instead of blocking via moflo.yaml: gates: task_status_gate: warn (or: off)\n');
1791
+ process.exit(2);
1792
+ }
1667
1793
  }
1668
1794
  }
1669
1795
  // No-source-files exemption (#1176, supersedes the original docs-only path).
@@ -1688,7 +1814,6 @@ switch (command) {
1688
1814
  break;
1689
1815
  }
1690
1816
  }
1691
- var s = readState();
1692
1817
  // Expire any credit whose fingerprint no longer matches the code before
1693
1818
  // reading the flags. This is what catches the mutations reset-edit-gates
1694
1819
  // structurally cannot see — Bash writes, git checkout/pull/merge, and the
@@ -1721,7 +1846,7 @@ switch (command) {
1721
1846
  var missing = [];
1722
1847
  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)');
1723
1848
  if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
1724
- if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
1849
+ if (config.learnings_gate && !s.learningsStored) missing.push(LEARNINGS_MISSING);
1725
1850
  if (missing.length === 0) break;
1726
1851
  process.stderr.write('BLOCKED: gh pr create requires the following before opening a PR:\n');
1727
1852
  for (var i = 0; i < missing.length; i++) {
@@ -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 } },
@@ -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.6';
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.6",
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.5",
101
+ "moflo": "^4.12.6",
102
102
  "tsx": "^4.21.0",
103
103
  "typescript": "^5.9.3",
104
104
  "vitest": "^4.0.0"