moflo 4.9.21 → 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 (170) 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-guidance-rules.md +5 -5
  18. package/.claude/helpers/gate.cjs +70 -3
  19. package/.claude/skills/fl/execution-modes.md +38 -15
  20. package/bin/gate.cjs +70 -3
  21. package/bin/lib/retired-files.mjs +146 -0
  22. package/bin/session-start-launcher.mjs +101 -0
  23. package/dist/src/cli/appliance/rvfa-builder.js +1 -1
  24. package/dist/src/cli/commands/agent.js +3 -9
  25. package/dist/src/cli/commands/hooks.js +1 -3
  26. package/dist/src/cli/commands/index.js +2 -0
  27. package/dist/src/cli/commands/retire.js +111 -0
  28. package/dist/src/cli/hooks/reasoningbank/index.js +7 -7
  29. package/dist/src/cli/init/executor.js +26 -54
  30. package/dist/src/cli/init/helpers-generator.js +66 -3
  31. package/dist/src/cli/init/settings-generator.js +12 -0
  32. package/dist/src/cli/mcp-tools/agent-tools.js +9 -27
  33. package/dist/src/cli/mcp-tools/hooks-tools.js +23 -21
  34. package/dist/src/cli/memory/controllers/semantic-router.js +18 -12
  35. package/dist/src/cli/memory/sona-optimizer.js +6 -6
  36. package/dist/src/cli/neural/domain/services/learning-service.js +3 -3
  37. package/dist/src/cli/services/agent-router.js +2 -5
  38. package/dist/src/cli/services/hook-block-hash.js +4 -0
  39. package/dist/src/cli/shared/events/example-usage.js +6 -6
  40. package/dist/src/cli/shared/hooks/task-hooks.js +8 -8
  41. package/dist/src/cli/version.js +1 -1
  42. package/package.json +3 -2
  43. package/retired-files.json +1989 -0
  44. package/src/cli/data/model-registry.json +2 -2
  45. package/.claude/agents/consensus/byzantine-coordinator.md +0 -63
  46. package/.claude/agents/consensus/crdt-synchronizer.md +0 -997
  47. package/.claude/agents/consensus/gossip-coordinator.md +0 -63
  48. package/.claude/agents/consensus/performance-benchmarker.md +0 -851
  49. package/.claude/agents/consensus/quorum-manager.md +0 -823
  50. package/.claude/agents/consensus/raft-manager.md +0 -63
  51. package/.claude/agents/consensus/security-manager.md +0 -622
  52. package/.claude/agents/data/ml/data-ml-model.md +0 -193
  53. package/.claude/agents/github/code-review-swarm.md +0 -538
  54. package/.claude/agents/github/github-modes.md +0 -172
  55. package/.claude/agents/github/issue-tracker.md +0 -311
  56. package/.claude/agents/github/multi-repo-swarm.md +0 -551
  57. package/.claude/agents/github/pr-manager.md +0 -183
  58. package/.claude/agents/github/project-board-sync.md +0 -508
  59. package/.claude/agents/github/release-manager.md +0 -360
  60. package/.claude/agents/github/release-swarm.md +0 -580
  61. package/.claude/agents/github/repo-architect.md +0 -391
  62. package/.claude/agents/github/swarm-issue.md +0 -566
  63. package/.claude/agents/github/swarm-pr.md +0 -414
  64. package/.claude/agents/github/sync-coordinator.md +0 -426
  65. package/.claude/agents/github/workflow-automation.md +0 -606
  66. package/.claude/agents/goal/code-goal-planner.md +0 -440
  67. package/.claude/agents/goal/goal-planner.md +0 -168
  68. package/.claude/agents/hive-mind/collective-intelligence-coordinator.md +0 -127
  69. package/.claude/agents/hive-mind/queen-coordinator.md +0 -198
  70. package/.claude/agents/hive-mind/scout-explorer.md +0 -233
  71. package/.claude/agents/hive-mind/swarm-memory-manager.md +0 -184
  72. package/.claude/agents/hive-mind/worker-specialist.md +0 -208
  73. package/.claude/agents/neural/safla-neural.md +0 -73
  74. package/.claude/agents/optimization/benchmark-suite.md +0 -665
  75. package/.claude/agents/optimization/load-balancer.md +0 -431
  76. package/.claude/agents/optimization/performance-monitor.md +0 -672
  77. package/.claude/agents/optimization/resource-allocator.md +0 -674
  78. package/.claude/agents/optimization/topology-optimizer.md +0 -808
  79. package/.claude/agents/reasoning/goal-planner.md +0 -67
  80. package/.claude/agents/sona/sona-learning-optimizer.md +0 -74
  81. package/.claude/agents/sparc/architecture.md +0 -472
  82. package/.claude/agents/sparc/pseudocode.md +0 -318
  83. package/.claude/agents/sparc/refinement.md +0 -525
  84. package/.claude/agents/sparc/specification.md +0 -276
  85. package/.claude/agents/specialized/mobile/spec-mobile-react-native.md +0 -225
  86. package/.claude/agents/swarm/adaptive-coordinator.md +0 -391
  87. package/.claude/agents/swarm/hierarchical-coordinator.md +0 -321
  88. package/.claude/agents/swarm/mesh-coordinator.md +0 -383
  89. package/.claude/agents/testing/production-validator.md +0 -395
  90. package/.claude/agents/testing/tdd-london-swarm.md +0 -244
  91. package/.claude/agents/v3/adr-architect.md +0 -184
  92. package/.claude/agents/v3/aidefence-guardian.md +0 -277
  93. package/.claude/agents/v3/claims-authorizer.md +0 -208
  94. package/.claude/agents/v3/collective-intelligence-coordinator.md +0 -988
  95. package/.claude/agents/v3/ddd-domain-expert.md +0 -220
  96. package/.claude/agents/v3/injection-analyst.md +0 -232
  97. package/.claude/agents/v3/memory-specialist.md +0 -987
  98. package/.claude/agents/v3/performance-engineer.md +0 -1225
  99. package/.claude/agents/v3/pii-detector.md +0 -146
  100. package/.claude/agents/v3/reasoningbank-learner.md +0 -213
  101. package/.claude/agents/v3/security-architect-aidefence.md +0 -405
  102. package/.claude/agents/v3/security-architect.md +0 -865
  103. package/.claude/agents/v3/security-auditor.md +0 -771
  104. package/.claude/agents/v3/sparc-orchestrator.md +0 -182
  105. package/.claude/agents/v3/swarm-memory-manager.md +0 -142
  106. package/.claude/agents/v3/v3-integration-architect.md +0 -205
  107. package/.claude/commands/claude-flow-help.md +0 -103
  108. package/.claude/commands/claude-flow-memory.md +0 -107
  109. package/.claude/commands/claude-flow-swarm.md +0 -205
  110. package/.claude/commands/flo-simplify.md +0 -101
  111. package/.claude/commands/github/README.md +0 -11
  112. package/.claude/commands/github/code-review-swarm.md +0 -514
  113. package/.claude/commands/github/code-review.md +0 -25
  114. package/.claude/commands/github/github-modes.md +0 -146
  115. package/.claude/commands/github/github-swarm.md +0 -113
  116. package/.claude/commands/github/issue-tracker.md +0 -284
  117. package/.claude/commands/github/issue-triage.md +0 -25
  118. package/.claude/commands/github/multi-repo-swarm.md +0 -519
  119. package/.claude/commands/github/pr-enhance.md +0 -26
  120. package/.claude/commands/github/pr-manager.md +0 -164
  121. package/.claude/commands/github/project-board-sync.md +0 -471
  122. package/.claude/commands/github/release-manager.md +0 -332
  123. package/.claude/commands/github/release-swarm.md +0 -544
  124. package/.claude/commands/github/repo-analyze.md +0 -25
  125. package/.claude/commands/github/repo-architect.md +0 -361
  126. package/.claude/commands/github/swarm-issue.md +0 -482
  127. package/.claude/commands/github/swarm-pr.md +0 -285
  128. package/.claude/commands/github/sync-coordinator.md +0 -294
  129. package/.claude/commands/github/workflow-automation.md +0 -442
  130. package/.claude/commands/hooks/README.md +0 -11
  131. package/.claude/commands/hooks/overview.md +0 -58
  132. package/.claude/commands/hooks/post-edit.md +0 -117
  133. package/.claude/commands/hooks/post-task.md +0 -112
  134. package/.claude/commands/hooks/pre-edit.md +0 -113
  135. package/.claude/commands/hooks/pre-task.md +0 -111
  136. package/.claude/commands/hooks/session-end.md +0 -118
  137. package/.claude/commands/hooks/setup.md +0 -103
  138. package/.claude/commands/sparc/analyzer.md +0 -42
  139. package/.claude/commands/sparc/architect.md +0 -43
  140. package/.claude/commands/sparc/ask.md +0 -86
  141. package/.claude/commands/sparc/batch-executor.md +0 -44
  142. package/.claude/commands/sparc/code.md +0 -78
  143. package/.claude/commands/sparc/coder.md +0 -44
  144. package/.claude/commands/sparc/debug.md +0 -72
  145. package/.claude/commands/sparc/debugger.md +0 -44
  146. package/.claude/commands/sparc/designer.md +0 -43
  147. package/.claude/commands/sparc/devops.md +0 -98
  148. package/.claude/commands/sparc/docs-writer.md +0 -69
  149. package/.claude/commands/sparc/documenter.md +0 -44
  150. package/.claude/commands/sparc/innovator.md +0 -44
  151. package/.claude/commands/sparc/integration.md +0 -72
  152. package/.claude/commands/sparc/mcp.md +0 -106
  153. package/.claude/commands/sparc/memory-manager.md +0 -44
  154. package/.claude/commands/sparc/optimizer.md +0 -44
  155. package/.claude/commands/sparc/orchestrator.md +0 -116
  156. package/.claude/commands/sparc/post-deployment-monitoring-mode.md +0 -72
  157. package/.claude/commands/sparc/refinement-optimization-mode.md +0 -72
  158. package/.claude/commands/sparc/researcher.md +0 -44
  159. package/.claude/commands/sparc/reviewer.md +0 -44
  160. package/.claude/commands/sparc/security-review.md +0 -69
  161. package/.claude/commands/sparc/sparc-modes.md +0 -139
  162. package/.claude/commands/sparc/sparc.md +0 -99
  163. package/.claude/commands/sparc/spec-pseudocode.md +0 -69
  164. package/.claude/commands/sparc/spell-manager.md +0 -44
  165. package/.claude/commands/sparc/supabase-admin.md +0 -337
  166. package/.claude/commands/sparc/swarm-coordinator.md +0 -44
  167. package/.claude/commands/sparc/tdd.md +0 -44
  168. package/.claude/commands/sparc/tester.md +0 -44
  169. package/.claude/commands/sparc/tutorial.md +0 -68
  170. package/.claude/commands/sparc.md +0 -151
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, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {} };
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;
@@ -111,6 +112,21 @@ var NS_NAV_RES = [
111
112
  /\b(class|function|method|component|service|entity|module)\b/,
112
113
  ];
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
+
114
130
  function classifyNamespaceHint(promptText) {
115
131
  var lower = (promptText || '').toLowerCase();
116
132
  if (NS_TEST_RE.test(lower)) return 'Memory namespace hint: use "tests" for test inventory and coverage lookups.';
@@ -154,6 +170,12 @@ function applyPromptStateReset(state, promptText) {
154
170
  // subsequent agents (parent + subagents that spawn their own agents) all
155
171
  // see the new classification on their first check-before-agent.
156
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;
157
179
  }
158
180
  // Match npm/yarn/pnpm/bun test, npx vitest|jest|..., bare runners at command-start only,
159
181
  // and language-native test commands. The bare-runner arm is anchored so that
@@ -305,6 +327,47 @@ switch (command) {
305
327
  writeState(s);
306
328
  }
307
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
+ }
308
371
  break;
309
372
  }
310
373
  case 'check-before-scan': {
@@ -508,7 +571,11 @@ switch (command) {
508
571
  break;
509
572
  }
510
573
  case 'session-reset': {
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: {} });
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() }));
512
579
  break;
513
580
  }
514
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
+ }
@@ -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
@@ -20,7 +20,7 @@ const AES_TAG_LEN = 16;
20
20
  const AES_ALG = 'aes-256-gcm';
21
21
  // ── Catalog ──────────────────────────────────────────────────
22
22
  const RUFLO_COMMANDS = 'init agent swarm memory mcp task session config status start workflow hooks hive-mind daemon neural security performance providers plugins deployment embeddings claims migrate process doctor completions'.split(' ');
23
- const AGENT_TYPES = 'coder reviewer tester planner researcher security-architect security-auditor memory-specialist performance-engineer hierarchical-coordinator mesh-coordinator adaptive-coordinator collective-intelligence-coordinator swarm-memory-manager byzantine-coordinator raft-manager gossip-coordinator consensus-builder crdt-synchronizer quorum-manager security-manager perf-analyzer performance-benchmarker task-orchestrator memory-coordinator smart-agent github-modes pr-manager code-review-swarm issue-tracker release-manager workflow-automation project-board-sync repo-architect multi-repo-swarm sparc-coord sparc-coder specification pseudocode architecture refinement backend-dev mobile-dev ml-developer cicd-engineer api-docs system-architect code-analyzer base-template-generator tdd-london-swarm production-validator'.split(' ');
23
+ const AGENT_TYPES = 'coder reviewer tester planner researcher security-auditor backend-dev frontend-dev database-dev cicd-engineer api-docs system-architect code-analyzer analyst base-template-generator test-long-runner'.split(' ');
24
24
  const HOOK_TYPES = 'pre-edit post-edit pre-command post-command pre-task post-task session-start session-end session-restore notify route explain pretrain build-agents transfer teammate-idle task-completed'.split(' ');
25
25
  const WORKER_TYPES = 'ultralearn optimize consolidate predict audit map preload deepdive document refactor benchmark testgaps'.split(' ');
26
26
  const OFFLINE_MODELS = [
@@ -68,13 +68,8 @@ const AGENT_TYPES = [
68
68
  { value: 'coordinator', label: 'Coordinator', hint: 'Multi-agent orchestration and spell management' },
69
69
  { value: 'analyst', label: 'Analyst', hint: 'Performance analysis and optimization' },
70
70
  { value: 'optimizer', label: 'Optimizer', hint: 'Performance optimization and bottleneck analysis' },
71
- { value: 'security-architect', label: 'Security Architect', hint: 'Security architecture and threat modeling' },
72
71
  { value: 'security-auditor', label: 'Security Auditor', hint: 'CVE remediation and security testing' },
73
- { value: 'memory-specialist', label: 'Memory Specialist', hint: 'AgentDB unification (150x-12,500x faster)' },
74
- { value: 'swarm-specialist', label: 'Swarm Specialist', hint: 'Unified coordination engine' },
75
- { value: 'performance-engineer', label: 'Performance Engineer', hint: '2.49x-7.47x optimization targets' },
76
- { value: 'core-architect', label: 'Core Architect', hint: 'Domain-driven design restructure' },
77
- { value: 'test-architect', label: 'Test Architect', hint: 'TDD London School methodology' }
72
+ { value: 'planner', label: 'Planner', hint: 'Task planning and breakdown' },
78
73
  ];
79
74
  // Agent spawn subcommand
80
75
  const spawnCommand = {
@@ -844,9 +839,8 @@ function getAgentCapabilities(type) {
844
839
  reviewer: ['code-review', 'security-audit', 'quality-check', 'documentation'],
845
840
  architect: ['system-design', 'pattern-analysis', 'scalability', 'documentation'],
846
841
  coordinator: ['task-orchestration', 'agent-management', 'spell-control'],
847
- 'security-architect': ['threat-modeling', 'security-patterns', 'compliance', 'audit'],
848
- 'memory-specialist': ['vector-search', 'agentdb', 'caching', 'optimization'],
849
- 'performance-engineer': ['benchmarking', 'profiling', 'optimization', 'monitoring']
842
+ 'security-auditor': ['threat-modeling', 'security-patterns', 'compliance', 'audit', 'vulnerability-scan'],
843
+ planner: ['task-breakdown', 'sequencing', 'estimation'],
850
844
  };
851
845
  return capabilities[type] || ['general'];
852
846
  }
@@ -21,9 +21,7 @@ const HOOK_TYPES = [
21
21
  // Agent routing options
22
22
  const AGENT_TYPES = [
23
23
  'coder', 'researcher', 'tester', 'reviewer', 'architect',
24
- 'security-architect', 'security-auditor', 'memory-specialist',
25
- 'swarm-specialist', 'performance-engineer', 'core-architect',
26
- 'test-architect', 'coordinator', 'analyst', 'optimizer'
24
+ 'security-auditor', 'coordinator', 'analyst', 'optimizer', 'planner',
27
25
  ];
28
26
  // Pre-edit subcommand
29
27
  const preEditCommand = {
@@ -54,6 +54,8 @@ const commandLoaders = {
54
54
  issues: () => import('./issues.js'),
55
55
  // Auto-update System (ADR-025)
56
56
  update: () => import('./update.js'),
57
+ // Retired-files manifest helper (#948 — moflo dev only)
58
+ retire: () => import('./retire.js'),
57
59
  // Full integration diagnostics
58
60
  diagnose: () => import('./diagnose.js'),
59
61
  // Benchmark Suite (Pre-training, Neural, Memory)
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `flo retire <path>` — record a shipped file as retired (#948).
3
+ *
4
+ * Run this inside the moflo source repo whenever a retirement PR deletes a
5
+ * `.claude/agents/**` or `.claude/skills/**` file. It computes content
6
+ * hashes for the last few moflo-shipped versions of the file (from git
7
+ * history) and appends an entry to `retired-files.json`. The launcher then
8
+ * prunes the matching file from consumer projects on their next upgrade —
9
+ * but only when their on-disk content matches a known-shipped hash, so
10
+ * customized files stay put.
11
+ *
12
+ * Refuses to run outside moflo's own repo because the seed script and
13
+ * `retired-files.json` live at the moflo package root and don't ship to
14
+ * consumer projects.
15
+ *
16
+ * Created with motailz.com
17
+ */
18
+ import { output } from '../output.js';
19
+ import { spawnSync } from 'child_process';
20
+ import { existsSync, readFileSync } from 'fs';
21
+ import { resolve, dirname } from 'path';
22
+ import { fileURLToPath } from 'url';
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ // dist/src/cli/commands/retire.js → repo root is up 5 dirs.
25
+ // In dev (tsx) src/cli/commands/retire.ts → up 4 dirs. Walk to find package.json#name === 'moflo'.
26
+ function findMofloRepoRoot(start) {
27
+ let dir = start;
28
+ const root = resolve(dir, '/');
29
+ while (dir !== root) {
30
+ const pkg = resolve(dir, 'package.json');
31
+ if (existsSync(pkg)) {
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(pkg, 'utf-8'));
34
+ if (parsed?.name === 'moflo')
35
+ return dir;
36
+ }
37
+ catch { /* keep walking */ }
38
+ }
39
+ dir = dirname(dir);
40
+ }
41
+ return null;
42
+ }
43
+ export const retireCommand = {
44
+ name: 'retire',
45
+ description: 'Record a retired shipped file in retired-files.json (moflo dev only) — usage: flo retire <path> [--retired-by #nnn]',
46
+ hidden: true,
47
+ options: [
48
+ {
49
+ name: 'retired-by',
50
+ description: 'GitHub PR/issue reference (e.g. #932)',
51
+ type: 'string',
52
+ },
53
+ {
54
+ name: 'retired-in',
55
+ description: 'moflo version that ships the retirement (defaults to current package.json version)',
56
+ type: 'string',
57
+ },
58
+ {
59
+ name: 'hashes',
60
+ description: 'Maximum number of historical content hashes to record (default 3)',
61
+ type: 'number',
62
+ default: 3,
63
+ },
64
+ ],
65
+ examples: [
66
+ { command: 'flo retire .claude/agents/v3/performance-engineer.md --retired-by #932', description: 'Record a retirement' },
67
+ { command: 'flo retire .claude/skills/skill-builder/SKILL.md --retired-by #945 --retired-in 4.9.21', description: 'Pin retiredIn' },
68
+ ],
69
+ action: async (ctx) => {
70
+ const repoRoot = findMofloRepoRoot(__filename) || findMofloRepoRoot(ctx.cwd);
71
+ if (!repoRoot) {
72
+ output.printError('flo retire must be run inside the moflo source repo');
73
+ output.printInfo('retired-files.json lives at the moflo package root and does not ship to consumer projects');
74
+ return { success: false, message: 'not in moflo repo', exitCode: 1 };
75
+ }
76
+ const path = ctx.args[0];
77
+ if (!path) {
78
+ output.printError('Missing required argument: <path>');
79
+ return { success: false, message: 'missing path', exitCode: 2 };
80
+ }
81
+ const scriptPath = resolve(repoRoot, 'scripts', 'build-retired-files.mjs');
82
+ if (!existsSync(scriptPath)) {
83
+ output.printError(`scripts/build-retired-files.mjs not found at ${scriptPath}`);
84
+ return { success: false, message: 'seed script missing', exitCode: 1 };
85
+ }
86
+ // Parser normalises kebab-case flag names to camelCase before storing
87
+ // (#787). Read as ctx.flags.<camelCase> — bracket-with-kebab is always
88
+ // undefined and ESLint blocks that pattern.
89
+ const args = ['--add', path];
90
+ if (ctx.flags.retiredBy)
91
+ args.push('--retired-by', String(ctx.flags.retiredBy));
92
+ if (ctx.flags.retiredIn)
93
+ args.push('--retired-in', String(ctx.flags.retiredIn));
94
+ if (ctx.flags.hashes)
95
+ args.push('--hashes', String(ctx.flags.hashes));
96
+ const result = spawnSync('node', [scriptPath, ...args], {
97
+ cwd: repoRoot,
98
+ stdio: 'inherit',
99
+ });
100
+ if (result.error) {
101
+ output.printError(`failed to invoke build-retired-files.mjs: ${result.error.message}`);
102
+ return { success: false, message: String(result.error), exitCode: 1 };
103
+ }
104
+ if (typeof result.status === 'number' && result.status !== 0) {
105
+ return { success: false, message: `exit ${result.status}`, exitCode: result.status };
106
+ }
107
+ return { success: true };
108
+ },
109
+ };
110
+ export default retireCommand;
111
+ //# sourceMappingURL=retire.js.map
@@ -33,15 +33,15 @@ const DEFAULT_CONFIG = {
33
33
  useMockEmbeddings: false,
34
34
  };
35
35
  /**
36
- * Agent mapping for routing
36
+ * Agent mapping for routing — keys must match agents shipped in .claude/agents/
37
+ * (or the canonical AgentType union).
37
38
  */
38
39
  const AGENT_PATTERNS = {
39
- 'security-architect': /security|auth|cve|vuln|encrypt|password|token/i,
40
- 'test-architect': /test|spec|mock|coverage|tdd|assert/i,
41
- 'performance-engineer': /perf|optim|fast|memory|cache|speed|slow/i,
42
- 'core-architect': /architect|design|ddd|domain|refactor|struct/i,
43
- 'swarm-specialist': /swarm|agent|coordinate|orchestrat|parallel/i,
44
- 'memory-specialist': /memory|agentdb|hnsw|vector|embedding/i,
40
+ 'security-auditor': /security|auth|cve|vuln|encrypt|password|token/i,
41
+ 'tester': /test|spec|mock|coverage|tdd|assert/i,
42
+ 'architect': /architect|design|ddd|domain|refactor|struct|perf|optim|fast|memory|cache|speed|slow/i,
43
+ 'coordinator': /swarm|agent|coordinate|orchestrat|parallel|hive/i,
44
+ 'researcher': /memory|agentdb|hnsw|vector|embedding|recall|persist/i,
45
45
  'coder': /fix|bug|implement|create|add|build|error|code/i,
46
46
  'reviewer': /review|quality|lint|check|audit/i,
47
47
  };