moflo 4.9.17 → 4.9.18

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.
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
  var fs = require('fs');
4
4
  var path = require('path');
5
+ var cp = require('child_process');
5
6
 
6
7
  var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
7
8
  var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
@@ -93,6 +94,36 @@ var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^
93
94
  // but NOT the simplify gate — /simplify already reviewed the production code; touching
94
95
  // a test file or fixture doesn't expose new untested surface for code review (#908).
95
96
  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;
97
+ // Docs-only PR exemption: text/markup/image extensions that cannot change runtime behaviour.
98
+ // If EVERY file in the PR diff matches this, skip testing/simplify/learnings gates.
99
+ // Anchored to end-of-path so e.g. `foo.md.js` does not match. Excludes lock files / configs
100
+ // on purpose — those are inert for edit-reset (above) but not "documentation".
101
+ var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
102
+
103
+ // Get the file list changed on the current branch vs the merge-base with origin/main
104
+ // (falling back to local main). Returns an array of repo-relative paths, or null on
105
+ // failure — in which case callers MUST fall through to the standard gate (fail-safe).
106
+ function getChangedFilesVsBase() {
107
+ var bases = ['origin/main', 'main', 'origin/master', 'master'];
108
+ var base = null;
109
+ for (var i = 0; i < bases.length; i++) {
110
+ try {
111
+ base = cp.execFileSync('git', ['merge-base', 'HEAD', bases[i]], {
112
+ cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
113
+ stdio: ['ignore', 'pipe', 'ignore']
114
+ }).trim();
115
+ if (base) break;
116
+ } catch (e) { /* try next */ }
117
+ }
118
+ if (!base) return null;
119
+ try {
120
+ var out = cp.execFileSync('git', ['diff', '--name-only', base + '...HEAD'], {
121
+ cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
122
+ stdio: ['ignore', 'pipe', 'ignore']
123
+ });
124
+ return out.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
125
+ } catch (e) { return null; }
126
+ }
96
127
 
97
128
  switch (command) {
98
129
  case 'check-before-agent': {
@@ -208,6 +239,15 @@ switch (command) {
208
239
  // optional ENV=val prefix segment catches `GH_TOKEN=x gh pr create`.
209
240
  var cmd = process.env.TOOL_INPUT_command || '';
210
241
  if (!/(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/.test(cmd)) break;
242
+ // Docs-only exemption: if every file changed vs the merge-base is a docs/image
243
+ // file (no runtime-behaviour surface), skip the testing/simplify/learnings gates
244
+ // and surface a one-line transparency note. Falls through to the standard gate
245
+ // on any failure (no base, no diff, exec error) — fail-safe by design.
246
+ var changed = getChangedFilesVsBase();
247
+ if (changed && changed.length > 0 && changed.every(function(f) { return DOCS_ONLY_RE.test(f); })) {
248
+ process.stdout.write('Docs-only PR (' + changed.length + ' file' + (changed.length === 1 ? '' : 's') + ') — skipping testing/simplify/learnings gates.\n');
249
+ break;
250
+ }
211
251
  var s = readState();
212
252
  var missing = [];
213
253
  if (config.testing_gate && !s.testsRun) missing.push('tests have not run since the last code edit (run npm test, vitest, jest, pytest, or similar)');
package/bin/gate.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
  var fs = require('fs');
4
4
  var path = require('path');
5
+ var cp = require('child_process');
5
6
 
6
7
  var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
7
8
  var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
@@ -93,6 +94,36 @@ var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^
93
94
  // but NOT the simplify gate — /simplify already reviewed the production code; touching
94
95
  // a test file or fixture doesn't expose new untested surface for code review (#908).
95
96
  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;
97
+ // Docs-only PR exemption: text/markup/image extensions that cannot change runtime behaviour.
98
+ // If EVERY file in the PR diff matches this, skip testing/simplify/learnings gates.
99
+ // Anchored to end-of-path so e.g. `foo.md.js` does not match. Excludes lock files / configs
100
+ // on purpose — those are inert for edit-reset (above) but not "documentation".
101
+ var DOCS_ONLY_RE = /\.(md|markdown|txt|rst|adoc|html?|pdf|png|jpe?g|gif|svg|webp|ico|bmp)$/i;
102
+
103
+ // Get the file list changed on the current branch vs the merge-base with origin/main
104
+ // (falling back to local main). Returns an array of repo-relative paths, or null on
105
+ // failure — in which case callers MUST fall through to the standard gate (fail-safe).
106
+ function getChangedFilesVsBase() {
107
+ var bases = ['origin/main', 'main', 'origin/master', 'master'];
108
+ var base = null;
109
+ for (var i = 0; i < bases.length; i++) {
110
+ try {
111
+ base = cp.execFileSync('git', ['merge-base', 'HEAD', bases[i]], {
112
+ cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
113
+ stdio: ['ignore', 'pipe', 'ignore']
114
+ }).trim();
115
+ if (base) break;
116
+ } catch (e) { /* try next */ }
117
+ }
118
+ if (!base) return null;
119
+ try {
120
+ var out = cp.execFileSync('git', ['diff', '--name-only', base + '...HEAD'], {
121
+ cwd: PROJECT_DIR, encoding: 'utf-8', timeout: 2000, windowsHide: true,
122
+ stdio: ['ignore', 'pipe', 'ignore']
123
+ });
124
+ return out.split('\n').map(function(l) { return l.trim(); }).filter(Boolean);
125
+ } catch (e) { return null; }
126
+ }
96
127
 
97
128
  switch (command) {
98
129
  case 'check-before-agent': {
@@ -208,6 +239,15 @@ switch (command) {
208
239
  // optional ENV=val prefix segment catches `GH_TOKEN=x gh pr create`.
209
240
  var cmd = process.env.TOOL_INPUT_command || '';
210
241
  if (!/(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/.test(cmd)) break;
242
+ // Docs-only exemption: if every file changed vs the merge-base is a docs/image
243
+ // file (no runtime-behaviour surface), skip the testing/simplify/learnings gates
244
+ // and surface a one-line transparency note. Falls through to the standard gate
245
+ // on any failure (no base, no diff, exec error) — fail-safe by design.
246
+ var changed = getChangedFilesVsBase();
247
+ if (changed && changed.length > 0 && changed.every(function(f) { return DOCS_ONLY_RE.test(f); })) {
248
+ process.stdout.write('Docs-only PR (' + changed.length + ' file' + (changed.length === 1 ? '' : 's') + ') — skipping testing/simplify/learnings gates.\n');
249
+ break;
250
+ }
211
251
  var s = readState();
212
252
  var missing = [];
213
253
  if (config.testing_gate && !s.testsRun) missing.push('tests have not run since the last code edit (run npm test, vitest, jest, pytest, or similar)');
@@ -23,26 +23,72 @@ async function runFixCommand(cmd) {
23
23
  }
24
24
  }
25
25
  /**
26
- * Fix missing hook wiring in settings.json by patching in entries for any
27
- * REQUIRED_HOOK_WIRING patterns that aren't present. Delegates to shared
28
- * repairHookWiring() to stay DRY with the upgrade path.
26
+ * Fix Gate Health failures: bin/.claude-helpers gate.cjs drift AND missing
27
+ * settings.json hook wiring. The check has three independent failure modes
28
+ * and the prior fix only handled hook wiring — leaving bin/helper drift
29
+ * unresolved while still claiming success (the "Auto-fixed 1 issue" lie that
30
+ * surfaced when #920 mirrored the docs-only PR exemption into only one of
31
+ * the two gate.cjs files).
32
+ *
33
+ * Sync direction is decided by which source file is "ahead" of its installed
34
+ * counterpart in `node_modules/moflo/`:
35
+ * - If only source `bin/gate.cjs` differs from installed bin → mirror bin → helper.
36
+ * - If only source `.claude/helpers/gate.cjs` differs from installed helper → mirror helper → bin.
37
+ * - If both are ahead with different content (genuine ambiguity) → bail
38
+ * and let the caller report failure; refuse to silently pick a side.
39
+ * - If `node_modules/moflo/` is missing entirely (consumer never installed,
40
+ * unusual layout) → bail.
29
41
  */
30
42
  async function fixGateHealthHooks() {
31
- const settingsPath = join(process.cwd(), '.claude', 'settings.json');
32
- if (!existsSync(settingsPath))
33
- return false;
34
- try {
35
- const raw = readFileSync(settingsPath, 'utf8');
36
- const settings = JSON.parse(raw);
37
- const { repaired } = repairHookWiring(settings);
38
- if (repaired.length === 0)
39
- return true; // nothing to fix
40
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
41
- return true;
43
+ const cwd = process.cwd();
44
+ let driftFixed = true; // true means "no drift to fix or drift resolved"
45
+ const binGate = join(cwd, 'bin', 'gate.cjs');
46
+ const helperGate = join(cwd, '.claude', 'helpers', 'gate.cjs');
47
+ const installedBin = join(cwd, 'node_modules', 'moflo', 'bin', 'gate.cjs');
48
+ const installedHelper = join(cwd, 'node_modules', 'moflo', '.claude', 'helpers', 'gate.cjs');
49
+ if (existsSync(binGate) && existsSync(helperGate)) {
50
+ try {
51
+ const binContent = readFileSync(binGate, 'utf8');
52
+ const helperContent = readFileSync(helperGate, 'utf8');
53
+ if (binContent !== helperContent) {
54
+ const installedBinContent = existsSync(installedBin) ? readFileSync(installedBin, 'utf8') : null;
55
+ const installedHelperContent = existsSync(installedHelper) ? readFileSync(installedHelper, 'utf8') : null;
56
+ const binAhead = installedBinContent !== null && binContent !== installedBinContent;
57
+ const helperAhead = installedHelperContent !== null && helperContent !== installedHelperContent;
58
+ if (binAhead && !helperAhead) {
59
+ writeFileSync(helperGate, binContent, 'utf-8');
60
+ }
61
+ else if (helperAhead && !binAhead) {
62
+ writeFileSync(binGate, helperContent, 'utf-8');
63
+ }
64
+ else {
65
+ // Both ahead with different content, OR neither ahead (no install
66
+ // to anchor on). Refuse to pick a side — surface the failure.
67
+ driftFixed = false;
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ driftFixed = false;
73
+ }
42
74
  }
43
- catch {
44
- return false;
75
+ // Hook-wiring repair (separate failure mode that this fixer also owns).
76
+ const settingsPath = join(cwd, '.claude', 'settings.json');
77
+ let wiringFixed = true;
78
+ if (existsSync(settingsPath)) {
79
+ try {
80
+ const raw = readFileSync(settingsPath, 'utf8');
81
+ const settings = JSON.parse(raw);
82
+ const { repaired } = repairHookWiring(settings);
83
+ if (repaired.length > 0) {
84
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
85
+ }
86
+ }
87
+ catch {
88
+ wiringFixed = false;
89
+ }
45
90
  }
91
+ return driftFixed && wiringFixed;
46
92
  }
47
93
  /**
48
94
  * Execute the fix for a failed/warned health check.
@@ -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.9.17';
5
+ export const VERSION = '4.9.18';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.17",
3
+ "version": "4.9.18",
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",
@@ -81,7 +81,7 @@
81
81
  "@typescript-eslint/eslint-plugin": "^7.18.0",
82
82
  "@typescript-eslint/parser": "^7.18.0",
83
83
  "eslint": "^8.0.0",
84
- "moflo": "^4.9.16",
84
+ "moflo": "^4.9.17",
85
85
  "tsx": "^4.21.0",
86
86
  "typescript": "^5.9.3",
87
87
  "vitest": "^4.0.0"