moflo 4.9.20 → 4.9.22

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 (240) hide show
  1. package/.claude/agents/analysis/analyze-code-quality.md +0 -121
  2. package/.claude/agents/analysis/code-analyzer.md +5 -26
  3. package/.claude/agents/architecture/system-design/arch-system-design.md +0 -119
  4. package/.claude/agents/base-template-generator.md +0 -1
  5. package/.claude/agents/core/coder.md +0 -22
  6. package/.claude/agents/core/planner.md +0 -16
  7. package/.claude/agents/core/researcher.md +0 -16
  8. package/.claude/agents/core/reviewer.md +0 -17
  9. package/.claude/agents/core/tester.md +0 -19
  10. package/.claude/agents/custom/test-long-runner.md +0 -2
  11. package/.claude/agents/development/dev-backend-api.md +0 -167
  12. package/.claude/agents/development/dev-database.md +43 -0
  13. package/.claude/agents/development/dev-frontend.md +42 -0
  14. package/.claude/agents/devops/ci-cd/ops-cicd-github.md +0 -112
  15. package/.claude/agents/documentation/api-docs/docs-api-openapi.md +0 -111
  16. package/.claude/agents/security/security-auditor.md +45 -0
  17. package/.claude/guidance/shipped/moflo-agent-rules.md +172 -0
  18. package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +73 -265
  19. package/.claude/guidance/shipped/moflo-cli-reference.md +6 -6
  20. package/.claude/guidance/shipped/moflo-core-guidance.md +66 -184
  21. package/.claude/guidance/shipped/moflo-cross-platform.md +1 -1
  22. package/.claude/guidance/shipped/moflo-error-handling.md +3 -3
  23. package/.claude/guidance/shipped/moflo-guidance-rules.md +17 -7
  24. package/.claude/guidance/shipped/moflo-memory-strategy.md +76 -182
  25. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +6 -8
  26. package/.claude/guidance/shipped/moflo-settings-injection.md +7 -9
  27. package/.claude/guidance/shipped/moflo-source-hygiene.md +5 -5
  28. package/.claude/guidance/shipped/moflo-spell-connectors.md +3 -4
  29. package/.claude/guidance/shipped/moflo-spell-custom-steps.md +3 -4
  30. package/.claude/guidance/shipped/moflo-spell-engine.md +40 -162
  31. package/.claude/guidance/shipped/moflo-spell-runner.md +134 -0
  32. package/.claude/guidance/shipped/moflo-spell-sandboxing.md +10 -57
  33. package/.claude/guidance/shipped/moflo-spell-troubleshooting.md +149 -0
  34. package/.claude/guidance/shipped/moflo-subagents.md +43 -114
  35. package/.claude/guidance/shipped/moflo-task-icons.md +4 -4
  36. package/.claude/guidance/shipped/moflo-user-facing-language.md +3 -3
  37. package/.claude/guidance/shipped/moflo-verbose-command-filtering.md +3 -3
  38. package/.claude/guidance/shipped/moflo-yaml-reference.md +4 -5
  39. package/.claude/helpers/gate.cjs +192 -15
  40. package/.claude/helpers/prompt-hook.mjs +4 -38
  41. package/.claude/helpers/simplify-classify.cjs +32 -11
  42. package/.claude/helpers/subagent-bootstrap.json +1 -1
  43. package/.claude/helpers/subagent-start.cjs +1 -1
  44. package/.claude/skills/connector-builder/SKILL.md +42 -429
  45. package/.claude/skills/connector-builder/templates/connector.md +189 -0
  46. package/.claude/skills/connector-builder/templates/step-command.md +176 -0
  47. package/.claude/skills/eldar/SKILL.md +7 -7
  48. package/.claude/skills/fl/SKILL.md +3 -3
  49. package/.claude/skills/fl/execution-modes.md +39 -16
  50. package/.claude/skills/fl/phases.md +3 -3
  51. package/.claude/skills/{simplify → flo-simplify}/SKILL.md +11 -11
  52. package/.claude/skills/guidance/SKILL.md +17 -9
  53. package/.claude/skills/memory-patterns/SKILL.md +1 -1
  54. package/.claude/skills/publish/SKILL.md +121 -36
  55. package/.claude/skills/reset-epic/SKILL.md +2 -2
  56. package/.claude/skills/spell-builder/SKILL.md +39 -226
  57. package/.claude/skills/spell-builder/architecture.md +1 -1
  58. package/.claude/skills/spell-builder/permissions.md +107 -0
  59. package/.claude/skills/spell-builder/preflight.md +101 -0
  60. package/.claude/skills/spell-schedule/SKILL.md +2 -3
  61. package/bin/gate.cjs +192 -15
  62. package/bin/lib/retired-files.mjs +146 -0
  63. package/bin/prompt-hook.mjs +4 -38
  64. package/bin/session-start-launcher.mjs +120 -1
  65. package/bin/setup-project.mjs +63 -69
  66. package/bin/simplify-classify.cjs +32 -11
  67. package/dist/src/cli/appliance/rvfa-builder.js +1 -1
  68. package/dist/src/cli/commands/agent.js +3 -9
  69. package/dist/src/cli/commands/doctor-checks-deep.js +4 -0
  70. package/dist/src/cli/commands/hooks.js +1 -3
  71. package/dist/src/cli/commands/index.js +2 -0
  72. package/dist/src/cli/commands/retire.js +111 -0
  73. package/dist/src/cli/hooks/reasoningbank/index.js +7 -7
  74. package/dist/src/cli/init/claudemd-generator.js +30 -33
  75. package/dist/src/cli/init/executor.js +53 -69
  76. package/dist/src/cli/init/helpers-generator.js +165 -52
  77. package/dist/src/cli/init/moflo-init.js +41 -114
  78. package/dist/src/cli/init/settings-generator.js +44 -14
  79. package/dist/src/cli/mcp-tools/agent-tools.js +9 -27
  80. package/dist/src/cli/mcp-tools/hooks-tools.js +23 -21
  81. package/dist/src/cli/memory/controllers/semantic-router.js +18 -12
  82. package/dist/src/cli/memory/sona-optimizer.js +6 -6
  83. package/dist/src/cli/neural/domain/services/learning-service.js +3 -3
  84. package/dist/src/cli/services/agent-router.js +2 -5
  85. package/dist/src/cli/services/hook-block-hash.js +11 -2
  86. package/dist/src/cli/services/hook-wiring.js +86 -3
  87. package/dist/src/cli/services/subagent-bootstrap.js +1 -1
  88. package/dist/src/cli/shared/events/example-usage.js +6 -6
  89. package/dist/src/cli/shared/hooks/task-hooks.js +8 -8
  90. package/dist/src/cli/version.js +1 -1
  91. package/package.json +3 -2
  92. package/retired-files.json +1989 -0
  93. package/scripts/post-install-bootstrap.mjs +19 -0
  94. package/src/cli/data/model-registry.json +2 -2
  95. package/.claude/agents/consensus/byzantine-coordinator.md +0 -63
  96. package/.claude/agents/consensus/crdt-synchronizer.md +0 -997
  97. package/.claude/agents/consensus/gossip-coordinator.md +0 -63
  98. package/.claude/agents/consensus/performance-benchmarker.md +0 -851
  99. package/.claude/agents/consensus/quorum-manager.md +0 -823
  100. package/.claude/agents/consensus/raft-manager.md +0 -63
  101. package/.claude/agents/consensus/security-manager.md +0 -622
  102. package/.claude/agents/data/ml/data-ml-model.md +0 -193
  103. package/.claude/agents/github/code-review-swarm.md +0 -538
  104. package/.claude/agents/github/github-modes.md +0 -172
  105. package/.claude/agents/github/issue-tracker.md +0 -311
  106. package/.claude/agents/github/multi-repo-swarm.md +0 -551
  107. package/.claude/agents/github/pr-manager.md +0 -183
  108. package/.claude/agents/github/project-board-sync.md +0 -508
  109. package/.claude/agents/github/release-manager.md +0 -360
  110. package/.claude/agents/github/release-swarm.md +0 -580
  111. package/.claude/agents/github/repo-architect.md +0 -391
  112. package/.claude/agents/github/swarm-issue.md +0 -566
  113. package/.claude/agents/github/swarm-pr.md +0 -414
  114. package/.claude/agents/github/sync-coordinator.md +0 -426
  115. package/.claude/agents/github/workflow-automation.md +0 -606
  116. package/.claude/agents/goal/code-goal-planner.md +0 -440
  117. package/.claude/agents/goal/goal-planner.md +0 -168
  118. package/.claude/agents/hive-mind/collective-intelligence-coordinator.md +0 -127
  119. package/.claude/agents/hive-mind/queen-coordinator.md +0 -198
  120. package/.claude/agents/hive-mind/scout-explorer.md +0 -233
  121. package/.claude/agents/hive-mind/swarm-memory-manager.md +0 -184
  122. package/.claude/agents/hive-mind/worker-specialist.md +0 -208
  123. package/.claude/agents/neural/safla-neural.md +0 -73
  124. package/.claude/agents/optimization/benchmark-suite.md +0 -665
  125. package/.claude/agents/optimization/load-balancer.md +0 -431
  126. package/.claude/agents/optimization/performance-monitor.md +0 -672
  127. package/.claude/agents/optimization/resource-allocator.md +0 -674
  128. package/.claude/agents/optimization/topology-optimizer.md +0 -808
  129. package/.claude/agents/reasoning/goal-planner.md +0 -67
  130. package/.claude/agents/sona/sona-learning-optimizer.md +0 -74
  131. package/.claude/agents/sparc/architecture.md +0 -472
  132. package/.claude/agents/sparc/pseudocode.md +0 -318
  133. package/.claude/agents/sparc/refinement.md +0 -525
  134. package/.claude/agents/sparc/specification.md +0 -276
  135. package/.claude/agents/specialized/mobile/spec-mobile-react-native.md +0 -225
  136. package/.claude/agents/swarm/adaptive-coordinator.md +0 -391
  137. package/.claude/agents/swarm/hierarchical-coordinator.md +0 -321
  138. package/.claude/agents/swarm/mesh-coordinator.md +0 -383
  139. package/.claude/agents/testing/production-validator.md +0 -395
  140. package/.claude/agents/testing/tdd-london-swarm.md +0 -244
  141. package/.claude/agents/v3/adr-architect.md +0 -184
  142. package/.claude/agents/v3/aidefence-guardian.md +0 -277
  143. package/.claude/agents/v3/claims-authorizer.md +0 -208
  144. package/.claude/agents/v3/collective-intelligence-coordinator.md +0 -988
  145. package/.claude/agents/v3/ddd-domain-expert.md +0 -220
  146. package/.claude/agents/v3/injection-analyst.md +0 -232
  147. package/.claude/agents/v3/memory-specialist.md +0 -987
  148. package/.claude/agents/v3/performance-engineer.md +0 -1225
  149. package/.claude/agents/v3/pii-detector.md +0 -146
  150. package/.claude/agents/v3/reasoningbank-learner.md +0 -213
  151. package/.claude/agents/v3/security-architect-aidefence.md +0 -405
  152. package/.claude/agents/v3/security-architect.md +0 -865
  153. package/.claude/agents/v3/security-auditor.md +0 -771
  154. package/.claude/agents/v3/sparc-orchestrator.md +0 -182
  155. package/.claude/agents/v3/swarm-memory-manager.md +0 -142
  156. package/.claude/agents/v3/v3-integration-architect.md +0 -205
  157. package/.claude/commands/claude-flow-help.md +0 -103
  158. package/.claude/commands/claude-flow-memory.md +0 -107
  159. package/.claude/commands/claude-flow-swarm.md +0 -205
  160. package/.claude/commands/github/README.md +0 -11
  161. package/.claude/commands/github/code-review-swarm.md +0 -514
  162. package/.claude/commands/github/code-review.md +0 -25
  163. package/.claude/commands/github/github-modes.md +0 -146
  164. package/.claude/commands/github/github-swarm.md +0 -113
  165. package/.claude/commands/github/issue-tracker.md +0 -284
  166. package/.claude/commands/github/issue-triage.md +0 -25
  167. package/.claude/commands/github/multi-repo-swarm.md +0 -519
  168. package/.claude/commands/github/pr-enhance.md +0 -26
  169. package/.claude/commands/github/pr-manager.md +0 -164
  170. package/.claude/commands/github/project-board-sync.md +0 -471
  171. package/.claude/commands/github/release-manager.md +0 -332
  172. package/.claude/commands/github/release-swarm.md +0 -544
  173. package/.claude/commands/github/repo-analyze.md +0 -25
  174. package/.claude/commands/github/repo-architect.md +0 -361
  175. package/.claude/commands/github/swarm-issue.md +0 -482
  176. package/.claude/commands/github/swarm-pr.md +0 -285
  177. package/.claude/commands/github/sync-coordinator.md +0 -294
  178. package/.claude/commands/github/workflow-automation.md +0 -442
  179. package/.claude/commands/hooks/README.md +0 -11
  180. package/.claude/commands/hooks/overview.md +0 -58
  181. package/.claude/commands/hooks/post-edit.md +0 -117
  182. package/.claude/commands/hooks/post-task.md +0 -112
  183. package/.claude/commands/hooks/pre-edit.md +0 -113
  184. package/.claude/commands/hooks/pre-task.md +0 -111
  185. package/.claude/commands/hooks/session-end.md +0 -118
  186. package/.claude/commands/hooks/setup.md +0 -103
  187. package/.claude/commands/simplify.md +0 -101
  188. package/.claude/commands/sparc/analyzer.md +0 -42
  189. package/.claude/commands/sparc/architect.md +0 -43
  190. package/.claude/commands/sparc/ask.md +0 -86
  191. package/.claude/commands/sparc/batch-executor.md +0 -44
  192. package/.claude/commands/sparc/code.md +0 -78
  193. package/.claude/commands/sparc/coder.md +0 -44
  194. package/.claude/commands/sparc/debug.md +0 -72
  195. package/.claude/commands/sparc/debugger.md +0 -44
  196. package/.claude/commands/sparc/designer.md +0 -43
  197. package/.claude/commands/sparc/devops.md +0 -98
  198. package/.claude/commands/sparc/docs-writer.md +0 -69
  199. package/.claude/commands/sparc/documenter.md +0 -44
  200. package/.claude/commands/sparc/innovator.md +0 -44
  201. package/.claude/commands/sparc/integration.md +0 -72
  202. package/.claude/commands/sparc/mcp.md +0 -106
  203. package/.claude/commands/sparc/memory-manager.md +0 -44
  204. package/.claude/commands/sparc/optimizer.md +0 -44
  205. package/.claude/commands/sparc/orchestrator.md +0 -116
  206. package/.claude/commands/sparc/post-deployment-monitoring-mode.md +0 -72
  207. package/.claude/commands/sparc/refinement-optimization-mode.md +0 -72
  208. package/.claude/commands/sparc/researcher.md +0 -44
  209. package/.claude/commands/sparc/reviewer.md +0 -44
  210. package/.claude/commands/sparc/security-review.md +0 -69
  211. package/.claude/commands/sparc/sparc-modes.md +0 -139
  212. package/.claude/commands/sparc/sparc.md +0 -99
  213. package/.claude/commands/sparc/spec-pseudocode.md +0 -69
  214. package/.claude/commands/sparc/spell-manager.md +0 -44
  215. package/.claude/commands/sparc/supabase-admin.md +0 -337
  216. package/.claude/commands/sparc/swarm-coordinator.md +0 -44
  217. package/.claude/commands/sparc/tdd.md +0 -44
  218. package/.claude/commands/sparc/tester.md +0 -44
  219. package/.claude/commands/sparc/tutorial.md +0 -68
  220. package/.claude/commands/sparc.md +0 -151
  221. package/.claude/guidance/shipped/moflo-session-start.md +0 -154
  222. package/.claude/guidance/shipped/moflo-spell-engine-architecture.md +0 -145
  223. package/.claude/skills/browser/SKILL.md +0 -204
  224. package/.claude/skills/github-code-review/SKILL.md +0 -1140
  225. package/.claude/skills/github-multi-repo/SKILL.md +0 -866
  226. package/.claude/skills/github-project-management/SKILL.md +0 -1272
  227. package/.claude/skills/github-release-management/SKILL.md +0 -1074
  228. package/.claude/skills/github-workflow-automation/SKILL.md +0 -1060
  229. package/.claude/skills/hive-mind-advanced/SKILL.md +0 -712
  230. package/.claude/skills/hooks-automation/SKILL.md +0 -1193
  231. package/.claude/skills/pair-programming/SKILL.md +0 -1202
  232. package/.claude/skills/performance-analysis/SKILL.md +0 -563
  233. package/.claude/skills/skill-builder/SKILL.md +0 -910
  234. package/.claude/skills/sparc-methodology/SKILL.md +0 -904
  235. package/.claude/skills/stream-chain/SKILL.md +0 -563
  236. package/.claude/skills/swarm-advanced/SKILL.md +0 -811
  237. package/.claude/skills/swarm-orchestration/SKILL.md +0 -179
  238. package/.claude/skills/verification-quality/SKILL.md +0 -649
  239. package/.claude/skills/worker-benchmarks/skill.md +0 -135
  240. 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: {}, flMode: null, swarmInitialized: false, hiveInitialized: false };
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
@@ -60,7 +60,7 @@ function writeState(s) {
60
60
 
61
61
  // Load moflo.yaml gate config (defaults: all enabled)
62
62
  function loadGateConfig() {
63
- var defaults = { memory_first: true, task_create_first: true, context_tracking: true, testing_gate: true, simplify_gate: true, learnings_gate: true };
63
+ 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 };
64
64
  try {
65
65
  var yamlPath = path.join(PROJECT_DIR, 'moflo.yaml');
66
66
  if (fs.existsSync(yamlPath)) {
@@ -71,6 +71,7 @@ function loadGateConfig() {
71
71
  if (/testing_gate:\s*false/i.test(content)) defaults.testing_gate = false;
72
72
  if (/simplify_gate:\s*false/i.test(content)) defaults.simplify_gate = false;
73
73
  if (/learnings_gate:\s*false/i.test(content)) defaults.learnings_gate = false;
74
+ if (/swarm_invocation_gate:\s*false/i.test(content)) defaults.swarm_invocation_gate = false;
74
75
  }
75
76
  } catch (e) { /* use defaults */ }
76
77
  return defaults;
@@ -83,6 +84,99 @@ var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state
83
84
  var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
84
85
  var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\b/i;
85
86
  var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\b/i;
87
+
88
+ // Namespace classification (#931). The hint used to be emitted on every prompt
89
+ // by prompt-hook.mjs which cost ~40 tokens × every prompt × every consumer.
90
+ // Now we classify here, store on workflow-state, and let check-before-agent
91
+ // emit it once when Claude is actually about to spawn an agent.
92
+ //
93
+ // SYNC: these regexes + classifyNamespaceHint + applyPromptStateReset are
94
+ // duplicated verbatim in src/cli/init/helpers-generator.ts (the embedded
95
+ // gate.cjs fallback used by `flo init` when source helpers can't be located).
96
+ // Any edit to either copy MUST be applied to both — there is no shared module
97
+ // because helpers-generator emits a self-contained string template.
98
+ var NS_LEARNINGS_RE = /\b(remember|recall|insight|lesson learned|gotcha|post.?mortem)\b|we (decid|agree|chose|said)/;
99
+ var NS_TEST_RE = /\b(test|spec|coverage|tested|test case|test cases|tests for|spec for)\b/;
100
+ var NS_EXPLICIT = [
101
+ { pattern: /\b(pattern|convention|best practice|style|coding rule)\b/, ns: 'patterns', label: 'code patterns and conventions' },
102
+ { pattern: /\b(code.?map|file structure|project structure|directory)\b/, ns: 'code-map', label: 'codebase navigation' },
103
+ ];
104
+ var NS_PATTERN_RES = [/\b(template|example|similar to|how do we|how should)\b/];
105
+ var NS_DOMAIN_RES = [
106
+ /\b(guidance|guide|docs|documentation|rules|how-to)\b/,
107
+ /\b(architecture|design|domain|tenant|migrat|schema|deploy)/,
108
+ /\b(rule|requirement|constraint|compliance)\b/,
109
+ ];
110
+ var NS_NAV_RES = [
111
+ /\b(find|where|which file|look up|locate|endpoint|route|url|path)\b/,
112
+ /\b(class|function|method|component|service|entity|module)\b/,
113
+ ];
114
+
115
+ // Detect whether the current prompt invoked /fl or /flo with a swarm/hive flag (#952).
116
+ // When set, check-before-agent BLOCKS the Agent spawn until the matching MCP init
117
+ // (mcp__moflo__swarm_init or mcp__moflo__hive-mind_init) has been recorded — the user
118
+ // explicitly opted in to the protected coordination surface, so falling back to
119
+ // raw Agent dispatch silently regresses headline moflo product capability.
120
+ //
121
+ // SYNC: duplicated verbatim in src/cli/init/helpers-generator.ts.
122
+ function detectFlMode(promptText) {
123
+ var p = promptText || '';
124
+ if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return null;
125
+ if (/(?:^|\s)(?:-s|--swarm)\b/.test(p)) return 'swarm';
126
+ if (/(?:^|\s)(?:-h|--hive)\b/.test(p)) return 'hive';
127
+ return null;
128
+ }
129
+
130
+ function classifyNamespaceHint(promptText) {
131
+ var lower = (promptText || '').toLowerCase();
132
+ if (NS_TEST_RE.test(lower)) return 'Memory namespace hint: use "tests" for test inventory and coverage lookups.';
133
+ if (NS_LEARNINGS_RE.test(lower)) return 'Memory namespace hint: use "learnings" for user-directed decisions and distilled insights.';
134
+ for (var i = 0; i < NS_EXPLICIT.length; i++) {
135
+ if (NS_EXPLICIT[i].pattern.test(lower)) return 'Memory namespace hint: use "' + NS_EXPLICIT[i].ns + '" for ' + NS_EXPLICIT[i].label + '.';
136
+ }
137
+ for (var j = 0; j < NS_DOMAIN_RES.length; j++) {
138
+ if (NS_DOMAIN_RES[j].test(lower)) return 'Memory namespace hint: search "guidance" and "learnings" for domain rules and project decisions.';
139
+ }
140
+ for (var k = 0; k < NS_PATTERN_RES.length; k++) {
141
+ if (NS_PATTERN_RES[k].test(lower)) return 'Memory namespace hint: use "patterns" for code patterns and conventions.';
142
+ }
143
+ for (var m = 0; m < NS_NAV_RES.length; m++) {
144
+ if (NS_NAV_RES[m].test(lower)) return 'Memory namespace hint: use "code-map" for codebase navigation.';
145
+ }
146
+ return '';
147
+ }
148
+
149
+ // Apply per-prompt state reset shared by `prompt-reminder` (full) and
150
+ // `prompt-state-reset` (defensive safety-net, no emission). Idempotent — both
151
+ // UserPromptSubmit hooks can run it without compounding any field. Caller
152
+ // owns interactionCount and the user-visible REMINDER/Context emissions, so
153
+ // this helper stays silent.
154
+ function applyPromptStateReset(state, promptText) {
155
+ state.memorySearched = false;
156
+ // Wipe per-actor memory tracking too — a new user prompt is a fresh window
157
+ // for both parent AND any subagents the parent may spawn during this turn.
158
+ state.memorySearchedBy = {};
159
+ // learningsStored is session-scoped — once stored, it stays true until session reset.
160
+ // Resetting per-prompt caused false blocks when PR creation was on a later prompt.
161
+ var DIRECTIVE_MAX_LEN = 20;
162
+ var escaped = /^@@\s*/.test(promptText || '');
163
+ state.memoryRequired = !escaped && (promptText || '').length >= 4 && (TASK_RE.test(promptText || '') || (promptText || '').length > DIRECTIVE_MAX_LEN);
164
+ // Stash namespace hint for check-before-agent to emit when Claude actually
165
+ // spawns an Agent (#931). Empty string when nothing matched — overwriting
166
+ // any stale value from the previous prompt.
167
+ state.lastNamespaceHint = classifyNamespaceHint(promptText);
168
+ // Per-actor emission tracking — each subagent's session gets the hint at
169
+ // most once per prompt, but a fresh prompt resets every actor's window so
170
+ // subsequent agents (parent + subagents that spawn their own agents) all
171
+ // see the new classification on their first check-before-agent.
172
+ state.lastNamespaceHintEmittedBy = {};
173
+ // #952 — derive flMode from the user prompt, and reset the matching init
174
+ // flag. Each /fl invocation must call its protected MCP init; the previous
175
+ // prompt's swarm/hive registration does not satisfy this prompt's gate.
176
+ state.flMode = detectFlMode(promptText);
177
+ state.swarmInitialized = false;
178
+ state.hiveInitialized = false;
179
+ }
86
180
  // Match npm/yarn/pnpm/bun test, npx vitest|jest|..., bare runners at command-start only,
87
181
  // and language-native test commands. The bare-runner arm is anchored so that
88
182
  // `npm install jest`, `grep -r vitest src/`, and similar don't false-positive.
@@ -203,6 +297,11 @@ switch (command) {
203
297
  // Advisory only — agent spawning is never blocked.
204
298
  // Memory-first enforcement happens at the scan/read gate layer.
205
299
  // SubagentStart hook injects guidance directive into subagent context.
300
+ //
301
+ // #931 — TaskCreate REMINDER and the namespace hint moved here from
302
+ // prompt-reminder. They only matter when Claude is actually about to spawn
303
+ // an Agent; emitting per-prompt cost ~90 tokens × every prompt × every
304
+ // consumer.
206
305
  var s = readState();
207
306
  if (config.task_create_first && !s.tasksCreated) {
208
307
  process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
@@ -210,6 +309,65 @@ switch (command) {
210
309
  if (config.memory_first && s.memoryRequired && !s.memorySearched) {
211
310
  process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents.\n');
212
311
  }
312
+ if (s.lastNamespaceHint) {
313
+ // Per-actor single-shot. Each session_id gets the hint at most once per
314
+ // prompt, but the hint itself stays available for other actors (e.g.
315
+ // a subagent that spawns its own agent has its own session_id and is
316
+ // entitled to a fresh emission). Falls back to a `_legacy_` bucket when
317
+ // Claude Code didn't forward a session_id (older host or direct CLI
318
+ // invocation), preserving the old "emit once globally" behavior. The
319
+ // map is wiped by applyPromptStateReset on every new prompt.
320
+ var sid = process.env.HOOK_SESSION_ID || '';
321
+ var emittedBy = s.lastNamespaceHintEmittedBy || {};
322
+ var bucket = sid || '_legacy_';
323
+ if (!emittedBy[bucket]) {
324
+ process.stdout.write(s.lastNamespaceHint + '\n');
325
+ emittedBy[bucket] = true;
326
+ s.lastNamespaceHintEmittedBy = emittedBy;
327
+ writeState(s);
328
+ }
329
+ }
330
+ // #952 — when /fl was invoked with -s/-h, the protected MCP init must run
331
+ // BEFORE any Agent spawn. Hard block: the user explicitly opted in to
332
+ // moflo's coordination surface, so silently dispatching `Agent` calls
333
+ // without `mcp__moflo__swarm_init` / `mcp__moflo__hive-mind_init` is the
334
+ // failure mode this gate exists to prevent (CLAUDE.md "⛔ Protected
335
+ // functionality — swarm + hive-mind"). Other Agent uses remain advisory.
336
+ if (config.swarm_invocation_gate) {
337
+ if (s.flMode === 'swarm' && !s.swarmInitialized) {
338
+ process.stderr.write('BLOCKED: /fl was invoked with -s/--swarm but mcp__moflo__swarm_init has not been called.\n');
339
+ process.stderr.write('Run mcp__moflo__swarm_init first, then mcp__moflo__agent_spawn for each role, then dispatch Agent.\n');
340
+ process.stderr.write('See .claude/skills/fl/execution-modes.md "SWARM mode" and CLAUDE.md "⛔ Protected functionality".\n');
341
+ process.stderr.write('Disable via moflo.yaml: gates: swarm_invocation_gate: false\n');
342
+ process.exit(2);
343
+ }
344
+ if (s.flMode === 'hive' && !s.hiveInitialized) {
345
+ process.stderr.write('BLOCKED: /fl was invoked with -h/--hive but mcp__moflo__hive-mind_init has not been called.\n');
346
+ process.stderr.write('Run mcp__moflo__hive-mind_init first, then dispatch Agent or hive-mind workers.\n');
347
+ process.stderr.write('See .claude/skills/fl/execution-modes.md "HIVE-MIND mode" and CLAUDE.md "⛔ Protected functionality".\n');
348
+ process.stderr.write('Disable via moflo.yaml: gates: swarm_invocation_gate: false\n');
349
+ process.exit(2);
350
+ }
351
+ }
352
+ break;
353
+ }
354
+ case 'record-swarm-init': {
355
+ // #952 — wired to mcp__moflo__swarm_init PostToolUse. Marks the gate
356
+ // satisfied so subsequent Agent spawns under /fl -s pass.
357
+ var s = readState();
358
+ if (!s.swarmInitialized) {
359
+ s.swarmInitialized = true;
360
+ writeState(s);
361
+ }
362
+ break;
363
+ }
364
+ case 'record-hive-init': {
365
+ // #952 — wired to mcp__moflo__hive-mind_init PostToolUse.
366
+ var s = readState();
367
+ if (!s.hiveInitialized) {
368
+ s.hiveInitialized = true;
369
+ writeState(s);
370
+ }
213
371
  break;
214
372
  }
215
373
  case 'check-before-scan': {
@@ -277,7 +435,8 @@ switch (command) {
277
435
  break;
278
436
  }
279
437
  case 'record-skill-run': {
280
- if ((process.env.TOOL_INPUT_skill || '') === 'simplify') {
438
+ var skName = (process.env.TOOL_INPUT_skill || '');
439
+ if (skName === 'simplify' || skName === 'flo-simplify') {
281
440
  var s = readState();
282
441
  var changed = false;
283
442
  if (!s.simplifyRun) { s.simplifyRun = true; changed = true; }
@@ -346,7 +505,7 @@ switch (command) {
346
505
  }
347
506
  var missing = [];
348
507
  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');
508
+ if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify has not run since the last code edit');
350
509
  if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
351
510
  if (missing.length === 0) break;
352
511
  process.stderr.write('BLOCKED: gh pr create requires the following before opening a PR:\n');
@@ -371,20 +530,16 @@ switch (command) {
371
530
  break;
372
531
  }
373
532
  case 'prompt-reminder': {
533
+ // Full per-prompt reset. Wired as the first UserPromptSubmit hook (via
534
+ // prompt-hook.mjs). Owns interactionCount + Context warnings; the
535
+ // TaskCreate REMINDER and namespace hint moved to check-before-agent
536
+ // (#931) so they only fire when Claude is actually about to spawn an
537
+ // Agent.
374
538
  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
539
  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);
540
+ applyPromptStateReset(s, prompt);
385
541
  s.interactionCount = (s.interactionCount || 0) + 1;
386
542
  writeState(s);
387
- if (!s.tasksCreated) console.log('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.');
388
543
  if (config.context_tracking) {
389
544
  var ic = s.interactionCount;
390
545
  if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
@@ -393,12 +548,34 @@ switch (command) {
393
548
  }
394
549
  break;
395
550
  }
551
+ case 'prompt-state-reset': {
552
+ // Defensive safety-net hook (#931 dedupe). Wired as the second
553
+ // UserPromptSubmit hook so an exception in prompt-hook.mjs doesn't skip
554
+ // the per-prompt state reset. Idempotent — applyPromptStateReset only
555
+ // sets fields to derived values, and we deliberately do NOT increment
556
+ // interactionCount or emit anything (that's prompt-reminder's job).
557
+ //
558
+ // Skip the disk write on the normal path: prompt-reminder runs first and
559
+ // already wrote the byte-identical post-reset state. Only writeState when
560
+ // the reset actually changed something (i.e., prompt-reminder was skipped
561
+ // because prompt-hook.mjs threw before invoking it).
562
+ var s = readState();
563
+ var prompt = process.env.CLAUDE_USER_PROMPT || '';
564
+ var before = JSON.stringify(s);
565
+ applyPromptStateReset(s, prompt);
566
+ if (JSON.stringify(s) !== before) writeState(s);
567
+ break;
568
+ }
396
569
  case 'compact-guidance': {
397
570
  console.log('Pre-Compact: Check CLAUDE.md for rules. Use memory search to recover context after compact.');
398
571
  break;
399
572
  }
400
573
  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 });
574
+ // Derive from STATE_DEFAULTS so adding a new state field requires only one
575
+ // edit (the defaults object) — the literal that used to live here drifted
576
+ // every time a field was added and is what motivated #952's audit of state
577
+ // shape consistency.
578
+ writeState(Object.assign({}, STATE_DEFAULTS, { sessionStart: new Date().toISOString() }));
402
579
  break;
403
580
  }
404
581
  default:
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Retired-shipped-files prune helper (#948).
3
+ *
4
+ * Closes the gap that retired agents (#932) and retired skills (#945) leave
5
+ * in consumer projects: the manifest cleanup at section 3 of the launcher
6
+ * only knows about files moflo previously synced, but `.claude/agents/` and
7
+ * `.claude/skills/` were not in the manifest before #948. So files retired
8
+ * before #948 lands stay on disk forever — Claude Code keeps loading them as
9
+ * subagents on every prompt, paying the per-prompt roster tokens we just
10
+ * spent #932 fixing.
11
+ *
12
+ * This module reads `retired-files.json` (shipped at the moflo package
13
+ * root) and, for each entry, only prunes the consumer-side path when the
14
+ * file's sha256 matches one of `knownContentHashes` — i.e. the file matches
15
+ * a version moflo actually shipped, so the consumer didn't customize it.
16
+ * Customized files are preserved and a one-line notice is emitted so the
17
+ * user can act.
18
+ *
19
+ * Manifest format:
20
+ *
21
+ * {
22
+ * "version": 1,
23
+ * "retired": [
24
+ * {
25
+ * "path": ".claude/agents/v3/performance-engineer.md",
26
+ * "retiredIn": "4.9.22",
27
+ * "retiredBy": "#932",
28
+ * "knownContentHashes": ["sha256:abc...", "sha256:def..."]
29
+ * }
30
+ * ]
31
+ * }
32
+ *
33
+ * @module bin/lib/retired-files
34
+ */
35
+
36
+ import { existsSync, readFileSync, unlinkSync } from 'fs';
37
+ import { resolve } from 'path';
38
+ import { createHash } from 'crypto';
39
+
40
+ /**
41
+ * Compute sha256 of file content. Returns null on read errors so the caller
42
+ * can decide what to do — we never want a transient stat/read failure to
43
+ * trigger a prune of a file we couldn't actually verify.
44
+ *
45
+ * @param {string} absPath
46
+ * @returns {string|null} `sha256:<hex>` or null
47
+ */
48
+ export function fileSha256(absPath) {
49
+ try {
50
+ const buf = readFileSync(absPath);
51
+ return 'sha256:' + createHash('sha256').update(buf).digest('hex');
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Load + validate the retired-files manifest. Returns `{ entries: [] }` for
59
+ * any failure mode (missing file, invalid JSON, wrong shape) — the launcher
60
+ * must never block on a corrupt manifest.
61
+ *
62
+ * @param {string} manifestPath - absolute path to retired-files.json
63
+ * @returns {{ entries: Array<{path:string, retiredIn?:string, retiredBy?:string, knownContentHashes:string[]}> }}
64
+ */
65
+ export function loadRetiredManifest(manifestPath) {
66
+ if (!existsSync(manifestPath)) return { entries: [] };
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
70
+ } catch { return { entries: [] }; }
71
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.retired)) {
72
+ return { entries: [] };
73
+ }
74
+ // Filter out malformed entries — guards against a hand-edited file
75
+ // dropping the wrong shape into a launcher that runs on N consumer
76
+ // machines. A skipped entry never auto-prunes (safest default).
77
+ const entries = parsed.retired.filter((entry) =>
78
+ entry &&
79
+ typeof entry.path === 'string' &&
80
+ entry.path.length > 0 &&
81
+ Array.isArray(entry.knownContentHashes) &&
82
+ entry.knownContentHashes.length > 0 &&
83
+ entry.knownContentHashes.every((h) => typeof h === 'string' && h.startsWith('sha256:'))
84
+ );
85
+ return { entries };
86
+ }
87
+
88
+ /**
89
+ * Decide what to do with a consumer-side path against a retirement entry:
90
+ *
91
+ * - 'absent' — path doesn't exist on disk; nothing to do
92
+ * - 'prune' — file exists and content hash matches a known-shipped
93
+ * value; safe to delete (consumer didn't customize)
94
+ * - 'preserve' — file exists but content differs from every known-shipped
95
+ * hash; consumer customized, leave alone
96
+ * - 'unknown' — file exists but its hash couldn't be read (transient
97
+ * error); leave alone, retry next session
98
+ *
99
+ * @param {string} projectRoot
100
+ * @param {{path:string, knownContentHashes:string[]}} entry
101
+ * @returns {{ action: 'absent'|'prune'|'preserve'|'unknown', actualHash: string|null }}
102
+ */
103
+ export function classifyRetiredFile(projectRoot, entry) {
104
+ const abs = resolve(projectRoot, entry.path);
105
+ if (!existsSync(abs)) return { action: 'absent', actualHash: null };
106
+ const actualHash = fileSha256(abs);
107
+ if (!actualHash) return { action: 'unknown', actualHash: null };
108
+ if (entry.knownContentHashes.includes(actualHash)) {
109
+ return { action: 'prune', actualHash };
110
+ }
111
+ return { action: 'preserve', actualHash };
112
+ }
113
+
114
+ /**
115
+ * Walk the manifest and apply prune decisions. Returns a small report so
116
+ * the caller can emit one summary line (the launcher pattern) instead of
117
+ * forcing it to track per-entry state.
118
+ *
119
+ * Failures are non-fatal — a single un-deletable file (Windows AV hold,
120
+ * EBUSY) must not stop pruning the rest. The caller surfaces the report.
121
+ *
122
+ * @param {string} projectRoot
123
+ * @param {string} manifestPath
124
+ * @returns {{ pruned: string[], preserved: string[], unknown: string[], failed: Array<{path:string, message:string}> }}
125
+ */
126
+ export function applyRetiredPrune(projectRoot, manifestPath) {
127
+ const { entries } = loadRetiredManifest(manifestPath);
128
+ const report = { pruned: [], preserved: [], unknown: [], failed: [] };
129
+ for (const entry of entries) {
130
+ const { action } = classifyRetiredFile(projectRoot, entry);
131
+ if (action === 'absent') continue;
132
+ if (action === 'preserve') { report.preserved.push(entry.path); continue; }
133
+ if (action === 'unknown') { report.unknown.push(entry.path); continue; }
134
+ // action === 'prune'
135
+ try {
136
+ unlinkSync(resolve(projectRoot, entry.path));
137
+ report.pruned.push(entry.path);
138
+ } catch (err) {
139
+ report.failed.push({
140
+ path: entry.path,
141
+ message: err && err.message ? err.message : String(err),
142
+ });
143
+ }
144
+ }
145
+ return report;
146
+ }
@@ -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);
@@ -14,6 +14,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
14
14
  import { mofloDir } from './lib/moflo-paths.mjs';
15
15
  import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
16
16
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
17
+ import { applyRetiredPrune } from './lib/retired-files.mjs';
17
18
 
18
19
  // Headless skip (#860). The daemon's headless workers spawn `claude --print`
19
20
  // with CLAUDE_CODE_HEADLESS=true (see src/cli/services/headless-worker-
@@ -681,6 +682,56 @@ try {
681
682
  }
682
683
  }
683
684
 
685
+ // ── Sync .claude/agents/ + .claude/skills/ recursively (#948) ──────
686
+ // Pre-#948, agents and skills weren't manifest-tracked at all, so any
687
+ // file moflo retired (e.g. the 49 ruflo-aspirational agents in #932 or
688
+ // skill-builder in #945) would linger forever in consumer projects —
689
+ // Claude Code kept loading them on every prompt, paying the per-prompt
690
+ // roster tokens we just spent #932 fixing. Walking these dirs through
691
+ // syncFile() puts every shipped file in `currentManifest`, which lets
692
+ // the cleanup loop below auto-prune retired files going forward.
693
+ //
694
+ // Limitation: this only catches retirements that happen AFTER #948.
695
+ // For files retired BEFORE #948 (#932 + #945), see the retired-files.json
696
+ // hash-gated prune block further down — it ships a static list with
697
+ // content hashes so we only delete files the consumer didn't customize.
698
+ //
699
+ // User-authored files at custom paths (.claude/agents/custom/foo.md,
700
+ // .claude/skills/<custom>/SKILL.md) are NEVER passed through syncFile()
701
+ // because they don't exist in node_modules/moflo/, so they never enter
702
+ // the manifest and never get pruned — same proven safety story as
703
+ // scripts/helpers above.
704
+ async function syncDirRecursive(srcDir, destPrefix) {
705
+ if (!existsSync(srcDir)) return;
706
+ let entries;
707
+ try {
708
+ entries = readdirSync(srcDir, { recursive: true, withFileTypes: true });
709
+ } catch (err) {
710
+ emitWarning(`${destPrefix} readdir failed (${errMessage(err)})`);
711
+ return;
712
+ }
713
+ for (const entry of entries) {
714
+ if (!entry.isFile()) continue;
715
+ if (!entry.name.toLowerCase().endsWith('.md')) continue;
716
+ const parent = entry.parentPath || entry.path || srcDir;
717
+ const absSrc = resolve(parent, entry.name);
718
+ const rel = absSrc.slice(srcDir.length + 1).split(/[\\/]/).join('/');
719
+ const absDest = resolve(projectRoot, destPrefix, rel);
720
+ try { mkdirSync(dirname(absDest), { recursive: true }); } catch (err) {
721
+ emitWarning(`${destPrefix} subdir mkdir failed for ${rel} (${errMessage(err)})`);
722
+ }
723
+ await syncFile(absSrc, absDest, `${destPrefix}/${rel}`);
724
+ }
725
+ }
726
+ await syncDirRecursive(
727
+ resolve(projectRoot, 'node_modules/moflo/.claude/agents'),
728
+ '.claude/agents',
729
+ );
730
+ await syncDirRecursive(
731
+ resolve(projectRoot, 'node_modules/moflo/.claude/skills'),
732
+ '.claude/skills',
733
+ );
734
+
684
735
  // Sync all shipped guidance files from node_modules/moflo/.claude/guidance/shipped/
685
736
  const guidanceDir = resolve(projectRoot, '.claude/guidance');
686
737
  const shippedDir = resolve(projectRoot, 'node_modules/moflo/.claude/guidance/shipped');
@@ -725,6 +776,56 @@ try {
725
776
  emitMutation('cleaned up retired files', `${removedFiles} removed`);
726
777
  }
727
778
 
779
+ // ── Hash-gated prune of pre-#948 retirements (Mechanism B) ─────────
780
+ // The manifest cleanup above only knows about files moflo previously
781
+ // synced. Agents and skills retired BEFORE #948 (#932's 49 agents and
782
+ // #945's skill-builder, plus older retirements) were never in any
783
+ // manifest — they exist on disk in consumer projects only because
784
+ // earlier moflo versions shipped them via the npm tarball + flo init.
785
+ // `retired-files.json` is the explicit, reviewable static list that
786
+ // closes that gap. Each entry pairs a path with the sha256 hashes of
787
+ // every content version moflo previously shipped, so we only auto-
788
+ // prune when the consumer's file matches a known-shipped hash —
789
+ // customized files (different hash) get preserved with a one-line
790
+ // notice the user can act on.
791
+ try {
792
+ const retiredManifestPath = resolve(
793
+ projectRoot,
794
+ 'node_modules/moflo/retired-files.json',
795
+ );
796
+ const report = applyRetiredPrune(projectRoot, retiredManifestPath);
797
+ if (report.pruned.length > 0) {
798
+ emitMutation(
799
+ 'pruned retired shipped files',
800
+ `${plural(report.pruned.length, 'file')} matching known-shipped content removed`,
801
+ );
802
+ }
803
+ if (report.preserved.length > 0) {
804
+ // stdout (not stderr) so Claude sees this in `additionalContext`
805
+ // and can surface to the user — these aren't failures, just
806
+ // consumer-customized files we deliberately left alone.
807
+ const sample = report.preserved.slice(0, 5).map((p) => ` - ${p}`).join('\n');
808
+ const more = report.preserved.length > 5
809
+ ? `\n …and ${report.preserved.length - 5} more`
810
+ : '';
811
+ try {
812
+ process.stdout.write(
813
+ `moflo: retained ${plural(report.preserved.length, 'customized retired file')} (delete manually if unwanted):\n${sample}${more}\n`,
814
+ );
815
+ } catch { /* non-fatal */ }
816
+ }
817
+ if (report.failed.length > 0) {
818
+ const sample = report.failed.slice(0, 3)
819
+ .map((f) => ` - ${f.path}: ${f.message}`).join('\n');
820
+ emitWarning(`${plural(report.failed.length, 'retired file')} failed to prune:\n${sample}`);
821
+ }
822
+ } catch (err) {
823
+ // Non-fatal — the consumer just doesn't get the prune this session.
824
+ // Next session retries; manifest-cleanup above still works for
825
+ // future retirements regardless.
826
+ emitWarning(`retired-files prune skipped (${errMessage(err)})`);
827
+ }
828
+
728
829
  // The daemon was already stopped above so the lock file is gone and
729
830
  // there's no live PID to recycle here. Section 4's `hooks.mjs
730
831
  // session-start` will spawn a fresh daemon under the current moflo
@@ -1093,7 +1194,7 @@ try {
1093
1194
  // Also prunes top-level mirrors whose source no longer exists in shipped/
1094
1195
  // (#839): pre-manifest-tracking mirrors never appeared in installed-files.json
1095
1196
  // so the section-3 manifest-diff cleanup can't reach them. The marker gate
1096
- // protects user files and `moflo-bootstrap.md` (distinct `moflo init` marker).
1197
+ // protects user-authored files; pre-#939 moflo-bootstrap.md is handled by 3c-939.
1097
1198
  try {
1098
1199
  const guidanceDir = resolve(projectRoot, '.claude/guidance');
1099
1200
  const shippedDir = resolve(projectRoot, 'node_modules/moflo/.claude/guidance/shipped');
@@ -1169,6 +1270,24 @@ try {
1169
1270
  }
1170
1271
  } catch { /* non-fatal */ }
1171
1272
 
1273
+ // ── 3c-939. Remove pre-#939 moflo-bootstrap.md duplicate ────────────────────
1274
+ // Before #939, `flo init` Step 7 (and `flo-setup`) renamed moflo-subagents.md
1275
+ // to moflo-bootstrap.md while the launcher's stage-3 ALSO synced the original
1276
+ // name — consumers ended up with two copies of the same content on disk. The
1277
+ // rename is gone; this prunes any leftover from older installs. Marker-gated
1278
+ // so we never delete a homonymous user-authored file.
1279
+ try {
1280
+ const legacyBootstrap = resolve(projectRoot, '.claude/guidance/moflo-bootstrap.md');
1281
+ if (existsSync(legacyBootstrap)) {
1282
+ const head = readFileSync(legacyBootstrap, 'utf-8').slice(0, 200);
1283
+ const ours = head.includes('AUTO-GENERATED by moflo init') || head.includes('AUTO-GENERATED by flo-setup');
1284
+ if (ours) {
1285
+ unlinkSync(legacyBootstrap);
1286
+ emitMutation('removed legacy moflo-bootstrap.md', 'duplicate of moflo-subagents.md (#939)');
1287
+ }
1288
+ }
1289
+ } catch { /* non-fatal */ }
1290
+
1172
1291
  // ── 3d-yaml-create. Create moflo.yaml if missing (#895) ────────────────────
1173
1292
  // Sibling self-heal to 3d-yaml: that block APPENDS new sections to existing
1174
1293
  // yaml; this block CREATES the file from the canonical template when no yaml