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.
- package/.claude/agents/base-template-generator.md +21 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +40 -0
- package/.claude/helpers/gate.cjs +117 -13
- package/.claude/skills/distill/SKILL.md +1 -1
- package/.claude/skills/fl/SKILL.md +35 -14
- package/.claude/skills/fl/sdd.md +4 -2
- package/.claude/skills/perf-audit/SKILL.md +1 -1
- package/.claude/skills/test-gaps/SKILL.md +1 -1
- package/bin/gate.cjs +117 -13
- package/bin/lib/retired-files.mjs +125 -5
- package/bin/lib/skill-categories.mjs +158 -0
- package/bin/session-start-launcher.mjs +79 -3
- package/dist/src/cli/commands/init.js +32 -11
- package/dist/src/cli/commands/sdd.js +119 -1
- package/dist/src/cli/index.js +15 -0
- package/dist/src/cli/init/executor.js +11 -4
- package/dist/src/cli/init/helpers-generator.js +99 -10
- package/dist/src/cli/init/index.js +1 -1
- package/dist/src/cli/init/types.js +27 -6
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -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':
|
package/dist/src/cli/index.js
CHANGED
|
@@ -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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
//
|
|
392
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
221
|
-
|
|
241
|
+
memory: true,
|
|
242
|
+
spells: true,
|
|
222
243
|
all: true,
|
|
223
244
|
},
|
|
224
245
|
commands: {
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.12.
|
|
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.
|
|
98
|
+
"moflo": "^4.12.1",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|