moflo 4.10.5 → 4.10.7
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/analysis/analyze-code-quality.md +14 -0
- package/.claude/agents/analysis/code-analyzer.md +14 -0
- package/.claude/agents/architecture/system-design/arch-system-design.md +14 -0
- package/.claude/agents/base-template-generator.md +14 -0
- package/.claude/agents/core/coder.md +14 -0
- package/.claude/agents/core/planner.md +14 -0
- package/.claude/agents/core/researcher.md +14 -0
- package/.claude/agents/core/reviewer.md +14 -0
- package/.claude/agents/core/tester.md +14 -0
- package/.claude/agents/custom/test-long-runner.md +14 -0
- package/.claude/agents/development/dev-backend-api.md +14 -0
- package/.claude/agents/development/dev-database.md +13 -0
- package/.claude/agents/development/dev-frontend.md +13 -0
- package/.claude/agents/devops/ci-cd/ops-cicd-github.md +14 -0
- package/.claude/agents/documentation/api-docs/docs-api-openapi.md +14 -0
- package/.claude/agents/security/security-auditor.md +13 -0
- package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +5 -3
- package/.claude/guidance/shipped/moflo-cli-reference.md +17 -31
- package/.claude/guidance/shipped/moflo-task-icons.md +10 -6
- package/.claude/guidance/shipped/moflo-yaml-reference.md +16 -2
- package/.claude/helpers/gate.cjs +101 -1
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/bin/gate.cjs +101 -1
- package/bin/session-start-launcher.mjs +205 -16
- package/bin/setup-project.mjs +38 -58
- package/dist/src/cli/commands/daemon.js +31 -10
- package/dist/src/cli/commands/doctor-checks-deep.js +105 -0
- package/dist/src/cli/commands/doctor-fixes.js +24 -0
- package/dist/src/cli/commands/doctor-registry.js +5 -1
- package/dist/src/cli/commands/retire.js +22 -17
- package/dist/src/cli/config/moflo-config.js +11 -0
- package/dist/src/cli/init/claudemd-generator.js +6 -2
- package/dist/src/cli/init/helpers-generator.js +36 -1
- package/dist/src/cli/init/moflo-init.js +13 -21
- package/dist/src/cli/init/settings-generator.js +4 -1
- package/dist/src/cli/services/claudemd-injection.js +173 -0
- package/dist/src/cli/services/hook-block-hash.js +8 -2
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/retired-files.json +305 -112
|
@@ -670,4 +670,109 @@ export async function checkHookBlockDrift() {
|
|
|
670
670
|
fix: 'set auto_update.hook_block_drift: regenerate in moflo.yaml, or claudeFlow.hooks.locked: true to suppress',
|
|
671
671
|
};
|
|
672
672
|
}
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// 12. CLAUDE.md Injection Drift Check
|
|
675
|
+
// ============================================================================
|
|
676
|
+
/**
|
|
677
|
+
* Detect when the consumer's `<root>/CLAUDE.md` MoFlo-injected block has
|
|
678
|
+
* drifted from the canonical block the current `claudemd-generator` produces.
|
|
679
|
+
* Analogue of `Hook Block Drift` for CLAUDE.md content.
|
|
680
|
+
*
|
|
681
|
+
* The session-start launcher refreshes shipped guidance files on every
|
|
682
|
+
* version change, but the CLAUDE.md injection is only rewritten by explicit
|
|
683
|
+
* `flo init` / `flo-setup`. Without this check, consumers carry stale
|
|
684
|
+
* injection content (and stale guidance pointers) indefinitely.
|
|
685
|
+
*
|
|
686
|
+
* Five states map to four reportable statuses:
|
|
687
|
+
* no-file → warn (run `flo init`)
|
|
688
|
+
* no-marker → warn (run `flo init` / `flo-setup`)
|
|
689
|
+
* legacy-marker → warn (auto-fixable — replace legacy block)
|
|
690
|
+
* in-sync → pass
|
|
691
|
+
* drifted → warn (auto-fixable — refresh block)
|
|
692
|
+
*/
|
|
693
|
+
export async function checkClaudeMdInjectionDrift() {
|
|
694
|
+
const projectDir = findConsumerProjectDir();
|
|
695
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
696
|
+
// Respect `auto_update.claudemd_injection_drift: off` for consumers who
|
|
697
|
+
// explicitly opt out (mirrors the launcher's behaviour and the Hook Block
|
|
698
|
+
// Drift check). Read the config first so the off-mode skip is cheap.
|
|
699
|
+
try {
|
|
700
|
+
const { loadMofloConfig } = await import('../config/moflo-config.js');
|
|
701
|
+
const cfg = loadMofloConfig(projectDir);
|
|
702
|
+
if (cfg.auto_update.claudemd_injection_drift === 'off') {
|
|
703
|
+
return {
|
|
704
|
+
name: 'CLAUDE.md Injection Drift',
|
|
705
|
+
status: 'pass',
|
|
706
|
+
message: 'drift check skipped — auto_update.claudemd_injection_drift: off',
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch { /* config read failure — fall through to drift check */ }
|
|
711
|
+
if (!existsSync(claudeMdPath)) {
|
|
712
|
+
return {
|
|
713
|
+
name: 'CLAUDE.md Injection Drift',
|
|
714
|
+
status: 'warn',
|
|
715
|
+
message: 'CLAUDE.md not found',
|
|
716
|
+
fix: 'npx moflo init',
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
let contents;
|
|
720
|
+
try {
|
|
721
|
+
contents = readFileSync(claudeMdPath, 'utf-8');
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
return {
|
|
725
|
+
name: 'CLAUDE.md Injection Drift',
|
|
726
|
+
status: 'warn',
|
|
727
|
+
message: `cannot read CLAUDE.md: ${errorDetail(e)}`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// Dynamic-import the generator + drift detector so the dist-vs-source
|
|
731
|
+
// path resolution stays consistent with the launcher.
|
|
732
|
+
const { generateClaudeMd } = await import('../init/claudemd-generator.js');
|
|
733
|
+
const { computeInjectionDrift } = await import('../services/claudemd-injection.js');
|
|
734
|
+
// Use `{}` (not DEFAULT_INIT_OPTIONS) to match the launcher's call —
|
|
735
|
+
// the generator ignores the argument, but matching call shape removes the
|
|
736
|
+
// possibility of a future generator change diverging the two surfaces.
|
|
737
|
+
const canonical = generateClaudeMd({});
|
|
738
|
+
const report = computeInjectionDrift(contents, canonical);
|
|
739
|
+
switch (report.state) {
|
|
740
|
+
case 'in-sync':
|
|
741
|
+
return {
|
|
742
|
+
name: 'CLAUDE.md Injection Drift',
|
|
743
|
+
status: 'pass',
|
|
744
|
+
message: 'CLAUDE.md injection block matches reference',
|
|
745
|
+
};
|
|
746
|
+
case 'no-marker':
|
|
747
|
+
return {
|
|
748
|
+
name: 'CLAUDE.md Injection Drift',
|
|
749
|
+
status: 'warn',
|
|
750
|
+
message: 'CLAUDE.md has no MOFLO:INJECTED:START block',
|
|
751
|
+
fix: 'npx flo-setup',
|
|
752
|
+
};
|
|
753
|
+
case 'legacy-marker':
|
|
754
|
+
return {
|
|
755
|
+
name: 'CLAUDE.md Injection Drift',
|
|
756
|
+
status: 'warn',
|
|
757
|
+
message: 'CLAUDE.md uses a legacy moflo marker pair (pre-MOFLO:INJECTED) — auto-fix replaces with current block',
|
|
758
|
+
fix: 'npx flo-setup --update',
|
|
759
|
+
};
|
|
760
|
+
case 'drifted':
|
|
761
|
+
return {
|
|
762
|
+
name: 'CLAUDE.md Injection Drift',
|
|
763
|
+
status: 'warn',
|
|
764
|
+
message: 'CLAUDE.md injection block has drifted from reference',
|
|
765
|
+
fix: 'npx flo-setup --update',
|
|
766
|
+
};
|
|
767
|
+
case 'no-file':
|
|
768
|
+
// Defensive — `existsSync` returned true above, so this branch is
|
|
769
|
+
// unreachable in practice. Return a sane status anyway.
|
|
770
|
+
return {
|
|
771
|
+
name: 'CLAUDE.md Injection Drift',
|
|
772
|
+
status: 'warn',
|
|
773
|
+
message: 'CLAUDE.md not found',
|
|
774
|
+
fix: 'npx moflo init',
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
673
778
|
//# sourceMappingURL=doctor-checks-deep.js.map
|
|
@@ -255,6 +255,30 @@ export async function autoFixCheck(check) {
|
|
|
255
255
|
'Gate Health': async () => {
|
|
256
256
|
return fixGateHealthHooks();
|
|
257
257
|
},
|
|
258
|
+
// Refresh the consumer's CLAUDE.md MoFlo block in place using the
|
|
259
|
+
// shared `applyInjectionReplacement` service. Idempotent: a re-run sees
|
|
260
|
+
// `state === 'in-sync'` and the autoFix dispatcher skips this entry.
|
|
261
|
+
'CLAUDE.md Injection Drift': async () => {
|
|
262
|
+
const projectRoot = findProjectRoot();
|
|
263
|
+
const claudeMdPath = join(projectRoot, 'CLAUDE.md');
|
|
264
|
+
try {
|
|
265
|
+
const { generateClaudeMd } = await import('../init/claudemd-generator.js');
|
|
266
|
+
const { applyInjectionReplacement } = await import('../services/claudemd-injection.js');
|
|
267
|
+
const canonical = generateClaudeMd({});
|
|
268
|
+
const existing = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, 'utf-8') : null;
|
|
269
|
+
const result = applyInjectionReplacement(existing, canonical);
|
|
270
|
+
if (!result.changed || !result.contents)
|
|
271
|
+
return false;
|
|
272
|
+
// atomicWriteFileSync guards against a concurrent reader (Claude Code
|
|
273
|
+
// re-scanning CLAUDE.md mid-fix) seeing a truncated file.
|
|
274
|
+
atomicWriteFileSync(claudeMdPath, result.contents);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
output.writeln(output.warning(` CLAUDE.md repair failed: ${errorDetail(e)}`));
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
258
282
|
'Embedding hygiene': async () => {
|
|
259
283
|
// The session-start launcher already runs the same migration BEFORE
|
|
260
284
|
// daemon/MCP boot — that's where consumer autoheal happens. Running
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Kept separate from `doctor.ts` so the orchestration file stays small and the
|
|
5
5
|
* registry can be inspected/extended without re-touching command-action code.
|
|
6
6
|
*/
|
|
7
|
-
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
7
|
+
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkClaudeMdInjectionDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
8
8
|
import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
|
|
9
9
|
import { checkDaemonVersionSkew } from './doctor-checks-version-skew.js';
|
|
10
10
|
import { checkEmbeddingCoverageTruth } from './doctor-checks-coverage-truth.js';
|
|
@@ -63,6 +63,7 @@ export const allChecks = [
|
|
|
63
63
|
checkHookExecution,
|
|
64
64
|
checkGateHealth,
|
|
65
65
|
checkHookBlockDrift,
|
|
66
|
+
checkClaudeMdInjectionDrift,
|
|
66
67
|
checkMofloDbBridge,
|
|
67
68
|
// Issue #818 / epic #798 — coordinator-path tripwires. They share the
|
|
68
69
|
// singleton coordinator with checkSubagentHealth above and assert by
|
|
@@ -127,6 +128,9 @@ export const componentMap = {
|
|
|
127
128
|
'gate': checkGateHealth,
|
|
128
129
|
'hook-drift': checkHookBlockDrift,
|
|
129
130
|
'drift': checkHookBlockDrift,
|
|
131
|
+
'claudemd-drift': checkClaudeMdInjectionDrift,
|
|
132
|
+
'claudemd': checkClaudeMdInjectionDrift,
|
|
133
|
+
'injection-drift': checkClaudeMdInjectionDrift,
|
|
130
134
|
'sandbox': checkSandboxTier,
|
|
131
135
|
'sandbox-tier': checkSandboxTier,
|
|
132
136
|
'moflodb': checkMofloDbBridge,
|
|
@@ -42,7 +42,7 @@ function findMofloRepoRoot(start) {
|
|
|
42
42
|
}
|
|
43
43
|
export const retireCommand = {
|
|
44
44
|
name: 'retire',
|
|
45
|
-
description: 'Record a retired shipped file in retired-files.json (moflo dev only) — usage: flo retire <path> [--retired-by #nnn]',
|
|
45
|
+
description: 'Record a retired shipped file in retired-files.json (moflo dev only) — usage: flo retire <path> [--retired-by #nnn] | flo retire --rebuild-hashes',
|
|
46
46
|
hidden: true,
|
|
47
47
|
options: [
|
|
48
48
|
{
|
|
@@ -56,15 +56,16 @@ export const retireCommand = {
|
|
|
56
56
|
type: 'string',
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
name: 'hashes',
|
|
60
|
-
description: '
|
|
61
|
-
type: '
|
|
62
|
-
default:
|
|
59
|
+
name: 'rebuild-hashes',
|
|
60
|
+
description: 'Recompute knownContentHashes[] for every existing entry from full git history (#1133 backfill)',
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
default: false,
|
|
63
63
|
},
|
|
64
64
|
],
|
|
65
65
|
examples: [
|
|
66
66
|
{ command: 'flo retire .claude/agents/v3/performance-engineer.md --retired-by #932', description: 'Record a retirement' },
|
|
67
67
|
{ command: 'flo retire .claude/skills/skill-builder/SKILL.md --retired-by #945 --retired-in 4.9.21', description: 'Pin retiredIn' },
|
|
68
|
+
{ command: 'flo retire --rebuild-hashes', description: 'Backfill every entry from full git history' },
|
|
68
69
|
],
|
|
69
70
|
action: async (ctx) => {
|
|
70
71
|
const repoRoot = findMofloRepoRoot(__filename) || findMofloRepoRoot(ctx.cwd);
|
|
@@ -73,11 +74,6 @@ export const retireCommand = {
|
|
|
73
74
|
output.printInfo('retired-files.json lives at the moflo package root and does not ship to consumer projects');
|
|
74
75
|
return { success: false, message: 'not in moflo repo', exitCode: 1 };
|
|
75
76
|
}
|
|
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
77
|
const scriptPath = resolve(repoRoot, 'scripts', 'build-retired-files.mjs');
|
|
82
78
|
if (!existsSync(scriptPath)) {
|
|
83
79
|
output.printError(`scripts/build-retired-files.mjs not found at ${scriptPath}`);
|
|
@@ -86,13 +82,22 @@ export const retireCommand = {
|
|
|
86
82
|
// Parser normalises kebab-case flag names to camelCase before storing
|
|
87
83
|
// (#787). Read as ctx.flags.<camelCase> — bracket-with-kebab is always
|
|
88
84
|
// undefined and ESLint blocks that pattern.
|
|
89
|
-
|
|
90
|
-
if (ctx.flags.
|
|
91
|
-
args
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
let args;
|
|
86
|
+
if (ctx.flags.rebuildHashes) {
|
|
87
|
+
args = ['--rebuild-hashes'];
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const path = ctx.args[0];
|
|
91
|
+
if (!path) {
|
|
92
|
+
output.printError('Missing required argument: <path> (or pass --rebuild-hashes)');
|
|
93
|
+
return { success: false, message: 'missing path', exitCode: 2 };
|
|
94
|
+
}
|
|
95
|
+
args = ['--add', path];
|
|
96
|
+
if (ctx.flags.retiredBy)
|
|
97
|
+
args.push('--retired-by', String(ctx.flags.retiredBy));
|
|
98
|
+
if (ctx.flags.retiredIn)
|
|
99
|
+
args.push('--retired-in', String(ctx.flags.retiredIn));
|
|
100
|
+
}
|
|
96
101
|
const result = spawnSync('node', [scriptPath, ...args], {
|
|
97
102
|
cwd: repoRoot,
|
|
98
103
|
stdio: 'inherit',
|
|
@@ -83,6 +83,7 @@ const DEFAULT_CONFIG = {
|
|
|
83
83
|
scripts: true,
|
|
84
84
|
helpers: true,
|
|
85
85
|
hook_block_drift: 'warn',
|
|
86
|
+
claudemd_injection_drift: 'regenerate',
|
|
86
87
|
},
|
|
87
88
|
sandbox: {
|
|
88
89
|
enabled: false,
|
|
@@ -212,6 +213,12 @@ function mergeConfig(raw, root) {
|
|
|
212
213
|
? v
|
|
213
214
|
: DEFAULT_CONFIG.auto_update.hook_block_drift;
|
|
214
215
|
})(),
|
|
216
|
+
claudemd_injection_drift: (() => {
|
|
217
|
+
const v = raw.auto_update?.claudemd_injection_drift ?? raw.autoUpdate?.claudemdInjectionDrift;
|
|
218
|
+
return v === 'regenerate' || v === 'off' || v === 'warn'
|
|
219
|
+
? v
|
|
220
|
+
: DEFAULT_CONFIG.auto_update.claudemd_injection_drift;
|
|
221
|
+
})(),
|
|
215
222
|
},
|
|
216
223
|
sandbox: {
|
|
217
224
|
enabled: raw.sandbox?.enabled ?? DEFAULT_CONFIG.sandbox.enabled,
|
|
@@ -409,6 +416,10 @@ auto_update:
|
|
|
409
416
|
# warn = print drift summary on session start (default)
|
|
410
417
|
# regenerate = auto-add missing hooks (only when no customisations)
|
|
411
418
|
# off = skip detection entirely
|
|
419
|
+
claudemd_injection_drift: regenerate # warn | regenerate | off
|
|
420
|
+
# regenerate = auto-refresh CLAUDE.md MoFlo block on drift (default)
|
|
421
|
+
# warn = print drift summary on session start
|
|
422
|
+
# off = skip detection entirely
|
|
412
423
|
|
|
413
424
|
# OS-level sandbox for spell bash steps
|
|
414
425
|
# Denylist always runs regardless of this setting
|
|
@@ -60,8 +60,12 @@ ${MARKER_END}`;
|
|
|
60
60
|
export { MARKER_START, MARKER_END, LEGACY_MARKER_STARTS, LEGACY_MARKER_ENDS };
|
|
61
61
|
/**
|
|
62
62
|
* Generate the MoFlo section to inject into CLAUDE.md.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
63
|
+
*
|
|
64
|
+
* Both parameters are accepted for backward compatibility but ignored — all
|
|
65
|
+
* templates produce the same minimal injection and the options shape is no
|
|
66
|
+
* longer consulted. Optional so callers from both the dev tree (TS) and the
|
|
67
|
+
* dogfood launcher (plain JS) can invoke as `generateClaudeMd()` /
|
|
68
|
+
* `generateClaudeMd({})` interchangeably.
|
|
65
69
|
*/
|
|
66
70
|
export function generateClaudeMd(_options, _template) {
|
|
67
71
|
return mofloSection() + '\n';
|
|
@@ -277,6 +277,10 @@ var command = process.argv[2];
|
|
|
277
277
|
|
|
278
278
|
var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
|
|
279
279
|
var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
|
|
280
|
+
// #1132 — Bash memory-first gate regexes. See bin/gate.cjs for documentation.
|
|
281
|
+
var CREDIT_MEMORY_SEARCH_RE = /semantic-search|memory search|memory retrieve|memory-search/;
|
|
282
|
+
var READ_LIKE_BASH_RE = /^\\s*(?:cat|head|tail|less|more|bat|xxd|od|hexdump)\\b|^\\s*(?:grep|rg|ag|fgrep|egrep|find|fd)\\b|^\\s*sed\\s+-n\\b|^\\s*awk\\s+(?!.*<<)|^\\s*type\\s+\\S*[\\\\/.]|^\\s*(?:Get-Content|gc|Select-String|sls)\\b/i;
|
|
283
|
+
var BASH_CARVE_OUT_RE = /^\\s*(npm|npx|pnpm|yarn|bun|node|deno|tsx|ts-node)\\s|^\\s*(git|gh|hub)\\s|^\\s*(docker|kubectl|helm|terraform)\\s|^\\s*(curl|wget|http|fetch)\\s|^\\s*(jq|yq|xq)\\s|^\\s*(echo|printf|true|false|sleep|test|\\[)\\s|^\\s*cat\\s+(<<|<<<)|^\\s*cat\\s+[^|]*\\s*>|^\\s*tee\\b|^\\s*find\\s+.+?-(delete|exec\\s+rm)\\b/;
|
|
280
284
|
var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\\b/i;
|
|
281
285
|
var TASK_RE = /\\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\\b/i;
|
|
282
286
|
|
|
@@ -340,6 +344,19 @@ function classifyNamespaceHint(promptText) {
|
|
|
340
344
|
return '';
|
|
341
345
|
}
|
|
342
346
|
|
|
347
|
+
// #1132 — command-shape namespace classifier for the bash-BLOCK message.
|
|
348
|
+
// SYNC: duplicated verbatim in bin/gate.cjs. See that file for rationale.
|
|
349
|
+
function classifyBashNamespaceHint(cmd) {
|
|
350
|
+
if (/^\\s*(?:grep|rg|ag|fgrep|egrep|find|fd|Select-String|sls)\\b/i.test(cmd)) {
|
|
351
|
+
return 'Memory namespace hint: use "code-map" for codebase navigation.';
|
|
352
|
+
}
|
|
353
|
+
if (/^\\s*(?:cat|head|tail|less|more|bat|type|Get-Content|gc)\\b.*\\.(?:md|mdx|rst|txt)\\b/i.test(cmd)
|
|
354
|
+
|| /^\\s*(?:cat|head|tail|less|more|bat|type|Get-Content|gc)\\b.*\\b(?:README|CLAUDE|CHANGELOG|CONTRIBUTING|LICENSE)\\b/i.test(cmd)) {
|
|
355
|
+
return 'Memory namespace hint: search "guidance" and "learnings" for project rules and decisions.';
|
|
356
|
+
}
|
|
357
|
+
return '';
|
|
358
|
+
}
|
|
359
|
+
|
|
343
360
|
function applyPromptStateReset(state, promptText) {
|
|
344
361
|
state.memorySearched = false;
|
|
345
362
|
state.memorySearchedBy = {};
|
|
@@ -467,11 +484,29 @@ switch (command) {
|
|
|
467
484
|
break;
|
|
468
485
|
}
|
|
469
486
|
case 'check-bash-memory': {
|
|
487
|
+
// #1132 — credit + block. See bin/gate.cjs for full documentation.
|
|
470
488
|
var cmd = process.env.TOOL_INPUT_command || '';
|
|
471
|
-
if (
|
|
489
|
+
if (CREDIT_MEMORY_SEARCH_RE.test(cmd)) {
|
|
472
490
|
var s = readState();
|
|
473
491
|
if (markMemorySearched(s)) writeState(s);
|
|
492
|
+
break;
|
|
474
493
|
}
|
|
494
|
+
if (!config.memory_first) break;
|
|
495
|
+
if (!READ_LIKE_BASH_RE.test(cmd)) break;
|
|
496
|
+
if (BASH_CARVE_OUT_RE.test(cmd)) break;
|
|
497
|
+
var s2 = readState();
|
|
498
|
+
if (!s2.memoryRequired || isMemorySearchedFor(s2)) break;
|
|
499
|
+
// Hint precedence: prompt classification → command-shape classification.
|
|
500
|
+
// See bin/gate.cjs check-bash-memory for full rationale.
|
|
501
|
+
var hint = s2.lastNamespaceHint || classifyBashNamespaceHint(cmd) || '';
|
|
502
|
+
process.stderr.write(
|
|
503
|
+
'BLOCKED: Search memory before reading files via Bash.\\n' +
|
|
504
|
+
'Example: mcp__moflo__memory_search { query: "<topic>", namespace: "<one of: guidance | code-map | patterns | learnings | tests>" }\\n' +
|
|
505
|
+
(hint ? hint + '\\n' : '') +
|
|
506
|
+
'On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\\n' +
|
|
507
|
+
'Disable per-gate via moflo.yaml: gates: memory_first: false\\n'
|
|
508
|
+
);
|
|
509
|
+
process.exit(2);
|
|
475
510
|
break;
|
|
476
511
|
}
|
|
477
512
|
case 'check-task-transition': {
|
|
@@ -15,7 +15,8 @@ import { execSync } from 'child_process';
|
|
|
15
15
|
import { locateMofloRootPath } from '../services/moflo-require.js';
|
|
16
16
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
17
17
|
import { discoverGuidanceDirs, discoverSrcDirs, discoverTestDirs, detectExtensions, renderMofloYaml, } from './moflo-yaml-template.js';
|
|
18
|
-
import { generateClaudeMd as generateMofloSection
|
|
18
|
+
import { generateClaudeMd as generateMofloSection } from './claudemd-generator.js';
|
|
19
|
+
import { applyInjectionReplacement } from '../services/claudemd-injection.js';
|
|
19
20
|
import { DEFAULT_INIT_OPTIONS } from './types.js';
|
|
20
21
|
export { discoverTestDirs };
|
|
21
22
|
// ============================================================================
|
|
@@ -400,29 +401,20 @@ function generateSkill(root, force) {
|
|
|
400
401
|
// ============================================================================
|
|
401
402
|
function generateClaudeMd(root, _force) {
|
|
402
403
|
const claudeMdPath = path.join(root, 'CLAUDE.md');
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
for (let i = 0; i < allStartMarkers.length; i++) {
|
|
410
|
-
if (existing.includes(allStartMarkers[i])) {
|
|
411
|
-
const startIdx = existing.indexOf(allStartMarkers[i]);
|
|
412
|
-
const endIdx = existing.indexOf(allEndMarkers[i]);
|
|
413
|
-
if (endIdx > startIdx) {
|
|
414
|
-
existing = existing.substring(0, startIdx) + existing.substring(endIdx + allEndMarkers[i].length);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// Single source of truth: claudemd-generator.ts owns the section content.
|
|
404
|
+
const existed = fs.existsSync(claudeMdPath);
|
|
405
|
+
const existing = existed ? fs.readFileSync(claudeMdPath, 'utf-8') : null;
|
|
406
|
+
// Single source of truth: claudemd-generator.ts owns the section content,
|
|
407
|
+
// claudemd-injection.ts owns the marker-replace logic. Replaces in place
|
|
408
|
+
// when a marker pair (current or legacy) already exists; otherwise creates
|
|
409
|
+
// a fresh CLAUDE.md or appends to a non-moflo one.
|
|
420
410
|
const canonical = generateMofloSection(DEFAULT_INIT_OPTIONS);
|
|
421
|
-
const
|
|
422
|
-
|
|
411
|
+
const result = applyInjectionReplacement(existing, canonical);
|
|
412
|
+
if (result.contents !== null && (result.changed || !existed)) {
|
|
413
|
+
fs.writeFileSync(claudeMdPath, result.contents, 'utf-8');
|
|
414
|
+
}
|
|
423
415
|
return {
|
|
424
416
|
name: 'CLAUDE.md',
|
|
425
|
-
status:
|
|
417
|
+
status: existed ? 'updated' : 'created',
|
|
426
418
|
detail: 'MoFlo section injected (~22 lines)',
|
|
427
419
|
};
|
|
428
420
|
}
|
|
@@ -233,6 +233,9 @@ function generateHooksConfig(config) {
|
|
|
233
233
|
hooks: [
|
|
234
234
|
{ type: 'command', command: gateHookCmd('check-dangerous-command'), timeout: 2000 },
|
|
235
235
|
{ type: 'command', command: gateHookCmd('check-before-pr'), timeout: 2000 },
|
|
236
|
+
// #1132 — moved from PostToolUse so process.exit(2) actually blocks
|
|
237
|
+
// read-like Bash that bypasses the Read/Glob/Grep gates via the shell.
|
|
238
|
+
{ type: 'command', command: gateHookCmd('check-bash-memory'), timeout: 2000 },
|
|
236
239
|
],
|
|
237
240
|
},
|
|
238
241
|
// #931 — TaskCreate REMINDER + namespace hint moved here from
|
|
@@ -272,7 +275,7 @@ function generateHooksConfig(config) {
|
|
|
272
275
|
{
|
|
273
276
|
matcher: '^Bash$',
|
|
274
277
|
hooks: [
|
|
275
|
-
|
|
278
|
+
// #1132 — check-bash-memory moved to PreToolUse (above).
|
|
276
279
|
{ type: 'command', command: gateHookCmd('record-test-run'), timeout: 2000 },
|
|
277
280
|
],
|
|
278
281
|
},
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLAUDE.md injection drift detection + replacement (#1142).
|
|
3
|
+
*
|
|
4
|
+
* Detects when a consumer's `<root>/CLAUDE.md` carries a MoFlo-injected block
|
|
5
|
+
* whose content has drifted from what the current generator produces. Catches
|
|
6
|
+
* the case where a consumer upgrades moflo (so guidance files refresh) but the
|
|
7
|
+
* CLAUDE.md injection — only rewritten by explicit `flo init` / `flo-setup` —
|
|
8
|
+
* stays frozen at the prior version's content, sometimes pointing at paths
|
|
9
|
+
* that no longer exist (e.g. `.claude/guidance/shipped/...` before the
|
|
10
|
+
* flat-layout cleanup).
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: This module must remain self-contained with ZERO imports from
|
|
13
|
+
* other moflo modules (mirrors the constraint on `services/hook-block-hash.ts`
|
|
14
|
+
* and `services/hook-wiring.ts`). It is dynamically imported at runtime by
|
|
15
|
+
* `bin/session-start-launcher.mjs` in consumer projects, where transitive
|
|
16
|
+
* dependencies may not resolve.
|
|
17
|
+
*
|
|
18
|
+
* The MoFlo block markers are duplicated from `init/claudemd-generator.ts` on
|
|
19
|
+
* purpose — the launcher cannot pull in TS dist of init/types.js at runtime,
|
|
20
|
+
* and a unit test asserts the two stay in sync.
|
|
21
|
+
*/
|
|
22
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Marker constants — kept in sync with init/claudemd-generator.ts
|
|
24
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
export const MARKER_START = '<!-- MOFLO:INJECTED:START -->';
|
|
26
|
+
export const MARKER_END = '<!-- MOFLO:INJECTED:END -->';
|
|
27
|
+
// Legacy markers from earlier moflo versions — detected on drift checks so we
|
|
28
|
+
// can offer to replace the legacy block with the current marker pair.
|
|
29
|
+
export const LEGACY_MARKER_STARTS = [
|
|
30
|
+
'<!-- MOFLO:START -->',
|
|
31
|
+
'<!-- MOFLO:SUBAGENT-PROTOCOL:START -->',
|
|
32
|
+
];
|
|
33
|
+
export const LEGACY_MARKER_ENDS = [
|
|
34
|
+
'<!-- MOFLO:END -->',
|
|
35
|
+
'<!-- MOFLO:SUBAGENT-PROTOCOL:END -->',
|
|
36
|
+
];
|
|
37
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Block extraction
|
|
39
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Locate the MoFlo-injected block in `claudeMdContents`, normalising line
|
|
42
|
+
* endings so a CRLF file matches an LF canonical block (Windows consumers
|
|
43
|
+
* regularly hit this — git autocrlf can flip the source bytes on checkout).
|
|
44
|
+
*
|
|
45
|
+
* Returns null when `contents` is null/undefined/empty, or when no marker
|
|
46
|
+
* pair is found. Includes the marker strings themselves in the extracted
|
|
47
|
+
* block, matching `MARKER_START…MARKER_END` exactly so a byte-for-byte
|
|
48
|
+
* compare against the canonical block works.
|
|
49
|
+
*/
|
|
50
|
+
export function extractInjectedBlock(claudeMdContents) {
|
|
51
|
+
if (!claudeMdContents)
|
|
52
|
+
return null;
|
|
53
|
+
const normalised = claudeMdContents.replace(/\r\n/g, '\n');
|
|
54
|
+
// Try the current marker pair first, then each legacy pair. markerIndex:
|
|
55
|
+
// 0 → current MARKER_START/MARKER_END
|
|
56
|
+
// 1+ → LEGACY_MARKER_STARTS[markerIndex - 1] / LEGACY_MARKER_ENDS[markerIndex - 1]
|
|
57
|
+
const starts = [MARKER_START, ...LEGACY_MARKER_STARTS];
|
|
58
|
+
const ends = [MARKER_END, ...LEGACY_MARKER_ENDS];
|
|
59
|
+
for (let i = 0; i < starts.length; i++) {
|
|
60
|
+
const startIdx = normalised.indexOf(starts[i]);
|
|
61
|
+
if (startIdx < 0)
|
|
62
|
+
continue;
|
|
63
|
+
const endIdx = normalised.indexOf(ends[i], startIdx + starts[i].length);
|
|
64
|
+
if (endIdx <= startIdx)
|
|
65
|
+
continue;
|
|
66
|
+
const endInclusive = endIdx + ends[i].length;
|
|
67
|
+
return {
|
|
68
|
+
block: normalised.substring(startIdx, endInclusive),
|
|
69
|
+
start: startIdx,
|
|
70
|
+
end: endInclusive,
|
|
71
|
+
markerIndex: i,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Drift detection
|
|
78
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Trim `canonical` to the bytes between (and including) the current MoFlo
|
|
81
|
+
* markers. `generateClaudeMd()` appends a trailing newline that callers
|
|
82
|
+
* commonly include in the result; the in-file block does not carry that
|
|
83
|
+
* newline, so we strip trailing whitespace before comparing.
|
|
84
|
+
*/
|
|
85
|
+
function canonicalBlock(canonical) {
|
|
86
|
+
return canonical.replace(/\r\n/g, '\n').trimEnd();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Classify a consumer's CLAUDE.md against the canonical injected block.
|
|
90
|
+
*
|
|
91
|
+
* `claudeMdContents` should be the result of `readFileSync(<root>/CLAUDE.md)`
|
|
92
|
+
* or null/undefined when the file is absent. `canonical` is the output of
|
|
93
|
+
* `generateClaudeMd({})` from `init/claudemd-generator.ts`.
|
|
94
|
+
*/
|
|
95
|
+
export function computeInjectionDrift(claudeMdContents, canonical) {
|
|
96
|
+
if (claudeMdContents === null || claudeMdContents === undefined) {
|
|
97
|
+
return { state: 'no-file' };
|
|
98
|
+
}
|
|
99
|
+
const extracted = extractInjectedBlock(claudeMdContents);
|
|
100
|
+
if (!extracted) {
|
|
101
|
+
return { state: 'no-marker' };
|
|
102
|
+
}
|
|
103
|
+
if (extracted.markerIndex > 0) {
|
|
104
|
+
return { state: 'legacy-marker', legacyMarkerIndex: extracted.markerIndex - 1 };
|
|
105
|
+
}
|
|
106
|
+
const currentBlock = extracted.block;
|
|
107
|
+
const wantBlock = canonicalBlock(canonical);
|
|
108
|
+
if (currentBlock === wantBlock) {
|
|
109
|
+
return { state: 'in-sync' };
|
|
110
|
+
}
|
|
111
|
+
return { state: 'drifted' };
|
|
112
|
+
}
|
|
113
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// Replacement
|
|
115
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
/**
|
|
117
|
+
* Apply the canonical block to `claudeMdContents`, returning the new
|
|
118
|
+
* contents and a `changed` flag indicating whether any bytes differ. The
|
|
119
|
+
* caller writes the file (or persists in-memory state) — this function does
|
|
120
|
+
* no I/O so it's safe to call from any execution context.
|
|
121
|
+
*
|
|
122
|
+
* Behavior by input state:
|
|
123
|
+
* - `no-file` → returns `{ contents: canonical, changed: true }` so the
|
|
124
|
+
* caller can write a fresh CLAUDE.md (e.g. `flo init` first-run).
|
|
125
|
+
* - `no-marker` → APPENDS the canonical block to the end of the existing
|
|
126
|
+
* contents (matches `bin/setup-project.mjs:updateClaudeMd` append path).
|
|
127
|
+
* - `legacy-marker` → REPLACES the legacy block in-place with the canonical block.
|
|
128
|
+
* - `in-sync` → no change.
|
|
129
|
+
* - `drifted` → REPLACES the existing block in-place with the canonical block.
|
|
130
|
+
*/
|
|
131
|
+
export function applyInjectionReplacement(claudeMdContents, canonical) {
|
|
132
|
+
const want = canonicalBlock(canonical);
|
|
133
|
+
if (claudeMdContents === null || claudeMdContents === undefined) {
|
|
134
|
+
return { contents: `# Project Configuration\n\n${want}\n`, changed: true, state: 'in-sync' };
|
|
135
|
+
}
|
|
136
|
+
const extracted = extractInjectedBlock(claudeMdContents);
|
|
137
|
+
if (!extracted) {
|
|
138
|
+
// No marker — append the canonical block to the end (idempotent for
|
|
139
|
+
// future runs because the appended block will then be located on
|
|
140
|
+
// subsequent extractions).
|
|
141
|
+
const sep = claudeMdContents.endsWith('\n') ? '\n' : '\n\n';
|
|
142
|
+
const next = claudeMdContents + sep + want + '\n';
|
|
143
|
+
return { contents: next, changed: true, state: 'in-sync' };
|
|
144
|
+
}
|
|
145
|
+
// We located a marker pair (current or legacy). If content already matches
|
|
146
|
+
// the canonical block, nothing to do.
|
|
147
|
+
if (extracted.markerIndex === 0 && extracted.block === want) {
|
|
148
|
+
return { contents: claudeMdContents, changed: false, state: 'in-sync' };
|
|
149
|
+
}
|
|
150
|
+
// Operate on the line-ending-normalised view so the byte offsets we record
|
|
151
|
+
// line up with the actual replacement window. The output keeps LF endings
|
|
152
|
+
// — the launcher and setup-project both write LF.
|
|
153
|
+
const normalised = claudeMdContents.replace(/\r\n/g, '\n');
|
|
154
|
+
const next = normalised.substring(0, extracted.start) + want + normalised.substring(extracted.end);
|
|
155
|
+
return { contents: next, changed: true, state: 'in-sync' };
|
|
156
|
+
}
|
|
157
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Human-readable status for healer + launcher output
|
|
159
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Short one-line summary describing a drift state. Used by `flo doctor` and
|
|
162
|
+
* the session-start launcher when reporting status to the user.
|
|
163
|
+
*/
|
|
164
|
+
export function formatInjectionDriftStatus(report) {
|
|
165
|
+
switch (report.state) {
|
|
166
|
+
case 'no-file': return 'CLAUDE.md not found';
|
|
167
|
+
case 'no-marker': return 'CLAUDE.md has no moflo injection block';
|
|
168
|
+
case 'legacy-marker': return 'CLAUDE.md uses a legacy moflo marker pair';
|
|
169
|
+
case 'in-sync': return 'CLAUDE.md injection block matches reference';
|
|
170
|
+
case 'drifted': return 'CLAUDE.md injection block has drifted from reference';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=claudemd-injection.js.map
|
|
@@ -57,7 +57,12 @@ export function getReferenceHookBlock() {
|
|
|
57
57
|
{ matcher: '^Read$', hooks: [gateHook('check-before-read', 3000)] },
|
|
58
58
|
{
|
|
59
59
|
matcher: '^Bash$',
|
|
60
|
-
hooks: [
|
|
60
|
+
hooks: [
|
|
61
|
+
gateHook('check-dangerous-command', 2000),
|
|
62
|
+
gateHook('check-before-pr', 2000),
|
|
63
|
+
// #1132 — moved from PostToolUse so process.exit(2) actually blocks.
|
|
64
|
+
gateHook('check-bash-memory', 2000),
|
|
65
|
+
],
|
|
61
66
|
},
|
|
62
67
|
// #931 — TaskCreate REMINDER + namespace hint advisory at Agent-spawn time.
|
|
63
68
|
// Routed via gate-hook.mjs so HOOK_SESSION_ID is forwarded for per-actor
|
|
@@ -72,8 +77,9 @@ export function getReferenceHookBlock() {
|
|
|
72
77
|
{ matcher: '^Agent$', hooks: [handler('post-task', 5000)] },
|
|
73
78
|
{ matcher: '^TaskCreate$', hooks: [gateCjs('record-task-created', 2000)] },
|
|
74
79
|
{
|
|
80
|
+
// #1132 — check-bash-memory moved to PreToolUse (above).
|
|
75
81
|
matcher: '^Bash$',
|
|
76
|
-
hooks: [gateHook('
|
|
82
|
+
hooks: [gateHook('record-test-run', 2000)],
|
|
77
83
|
},
|
|
78
84
|
{ matcher: '^Skill$', hooks: [gateHook('record-skill-run', 2000)] },
|
|
79
85
|
{ matcher: '^mcp__moflo__memory_(search|retrieve|list|stats|store)$', hooks: [gateHook('record-memory-searched', 3000)] },
|
|
@@ -21,7 +21,7 @@ const BOOTSTRAP_JSON_REL = '.claude/helpers/subagent-bootstrap.json';
|
|
|
21
21
|
// Defense-in-depth copy of the canonical directive in subagent-bootstrap.json.
|
|
22
22
|
// Kept as a single-line literal so the parity test can verify it matches the
|
|
23
23
|
// JSON via plain substring containment.
|
|
24
|
-
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and
|
|
24
|
+
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, Read, and read-like Bash (cat/head/tail/grep/find/sed/awk and the Windows/PowerShell equivalents) calls until you do this. Pick the namespace by task: `guidance` for rules and conventions, `code-map` for file structure, `patterns` for proven solutions, `learnings` for past corrections, `tests` for test inventory. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol. When a search hit carries `navigation`, you MUST call mcp__moflo__memory_get_neighbors to traverse — calling mcp__moflo__memory_retrieve on every hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`.';
|
|
25
25
|
function loadDirective() {
|
|
26
26
|
const jsonPath = locateMofloRootPath(BOOTSTRAP_JSON_REL);
|
|
27
27
|
if (!jsonPath) {
|
package/dist/src/cli/version.js
CHANGED