moflo 4.9.19 → 4.9.21

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.
Files changed (82) hide show
  1. package/.claude/commands/{simplify.md → flo-simplify.md} +4 -4
  2. package/.claude/guidance/shipped/moflo-agent-rules.md +172 -0
  3. package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +73 -265
  4. package/.claude/guidance/shipped/moflo-cli-reference.md +6 -6
  5. package/.claude/guidance/shipped/moflo-core-guidance.md +66 -184
  6. package/.claude/guidance/shipped/moflo-cross-platform.md +1 -1
  7. package/.claude/guidance/shipped/moflo-error-handling.md +3 -3
  8. package/.claude/guidance/shipped/moflo-guidance-rules.md +17 -7
  9. package/.claude/guidance/shipped/moflo-memory-strategy.md +76 -182
  10. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +6 -8
  11. package/.claude/guidance/shipped/moflo-settings-injection.md +7 -9
  12. package/.claude/guidance/shipped/moflo-source-hygiene.md +5 -5
  13. package/.claude/guidance/shipped/moflo-spell-connectors.md +3 -4
  14. package/.claude/guidance/shipped/moflo-spell-custom-steps.md +3 -4
  15. package/.claude/guidance/shipped/moflo-spell-engine.md +40 -162
  16. package/.claude/guidance/shipped/moflo-spell-runner.md +134 -0
  17. package/.claude/guidance/shipped/moflo-spell-sandboxing.md +10 -57
  18. package/.claude/guidance/shipped/moflo-spell-troubleshooting.md +149 -0
  19. package/.claude/guidance/shipped/moflo-subagents.md +43 -114
  20. package/.claude/guidance/shipped/moflo-task-icons.md +4 -4
  21. package/.claude/guidance/shipped/moflo-user-facing-language.md +3 -3
  22. package/.claude/guidance/shipped/moflo-verbose-command-filtering.md +3 -3
  23. package/.claude/guidance/shipped/moflo-yaml-reference.md +4 -5
  24. package/.claude/helpers/gate.cjs +124 -14
  25. package/.claude/helpers/prompt-hook.mjs +4 -38
  26. package/.claude/helpers/simplify-classify.cjs +32 -11
  27. package/.claude/helpers/subagent-bootstrap.json +1 -1
  28. package/.claude/helpers/subagent-start.cjs +1 -1
  29. package/.claude/skills/connector-builder/SKILL.md +42 -429
  30. package/.claude/skills/connector-builder/templates/connector.md +189 -0
  31. package/.claude/skills/connector-builder/templates/step-command.md +176 -0
  32. package/.claude/skills/eldar/SKILL.md +7 -7
  33. package/.claude/skills/fl/SKILL.md +3 -3
  34. package/.claude/skills/fl/execution-modes.md +3 -3
  35. package/.claude/skills/fl/phases.md +3 -3
  36. package/.claude/skills/{simplify → flo-simplify}/SKILL.md +11 -11
  37. package/.claude/skills/guidance/SKILL.md +17 -9
  38. package/.claude/skills/memory-patterns/SKILL.md +1 -1
  39. package/.claude/skills/publish/SKILL.md +121 -36
  40. package/.claude/skills/reset-epic/SKILL.md +2 -2
  41. package/.claude/skills/spell-builder/SKILL.md +39 -226
  42. package/.claude/skills/spell-builder/architecture.md +1 -1
  43. package/.claude/skills/spell-builder/permissions.md +107 -0
  44. package/.claude/skills/spell-builder/preflight.md +101 -0
  45. package/.claude/skills/spell-schedule/SKILL.md +2 -3
  46. package/bin/gate.cjs +124 -14
  47. package/bin/prompt-hook.mjs +4 -38
  48. package/bin/session-start-launcher.mjs +66 -1
  49. package/bin/setup-project.mjs +63 -69
  50. package/bin/simplify-classify.cjs +32 -11
  51. package/dist/src/cli/commands/doctor-checks-deep.js +4 -0
  52. package/dist/src/cli/init/claudemd-generator.js +30 -33
  53. package/dist/src/cli/init/executor.js +28 -16
  54. package/dist/src/cli/init/helpers-generator.js +101 -51
  55. package/dist/src/cli/init/moflo-init.js +41 -114
  56. package/dist/src/cli/init/settings-generator.js +32 -14
  57. package/dist/src/cli/services/hook-block-hash.js +7 -2
  58. package/dist/src/cli/services/hook-wiring.js +86 -3
  59. package/dist/src/cli/services/subagent-bootstrap.js +1 -1
  60. package/dist/src/cli/version.js +1 -1
  61. package/package.json +2 -2
  62. package/scripts/post-install-bootstrap.mjs +19 -0
  63. package/.claude/guidance/shipped/moflo-session-start.md +0 -154
  64. package/.claude/guidance/shipped/moflo-spell-engine-architecture.md +0 -145
  65. package/.claude/skills/browser/SKILL.md +0 -204
  66. package/.claude/skills/github-code-review/SKILL.md +0 -1140
  67. package/.claude/skills/github-multi-repo/SKILL.md +0 -866
  68. package/.claude/skills/github-project-management/SKILL.md +0 -1272
  69. package/.claude/skills/github-release-management/SKILL.md +0 -1074
  70. package/.claude/skills/github-workflow-automation/SKILL.md +0 -1060
  71. package/.claude/skills/hive-mind-advanced/SKILL.md +0 -712
  72. package/.claude/skills/hooks-automation/SKILL.md +0 -1193
  73. package/.claude/skills/pair-programming/SKILL.md +0 -1202
  74. package/.claude/skills/performance-analysis/SKILL.md +0 -563
  75. package/.claude/skills/skill-builder/SKILL.md +0 -910
  76. package/.claude/skills/sparc-methodology/SKILL.md +0 -904
  77. package/.claude/skills/stream-chain/SKILL.md +0 -563
  78. package/.claude/skills/swarm-advanced/SKILL.md +0 -811
  79. package/.claude/skills/swarm-orchestration/SKILL.md +0 -179
  80. package/.claude/skills/verification-quality/SKILL.md +0 -649
  81. package/.claude/skills/worker-benchmarks/skill.md +0 -135
  82. package/.claude/skills/worker-integration/skill.md +0 -154
package/bin/gate.cjs CHANGED
@@ -7,7 +7,7 @@ var cp = require('child_process');
7
7
  var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
8
8
  var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
9
9
 
10
- var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, simplifySnapshotSha: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null };
10
+ var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, simplifySnapshotSha: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {} };
11
11
 
12
12
  // Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
13
13
  // is session-wide, so once the parent searches memory, every spawned subagent
@@ -83,6 +83,78 @@ var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state
83
83
  var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
84
84
  var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\b/i;
85
85
  var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\b/i;
86
+
87
+ // Namespace classification (#931). The hint used to be emitted on every prompt
88
+ // by prompt-hook.mjs which cost ~40 tokens × every prompt × every consumer.
89
+ // Now we classify here, store on workflow-state, and let check-before-agent
90
+ // emit it once when Claude is actually about to spawn an agent.
91
+ //
92
+ // SYNC: these regexes + classifyNamespaceHint + applyPromptStateReset are
93
+ // duplicated verbatim in src/cli/init/helpers-generator.ts (the embedded
94
+ // gate.cjs fallback used by `flo init` when source helpers can't be located).
95
+ // Any edit to either copy MUST be applied to both — there is no shared module
96
+ // because helpers-generator emits a self-contained string template.
97
+ var NS_LEARNINGS_RE = /\b(remember|recall|insight|lesson learned|gotcha|post.?mortem)\b|we (decid|agree|chose|said)/;
98
+ var NS_TEST_RE = /\b(test|spec|coverage|tested|test case|test cases|tests for|spec for)\b/;
99
+ var NS_EXPLICIT = [
100
+ { pattern: /\b(pattern|convention|best practice|style|coding rule)\b/, ns: 'patterns', label: 'code patterns and conventions' },
101
+ { pattern: /\b(code.?map|file structure|project structure|directory)\b/, ns: 'code-map', label: 'codebase navigation' },
102
+ ];
103
+ var NS_PATTERN_RES = [/\b(template|example|similar to|how do we|how should)\b/];
104
+ var NS_DOMAIN_RES = [
105
+ /\b(guidance|guide|docs|documentation|rules|how-to)\b/,
106
+ /\b(architecture|design|domain|tenant|migrat|schema|deploy)/,
107
+ /\b(rule|requirement|constraint|compliance)\b/,
108
+ ];
109
+ var NS_NAV_RES = [
110
+ /\b(find|where|which file|look up|locate|endpoint|route|url|path)\b/,
111
+ /\b(class|function|method|component|service|entity|module)\b/,
112
+ ];
113
+
114
+ function classifyNamespaceHint(promptText) {
115
+ var lower = (promptText || '').toLowerCase();
116
+ if (NS_TEST_RE.test(lower)) return 'Memory namespace hint: use "tests" for test inventory and coverage lookups.';
117
+ if (NS_LEARNINGS_RE.test(lower)) return 'Memory namespace hint: use "learnings" for user-directed decisions and distilled insights.';
118
+ for (var i = 0; i < NS_EXPLICIT.length; i++) {
119
+ if (NS_EXPLICIT[i].pattern.test(lower)) return 'Memory namespace hint: use "' + NS_EXPLICIT[i].ns + '" for ' + NS_EXPLICIT[i].label + '.';
120
+ }
121
+ for (var j = 0; j < NS_DOMAIN_RES.length; j++) {
122
+ if (NS_DOMAIN_RES[j].test(lower)) return 'Memory namespace hint: search "guidance" and "learnings" for domain rules and project decisions.';
123
+ }
124
+ for (var k = 0; k < NS_PATTERN_RES.length; k++) {
125
+ if (NS_PATTERN_RES[k].test(lower)) return 'Memory namespace hint: use "patterns" for code patterns and conventions.';
126
+ }
127
+ for (var m = 0; m < NS_NAV_RES.length; m++) {
128
+ if (NS_NAV_RES[m].test(lower)) return 'Memory namespace hint: use "code-map" for codebase navigation.';
129
+ }
130
+ return '';
131
+ }
132
+
133
+ // Apply per-prompt state reset shared by `prompt-reminder` (full) and
134
+ // `prompt-state-reset` (defensive safety-net, no emission). Idempotent — both
135
+ // UserPromptSubmit hooks can run it without compounding any field. Caller
136
+ // owns interactionCount and the user-visible REMINDER/Context emissions, so
137
+ // this helper stays silent.
138
+ function applyPromptStateReset(state, promptText) {
139
+ state.memorySearched = false;
140
+ // Wipe per-actor memory tracking too — a new user prompt is a fresh window
141
+ // for both parent AND any subagents the parent may spawn during this turn.
142
+ state.memorySearchedBy = {};
143
+ // learningsStored is session-scoped — once stored, it stays true until session reset.
144
+ // Resetting per-prompt caused false blocks when PR creation was on a later prompt.
145
+ var DIRECTIVE_MAX_LEN = 20;
146
+ var escaped = /^@@\s*/.test(promptText || '');
147
+ state.memoryRequired = !escaped && (promptText || '').length >= 4 && (TASK_RE.test(promptText || '') || (promptText || '').length > DIRECTIVE_MAX_LEN);
148
+ // Stash namespace hint for check-before-agent to emit when Claude actually
149
+ // spawns an Agent (#931). Empty string when nothing matched — overwriting
150
+ // any stale value from the previous prompt.
151
+ state.lastNamespaceHint = classifyNamespaceHint(promptText);
152
+ // Per-actor emission tracking — each subagent's session gets the hint at
153
+ // most once per prompt, but a fresh prompt resets every actor's window so
154
+ // subsequent agents (parent + subagents that spawn their own agents) all
155
+ // see the new classification on their first check-before-agent.
156
+ state.lastNamespaceHintEmittedBy = {};
157
+ }
86
158
  // Match npm/yarn/pnpm/bun test, npx vitest|jest|..., bare runners at command-start only,
87
159
  // and language-native test commands. The bare-runner arm is anchored so that
88
160
  // `npm install jest`, `grep -r vitest src/`, and similar don't false-positive.
@@ -203,6 +275,11 @@ switch (command) {
203
275
  // Advisory only — agent spawning is never blocked.
204
276
  // Memory-first enforcement happens at the scan/read gate layer.
205
277
  // SubagentStart hook injects guidance directive into subagent context.
278
+ //
279
+ // #931 — TaskCreate REMINDER and the namespace hint moved here from
280
+ // prompt-reminder. They only matter when Claude is actually about to spawn
281
+ // an Agent; emitting per-prompt cost ~90 tokens × every prompt × every
282
+ // consumer.
206
283
  var s = readState();
207
284
  if (config.task_create_first && !s.tasksCreated) {
208
285
  process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
@@ -210,6 +287,24 @@ switch (command) {
210
287
  if (config.memory_first && s.memoryRequired && !s.memorySearched) {
211
288
  process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents.\n');
212
289
  }
290
+ if (s.lastNamespaceHint) {
291
+ // Per-actor single-shot. Each session_id gets the hint at most once per
292
+ // prompt, but the hint itself stays available for other actors (e.g.
293
+ // a subagent that spawns its own agent has its own session_id and is
294
+ // entitled to a fresh emission). Falls back to a `_legacy_` bucket when
295
+ // Claude Code didn't forward a session_id (older host or direct CLI
296
+ // invocation), preserving the old "emit once globally" behavior. The
297
+ // map is wiped by applyPromptStateReset on every new prompt.
298
+ var sid = process.env.HOOK_SESSION_ID || '';
299
+ var emittedBy = s.lastNamespaceHintEmittedBy || {};
300
+ var bucket = sid || '_legacy_';
301
+ if (!emittedBy[bucket]) {
302
+ process.stdout.write(s.lastNamespaceHint + '\n');
303
+ emittedBy[bucket] = true;
304
+ s.lastNamespaceHintEmittedBy = emittedBy;
305
+ writeState(s);
306
+ }
307
+ }
213
308
  break;
214
309
  }
215
310
  case 'check-before-scan': {
@@ -277,7 +372,8 @@ switch (command) {
277
372
  break;
278
373
  }
279
374
  case 'record-skill-run': {
280
- if ((process.env.TOOL_INPUT_skill || '') === 'simplify') {
375
+ var skName = (process.env.TOOL_INPUT_skill || '');
376
+ if (skName === 'simplify' || skName === 'flo-simplify') {
281
377
  var s = readState();
282
378
  var changed = false;
283
379
  if (!s.simplifyRun) { s.simplifyRun = true; changed = true; }
@@ -346,7 +442,7 @@ switch (command) {
346
442
  }
347
443
  var missing = [];
348
444
  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)');
349
- if (config.simplify_gate && !s.simplifyRun) missing.push('/simplify has not run since the last code edit');
445
+ if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify has not run since the last code edit');
350
446
  if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
351
447
  if (missing.length === 0) break;
352
448
  process.stderr.write('BLOCKED: gh pr create requires the following before opening a PR:\n');
@@ -371,20 +467,16 @@ switch (command) {
371
467
  break;
372
468
  }
373
469
  case 'prompt-reminder': {
470
+ // Full per-prompt reset. Wired as the first UserPromptSubmit hook (via
471
+ // prompt-hook.mjs). Owns interactionCount + Context warnings; the
472
+ // TaskCreate REMINDER and namespace hint moved to check-before-agent
473
+ // (#931) so they only fire when Claude is actually about to spawn an
474
+ // Agent.
374
475
  var s = readState();
375
- s.memorySearched = false;
376
- // Wipe per-actor memory tracking too — a new user prompt is a fresh window
377
- // for both parent AND any subagents the parent may spawn during this turn.
378
- s.memorySearchedBy = {};
379
- // learningsStored is session-scoped — once stored, it stays true until session reset.
380
- // Resetting per-prompt caused false blocks when PR creation was on a later prompt.
381
476
  var prompt = process.env.CLAUDE_USER_PROMPT || '';
382
- var DIRECTIVE_MAX_LEN = 20;
383
- var escaped = /^@@\s*/.test(prompt);
384
- s.memoryRequired = !escaped && prompt.length >= 4 && (TASK_RE.test(prompt) || prompt.length > DIRECTIVE_MAX_LEN);
477
+ applyPromptStateReset(s, prompt);
385
478
  s.interactionCount = (s.interactionCount || 0) + 1;
386
479
  writeState(s);
387
- if (!s.tasksCreated) console.log('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.');
388
480
  if (config.context_tracking) {
389
481
  var ic = s.interactionCount;
390
482
  if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
@@ -393,12 +485,30 @@ switch (command) {
393
485
  }
394
486
  break;
395
487
  }
488
+ case 'prompt-state-reset': {
489
+ // Defensive safety-net hook (#931 dedupe). Wired as the second
490
+ // UserPromptSubmit hook so an exception in prompt-hook.mjs doesn't skip
491
+ // the per-prompt state reset. Idempotent — applyPromptStateReset only
492
+ // sets fields to derived values, and we deliberately do NOT increment
493
+ // interactionCount or emit anything (that's prompt-reminder's job).
494
+ //
495
+ // Skip the disk write on the normal path: prompt-reminder runs first and
496
+ // already wrote the byte-identical post-reset state. Only writeState when
497
+ // the reset actually changed something (i.e., prompt-reminder was skipped
498
+ // because prompt-hook.mjs threw before invoking it).
499
+ var s = readState();
500
+ var prompt = process.env.CLAUDE_USER_PROMPT || '';
501
+ var before = JSON.stringify(s);
502
+ applyPromptStateReset(s, prompt);
503
+ if (JSON.stringify(s) !== before) writeState(s);
504
+ break;
505
+ }
396
506
  case 'compact-guidance': {
397
507
  console.log('Pre-Compact: Check CLAUDE.md for rules. Use memory search to recover context after compact.');
398
508
  break;
399
509
  }
400
510
  case 'session-reset': {
401
- writeState({ tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, interactionCount: 0, sessionStart: new Date().toISOString(), lastBlockedAt: null });
511
+ writeState({ tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, interactionCount: 0, sessionStart: new Date().toISOString(), lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {} });
402
512
  break;
403
513
  }
404
514
  default:
@@ -33,43 +33,9 @@ try {
33
33
  });
34
34
  } catch (err) { output = (err && err.stdout) || ''; }
35
35
 
36
- // Classify prompt for namespace hint
37
- var lower = userPrompt.toLowerCase();
38
-
39
- var LEARNINGS_HINTS = /\b(remember|recall|insight|lesson learned|gotcha|post.?mortem)\b|we (decid|agree|chose|said)/;
40
- var TEST_HINTS = /\b(test|spec|coverage|tested|test case|test cases|tests for|spec for)\b/;
41
- var EXPLICIT_NS = [
42
- { pattern: /\b(pattern|convention|best practice|style|coding rule)\b/, ns: 'patterns', label: 'code patterns and conventions' },
43
- { pattern: /\b(code.?map|file structure|project structure|directory)\b/, ns: 'code-map', label: 'codebase navigation' },
44
- ];
45
- var PATTERN_HINTS = [/\b(template|example|similar to|how do we|how should)\b/];
46
- var DOMAIN_HINTS = [
47
- /\b(guidance|guide|docs|documentation|rules|how-to)\b/,
48
- /\b(architecture|design|domain|tenant|migrat|schema|deploy)/,
49
- /\b(rule|requirement|constraint|compliance)\b/,
50
- ];
51
- var NAV_PATTERNS = [
52
- /\b(find|where|which file|look up|locate|endpoint|route|url|path)\b/,
53
- /\b(class|function|method|component|service|entity|module)\b/,
54
- ];
55
-
56
- var nsHint = '';
57
- if (TEST_HINTS.test(lower)) {
58
- nsHint = 'Memory namespace hint: use "tests" for test inventory and coverage lookups.';
59
- } else if (LEARNINGS_HINTS.test(lower)) {
60
- nsHint = 'Memory namespace hint: use "learnings" for user-directed decisions and distilled insights.';
61
- } else {
62
- var found = EXPLICIT_NS.find(function(e) { return e.pattern.test(lower); });
63
- if (found) {
64
- nsHint = 'Memory namespace hint: use "' + found.ns + '" for ' + found.label + '.';
65
- } else if (DOMAIN_HINTS.some(function(p) { return p.test(lower); })) {
66
- nsHint = 'Memory namespace hint: search "guidance" and "learnings" for domain rules and project decisions.';
67
- } else if (PATTERN_HINTS.some(function(p) { return p.test(lower); })) {
68
- nsHint = 'Memory namespace hint: use "patterns" for code patterns and conventions.';
69
- } else if (NAV_PATTERNS.some(function(p) { return p.test(lower); })) {
70
- nsHint = 'Memory namespace hint: use "code-map" for codebase navigation.';
71
- }
72
- }
36
+ // #931 Namespace hint classification moved into gate.cjs (computed by
37
+ // prompt-reminder, stored on workflow-state.json, emitted once by
38
+ // check-before-agent at Agent-spawn time). No per-prompt token leak now.
73
39
 
74
40
  // #867 — surface post-install restart notice. File is written by
75
41
  // scripts/post-install-notice.mjs and cleared by the SessionStart launcher
@@ -84,6 +50,6 @@ try {
84
50
  }
85
51
  } catch (e) { /* ENOENT or malformed — silent fast-path */ }
86
52
 
87
- var parts = [restartNotice, output.trim(), nsHint].filter(Boolean);
53
+ var parts = [restartNotice, output.trim()].filter(Boolean);
88
54
  if (parts.length) process.stdout.write(parts.join('\n\n') + '\n');
89
55
  process.exit(0);
@@ -53,6 +53,36 @@ function findProjectRoot() {
53
53
 
54
54
  const projectRoot = findProjectRoot();
55
55
 
56
+ // Dogfood guard (#928). When this launcher runs inside the moflo repo itself,
57
+ // .claude/scripts/, .claude/helpers/, and .claude/guidance/ are committed git
58
+ // files — they ARE moflo's source of truth, not destinations to be re-synced
59
+ // from node_modules. Drift heal would silently revert any post-publish edit
60
+ // to one of those files (e.g. story #927's gate.cjs got reverted overnight
61
+ // because manifest.size still pointed at the previously-published version).
62
+ // Detection is `package.json#name === "moflo"` — the project's own package.json,
63
+ // NOT node_modules/moflo/package.json. Defaults to false on any read/parse
64
+ // error so a corrupt package.json never silently disables drift heal in a
65
+ // real consumer.
66
+ //
67
+ // Workspace caveat: findProjectRoot() walks up to the nearest package.json,
68
+ // so in a workspace child (packages/foo/) the read sees the child package
69
+ // (name !== "moflo") and the guard stays false. moflo isn't a workspace
70
+ // today; if it ever becomes one, run sessions from the repo root or extend
71
+ // this check to walk further up.
72
+ let isMofloDogfood = false;
73
+ try {
74
+ const projectPkgPath = resolve(projectRoot, 'package.json');
75
+ if (existsSync(projectPkgPath)) {
76
+ const projectPkg = JSON.parse(readFileSync(projectPkgPath, 'utf-8'));
77
+ isMofloDogfood = projectPkg?.name === 'moflo';
78
+ }
79
+ } catch (err) {
80
+ // Defaults to false — safer than accidentally disabling drift heal in a
81
+ // real consumer. Surface the failure so a corrupt project package.json
82
+ // doesn't silently change launcher behavior (per feedback_no_silent_failures).
83
+ process.stderr.write(`[moflo] dogfood-guard package.json read failed: ${err && err.message ? err.message : String(err)}\n`);
84
+ }
85
+
56
86
  // Visible mutation reporter (#716). Claude Code's SessionStart hook captures
57
87
  // stdout as `additionalContext`, so each line here surfaces to Claude — and
58
88
  // through it to the user — explaining what the launcher just changed. Keep
@@ -360,6 +390,8 @@ try {
360
390
  }
361
391
  }
362
392
  }
393
+ // Dogfood (#928): never drift-heal moflo's own committed copies.
394
+ if (isMofloDogfood) manifestDrifted = false;
363
395
 
364
396
  if (installedVersion !== cachedVersion || manifestDrifted) {
365
397
  if (installedVersion !== cachedVersion) {
@@ -444,6 +476,20 @@ try {
444
476
 
445
477
  const binDir = resolve(projectRoot, 'node_modules/moflo/bin');
446
478
 
479
+ // Dogfood (#928): in moflo's own repo, the destinations under
480
+ // .claude/scripts/, .claude/helpers/, .claude/guidance/ are committed
481
+ // git files — copying node_modules/moflo content over them clobbers
482
+ // in-flight work (the same bug that silently reverted #927 between
483
+ // commit and publish). Skip the sync, cleanup, and manifest write
484
+ // entirely; queue the version-stamp write so we don't re-enter this
485
+ // branch on every subsequent session. Daemon recycle still happens
486
+ // (the stopDaemon call earlier handled this) and 3a-pre will spawn a
487
+ // fresh daemon under the new code.
488
+ if (isMofloDogfood) {
489
+ pendingVersionStampWrite = { path: versionStampPath, version: installedVersion };
490
+ emitMutation('skipped file-sync', 'moflo dogfood — committed dogfood copies preserved');
491
+ } else {
492
+
447
493
  // ── Manifest-based auto-update ──────────────────────────────────────
448
494
  //
449
495
  // IMPORTANT: Every file moflo installs into the destination project
@@ -709,6 +755,7 @@ try {
709
755
  // queued for 3g.
710
756
  emitWarning(`manifest write failed (${errMessage(err)})`);
711
757
  }
758
+ } // end !isMofloDogfood file-sync branch (#928)
712
759
  }
713
760
  }
714
761
  } catch (err) {
@@ -1046,7 +1093,7 @@ try {
1046
1093
  // Also prunes top-level mirrors whose source no longer exists in shipped/
1047
1094
  // (#839): pre-manifest-tracking mirrors never appeared in installed-files.json
1048
1095
  // so the section-3 manifest-diff cleanup can't reach them. The marker gate
1049
- // protects user files and `moflo-bootstrap.md` (distinct `moflo init` marker).
1096
+ // protects user-authored files; pre-#939 moflo-bootstrap.md is handled by 3c-939.
1050
1097
  try {
1051
1098
  const guidanceDir = resolve(projectRoot, '.claude/guidance');
1052
1099
  const shippedDir = resolve(projectRoot, 'node_modules/moflo/.claude/guidance/shipped');
@@ -1122,6 +1169,24 @@ try {
1122
1169
  }
1123
1170
  } catch { /* non-fatal */ }
1124
1171
 
1172
+ // ── 3c-939. Remove pre-#939 moflo-bootstrap.md duplicate ────────────────────
1173
+ // Before #939, `flo init` Step 7 (and `flo-setup`) renamed moflo-subagents.md
1174
+ // to moflo-bootstrap.md while the launcher's stage-3 ALSO synced the original
1175
+ // name — consumers ended up with two copies of the same content on disk. The
1176
+ // rename is gone; this prunes any leftover from older installs. Marker-gated
1177
+ // so we never delete a homonymous user-authored file.
1178
+ try {
1179
+ const legacyBootstrap = resolve(projectRoot, '.claude/guidance/moflo-bootstrap.md');
1180
+ if (existsSync(legacyBootstrap)) {
1181
+ const head = readFileSync(legacyBootstrap, 'utf-8').slice(0, 200);
1182
+ const ours = head.includes('AUTO-GENERATED by moflo init') || head.includes('AUTO-GENERATED by flo-setup');
1183
+ if (ours) {
1184
+ unlinkSync(legacyBootstrap);
1185
+ emitMutation('removed legacy moflo-bootstrap.md', 'duplicate of moflo-subagents.md (#939)');
1186
+ }
1187
+ }
1188
+ } catch { /* non-fatal */ }
1189
+
1125
1190
  // ── 3d-yaml-create. Create moflo.yaml if missing (#895) ────────────────────
1126
1191
  // Sibling self-heal to 3d-yaml: that block APPENDS new sections to existing
1127
1192
  // yaml; this block CREATES the file from the canonical template when no yaml
@@ -11,78 +11,50 @@
11
11
  * flo-setup --check # Check if setup is current
12
12
  *
13
13
  * What it does:
14
- * 1. Copies .claude/guidance/shipped/moflo-subagents.md → project's .claude/guidance/moflo-bootstrap.md
14
+ * 1. Copies moflo/.claude/guidance/shipped/moflo-subagents.md → project's .claude/guidance/moflo-subagents.md
15
15
  * 2. Appends a subagent protocol section to CLAUDE.md (idempotent, with markers)
16
16
  *
17
+ * Historical note (#939): pre-fix builds renamed the destination to
18
+ * moflo-bootstrap.md, but the launcher's stage-3 also synced the original name
19
+ * — consumers ended up with two copies of the same content. The launcher now
20
+ * prunes any legacy moflo-bootstrap.md on first session-start after upgrade.
21
+ *
17
22
  * The project can layer its own guidance files on top for
18
23
  * project-specific rules (companyId, entity templates, etc.).
24
+ *
25
+ * The CLAUDE.md content is owned by `src/cli/init/claudemd-generator.ts` (compiled to
26
+ * `dist/src/cli/init/claudemd-generator.js`). This script is a thin wrapper so consumers
27
+ * always get byte-identical output regardless of which entry point they use
28
+ * (`flo init` vs `flo-setup`).
19
29
  */
20
30
 
21
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
31
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'node:fs';
22
32
  import { dirname, resolve, join } from 'node:path';
23
33
  import { fileURLToPath } from 'node:url';
24
-
25
- const __dirname = dirname(fileURLToPath(import.meta.url));
26
- const mofloRoot = resolve(__dirname, '..');
34
+ import { mofloInternalURL } from './lib/moflo-resolve.mjs';
35
+
36
+ // Resolve moflo's installed package root via Node module resolution so the script
37
+ // works identically from bin/ (canonical) or from .claude/scripts/ (synced copy).
38
+ const mofloRoot = dirname(fileURLToPath(mofloInternalURL('package.json')));
39
+
40
+ // Single source of truth: claudemd-generator.ts owns the section content.
41
+ // Use the shared mofloInternalURL helper so the script works identically when
42
+ // invoked from bin/ (canonical) or from .claude/scripts/ (synced copy).
43
+ const {
44
+ generateClaudeMd,
45
+ MARKER_START,
46
+ MARKER_END,
47
+ LEGACY_MARKER_STARTS,
48
+ LEGACY_MARKER_ENDS,
49
+ } = await import(mofloInternalURL('dist/src/cli/init/claudemd-generator.js'));
27
50
 
28
51
  const args = process.argv.slice(2);
29
52
  const updateOnly = args.includes('--update');
30
53
  const checkOnly = args.includes('--check');
31
54
 
32
- // Markers for idempotent CLAUDE.md updates — keep in sync with claudemd-generator.ts
33
- const MARKER_START = '<!-- MOFLO:INJECTED:START -->';
34
- const MARKER_END = '<!-- MOFLO:INJECTED:END -->';
35
- // Legacy markers to detect and replace
36
- const LEGACY_STARTS = ['<!-- MOFLO:SUBAGENT-PROTOCOL:START -->', '<!-- MOFLO:START -->'];
37
- const LEGACY_ENDS = ['<!-- MOFLO:SUBAGENT-PROTOCOL:END -->', '<!-- MOFLO:END -->'];
38
-
39
- // Minimal injection — just enough for Claude to work with moflo.
40
- // All detailed docs live in .claude/guidance/shipped/moflo-core-guidance.md.
41
- const CLAUDE_MD_SECTION = `${MARKER_START}
42
- ## MoFlo — AI Agent Orchestration
43
-
44
- This project uses [MoFlo](https://github.com/eric-cielo/moflo) for AI-assisted development workflows.
45
-
46
- ### FIRST ACTION ON EVERY PROMPT: Search Memory
47
-
48
- Your first tool call for every new user prompt MUST be a memory search. Do this BEFORE Glob, Grep, Read, or any file exploration.
49
-
50
- \`\`\`
51
- mcp__moflo__memory_search — query: "<task description>", namespace: "guidance" or "patterns" or "learnings" or "code-map" or "tests"
52
- \`\`\`
53
-
54
- Search \`guidance\`, \`patterns\`, and \`learnings\` namespaces on every prompt. Search \`code-map\` when navigating the codebase, \`tests\` when looking for test inventory or coverage.
55
- When the user asks you to remember something: \`mcp__moflo__memory_store\` with namespace \`learnings\`.
56
-
57
- ### Workflow Gates (enforced automatically)
58
-
59
- - **Memory-first**: Must search memory before Glob/Grep/Read
60
- - **TaskCreate-first**: Must call TaskCreate before spawning Agent tool
61
-
62
- - **Task Icons**: \`TaskCreate\` MUST use ICON+[Role] format — see \`.claude/guidance/moflo-task-icons.md\`
63
-
64
- ### MCP Tools (preferred over CLI)
65
-
66
- | Tool | Purpose |
67
- |------|---------|
68
- | \`mcp__moflo__memory_search\` | Semantic search across indexed knowledge |
69
- | \`mcp__moflo__memory_store\` | Store patterns and decisions |
70
- | \`mcp__moflo__hooks_route\` | Route task to optimal agent type |
71
- | \`mcp__moflo__hooks_pre-task\` | Record task start |
72
- | \`mcp__moflo__hooks_post-task\` | Record task completion for learning |
73
-
74
- ### CLI Fallback
75
-
76
- \`\`\`bash
77
- flo-search "[query]" --namespace guidance # Semantic search
78
- flo doctor --fix # Health check
79
- \`\`\`
80
-
81
- ### Full Reference
82
-
83
- For CLI commands, hooks, agents, swarm config, memory commands, and moflo.yaml options, see:
84
- \`.claude/guidance/shipped/moflo-core-guidance.md\`
85
- ${MARKER_END}`;
55
+ // Canonical section content (owned by claudemd-generator.ts). Trim the trailing newline
56
+ // that generateClaudeMd appends so the marker-replace logic below stays exact.
57
+ const CLAUDE_MD_SECTION = generateClaudeMd({}).trimEnd();
86
58
 
87
59
  function log(msg) {
88
60
  console.log(`[flo-setup] ${msg}`);
@@ -115,16 +87,16 @@ function findProjectRoot() {
115
87
  return null;
116
88
  }
117
89
 
118
- function copyBootstrap(projectRoot) {
90
+ function copySubagentsGuidance(projectRoot) {
119
91
  const shippedSource = join(mofloRoot, '.claude', 'guidance', 'shipped', 'moflo-subagents.md');
120
92
  const source = existsSync(shippedSource)
121
93
  ? shippedSource
122
94
  : join(mofloRoot, '.claude', 'guidance', 'moflo-subagents.md');
123
95
  const targetDir = join(projectRoot, '.claude', 'guidance');
124
- const target = join(targetDir, 'moflo-bootstrap.md');
96
+ const target = join(targetDir, 'moflo-subagents.md');
125
97
 
126
98
  if (!existsSync(source)) {
127
- log('❌ Source bootstrap not found in moflo package');
99
+ log('❌ Source subagents guidance not found in moflo package');
128
100
  return false;
129
101
  }
130
102
 
@@ -143,12 +115,12 @@ function copyBootstrap(projectRoot) {
143
115
  const existing = readFileSync(target, 'utf-8');
144
116
  const newContent = header + content;
145
117
  if (existing === newContent) {
146
- log('✅ moflo-bootstrap.md is current');
118
+ log('✅ moflo-subagents.md is current');
147
119
  return true;
148
120
  }
149
- log('📝 Updating moflo-bootstrap.md');
121
+ log('📝 Updating moflo-subagents.md');
150
122
  } else {
151
- log('📝 Creating .claude/guidance/moflo-bootstrap.md');
123
+ log('📝 Creating .claude/guidance/moflo-subagents.md');
152
124
  }
153
125
 
154
126
  if (!checkOnly) {
@@ -157,6 +129,25 @@ function copyBootstrap(projectRoot) {
157
129
  return true;
158
130
  }
159
131
 
132
+ // Best-effort cleanup of pre-#939 moflo-bootstrap.md left over from prior
133
+ // installs. Only removes the file if it carries one of our auto-generated
134
+ // headers — never deletes a homonymous user-authored file. Idempotent.
135
+ function cleanupLegacyBootstrap(projectRoot) {
136
+ const legacy = join(projectRoot, '.claude', 'guidance', 'moflo-bootstrap.md');
137
+ if (!existsSync(legacy)) return;
138
+ try {
139
+ const head = readFileSync(legacy, 'utf-8').slice(0, 200);
140
+ const ours = head.includes('AUTO-GENERATED by moflo init') || head.includes('AUTO-GENERATED by flo-setup');
141
+ if (!ours) return;
142
+ if (!checkOnly) {
143
+ unlinkSync(legacy);
144
+ log('🧹 Removed legacy moflo-bootstrap.md (pre-#939 duplicate of moflo-subagents.md)');
145
+ } else {
146
+ log('⚠️ Legacy moflo-bootstrap.md present — will be removed on next non-check run');
147
+ }
148
+ } catch { /* non-fatal */ }
149
+ }
150
+
160
151
  function updateClaudeMd(projectRoot) {
161
152
  const claudeMdPath = join(projectRoot, 'CLAUDE.md');
162
153
 
@@ -173,8 +164,8 @@ function updateClaudeMd(projectRoot) {
173
164
  const content = readFileSync(claudeMdPath, 'utf-8');
174
165
 
175
166
  // Check for current or legacy markers and replace
176
- const allStarts = [MARKER_START, ...LEGACY_STARTS];
177
- const allEnds = [MARKER_END, ...LEGACY_ENDS];
167
+ const allStarts = [MARKER_START, ...LEGACY_MARKER_STARTS];
168
+ const allEnds = [MARKER_END, ...LEGACY_MARKER_ENDS];
178
169
 
179
170
  for (let i = 0; i < allStarts.length; i++) {
180
171
  if (content.includes(allStarts[i])) {
@@ -235,8 +226,11 @@ function main() {
235
226
  log(`Project: ${projectRoot}`);
236
227
  console.log('');
237
228
 
238
- // Step 1: Copy bootstrap file
239
- const bootstrapOk = copyBootstrap(projectRoot);
229
+ // Step 1: Copy subagents guidance under its real name (#939 — was moflo-bootstrap.md)
230
+ const bootstrapOk = copySubagentsGuidance(projectRoot);
231
+
232
+ // Step 1b: Remove pre-#939 moflo-bootstrap.md if it still exists from an older install
233
+ cleanupLegacyBootstrap(projectRoot);
240
234
 
241
235
  // Step 2: Update CLAUDE.md (skip on --update, only refresh the file)
242
236
  const claudeOk = updateOnly ? true : updateClaudeMd(projectRoot);
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * /simplify diff classifier — issue #908.
3
+ * /flo-simplify diff classifier.
4
4
  *
5
5
  * Decides which review tier the current diff warrants and returns a JSON
6
- * dispatch decision. The /simplify skill MUST call this first so routing is
6
+ * dispatch decision. The /flo-simplify skill MUST call this first so routing is
7
7
  * deterministic and unit-testable instead of a prose decision Claude makes
8
8
  * over and over per run.
9
9
  *
10
- * Rule (per user direction): default to single-agent Sonnet review. Only
11
- * escalate to a 3-agent fan-out when diff signals genuinely warrant it.
12
- * Opus is never selected — the existing skill already documents that.
10
+ * Rule: default to single-agent Sonnet review. Only escalate to a 3-agent
11
+ * fan-out when diff signals genuinely warrant it. Opus is never selected —
12
+ * the existing skill already documents that.
13
13
  *
14
14
  * Outputs JSON:
15
15
  * {
@@ -21,7 +21,8 @@
21
21
  * }
22
22
  *
23
23
  * Usage:
24
- * node bin/simplify-classify.cjs [--base main]
24
+ * node bin/simplify-classify.cjs # auto-detects default branch
25
+ * node bin/simplify-classify.cjs --base develop # explicit override
25
26
  * node bin/simplify-classify.cjs --diff <unified-diff-on-stdin>
26
27
  *
27
28
  * The --diff stdin form exists so unit tests can drive the classifier
@@ -31,7 +32,7 @@
31
32
 
32
33
  const { execSync } = require('child_process');
33
34
 
34
- // Paths where new logic warrants the 3-agent fan-out (issue #908).
35
+ // Paths where new logic warrants the 3-agent fan-out.
35
36
  // Mechanical edits inside these paths are still SMALL; only adding/removing
36
37
  // declarations triggers escalation.
37
38
  const SECURITY_PATHS = [
@@ -49,6 +50,25 @@ function safeExec(cmd) {
49
50
  catch { return ''; }
50
51
  }
51
52
 
53
+ // Detect the consumer's default branch. Hardcoding 'main' silently miscalibrates
54
+ // classification on repos that use 'master', 'develop', etc. — empty diff →
55
+ // TRIVIAL → gate stamps clean without any real review.
56
+ let _cachedDefaultBranch = null;
57
+ function detectDefaultBranch() {
58
+ if (_cachedDefaultBranch !== null) return _cachedDefaultBranch;
59
+
60
+ // Preferred: origin/HEAD points to whatever the remote considers default.
61
+ const symbolic = safeExec('git symbolic-ref --short refs/remotes/origin/HEAD').trim();
62
+ if (symbolic.startsWith('origin/')) return (_cachedDefaultBranch = symbolic.slice('origin/'.length));
63
+
64
+ // Fallback: local init.defaultBranch (set by `git init -b <name>` or config).
65
+ const configured = safeExec('git config --get init.defaultBranch').trim();
66
+ if (configured) return (_cachedDefaultBranch = configured);
67
+
68
+ // Last resort: 'main' (most common modern default).
69
+ return (_cachedDefaultBranch = 'main');
70
+ }
71
+
52
72
  function readDiffFromGit(base) {
53
73
  // Combined diff: committed-since-base + working-tree
54
74
  const committed = safeExec(`git diff ${base}...HEAD`);
@@ -186,14 +206,15 @@ function classifyDiff(diffText) {
186
206
  return decide(parseDiff(diffText));
187
207
  }
188
208
 
189
- function classifyFromGit(base = 'main') {
190
- return classifyDiff(readDiffFromGit(base));
209
+ function classifyFromGit(base) {
210
+ const resolved = base || detectDefaultBranch();
211
+ return classifyDiff(readDiffFromGit(resolved));
191
212
  }
192
213
 
193
214
  if (require.main === module) {
194
215
  const args = process.argv.slice(2);
195
216
  const baseIdx = args.indexOf('--base');
196
- const base = baseIdx >= 0 ? args[baseIdx + 1] : 'main';
217
+ const base = baseIdx >= 0 ? args[baseIdx + 1] : detectDefaultBranch();
197
218
  const stdinDiff = args.includes('--diff') || args.includes('--stdin');
198
219
 
199
220
  let result;
@@ -211,4 +232,4 @@ if (require.main === module) {
211
232
  }
212
233
  }
213
234
 
214
- module.exports = { parseDiff, decide, classifyDiff, classifyFromGit };
235
+ module.exports = { parseDiff, decide, classifyDiff, classifyFromGit, detectDefaultBranch };
@@ -425,6 +425,10 @@ const REQUIRED_GATE_CASES = [
425
425
  'check-before-pr',
426
426
  'check-dangerous-command',
427
427
  'prompt-reminder',
428
+ // #931 — Defensive safety-net for the second UserPromptSubmit hook. State
429
+ // reset only, no emission. doctor warns if a consumer's gate.cjs is too old
430
+ // to handle it.
431
+ 'prompt-state-reset',
428
432
  'session-reset',
429
433
  ];
430
434
  // Import + re-export from the self-contained hook-wiring module (single source of truth).