moflo 4.9.22 → 4.9.24
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/guidance/shipped/moflo-cli-reference.md +19 -16
- package/.claude/guidance/shipped/moflo-core-guidance.md +0 -2
- package/.claude/guidance/shipped/moflo-spell-runner.md +1 -0
- package/.claude/guidance/shipped/moflo-spell-scheduling.md +225 -0
- package/.claude/guidance/shipped/moflo-spell-troubleshooting.md +1 -0
- package/.claude/skills/fl/phases.md +67 -0
- package/.claude/skills/spell-schedule/SKILL.md +18 -5
- package/README.md +1 -1
- package/bin/index-guidance.mjs +32 -6
- package/bin/session-start-launcher.mjs +15 -8
- package/dist/src/cli/commands/daemon.js +13 -17
- package/dist/src/cli/commands/hooks.js +3 -6
- package/dist/src/cli/commands/spell-schedule.js +237 -49
- package/dist/src/cli/init/settings-generator.js +5 -6
- package/dist/src/cli/mcp-tools/memory-tools.js +16 -5
- package/dist/src/cli/memory/bridge-embedder.js +26 -6
- package/dist/src/cli/memory/bridge-entries.js +33 -15
- package/dist/src/cli/services/daemon-autostart-lifecycle.js +62 -0
- package/dist/src/cli/services/daemon-dashboard.js +192 -18
- package/dist/src/cli/services/daemon-readiness.js +19 -31
- package/dist/src/cli/services/ephemeral-namespace-purge.js +61 -33
- package/dist/src/cli/services/headless-worker-executor.js +7 -94
- package/dist/src/cli/services/worker-daemon.js +40 -66
- package/dist/src/cli/spells/core/runner.js +12 -0
- package/dist/src/cli/spells/scheduler/scheduler.js +24 -9
- package/dist/src/cli/spells/schema/validator.js +2 -1
- package/dist/src/cli/spells/schema/validators/top-level.js +18 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +4 -2
|
@@ -1,49 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Idempotent
|
|
2
|
+
* Idempotent session-start memory cleanup for moflo's memory DB
|
|
3
|
+
* (`.moflo/moflo.db`).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* they store internal moflo run-tracking — not user knowledge — and embedding
|
|
6
|
-
* them polluted the search index:
|
|
5
|
+
* Two passes run in a single sql.js open:
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - `test-bridge-fix` (single-row leftover from a one-off test)
|
|
7
|
+
* 1. **Hard-purge** namespaces in {@link PURGE_ON_SESSION_START_NAMESPACES} —
|
|
8
|
+
* `hive-mind`, `epic-state`, `test-bridge-fix`. These store internal
|
|
9
|
+
* run-tracking that does not need to survive a session restart. (#729)
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* 2. **Retention trim** the `tasklist` namespace down to the most recent
|
|
12
|
+
* {@link TASKLIST_RETENTION_CAP} rows. `tasklist` is the dashboard's
|
|
13
|
+
* "Flo Runs" tab data source (`daemon-dashboard.ts handleSpells`); the
|
|
14
|
+
* pre-#968 contract hard-purged it on every session start, leaving the tab
|
|
15
|
+
* permanently empty. Trim instead so users see recent history without
|
|
16
|
+
* unbounded growth.
|
|
17
|
+
*
|
|
18
|
+
* Both passes share the file open + final VACUUM + atomic write, so disk I/O
|
|
19
|
+
* is the same as before. Writes back to disk only when something changed.
|
|
17
20
|
*
|
|
18
21
|
* Lives in `services/` so it has no dependency on the CLI command machinery.
|
|
19
|
-
* That lets `bin/session-start-launcher.mjs` dynamic-import it and run
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* That lets `bin/session-start-launcher.mjs` dynamic-import it and run in
|
|
23
|
+
* foreground BEFORE long-lived sql.js consumers (MCP server, daemon) open
|
|
24
|
+
* the DB — sql.js dumps the whole snapshot on every flush and would
|
|
22
25
|
* otherwise clobber our cleanup (see #727's clobber-hazard analysis).
|
|
23
26
|
*
|
|
24
27
|
* @module cli/services/ephemeral-namespace-purge
|
|
25
28
|
*/
|
|
26
29
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
27
|
-
import {
|
|
30
|
+
import { PURGE_ON_SESSION_START_NAMESPACES, TASKLIST_RETENTION_CAP, } from '../memory/bridge-embedder.js';
|
|
28
31
|
import { mofloImport } from './moflo-require.js';
|
|
29
32
|
import { atomicWriteFileSync } from './atomic-file-write.js';
|
|
30
33
|
import { memoryDbPath } from './moflo-paths.js';
|
|
31
34
|
/**
|
|
32
|
-
* Hard-delete
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* purge never blocks
|
|
35
|
+
* Hard-delete rows in {@link PURGE_ON_SESSION_START_NAMESPACES} and trim the
|
|
36
|
+
* `tasklist` namespace to its retention cap, then VACUUM. Returns
|
|
37
|
+
* `{ purged: 0, trimmed: 0 }` on the happy path: no DB, sql.js unavailable,
|
|
38
|
+
* schema lacks `memory_entries`, or nothing to clean. Errors propagate to
|
|
39
|
+
* the caller (the launcher absorbs them so a failed purge never blocks
|
|
40
|
+
* session start).
|
|
37
41
|
*/
|
|
38
42
|
export async function purgeEphemeralNamespaces(options = {}) {
|
|
39
43
|
const fs = await import('fs');
|
|
40
44
|
const path = await import('path');
|
|
41
45
|
const dbPath = path.resolve(options.dbPath ?? memoryDbPath(process.cwd()));
|
|
42
46
|
if (!fs.existsSync(dbPath))
|
|
43
|
-
return { purged: 0 };
|
|
47
|
+
return { purged: 0, trimmed: 0 };
|
|
44
48
|
const initSqlJs = (await mofloImport('sql.js'))?.default;
|
|
45
49
|
if (!initSqlJs)
|
|
46
|
-
return { purged: 0 };
|
|
50
|
+
return { purged: 0, trimmed: 0 };
|
|
47
51
|
const SQL = await initSqlJs();
|
|
48
52
|
const buffer = fs.readFileSync(dbPath);
|
|
49
53
|
const db = new SQL.Database(buffer);
|
|
@@ -52,21 +56,45 @@ export async function purgeEphemeralNamespaces(options = {}) {
|
|
|
52
56
|
// a no-op so we don't VACUUM unrelated SQLite files.
|
|
53
57
|
const probe = db.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries' LIMIT 1`);
|
|
54
58
|
if (!probe[0]?.values?.[0])
|
|
55
|
-
return { purged: 0 };
|
|
56
|
-
|
|
59
|
+
return { purged: 0, trimmed: 0 };
|
|
60
|
+
// Single COUNT pass to gate both DELETEs — a clean DB is the steady
|
|
61
|
+
// state and we don't want two no-op DELETEs (with their query-planner
|
|
62
|
+
// overhead) on every session start.
|
|
63
|
+
const namespaces = Array.from(PURGE_ON_SESSION_START_NAMESPACES);
|
|
64
|
+
const cap = options.tasklistRetentionCap ?? TASKLIST_RETENTION_CAP;
|
|
57
65
|
const placeholders = namespaces.map(() => '?').join(', ');
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
const countRows = db.exec(`SELECT
|
|
67
|
+
(SELECT COUNT(*) FROM memory_entries WHERE namespace IN (${placeholders})) AS purgeable,
|
|
68
|
+
(SELECT COUNT(*) FROM memory_entries WHERE namespace = 'tasklist') AS tasklistTotal`, namespaces);
|
|
69
|
+
const counts = countRows[0]?.values?.[0] ?? [0, 0];
|
|
70
|
+
const purgeable = Number(counts[0] ?? 0);
|
|
71
|
+
const tasklistTotal = Number(counts[1] ?? 0);
|
|
72
|
+
let purged = 0;
|
|
73
|
+
if (purgeable > 0) {
|
|
74
|
+
db.run(`DELETE FROM memory_entries WHERE namespace IN (${placeholders})`, namespaces);
|
|
75
|
+
purged = db.getRowsModified?.() ?? 0;
|
|
76
|
+
}
|
|
77
|
+
let trimmed = 0;
|
|
78
|
+
if (tasklistTotal > cap) {
|
|
79
|
+
// Keep the newest `cap` rows by created_at, falling back to `id DESC`
|
|
80
|
+
// for legacy rows that predate the created_at-not-null schema (#728-era).
|
|
81
|
+
db.run(`DELETE FROM memory_entries
|
|
82
|
+
WHERE namespace = 'tasklist'
|
|
83
|
+
AND id NOT IN (
|
|
84
|
+
SELECT id FROM memory_entries
|
|
85
|
+
WHERE namespace = 'tasklist'
|
|
86
|
+
ORDER BY created_at DESC, id DESC
|
|
87
|
+
LIMIT ?
|
|
88
|
+
)`, [cap]);
|
|
89
|
+
trimmed = db.getRowsModified?.() ?? 0;
|
|
90
|
+
}
|
|
91
|
+
if (purged === 0 && trimmed === 0)
|
|
92
|
+
return { purged: 0, trimmed: 0 };
|
|
65
93
|
// VACUUM has to run outside any open transaction; sql.js auto-commits
|
|
66
94
|
// each `db.run`, so this is safe to chain.
|
|
67
95
|
db.run('VACUUM');
|
|
68
96
|
atomicWriteFileSync(dbPath, db.export());
|
|
69
|
-
return { purged };
|
|
97
|
+
return { purged, trimmed };
|
|
70
98
|
}
|
|
71
99
|
finally {
|
|
72
100
|
db.close();
|
|
@@ -2,21 +2,14 @@
|
|
|
2
2
|
* Headless Worker Executor
|
|
3
3
|
* Enables workers to invoke Claude Code in headless mode with configurable sandbox profiles.
|
|
4
4
|
*
|
|
5
|
-
* ADR-020: Headless Worker Integration Architecture
|
|
5
|
+
* ADR-020: Headless Worker Integration Architecture (#970 dropped the
|
|
6
|
+
* `audit`/`document`/`predict` workers — those entries from the original
|
|
7
|
+
* ADR are superseded; the rest still applies).
|
|
6
8
|
* - Integrates with CLAUDE_CODE_HEADLESS and CLAUDE_CODE_SANDBOX_MODE environment variables
|
|
7
9
|
* - Provides process pool for concurrent execution
|
|
8
10
|
* - Builds context from file glob patterns
|
|
9
11
|
* - Supports prompt templates and output parsing
|
|
10
12
|
* - Implements timeout and graceful error handling
|
|
11
|
-
*
|
|
12
|
-
* Key Features:
|
|
13
|
-
* - Process pool with configurable maxConcurrent
|
|
14
|
-
* - Context building from file glob patterns with caching
|
|
15
|
-
* - Prompt template system with context injection
|
|
16
|
-
* - Output parsing (text, json, markdown)
|
|
17
|
-
* - Timeout handling with graceful termination
|
|
18
|
-
* - Execution logging for debugging
|
|
19
|
-
* - Event emission for monitoring
|
|
20
13
|
*/
|
|
21
14
|
import { spawn, execSync } from 'child_process';
|
|
22
15
|
import { EventEmitter } from 'events';
|
|
@@ -27,17 +20,14 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
27
20
|
// Constants
|
|
28
21
|
// ============================================
|
|
29
22
|
/**
|
|
30
|
-
* Array of headless worker types for runtime checking
|
|
23
|
+
* Array of headless worker types for runtime checking.
|
|
31
24
|
*/
|
|
32
25
|
export const HEADLESS_WORKER_TYPES = [
|
|
33
|
-
'audit',
|
|
34
26
|
'optimize',
|
|
35
27
|
'testgaps',
|
|
36
|
-
'document',
|
|
37
28
|
'ultralearn',
|
|
38
29
|
'refactor',
|
|
39
30
|
'deepdive',
|
|
40
|
-
'predict',
|
|
41
31
|
];
|
|
42
32
|
/**
|
|
43
33
|
* Array of local worker types
|
|
@@ -57,37 +47,11 @@ const MODEL_IDS = {
|
|
|
57
47
|
haiku: 'claude-haiku-4-5-20251001',
|
|
58
48
|
};
|
|
59
49
|
/**
|
|
60
|
-
* Default headless worker configurations based on ADR-020
|
|
50
|
+
* Default headless worker configurations based on ADR-020 (the
|
|
51
|
+
* `audit`/`document`/`predict` entries from the original ADR were dropped
|
|
52
|
+
* in #970 — see worker-daemon.ts header for rationale).
|
|
61
53
|
*/
|
|
62
54
|
export const HEADLESS_WORKER_CONFIGS = {
|
|
63
|
-
audit: {
|
|
64
|
-
type: 'audit',
|
|
65
|
-
mode: 'headless',
|
|
66
|
-
intervalMs: 30 * 60 * 1000,
|
|
67
|
-
priority: 'critical',
|
|
68
|
-
description: 'AI-powered security analysis',
|
|
69
|
-
enabled: true,
|
|
70
|
-
headless: {
|
|
71
|
-
promptTemplate: `Analyze this codebase for security vulnerabilities:
|
|
72
|
-
- Check for hardcoded secrets (API keys, passwords)
|
|
73
|
-
- Identify SQL injection risks
|
|
74
|
-
- Find XSS vulnerabilities
|
|
75
|
-
- Check for insecure dependencies
|
|
76
|
-
- Identify authentication/authorization issues
|
|
77
|
-
|
|
78
|
-
Provide a JSON report with:
|
|
79
|
-
{
|
|
80
|
-
"vulnerabilities": [{ "severity": "high|medium|low", "file": "...", "line": N, "description": "..." }],
|
|
81
|
-
"riskScore": 0-100,
|
|
82
|
-
"recommendations": ["..."]
|
|
83
|
-
}`,
|
|
84
|
-
sandbox: 'strict',
|
|
85
|
-
model: 'haiku',
|
|
86
|
-
outputFormat: 'json',
|
|
87
|
-
contextPatterns: ['**/*.ts', '**/*.js', '**/.env*', '**/package.json'],
|
|
88
|
-
timeoutMs: 5 * 60 * 1000,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
55
|
optimize: {
|
|
92
56
|
type: 'optimize',
|
|
93
57
|
mode: 'headless',
|
|
@@ -134,29 +98,6 @@ For each gap, provide a test skeleton.`,
|
|
|
134
98
|
timeoutMs: 10 * 60 * 1000,
|
|
135
99
|
},
|
|
136
100
|
},
|
|
137
|
-
document: {
|
|
138
|
-
type: 'document',
|
|
139
|
-
mode: 'headless',
|
|
140
|
-
intervalMs: 120 * 60 * 1000,
|
|
141
|
-
priority: 'low',
|
|
142
|
-
description: 'AI documentation generation',
|
|
143
|
-
enabled: false,
|
|
144
|
-
headless: {
|
|
145
|
-
promptTemplate: `Generate documentation for undocumented code:
|
|
146
|
-
- Add JSDoc comments to functions
|
|
147
|
-
- Create README sections for modules
|
|
148
|
-
- Document API endpoints
|
|
149
|
-
- Add inline comments for complex logic
|
|
150
|
-
- Generate usage examples
|
|
151
|
-
|
|
152
|
-
Focus on public APIs and exported functions.`,
|
|
153
|
-
sandbox: 'permissive',
|
|
154
|
-
model: 'haiku',
|
|
155
|
-
outputFormat: 'markdown',
|
|
156
|
-
contextPatterns: ['src/**/*.ts'],
|
|
157
|
-
timeoutMs: 10 * 60 * 1000,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
101
|
ultralearn: {
|
|
161
102
|
type: 'ultralearn',
|
|
162
103
|
mode: 'headless',
|
|
@@ -232,34 +173,6 @@ Provide comprehensive report.`,
|
|
|
232
173
|
timeoutMs: 15 * 60 * 1000,
|
|
233
174
|
},
|
|
234
175
|
},
|
|
235
|
-
predict: {
|
|
236
|
-
type: 'predict',
|
|
237
|
-
mode: 'headless',
|
|
238
|
-
intervalMs: 10 * 60 * 1000,
|
|
239
|
-
priority: 'low',
|
|
240
|
-
description: 'Predictive preloading',
|
|
241
|
-
enabled: false,
|
|
242
|
-
headless: {
|
|
243
|
-
promptTemplate: `Based on recent activity, predict what the developer needs:
|
|
244
|
-
- Files likely to be edited next
|
|
245
|
-
- Tests that should be run
|
|
246
|
-
- Documentation to reference
|
|
247
|
-
- Dependencies to check
|
|
248
|
-
|
|
249
|
-
Provide preload suggestions as JSON:
|
|
250
|
-
{
|
|
251
|
-
"filesToPreload": ["..."],
|
|
252
|
-
"testsToRun": ["..."],
|
|
253
|
-
"docsToReference": ["..."],
|
|
254
|
-
"confidence": 0.0-1.0
|
|
255
|
-
}`,
|
|
256
|
-
sandbox: 'strict',
|
|
257
|
-
model: 'haiku',
|
|
258
|
-
outputFormat: 'json',
|
|
259
|
-
contextPatterns: ['.moflo/metrics/*.json'],
|
|
260
|
-
timeoutMs: 2 * 60 * 1000,
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
176
|
};
|
|
264
177
|
/**
|
|
265
178
|
* Local worker configurations
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Worker Daemon Service
|
|
3
|
-
* Node.js-based background worker system that auto-runs like shell daemons
|
|
3
|
+
* Node.js-based background worker system that auto-runs like shell daemons.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - map: Codebase mapping (
|
|
7
|
-
* - audit: Security analysis (10 min interval)
|
|
5
|
+
* Default workers:
|
|
6
|
+
* - map: Codebase mapping (15 min interval)
|
|
8
7
|
* - optimize: Performance optimization (15 min interval)
|
|
9
8
|
* - consolidate: Memory consolidation (30 min interval)
|
|
10
9
|
* - testgaps: Test coverage analysis (20 min interval)
|
|
10
|
+
*
|
|
11
|
+
* Manual-trigger-only workers (disabled by default, no scheduled run):
|
|
12
|
+
* ultralearn, refactor, deepdive, benchmark, preload.
|
|
13
|
+
*
|
|
14
|
+
* The `audit`, `predict`, and `document` workers were removed in #970 —
|
|
15
|
+
* they were default-disabled with no surfacing layer for findings, and the
|
|
16
|
+
* dashboard rendered them as "disabled" rows that read as broken. If a
|
|
17
|
+
* security or doc scan returns it should land as an opt-in `flo doctor`
|
|
18
|
+
* one-shot with a real findings UI, not as a recurring background worker.
|
|
11
19
|
*/
|
|
12
20
|
import { EventEmitter } from 'events';
|
|
13
21
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
@@ -21,18 +29,32 @@ import { attachSignalHandlers } from '../shared/resilience/signal-handlers.js';
|
|
|
21
29
|
import { calculateDelay } from '../production/retry.js';
|
|
22
30
|
import { CircuitBreaker } from '../production/circuit-breaker.js';
|
|
23
31
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
32
|
+
/**
|
|
33
|
+
* Runtime allow-list of known {@link WorkerType} values. Used by
|
|
34
|
+
* `initializeWorkerStates` to silently drop entries from a stale
|
|
35
|
+
* `daemon-state.json` after a worker is removed from the union (#970).
|
|
36
|
+
* Keep in sync with the `WorkerType` definition above.
|
|
37
|
+
*/
|
|
38
|
+
const KNOWN_WORKER_TYPES = new Set([
|
|
39
|
+
'ultralearn',
|
|
40
|
+
'optimize',
|
|
41
|
+
'consolidate',
|
|
42
|
+
'map',
|
|
43
|
+
'preload',
|
|
44
|
+
'deepdive',
|
|
45
|
+
'refactor',
|
|
46
|
+
'benchmark',
|
|
47
|
+
'testgaps',
|
|
48
|
+
]);
|
|
49
|
+
function isKnownWorkerType(value) {
|
|
50
|
+
return typeof value === 'string' && KNOWN_WORKER_TYPES.has(value);
|
|
51
|
+
}
|
|
24
52
|
// Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
|
|
25
53
|
const DEFAULT_WORKERS = [
|
|
26
54
|
{ type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
|
|
27
|
-
// Default-disabled until the perf regression in #631 is remediated. The
|
|
28
|
-
// worker averages 238 s/run on real installs, saturating cores back-to-back
|
|
29
|
-
// when scheduled at the 10-minute interval. Re-enable here when #631 ships.
|
|
30
|
-
{ type: 'audit', intervalMs: 10 * 60 * 1000, offsetMs: 2 * 60 * 1000, priority: 'critical', description: 'Security analysis', enabled: false },
|
|
31
55
|
{ type: 'optimize', intervalMs: 15 * 60 * 1000, offsetMs: 4 * 60 * 1000, priority: 'high', description: 'Performance optimization', enabled: true },
|
|
32
56
|
{ type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory consolidation', enabled: true },
|
|
33
57
|
{ type: 'testgaps', intervalMs: 20 * 60 * 1000, offsetMs: 8 * 60 * 1000, priority: 'normal', description: 'Test coverage analysis', enabled: true },
|
|
34
|
-
{ type: 'predict', intervalMs: 10 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Predictive preloading', enabled: false },
|
|
35
|
-
{ type: 'document', intervalMs: 60 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Auto-documentation', enabled: false },
|
|
36
58
|
];
|
|
37
59
|
// Worker timeout (5 minutes max per worker)
|
|
38
60
|
const DEFAULT_WORKER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -326,9 +348,16 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
326
348
|
if (typeof saved.config?.workerTimeoutMs === 'number' && saved.config.workerTimeoutMs > 0) {
|
|
327
349
|
this.config.workerTimeoutMs = saved.config.workerTimeoutMs;
|
|
328
350
|
}
|
|
329
|
-
// Restore worker runtime states (runCount, successCount, etc.)
|
|
351
|
+
// Restore worker runtime states (runCount, successCount, etc.).
|
|
352
|
+
// Unknown worker types (left over from a prior moflo version where
|
|
353
|
+
// `audit`/`predict`/`document` existed) are silently dropped — see
|
|
354
|
+
// KNOWN_WORKER_TYPES + #970 — so consumers upgrading don't crash on
|
|
355
|
+
// stale state, and the orphan entries don't get re-persisted on the
|
|
356
|
+
// next saveState.
|
|
330
357
|
if (saved.workers) {
|
|
331
358
|
for (const [type, state] of Object.entries(saved.workers)) {
|
|
359
|
+
if (!isKnownWorkerType(type))
|
|
360
|
+
continue;
|
|
332
361
|
const savedState = state;
|
|
333
362
|
const lastRunValue = savedState.lastRun;
|
|
334
363
|
const restoredState = {
|
|
@@ -749,18 +778,12 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
749
778
|
switch (workerConfig.type) {
|
|
750
779
|
case 'map':
|
|
751
780
|
return this.runMapWorker();
|
|
752
|
-
case 'audit':
|
|
753
|
-
return this.runAuditWorkerLocal();
|
|
754
781
|
case 'optimize':
|
|
755
782
|
return this.runOptimizeWorkerLocal();
|
|
756
783
|
case 'consolidate':
|
|
757
784
|
return this.runConsolidateWorker();
|
|
758
785
|
case 'testgaps':
|
|
759
786
|
return this.runTestGapsWorkerLocal();
|
|
760
|
-
case 'predict':
|
|
761
|
-
return this.runPredictWorkerLocal();
|
|
762
|
-
case 'document':
|
|
763
|
-
return this.runDocumentWorkerLocal();
|
|
764
787
|
case 'ultralearn':
|
|
765
788
|
return this.runUltralearnWorkerLocal();
|
|
766
789
|
case 'refactor':
|
|
@@ -797,31 +820,6 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
797
820
|
writeFileSync(metricsFile, JSON.stringify(map, null, 2));
|
|
798
821
|
return map;
|
|
799
822
|
}
|
|
800
|
-
/**
|
|
801
|
-
* Local audit worker (fallback when headless unavailable)
|
|
802
|
-
*/
|
|
803
|
-
async runAuditWorkerLocal() {
|
|
804
|
-
// Basic security checks
|
|
805
|
-
const auditFile = join(this.projectRoot, '.moflo', 'metrics', 'security-audit.json');
|
|
806
|
-
const metricsDir = join(this.projectRoot, '.moflo', 'metrics');
|
|
807
|
-
if (!existsSync(metricsDir)) {
|
|
808
|
-
mkdirSync(metricsDir, { recursive: true });
|
|
809
|
-
}
|
|
810
|
-
const audit = {
|
|
811
|
-
timestamp: new Date().toISOString(),
|
|
812
|
-
mode: 'local',
|
|
813
|
-
checks: {
|
|
814
|
-
envFilesProtected: !existsSync(join(this.projectRoot, '.env.local')),
|
|
815
|
-
gitIgnoreExists: existsSync(join(this.projectRoot, '.gitignore')),
|
|
816
|
-
noHardcodedSecrets: true, // Would need actual scanning
|
|
817
|
-
},
|
|
818
|
-
riskLevel: 'low',
|
|
819
|
-
recommendations: [],
|
|
820
|
-
note: 'Install Claude Code CLI for AI-powered security analysis',
|
|
821
|
-
};
|
|
822
|
-
writeFileSync(auditFile, JSON.stringify(audit, null, 2));
|
|
823
|
-
return audit;
|
|
824
|
-
}
|
|
825
823
|
/**
|
|
826
824
|
* Local optimize worker (fallback when headless unavailable)
|
|
827
825
|
*/
|
|
@@ -883,30 +881,6 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
883
881
|
writeFileSync(testGapsFile, JSON.stringify(result, null, 2));
|
|
884
882
|
return result;
|
|
885
883
|
}
|
|
886
|
-
/**
|
|
887
|
-
* Local predict worker (fallback when headless unavailable)
|
|
888
|
-
*/
|
|
889
|
-
async runPredictWorkerLocal() {
|
|
890
|
-
return {
|
|
891
|
-
timestamp: new Date().toISOString(),
|
|
892
|
-
mode: 'local',
|
|
893
|
-
predictions: [],
|
|
894
|
-
preloaded: [],
|
|
895
|
-
note: 'Install Claude Code CLI for AI-powered predictions',
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Local document worker (fallback when headless unavailable)
|
|
900
|
-
*/
|
|
901
|
-
async runDocumentWorkerLocal() {
|
|
902
|
-
return {
|
|
903
|
-
timestamp: new Date().toISOString(),
|
|
904
|
-
mode: 'local',
|
|
905
|
-
filesDocumented: 0,
|
|
906
|
-
suggestedDocs: [],
|
|
907
|
-
note: 'Install Claude Code CLI for AI-powered documentation generation',
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
884
|
/**
|
|
911
885
|
* Local ultralearn worker (fallback when headless unavailable)
|
|
912
886
|
*/
|
|
@@ -150,6 +150,18 @@ export class SpellCaster {
|
|
|
150
150
|
message: err.message,
|
|
151
151
|
}], definition.name);
|
|
152
152
|
}
|
|
153
|
+
// Per-spell sandbox requirement (#878) — "more strict wins": the spell
|
|
154
|
+
// can opt in to sandboxing even when the global config is off, and the
|
|
155
|
+
// runner refuses to cast if no OS sandbox is active.
|
|
156
|
+
if (definition.sandbox?.required === true && !effectiveSandbox.useOsSandbox) {
|
|
157
|
+
return this.failureResult(spellId, startTime, [{
|
|
158
|
+
code: 'SANDBOX_REQUIRED',
|
|
159
|
+
message: `Spell "${definition.name}" requires an OS sandbox but none is active ` +
|
|
160
|
+
`(${effectiveSandbox.displayStatus}). Enable sandboxing by setting ` +
|
|
161
|
+
`\`sandbox.enabled: true\` in moflo.yaml (and \`sandbox.tier: auto\` ` +
|
|
162
|
+
`or \`full\`), or remove \`sandbox.required: true\` from the spell.`,
|
|
163
|
+
}], definition.name);
|
|
164
|
+
}
|
|
153
165
|
return this.executeSteps(definition, resolvedArgs, spellId, options, startTime, effectiveSandbox);
|
|
154
166
|
}
|
|
155
167
|
async dryRun(definition, resolvedArgs, options = {}) {
|
|
@@ -369,15 +369,30 @@ export class SpellScheduler {
|
|
|
369
369
|
duration: completedAt - now,
|
|
370
370
|
};
|
|
371
371
|
await this.memory.write(NAMESPACE_EXECUTIONS, executionId, finalRecord);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
:
|
|
379
|
-
|
|
380
|
-
|
|
372
|
+
// SANDBOX_REQUIRED (#878) is a configuration mismatch, not a runtime
|
|
373
|
+
// failure — surface it as a skip so dashboards/oncall don't page on a
|
|
374
|
+
// missing sandbox the user can resolve in moflo.yaml.
|
|
375
|
+
const sandboxRequiredErr = result.errors.find(e => e.code === 'SANDBOX_REQUIRED');
|
|
376
|
+
if (!result.success && sandboxRequiredErr) {
|
|
377
|
+
this.emit({
|
|
378
|
+
type: 'schedule:skipped',
|
|
379
|
+
scheduleId: schedule.id,
|
|
380
|
+
spellName: schedule.spellName,
|
|
381
|
+
message: sandboxRequiredErr.message,
|
|
382
|
+
timestamp: completedAt,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
this.emit({
|
|
387
|
+
type: result.success ? 'schedule:completed' : 'schedule:failed',
|
|
388
|
+
scheduleId: schedule.id,
|
|
389
|
+
spellName: schedule.spellName,
|
|
390
|
+
message: result.success
|
|
391
|
+
? `Completed in ${finalRecord.duration}ms`
|
|
392
|
+
: `Failed: ${finalRecord.error}`,
|
|
393
|
+
timestamp: completedAt,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
381
396
|
}
|
|
382
397
|
catch (err) {
|
|
383
398
|
const completedAt = Date.now();
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { isValidMofloLevel } from '../core/capability-validator.js';
|
|
17
17
|
import { MOFLO_LEVEL_ORDER } from '../types/step-command.types.js';
|
|
18
18
|
import { validateSchedule } from '../scheduler/cron-parser.js';
|
|
19
|
-
import { validateTopLevel, validateArguments, matchesArgumentType } from './validators/top-level.js';
|
|
19
|
+
import { validateTopLevel, validateArguments, matchesArgumentType, validateSandbox } from './validators/top-level.js';
|
|
20
20
|
import { validateSteps } from './validators/steps.js';
|
|
21
21
|
import { validatePrerequisites } from './validators/prerequisites.js';
|
|
22
22
|
import { validateVariableReferences } from './validators/references.js';
|
|
@@ -27,6 +27,7 @@ import { detectCircularJumps } from './validators/jumps.js';
|
|
|
27
27
|
export function validateSpellDefinition(def, options) {
|
|
28
28
|
const errors = [];
|
|
29
29
|
validateTopLevel(def, errors);
|
|
30
|
+
validateSandbox(def, errors);
|
|
30
31
|
if (def.mofloLevel !== undefined && !isValidMofloLevel(def.mofloLevel)) {
|
|
31
32
|
errors.push({
|
|
32
33
|
path: 'mofloLevel',
|
|
@@ -58,6 +58,24 @@ export function validateArguments(args, errors) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate the optional `sandbox` block on a spell definition.
|
|
63
|
+
* Accepts: missing, `{}`, `{ required: boolean }`.
|
|
64
|
+
* Rejects: non-object value, non-boolean `required`.
|
|
65
|
+
*/
|
|
66
|
+
export function validateSandbox(def, errors) {
|
|
67
|
+
const sandbox = def.sandbox;
|
|
68
|
+
if (sandbox === undefined)
|
|
69
|
+
return;
|
|
70
|
+
if (sandbox === null || typeof sandbox !== 'object' || Array.isArray(sandbox)) {
|
|
71
|
+
errors.push({ path: 'sandbox', message: 'sandbox must be an object' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { required } = sandbox;
|
|
75
|
+
if (required !== undefined && typeof required !== 'boolean') {
|
|
76
|
+
errors.push({ path: 'sandbox.required', message: 'sandbox.required must be a boolean' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
/** Check whether a value matches a declared ArgumentType. */
|
|
62
80
|
export function matchesArgumentType(value, type) {
|
|
63
81
|
switch (type) {
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.24",
|
|
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"src/cli/spells/definitions/**/*.yaml",
|
|
30
30
|
"!**/*.test.js",
|
|
31
31
|
"!**/*.spec.js",
|
|
32
|
+
"!**/*.perf.js",
|
|
32
33
|
"!**/__tests__/**",
|
|
33
34
|
".claude/commands/**/*.md",
|
|
34
35
|
".claude/agents/**/*.md",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"test:ui": "vitest --ui",
|
|
55
56
|
"test:smoke": "node harness/consumer-smoke/run.mjs",
|
|
56
57
|
"test:smoke:populated": "node harness/consumer-smoke/run-populated.mjs",
|
|
58
|
+
"bench": "vitest run --config vitest.bench.config.ts",
|
|
57
59
|
"lint": "eslint src/ bin/ .claude/scripts/ --ext .ts,.tsx,.mts,.cts,.js,.mjs,.cjs --max-warnings 0",
|
|
58
60
|
"security:audit": "npm audit --omit=dev --audit-level high",
|
|
59
61
|
"security:fix": "npm audit fix",
|
|
@@ -82,7 +84,7 @@
|
|
|
82
84
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
83
85
|
"@typescript-eslint/parser": "^7.18.0",
|
|
84
86
|
"eslint": "^8.0.0",
|
|
85
|
-
"moflo": "^4.9.
|
|
87
|
+
"moflo": "^4.9.23",
|
|
86
88
|
"tsx": "^4.21.0",
|
|
87
89
|
"typescript": "^5.9.3",
|
|
88
90
|
"vitest": "^4.0.0"
|