moflo 4.10.4 → 4.10.6
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 +1 -1
- 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/lib/daemon-recycler.mjs +203 -0
- package/bin/session-start-launcher.mjs +173 -77
- package/dist/src/cli/commands/daemon.js +43 -18
- package/dist/src/cli/commands/retire.js +22 -17
- package/dist/src/cli/init/helpers-generator.js +36 -1
- package/dist/src/cli/init/settings-generator.js +5 -2
- package/dist/src/cli/services/hook-block-hash.js +9 -3
- 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
|
@@ -470,33 +470,58 @@ export async function killBackgroundDaemon(projectRoot) {
|
|
|
470
470
|
return false;
|
|
471
471
|
}
|
|
472
472
|
try {
|
|
473
|
-
//
|
|
473
|
+
// Platform-split shutdown. On Linux/macOS we try SIGTERM first so the
|
|
474
|
+
// daemon's shutdown handlers (sql.js flush, lock release) can run; force-
|
|
475
|
+
// kill only if it doesn't exit within the graceful window.
|
|
476
|
+
//
|
|
477
|
+
// On Windows there is no SIGTERM equivalent for our headless detached
|
|
478
|
+
// Node daemon — `taskkill /PID` (no /F) sends a window-close message
|
|
479
|
+
// that a non-GUI process can't receive, so it always fails with the
|
|
480
|
+
// visible error 'process can only be terminated forcefully'. The prior
|
|
481
|
+
// implementation invoked it anyway, ate the error in a bare catch, then
|
|
482
|
+
// slept 1s before escalating to /F. Skip the dead step: go straight to
|
|
483
|
+
// /F /T (tree-kill, in case a worker child outlived its parent) on Win.
|
|
484
|
+
//
|
|
485
|
+
// #1136: previously this used `setTimeout(1000)` after SIGTERM and
|
|
486
|
+
// skipped post-SIGKILL verification. Under load on a populated DB the
|
|
487
|
+
// daemon's shutdown handler (worker-daemon.ts:582 → scheduler.stop +
|
|
488
|
+
// saveState) can exceed 1s — SIGKILL hit mid-sql.js-dump and left
|
|
489
|
+
// torn pages in `.moflo/moflo.db` (the page-ref / rowid-order
|
|
490
|
+
// signature seen in the populated-consumer smoke). Bring the kill
|
|
491
|
+
// window in line with the launcher's stopDaemon
|
|
492
|
+
// (bin/session-start-launcher.mjs:425): 3s graceful poll + 1s force
|
|
493
|
+
// poll, both verifying liveness before returning.
|
|
474
494
|
if (process.platform === 'win32') {
|
|
475
|
-
// SIGTERM silently force-kills on Windows; use taskkill for clean shutdown
|
|
476
495
|
try {
|
|
477
|
-
execFileSync('taskkill', ['/PID', String(holderPid)], { windowsHide: true });
|
|
496
|
+
execFileSync('taskkill', ['/F', '/T', '/PID', String(holderPid)], { windowsHide: true });
|
|
478
497
|
}
|
|
479
498
|
catch {
|
|
480
|
-
//
|
|
499
|
+
// Already exiting / unreachable — verified via poll below.
|
|
500
|
+
}
|
|
501
|
+
const forceDeadline = Date.now() + 1000;
|
|
502
|
+
while (Date.now() < forceDeadline && isProcessRunning(holderPid)) {
|
|
503
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
481
504
|
}
|
|
482
505
|
}
|
|
483
506
|
else {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// Wait a moment then force kill if needed
|
|
487
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
488
|
-
try {
|
|
489
|
-
process.kill(holderPid, 0);
|
|
490
|
-
// Still alive, force kill
|
|
491
|
-
if (process.platform === 'win32') {
|
|
492
|
-
execFileSync('taskkill', ['/F', '/PID', String(holderPid)], { windowsHide: true });
|
|
507
|
+
try {
|
|
508
|
+
process.kill(holderPid, 'SIGTERM');
|
|
493
509
|
}
|
|
494
|
-
|
|
495
|
-
|
|
510
|
+
catch { /* already dead */ }
|
|
511
|
+
const gracefulDeadline = Date.now() + 3000;
|
|
512
|
+
while (Date.now() < gracefulDeadline && isProcessRunning(holderPid)) {
|
|
513
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
514
|
+
}
|
|
515
|
+
if (isProcessRunning(holderPid)) {
|
|
516
|
+
try {
|
|
517
|
+
process.kill(holderPid, 'SIGKILL');
|
|
518
|
+
}
|
|
519
|
+
catch { /* already dead */ }
|
|
520
|
+
const forceDeadline = Date.now() + 1000;
|
|
521
|
+
while (Date.now() < forceDeadline && isProcessRunning(holderPid)) {
|
|
522
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
523
|
+
}
|
|
496
524
|
}
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
// Process terminated
|
|
500
525
|
}
|
|
501
526
|
// Release lock
|
|
502
527
|
releaseDaemonLock(projectRoot, holderPid, true);
|
|
@@ -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',
|
|
@@ -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': {
|
|
@@ -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
|
},
|
|
@@ -358,7 +361,7 @@ function generateHooksConfig(config) {
|
|
|
358
361
|
{
|
|
359
362
|
type: 'command',
|
|
360
363
|
command: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/session-start-launcher.mjs"',
|
|
361
|
-
timeout:
|
|
364
|
+
timeout: 5000,
|
|
362
365
|
},
|
|
363
366
|
{
|
|
364
367
|
type: 'command',
|
|
@@ -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)] },
|
|
@@ -94,7 +100,7 @@ export function getReferenceHookBlock() {
|
|
|
94
100
|
],
|
|
95
101
|
SessionStart: [
|
|
96
102
|
{
|
|
97
|
-
hooks: [scriptHook('session-start-launcher.mjs',
|
|
103
|
+
hooks: [scriptHook('session-start-launcher.mjs', 5000), autoMemory('import', 8000)],
|
|
98
104
|
},
|
|
99
105
|
],
|
|
100
106
|
Stop: [
|
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.6",
|
|
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.10.
|
|
98
|
+
"moflo": "^4.10.5",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|