moflo 4.12.0 → 4.12.2

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.
@@ -16,12 +16,14 @@
16
16
  * flo sdd path <slug> [spec|plan] Print the artifact path (for skill shell-out)
17
17
  * flo sdd embed <slug> Print spec+plan as a PR-body block (#1297)
18
18
  * flo sdd index Re-index specs into memory now (also runs at session start)
19
+ * flo sdd mode --args "<flo args>" Resolve sdd/verify/merge for a /flo run as JSON
19
20
  */
20
21
  import { spawnSync } from 'node:child_process';
21
22
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
22
23
  import { dirname, join } from 'node:path';
23
24
  import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
24
25
  import { findProjectRoot } from '../services/project-root.js';
26
+ import { loadMofloConfig } from '../config/moflo-config.js';
25
27
  import { locateMofloRootPath } from '../services/moflo-require.js';
26
28
  import { artifactPath, assertReviewed, listSpecs, newArtifact, readArtifact, slugify, specExists, validateArtifact, writeArtifact, } from '../sdd/index.js';
27
29
  import { defaultPlanBody, defaultSpecBody } from '../sdd/templates.js';
@@ -226,6 +228,119 @@ function cmdList(ctx) {
226
228
  }
227
229
  return { success: true, data: specs };
228
230
  }
231
+ /**
232
+ * Resolve the effective `/flo` run modifiers from an argument string + moflo.yaml
233
+ * and print them as JSON. This is the FALLBACK path for the authoritative
234
+ * announcement that `bin/gate.cjs prompt-reminder` normally injects on every
235
+ * `/flo` prompt — consumers with hooks disabled (or a gate.cjs predating this
236
+ * change) still get a deterministic answer by shelling out to this.
237
+ *
238
+ * Exists because the `/flo` skill used to carry `let sddMode = false` as a
239
+ * literal beside a comment saying "seed from moflo.yaml"; the model executed the
240
+ * literal and `sdd.default: true` was silently ignored. Config-derived run modes
241
+ * must be COMPUTED, never described in prose next to a contradicting default.
242
+ *
243
+ * Precedence per key: `--no-X` > `-x`/`--X` > moflo.yaml > built-in. Built-in
244
+ * defaults differ deliberately: sdd is opt-in (false), verify is opt-out (true,
245
+ * #1294), merge is opt-in (false, #1285).
246
+ *
247
+ * SYNC: mirrors `resolveFloRun` in bin/gate.cjs (and its copy in
248
+ * init/helpers-generator.ts). Any precedence change MUST land in all three.
249
+ */
250
+ function cmdMode(ctx) {
251
+ // MUST be passed as `--args=<value>`, not `--args <value>`: the flag parser
252
+ // only consumes a following token as a value when it does not start with `-`,
253
+ // so `--args "-sd 42"` would silently degrade to `args=true` and resolve every
254
+ // run to the bare config default. The `=` form bypasses that lookahead.
255
+ // parseValue may coerce a bare numeric ("42") to a number — coerce back.
256
+ const flagArgs = ctx.flags?.args;
257
+ const raw = (typeof flagArgs === 'string' || typeof flagArgs === 'number')
258
+ ? String(flagArgs)
259
+ : (ctx.args ?? []).slice(1).join(' ');
260
+ // Tokenize on whitespace so `-sd` is matched as a whole token — it is NOT
261
+ // `-s` (swarm) + `d`.
262
+ const tokens = raw.trim().split(/\s+/).filter(Boolean);
263
+ const has = (...names) => names.some((n) => tokens.includes(n));
264
+ const cfg = loadMofloConfig(projectRoot(ctx));
265
+ let workflow = 'full';
266
+ if (has('-wf', '--workflow'))
267
+ workflow = 'spell-engine';
268
+ else if (has('-r', '--research'))
269
+ workflow = 'research';
270
+ else if (has('-t', '--ticket'))
271
+ workflow = 'ticket';
272
+ const epicBranch = has('--epic-branch');
273
+ let sdd = false;
274
+ let sddSrc = 'default';
275
+ if (has('--no-sdd')) {
276
+ sdd = false;
277
+ sddSrc = 'flag';
278
+ }
279
+ else if (has('-sd', '--sdd')) {
280
+ sdd = true;
281
+ sddSrc = 'flag';
282
+ }
283
+ else if (cfg.sdd.default) {
284
+ sdd = true;
285
+ sddSrc = 'moflo.yaml sdd.default';
286
+ }
287
+ let verify;
288
+ let verifySrc = 'default';
289
+ if (has('--no-verify')) {
290
+ verify = false;
291
+ verifySrc = 'flag';
292
+ }
293
+ else if (has('-v', '--verify')) {
294
+ verify = true;
295
+ verifySrc = 'flag';
296
+ }
297
+ else if (!cfg.gates.verify_before_done) {
298
+ verify = false;
299
+ verifySrc = 'moflo.yaml gates.verify_before_done';
300
+ }
301
+ else {
302
+ verify = true;
303
+ }
304
+ // --sdd implies --verify: a spec/plan without an enforced verify step drifts.
305
+ if (sdd && !verify && verifySrc !== 'flag')
306
+ verify = true;
307
+ let merge = false;
308
+ let mergeSrc = 'default';
309
+ if (has('--no-merge')) {
310
+ merge = false;
311
+ mergeSrc = 'flag';
312
+ }
313
+ else if (has('-m', '--merge')) {
314
+ merge = true;
315
+ mergeSrc = 'flag';
316
+ }
317
+ else if (cfg.merge.auto) {
318
+ merge = true;
319
+ mergeSrc = 'moflo.yaml merge.auto';
320
+ }
321
+ // Applicability: -t/-r never implement, so verify is a no-op there; -r and -wf
322
+ // produce no spec artifacts (in -t the spec/plan goes INTO the ticket, so sdd
323
+ // stays on). Only a full, non-epic-branch run opens a PR to merge.
324
+ // Re-attribute anything applicability turned off — a false must never carry the
325
+ // source of the value it no longer has. SYNC: mirrors bin/gate.cjs.
326
+ if (workflow === 'ticket' || workflow === 'research') {
327
+ if (verify)
328
+ verifySrc = `${workflow} mode does not implement`;
329
+ verify = false;
330
+ }
331
+ if (workflow === 'research' || workflow === 'spell-engine') {
332
+ if (sdd)
333
+ sddSrc = `${workflow} mode produces no spec artifacts`;
334
+ sdd = false;
335
+ }
336
+ if (workflow !== 'full' || epicBranch) {
337
+ if (merge)
338
+ mergeSrc = epicBranch ? '--epic-branch owns merging' : `${workflow} mode opens no PR`;
339
+ merge = false;
340
+ }
341
+ console.log(JSON.stringify({ workflow, sdd, verify, merge, sddSrc, verifySrc, mergeSrc }, null, 2));
342
+ return { success: true };
343
+ }
229
344
  function cmdPath(ctx) {
230
345
  const root = projectRoot(ctx);
231
346
  const slug = slugify(ctx.args[1] || '');
@@ -294,7 +409,8 @@ Spec-Driven Development artifacts (.moflo/specs/<slug>/{spec,plan}.md):
294
409
  list List every spec slug
295
410
  path <slug> [spec|plan] Print the artifact path
296
411
  embed <slug> Print spec+plan as a collapsible block for the PR body
297
- index Re-index specs into memory now`;
412
+ index Re-index specs into memory now
413
+ mode --args "<flo args>" Resolve sdd/verify/merge for a /flo run as JSON (moflo.yaml + flags)`;
298
414
  const sddCommand = {
299
415
  name: 'sdd',
300
416
  description: 'Spec-Driven Development artifacts (spec → plan → implement → verify)',
@@ -322,6 +438,8 @@ const sddCommand = {
322
438
  return cmdStatus(ctx);
323
439
  case 'list':
324
440
  return cmdList(ctx);
441
+ case 'mode':
442
+ return cmdMode(ctx);
325
443
  case 'path':
326
444
  return cmdPath(ctx);
327
445
  case 'embed':
@@ -464,6 +464,21 @@ export class CLI {
464
464
  * Non-blocking — fires and forgets. Errors are silently swallowed.
465
465
  */
466
466
  async maybeAutoStartDaemon() {
467
+ // Test-only kill switch. A test that shells the CLI into a throwaway tmp
468
+ // project gets `daemon.auto_start: true` from DEFAULT_CONFIG (no moflo.yaml
469
+ // there to say otherwise), so EVERY such invocation spawned a detached,
470
+ // unref'd daemon. When the test then removed its tmp dir, the daemon
471
+ // survived holding a deleted cwd — no lockfile left to contend for, no
472
+ // project root to match, so nothing ever reclaimed it. One full suite run
473
+ // leaked several; they accumulated across runs into dozens of ~87MB
474
+ // processes that only `doctor --fix` could reap.
475
+ //
476
+ // Set in vitest.setup.ts alongside MOFLO_TEST_TRUST_DAEMON_PID and
477
+ // MOFLO_TEST_SKIP_ORPHAN_SCAN. Subprocesses inherit it, which is what makes
478
+ // it reach the spawned CLI. Production never sets it; a test that genuinely
479
+ // wants autostart can delete it in its own beforeEach, same as the others.
480
+ if (process.env.MOFLO_TEST_SKIP_DAEMON_AUTOSTART === '1')
481
+ return;
467
482
  const config = loadMofloConfig(process.cwd());
468
483
  if (!config.daemon.auto_start)
469
484
  return;
@@ -11,7 +11,7 @@ import { dirname } from 'path';
11
11
  // ESM-compatible __dirname
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
14
- import { detectPlatform, DEFAULT_INIT_OPTIONS } from './types.js';
14
+ import { detectPlatform, DEFAULT_INIT_OPTIONS, SKILL_CATEGORIES } from './types.js';
15
15
  import { generateSettingsJson, generateSettings } from './settings-generator.js';
16
16
  import { generateMCPJson } from './mcp-generator.js';
17
17
  import { generatePreCommitHook, generatePostCommitHook, generateAutoMemoryHook, generateGateScript, generateGateHookScript, generatePromptHookScript, generateHookHandlerScript, } from './helpers-generator.js';
@@ -31,6 +31,8 @@ import { readMofloEnv } from '../services/env-compat.js';
31
31
  // cruft). New additions MUST pass the same audit, otherwise the drift-guard
32
32
  // test (skills-classification-drift.test.ts) fails. See INTERNAL_SKILLS for
33
33
  // skills that ship in the tarball but are deliberately NOT installed.
34
+ // Typed by SkillCategory (not `string`) so this map and `SkillsConfig` cannot
35
+ // drift apart again — see SKILL_CATEGORIES in types.ts for what that drift cost.
34
36
  export const SKILLS_MAP = {
35
37
  core: [
36
38
  'commune',
@@ -915,9 +917,14 @@ async function copySkills(targetDir, options, result) {
915
917
  Object.values(SKILLS_MAP).forEach(skills => skillsToCopy.push(...skills));
916
918
  }
917
919
  else {
918
- for (const [key, skills] of Object.entries(SKILLS_MAP)) {
919
- if (skillsConfig[key])
920
- skillsToCopy.push(...skills);
920
+ // Iterate SKILL_CATEGORIES rather than Object.entries + a cast. The cast
921
+ // was what let `SKILLS_MAP`'s keys drift away from `SkillsConfig`'s without
922
+ // a type error, silently disabling the `memory` and `spells` categories
923
+ // (#1308). Indexing a typed Record by the category union needs no cast, so
924
+ // the compiler now enforces that every category is selectable.
925
+ for (const category of SKILL_CATEGORIES) {
926
+ if (skillsConfig[category])
927
+ skillsToCopy.push(...SKILLS_MAP[category]);
921
928
  }
922
929
  }
923
930
  // Find source skills directory
@@ -293,6 +293,18 @@ function loadSddConfig() {
293
293
  return out;
294
294
  }
295
295
 
296
+ // #1285 — parse the top-level merge: block. Block-scoped like loadSddConfig.
297
+ // SYNC: mirrors bin/gate.cjs loadMergeConfig.
298
+ function loadMergeConfig() {
299
+ var out = { auto: false };
300
+ var content = MOFLO_YAML;
301
+ if (!content) return out;
302
+ var block = content.match(/^merge:[ \\t]*\\r?\\n((?:[ \\t]+.*(?:\\r?\\n|$))*)/m);
303
+ if (!block) return out;
304
+ if (/^\\s*auto:\\s*true\\b/im.test(block[1])) out.auto = true;
305
+ return out;
306
+ }
307
+
296
308
  // #1297 — read moflo.yaml once (both loaders parse it; gate fires on every
297
309
  // Write/Edit). SYNC: mirrors bin/gate.cjs readMofloYaml.
298
310
  function readMofloYaml() {
@@ -303,9 +315,16 @@ var MOFLO_YAML = readMofloYaml();
303
315
 
304
316
  var config = loadGateConfig();
305
317
  var sddConf = loadSddConfig();
318
+ var mergeConf = loadMergeConfig();
306
319
  var command = process.argv[2];
307
320
 
308
321
  var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
322
+
323
+ // Appended to every memory-first denial so a context audit doesn't read gate
324
+ // enforcement as permission misconfiguration (#1307 finding 5). SYNC: mirrors
325
+ // bin/gate.cjs GATE_ORIGIN_NOTE / GATE_DISABLE_NOTE.
326
+ var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule — allow-rules cannot override it.';
327
+ var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
309
328
  // #1294 Finding 3 — exempt ephemeral reads/scans under the OS temp dir
310
329
  // (background-task output, scratchpads) from the memory-first gate. Mirrors
311
330
  // bin/gate.cjs isEphemeralPath. Cross-platform via os.tmpdir(); normalizes a
@@ -388,13 +407,58 @@ function detectFlMode(promptText) {
388
407
  return null;
389
408
  }
390
409
 
391
- // #1297 arm the SDD implement gate from a /flo prompt. SYNC: mirrors bin/gate.cjs.
392
- function detectSddMode(promptText) {
410
+ // Resolve ALL /flo run modifiers from the prompt + moflo.yaml. Single source of
411
+ // truth for gate arming AND the authoritative announcement below — a second
412
+ // implementation is how sdd.default got silently ignored. Precedence per key:
413
+ // --no-X > -x/--X > moflo.yaml > built-in (sdd opt-in, verify opt-out, merge
414
+ // opt-in). SYNC: mirrors bin/gate.cjs resolveFloRun.
415
+ function resolveFloRun(promptText) {
393
416
  var p = promptText || '';
394
- if (!/^\\s*\\/(?:fl|flo)\\b/i.test(p)) return false;
395
- if (/(?:^|\\s)--no-sdd\\b/.test(p)) return false;
396
- if (/(?:^|\\s)(?:-sd|--sdd)\\b/.test(p)) return true;
397
- return !!sddConf.default;
417
+ var out = { isFlo: false, workflow: 'full', sdd: false, verify: false, merge: false,
418
+ sddSrc: 'default', verifySrc: 'default', mergeSrc: 'default' };
419
+ if (!/^\\s*\\/(?:fl|flo)\\b/i.test(p)) return out;
420
+ out.isFlo = true;
421
+
422
+ if (/(?:^|\\s)(?:-wf|--workflow)\\b/.test(p)) out.workflow = 'spell-engine';
423
+ else if (/(?:^|\\s)(?:-r|--research)\\b/.test(p)) out.workflow = 'research';
424
+ else if (/(?:^|\\s)(?:-t|--ticket)\\b/.test(p)) out.workflow = 'ticket';
425
+ var epicBranch = /(?:^|\\s)--epic-branch\\b/.test(p);
426
+
427
+ if (/(?:^|\\s)--no-sdd\\b/.test(p)) { out.sdd = false; out.sddSrc = 'flag'; }
428
+ else if (/(?:^|\\s)(?:-sd|--sdd)\\b/.test(p)) { out.sdd = true; out.sddSrc = 'flag'; }
429
+ else if (sddConf.default) { out.sdd = true; out.sddSrc = 'moflo.yaml sdd.default'; }
430
+
431
+ if (/(?:^|\\s)--no-verify\\b/.test(p)) { out.verify = false; out.verifySrc = 'flag'; }
432
+ else if (/(?:^|\\s)(?:-v|--verify)\\b/.test(p)) { out.verify = true; out.verifySrc = 'flag'; }
433
+ else if (!config.verify_before_done) { out.verify = false; out.verifySrc = 'moflo.yaml gates.verify_before_done'; }
434
+ else { out.verify = true; out.verifySrc = 'default'; }
435
+ if (out.sdd && !out.verify && out.verifySrc !== 'flag') out.verify = true;
436
+
437
+ if (/(?:^|\\s)--no-merge\\b/.test(p)) { out.merge = false; out.mergeSrc = 'flag'; }
438
+ else if (/(?:^|\\s)(?:-m|--merge)\\b/.test(p)) { out.merge = true; out.mergeSrc = 'flag'; }
439
+ else if (mergeConf.auto) { out.merge = true; out.mergeSrc = 'moflo.yaml merge.auto'; }
440
+
441
+ // Re-attribute anything applicability turned off — a false must never carry
442
+ // the source of the value it no longer has. SYNC: mirrors bin/gate.cjs.
443
+ if (out.workflow === 'ticket' || out.workflow === 'research') {
444
+ if (out.verify) out.verifySrc = out.workflow + ' mode does not implement';
445
+ out.verify = false;
446
+ }
447
+ if (out.workflow === 'research' || out.workflow === 'spell-engine') {
448
+ if (out.sdd) out.sddSrc = out.workflow + ' mode produces no spec artifacts';
449
+ out.sdd = false;
450
+ }
451
+ if (out.workflow !== 'full' || epicBranch) {
452
+ if (out.merge) out.mergeSrc = epicBranch ? '--epic-branch owns merging' : out.workflow + ' mode opens no PR';
453
+ out.merge = false;
454
+ }
455
+ return out;
456
+ }
457
+
458
+ // #1297 — arm the SDD implement gate from a /flo prompt. Thin wrapper so the
459
+ // armed decision and the announced decision can never disagree.
460
+ function detectSddMode(promptText) {
461
+ return resolveFloRun(promptText).sdd;
398
462
  }
399
463
 
400
464
  // SDD specs-root resolution + artifact helpers for check-before-implement.
@@ -570,7 +634,7 @@ switch (command) {
570
634
  var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
571
635
  if (isEphemeralPath(process.env.TOOL_INPUT_path)) break;
572
636
  if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
573
- process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search.\\n');
637
+ process.stderr.write('BLOCKED [moflo memory_first gate]: Search memory before exploring files. Use mcp__moflo__memory_search.\\n' + GATE_ORIGIN_NOTE + '\\n' + GATE_DISABLE_NOTE + '\\n');
574
638
  process.exit(2);
575
639
  }
576
640
  case 'check-before-read': {
@@ -580,7 +644,7 @@ switch (command) {
580
644
  var fp = process.env.TOOL_INPUT_file_path || '';
581
645
  if (isEphemeralPath(fp)) break;
582
646
  if (fp.indexOf('.claude/guidance/') < 0 && fp.indexOf('.claude\\\\guidance\\\\') < 0) break;
583
- process.stderr.write('BLOCKED: Search memory before reading guidance files. Use mcp__moflo__memory_search.\\n');
647
+ process.stderr.write('BLOCKED [moflo memory_first gate]: Search memory before reading guidance files. Use mcp__moflo__memory_search.\\n' + GATE_ORIGIN_NOTE + '\\n' + GATE_DISABLE_NOTE + '\\n');
584
648
  process.exit(2);
585
649
  }
586
650
  case 'record-task-created': {
@@ -612,11 +676,12 @@ switch (command) {
612
676
  // See bin/gate.cjs check-bash-memory for full rationale.
613
677
  var hint = s2.lastNamespaceHint || classifyBashNamespaceHint(cmd) || '';
614
678
  process.stderr.write(
615
- 'BLOCKED: Search memory before reading files via Bash.\\n' +
679
+ 'BLOCKED [moflo memory_first gate]: Search memory before reading files via Bash.\\n' +
616
680
  'Example: mcp__moflo__memory_search { query: "<topic>", namespace: "<one of: guidance | code-map | patterns | learnings | tests>" }\\n' +
617
681
  (hint ? hint + '\\n' : '') +
618
682
  'On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\\n' +
619
- 'Disable per-gate via moflo.yaml: gates: memory_first: false\\n'
683
+ GATE_ORIGIN_NOTE + '\\n' +
684
+ GATE_DISABLE_NOTE + '\\n'
620
685
  );
621
686
  process.exit(2);
622
687
  break;
@@ -779,6 +844,30 @@ switch (command) {
779
844
  applyPromptStateReset(s, prompt);
780
845
  s.interactionCount = (s.interactionCount || 0) + 1;
781
846
  writeState(s);
847
+ // Announce the resolved /flo run modifiers. moflo.yaml was already parsed in
848
+ // THIS process (fresh per prompt — a git pull or mid-session yaml edit is
849
+ // picked up automatically, no cache to invalidate), so this costs no extra
850
+ // read. SYNC: mirrors bin/gate.cjs prompt-reminder.
851
+ var floRun = resolveFloRun(prompt);
852
+ if (floRun.isFlo) {
853
+ console.log(
854
+ '[moflo] /flo run modes (AUTHORITATIVE — use verbatim; do NOT re-derive from the skill defaults): ' +
855
+ 'sdd=' + (floRun.sdd ? 'ON' : 'off') +
856
+ ' verify=' + (floRun.verify ? 'ON' : 'off') +
857
+ ' merge=' + (floRun.merge ? 'ON' : 'off') +
858
+ ' [workflow=' + floRun.workflow + ']'
859
+ );
860
+ if (floRun.sdd && floRun.sddSrc !== 'flag') {
861
+ console.log(
862
+ '[moflo] sdd is ON via ' + floRun.sddSrc + ' — the spec→plan→implement→verify cycle is ' +
863
+ 'MANDATORY this run. Author the spec before editing source (the sdd_gate blocks source ' +
864
+ 'Write/Edit until a reviewed plan exists). One-off opt out: re-run with --no-sdd.'
865
+ );
866
+ }
867
+ if (floRun.merge && floRun.mergeSrc !== 'flag') {
868
+ console.log('[moflo] merge is ON via ' + floRun.mergeSrc + ' — the PR will be auto-merged. Opt out: --no-merge.');
869
+ }
870
+ }
782
871
  if (config.context_tracking) {
783
872
  var ic = s.interactionCount;
784
873
  if (ic > 30) console.log('Context: CRITICAL. Commit, store learnings, suggest new session.');
@@ -3,7 +3,7 @@
3
3
  * Comprehensive initialization system for Claude Code integration
4
4
  */
5
5
  // Types
6
- export { DEFAULT_INIT_OPTIONS, MINIMAL_INIT_OPTIONS, FULL_INIT_OPTIONS, detectPlatform, } from './types.js';
6
+ export { SKILL_CATEGORIES, DEFAULT_INIT_OPTIONS, MINIMAL_INIT_OPTIONS, FULL_INIT_OPTIONS, detectPlatform, } from './types.js';
7
7
  // Generators
8
8
  export { generateSettings, generateSettingsJson, } from './settings-generator.js';
9
9
  export { generateMCPConfig, generateMCPJson, generateMCPCommands, } from './mcp-generator.js';
@@ -4,6 +4,21 @@
4
4
  */
5
5
  import os from 'os';
6
6
  import path from 'path';
7
+ /**
8
+ * The skill categories `SKILLS_MAP` partitions shipped skills into.
9
+ *
10
+ * This list is the single source of truth (#1308). `SkillsConfig` is DERIVED
11
+ * from it and `SKILLS_MAP` is TYPED by it, so the two can never drift again.
12
+ *
13
+ * They did drift, silently, and it disabled the whole selection mechanism:
14
+ * `SkillsConfig` declared `core`/`github`/`browser` while `SKILLS_MAP` keyed on
15
+ * `core`/`memory`/`spells`. `copySkills` joins them by key, so `memory` and
16
+ * `spells` resolved to `undefined` → falsy → never installed, while
17
+ * `github`/`browser` selected nothing at all. An `as keyof typeof` cast in the
18
+ * lookup suppressed the type error that would otherwise have caught it. Adding
19
+ * a category to one side without the other is now a compile error.
20
+ */
21
+ export const SKILL_CATEGORIES = ['core', 'memory', 'spells'];
7
22
  /**
8
23
  * Detect current platform
9
24
  */
@@ -69,10 +84,15 @@ export const DEFAULT_INIT_OPTIONS = {
69
84
  timeout: 5000,
70
85
  continueOnError: true,
71
86
  },
87
+ // Every category. This is not a behaviour change: the session-start launcher
88
+ // syncs all shipped skills on every run regardless of what init selected, so
89
+ // consumers already end up with all of them. Pre-#1308 this said
90
+ // `core/github/browser`, which resolved to core-only — meaning `flo init` and
91
+ // the launcher disagreed, and init's answer simply lost. Now they agree.
72
92
  skills: {
73
93
  core: true,
74
- github: true,
75
- browser: true,
94
+ memory: true,
95
+ spells: true,
76
96
  all: false,
77
97
  },
78
98
  commands: {
@@ -159,10 +179,11 @@ export const MINIMAL_INIT_OPTIONS = {
159
179
  stop: false,
160
180
  notification: false,
161
181
  },
182
+ // Minimal deliberately stays core-only — that is the point of the preset.
162
183
  skills: {
163
184
  core: true,
164
- github: false,
165
- browser: false,
185
+ memory: false,
186
+ spells: false,
166
187
  all: false,
167
188
  },
168
189
  agents: {
@@ -217,8 +238,8 @@ export const FULL_INIT_OPTIONS = {
217
238
  },
218
239
  skills: {
219
240
  core: true,
220
- github: true,
221
- browser: true,
241
+ memory: true,
242
+ spells: true,
222
243
  all: true,
223
244
  },
224
245
  commands: {
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.12.0';
5
+ export const VERSION = '4.12.2';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.12.0",
3
+ "version": "4.12.2",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.11.10-rc.10",
98
+ "moflo": "^4.12.1",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"