moflo 4.10.27 → 4.10.28

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.
@@ -229,9 +229,15 @@ Confirm the published version matches what we just built.
229
229
 
230
230
  ```bash
231
231
  npm install moflo@<new-version> --save-dev
232
+ npm install --package-lock-only --force
233
+ npm ci --dry-run --legacy-peer-deps
232
234
  ```
233
235
 
234
- This updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
236
+ The first command updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
237
+
238
+ The second command (`--package-lock-only --force`) backfills cross-platform optional-dependency entries that npm omits from the lockfile when `npm install` runs on a single host platform. Without it, transitive optional deps like the nested `@rolldown/binding-wasm32-wasi/node_modules/@emnapi/*` entries get dropped when publishing from Windows, and the resulting lockfile fails `npm ci` on Ubuntu/macOS CI legs. This produced the green-`release-smoke` → red-`ci.yml` divergence in run `26487580745` (moflo@4.10.27 install commit).
239
+
240
+ The final `npm ci --dry-run --legacy-peer-deps` proves the resulting lockfile is in sync with `package.json` before we commit it. If this exits non-zero, stop and investigate — do not commit a broken lockfile.
235
241
 
236
242
  ### Step 13: Final Commit
237
243
 
@@ -673,6 +673,58 @@ export async function checkMofloYamlCompliance(cwd = process.cwd()) {
673
673
  fix: 'Restart Claude Code (yaml-upgrader auto-appends) or `npx moflo init --force`',
674
674
  };
675
675
  }
676
+ /**
677
+ * #1229 — standing tripwire for a silently-disabled memory_first gate.
678
+ *
679
+ * `gates.memory_first` is the only enforcement of the memory-search-first
680
+ * protocol. When it is `false` in moflo.yaml the protocol is off repo-wide
681
+ * with no per-prompt signal — and because the disabled gate is itself what
682
+ * surfaces the stored "never disable memory_first" learning, the situation
683
+ * self-conceals. Historically a daemon-spawned headless analysis worker could
684
+ * write this value unprompted to unblock itself (the `optimize` worker editing
685
+ * `moflo.yaml` to `memory_first: false # Temporarily disabled for performance
686
+ * analysis`); that root cause is fixed by making those workers read-only, but
687
+ * this check is the loud, standing detector so the state never goes unnoticed
688
+ * again — whatever writes it (worker, agent, or a deliberate consumer edit).
689
+ *
690
+ * Warn rather than fail: disabling the gate is a legitimate (if rare) choice;
691
+ * the point is that it can never be silent. Matches only an *active* (un-
692
+ * commented) `memory_first: false` line and is EOL-agnostic so it behaves
693
+ * identically on CRLF (Windows) and LF (POSIX) checkouts.
694
+ *
695
+ * Exported with a cwd param so tests can target a temp root without touching
696
+ * process.cwd().
697
+ */
698
+ export async function checkMemoryFirstGate(cwd = process.cwd()) {
699
+ const yamlPath = join(cwd, 'moflo.yaml');
700
+ // Absence is covered by checkMofloYamlCompliance; with no file the gate
701
+ // defaults to enabled, so there is nothing to warn about here.
702
+ if (!existsSync(yamlPath)) {
703
+ return { name: 'Memory-First Gate', status: 'pass', message: 'Enabled (default — no moflo.yaml override)' };
704
+ }
705
+ let content;
706
+ try {
707
+ content = readFileSync(yamlPath, 'utf-8');
708
+ }
709
+ catch (e) {
710
+ return { name: 'Memory-First Gate', status: 'warn', message: `Unable to read moflo.yaml: ${errorDetail(e)}` };
711
+ }
712
+ // Active value only: a line whose first non-whitespace token is
713
+ // `memory_first: false`. A commented `# memory_first: false` is ignored
714
+ // because the leading `#` defeats the `^\s*memory_first` anchor.
715
+ const disabled = content
716
+ .split(/\r?\n/)
717
+ .some((line) => /^\s*memory_first:\s*false\b/i.test(line));
718
+ if (disabled) {
719
+ return {
720
+ name: 'Memory-First Gate',
721
+ status: 'warn',
722
+ message: 'DISABLED in moflo.yaml — memory-search-first protocol is off repo-wide and silent per-prompt',
723
+ fix: 'Restore `gates.memory_first: true` (e.g. `git checkout -- moflo.yaml`) unless you disabled it deliberately',
724
+ };
725
+ }
726
+ return { name: 'Memory-First Gate', status: 'pass', message: 'Enabled' };
727
+ }
676
728
  /**
677
729
  * #981 / #987 — surfaces the single-writer-architecture safety net.
678
730
  *
@@ -12,7 +12,7 @@ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
12
12
  import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
13
13
  import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
14
14
  import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
15
- import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
15
+ import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMemoryFirstGate, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
16
16
  import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
17
17
  import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
18
18
  import { checkIntelligence } from './doctor-checks-intelligence.js';
@@ -34,6 +34,8 @@ export const allChecks = [
34
34
  checkGitRepo,
35
35
  checkConfigFile,
36
36
  checkMofloYamlCompliance,
37
+ // #1229 — loud tripwire for a silently-disabled memory_first gate.
38
+ checkMemoryFirstGate,
37
39
  checkStatusLine,
38
40
  checkDaemonStatus,
39
41
  checkDaemonVersionSkew,
@@ -97,6 +99,8 @@ export const componentMap = {
97
99
  'config': checkConfigFile,
98
100
  'yaml': checkMofloYamlCompliance,
99
101
  'moflo-yaml': checkMofloYamlCompliance,
102
+ 'memory-first': checkMemoryFirstGate,
103
+ 'memory-gate': checkMemoryFirstGate,
100
104
  'statusline': checkStatusLine,
101
105
  'status-line': checkStatusLine,
102
106
  'daemon': checkDaemonStatus,
@@ -46,6 +46,28 @@ const MODEL_IDS = {
46
46
  opus: 'claude-opus-4-6',
47
47
  haiku: 'claude-haiku-4-5-20251001',
48
48
  };
49
+ /**
50
+ * Tool allowlist for every headless worker. These workers are READ-ONLY by
51
+ * design: they analyse the codebase and produce a report. Moflo persists their
52
+ * stdout to `.moflo/reports/<type>.<ext>` itself (see `executeInternal`), so
53
+ * they never need write capability to deliver their output.
54
+ *
55
+ * Restricting tools here is a GATE-INTEGRITY guarantee, not a tidy-up. The
56
+ * spawn previously ran `claude --print <prompt>` with no tool restriction, so
57
+ * a daemon-spawned worker inherited the consumer's permissions and could edit
58
+ * any file in their tree. The `optimize` ("Performance optimization") worker
59
+ * is `enabled: true` by default and runs unattended; given write access and a
60
+ * "analyze this codebase for performance optimizations" prompt, it reasons its
61
+ * way into editing `moflo.yaml` to `memory_first: false`
62
+ * (`# Temporarily disabled for performance analysis`) to unblock its own
63
+ * non-memory-first tool calls against the gate — silently disabling the
64
+ * memory-first protocol repo-wide and dirtying the working tree every run.
65
+ * Worse, the gate it disables is what surfaces the "never disable memory_first"
66
+ * learning, so the loop self-conceals. A read-only allowlist makes that edit
67
+ * physically impossible while leaving full analysis capability (Read/Glob/Grep)
68
+ * intact. See issue #1229.
69
+ */
70
+ const HEADLESS_WORKER_ALLOWED_TOOLS = 'Read,Glob,Grep';
49
71
  /**
50
72
  * Default headless worker configurations based on ADR-020 (the
51
73
  * `audit`/`document`/`predict` entries from the original ADR were dropped
@@ -811,7 +833,7 @@ export class HeadlessWorkerExecutor extends EventEmitter {
811
833
  buildPrompt(template, context, reportPath) {
812
834
  const ioInstructions = `## Output
813
835
 
814
- Save the full report to \`${reportPath}\` using the Write tool. Overwrite any prior content at that path. DO NOT create any other files anywhere in the project; if you need to suggest test skeletons or code samples, include them inline in the report as fenced code blocks. Moflo persists the same output to that path after you finish, so the location is authoritative.`;
836
+ Save the full report to \`${reportPath}\` by emitting it as your response moflo writes your output to that path after you finish, so that location is authoritative. You are a READ-ONLY analysis worker and have no write tools: do not attempt to create or modify any file in the project, and in particular never edit \`moflo.yaml\`, anything under \`.claude/\`, or any gate/config (the memory-first gate stays on comply with it, never disable it). Include any test skeletons or code samples inline in the report as fenced code blocks.`;
815
837
  if (!context) {
816
838
  return `${template}
817
839
 
@@ -845,8 +867,11 @@ ${ioInstructions}`;
845
867
  };
846
868
  // Set model
847
869
  env.ANTHROPIC_MODEL = MODEL_IDS[options.model];
848
- // Spawn claude CLI process
849
- const child = spawn('claude', ['--print', prompt], {
870
+ // Spawn claude CLI process. Keep `--print` first and the prompt second
871
+ // (callers/tests rely on argv[0]/argv[1]); the read-only allowlist is
872
+ // appended so these analysis workers cannot mutate the consumer's tree
873
+ // (e.g. disable a gate in moflo.yaml). See HEADLESS_WORKER_ALLOWED_TOOLS.
874
+ const child = spawn('claude', ['--print', prompt, '--allowedTools', HEADLESS_WORKER_ALLOWED_TOOLS], {
850
875
  cwd: this.projectRoot,
851
876
  env,
852
877
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -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.10.27';
5
+ export const VERSION = '4.10.28';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.27",
3
+ "version": "4.10.28",
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",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.10.26",
98
+ "moflo": "^4.10.27",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"