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
|
@@ -19,7 +19,7 @@ const startCommand = {
|
|
|
19
19
|
name: 'start',
|
|
20
20
|
description: 'Start the worker daemon with all enabled background workers',
|
|
21
21
|
options: [
|
|
22
|
-
{ name: 'workers', short: 'w', type: 'string', description: 'Comma-separated list of workers to enable (default: map,
|
|
22
|
+
{ name: 'workers', short: 'w', type: 'string', description: 'Comma-separated list of workers to enable (default: map,optimize,consolidate,testgaps)' },
|
|
23
23
|
{ name: 'quiet', short: 'Q', type: 'boolean', description: 'Suppress output' },
|
|
24
24
|
{ name: 'background', short: 'b', type: 'boolean', description: 'Run daemon in background (detached process)', default: true },
|
|
25
25
|
{ name: 'foreground', short: 'f', type: 'boolean', description: 'Run daemon in foreground (blocks terminal)' },
|
|
@@ -33,7 +33,7 @@ const startCommand = {
|
|
|
33
33
|
examples: [
|
|
34
34
|
{ command: 'claude-flow daemon start', description: 'Start daemon in background (default)' },
|
|
35
35
|
{ command: 'claude-flow daemon start --foreground', description: 'Start in foreground (blocks terminal)' },
|
|
36
|
-
{ command: 'claude-flow daemon start -w map,
|
|
36
|
+
{ command: 'claude-flow daemon start -w map,optimize', description: 'Start with specific workers' },
|
|
37
37
|
{ command: 'claude-flow daemon start --headless --sandbox strict', description: 'Start with headless workers in strict sandbox' },
|
|
38
38
|
],
|
|
39
39
|
action: async (ctx) => {
|
|
@@ -128,7 +128,7 @@ const startCommand = {
|
|
|
128
128
|
`Max Concurrent: ${status.config.maxConcurrent}`,
|
|
129
129
|
`Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
|
|
130
130
|
`Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
|
|
131
|
-
...(dashboard ? [`
|
|
131
|
+
...(dashboard ? [`The Luminarium: http://localhost:${dashboard.port}`] : []),
|
|
132
132
|
].join('\n'), 'Daemon Status');
|
|
133
133
|
output.writeln();
|
|
134
134
|
output.writeln(output.bold('Scheduled Workers'));
|
|
@@ -215,10 +215,10 @@ async function attachDaemonServices(daemon, opts) {
|
|
|
215
215
|
schedulerEnabledInConfig: schedulerConfig.enabled,
|
|
216
216
|
});
|
|
217
217
|
if (opts.verbose)
|
|
218
|
-
output.printSuccess(`
|
|
218
|
+
output.printSuccess(`The Luminarium: http://localhost:${dashboard.port}`);
|
|
219
219
|
}
|
|
220
220
|
catch (err) {
|
|
221
|
-
logWarn(`
|
|
221
|
+
logWarn(`The Luminarium failed to start: ${errorDetail(err)}`);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
if (!schedulerConfig.enabled) {
|
|
@@ -378,7 +378,7 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
|
|
|
378
378
|
if (!quiet) {
|
|
379
379
|
output.printSuccess(`Daemon started in background (PID: ${pid})`);
|
|
380
380
|
if (!noDashboard) {
|
|
381
|
-
output.printInfo(`
|
|
381
|
+
output.printInfo(`The Luminarium: http://localhost:${dashboardPort ?? DEFAULT_DASHBOARD_PORT}`);
|
|
382
382
|
}
|
|
383
383
|
output.printInfo(`Logs: ${logFile}`);
|
|
384
384
|
output.printInfo(`Stop with: claude-flow daemon stop`);
|
|
@@ -624,15 +624,14 @@ const triggerCommand = {
|
|
|
624
624
|
],
|
|
625
625
|
examples: [
|
|
626
626
|
{ command: 'claude-flow daemon trigger -w map', description: 'Trigger the map worker' },
|
|
627
|
-
{ command: 'claude-flow daemon trigger -w
|
|
628
|
-
{ command: 'claude-flow daemon trigger -w audit --headless', description: 'Trigger audit in headless sandbox' },
|
|
627
|
+
{ command: 'claude-flow daemon trigger -w optimize --headless', description: 'Trigger optimize in headless sandbox' },
|
|
629
628
|
],
|
|
630
629
|
action: async (ctx) => {
|
|
631
630
|
const workerType = ctx.flags.worker;
|
|
632
631
|
if (!workerType) {
|
|
633
632
|
output.printError('Worker type is required. Use --worker or -w flag.');
|
|
634
633
|
output.writeln();
|
|
635
|
-
output.writeln('Available workers: map,
|
|
634
|
+
output.writeln('Available workers: map, optimize, consolidate, testgaps, ultralearn, refactor, benchmark, deepdive, preload');
|
|
636
635
|
return { success: false, exitCode: 1 };
|
|
637
636
|
}
|
|
638
637
|
try {
|
|
@@ -668,8 +667,8 @@ const enableCommand = {
|
|
|
668
667
|
{ name: 'disable', short: 'd', type: 'boolean', description: 'Disable instead of enable' },
|
|
669
668
|
],
|
|
670
669
|
examples: [
|
|
671
|
-
{ command: 'claude-flow daemon enable -w
|
|
672
|
-
{ command: 'claude-flow daemon enable -w
|
|
670
|
+
{ command: 'claude-flow daemon enable -w testgaps', description: 'Enable testgaps worker' },
|
|
671
|
+
{ command: 'claude-flow daemon enable -w refactor --disable', description: 'Disable refactor worker' },
|
|
673
672
|
],
|
|
674
673
|
action: async (ctx) => {
|
|
675
674
|
const workerType = ctx.flags.worker;
|
|
@@ -799,7 +798,7 @@ export const daemonCommand = {
|
|
|
799
798
|
{ command: 'claude-flow daemon start --headless', description: 'Start with headless workers (E2B sandbox)' },
|
|
800
799
|
{ command: 'claude-flow daemon status', description: 'Check daemon status' },
|
|
801
800
|
{ command: 'claude-flow daemon stop', description: 'Stop the daemon' },
|
|
802
|
-
{ command: 'claude-flow daemon trigger -w
|
|
801
|
+
{ command: 'claude-flow daemon trigger -w optimize', description: 'Run the optimize worker' },
|
|
803
802
|
{ command: 'claude-flow daemon install', description: 'Register as OS login service' },
|
|
804
803
|
{ command: 'claude-flow daemon uninstall', description: 'Remove OS login service' },
|
|
805
804
|
],
|
|
@@ -808,7 +807,7 @@ export const daemonCommand = {
|
|
|
808
807
|
output.writeln(output.bold('MoFlo Daemon - Background Task Management'));
|
|
809
808
|
output.writeln();
|
|
810
809
|
output.writeln('Node.js-based background worker system that auto-runs like shell daemons.');
|
|
811
|
-
output.writeln('Manages
|
|
810
|
+
output.writeln('Manages 9 specialized workers for continuous optimization and monitoring.');
|
|
812
811
|
output.writeln();
|
|
813
812
|
output.writeln(output.bold('Headless Mode'));
|
|
814
813
|
output.writeln('Workers can run in headless mode using E2B sandboxes for isolated execution.');
|
|
@@ -816,13 +815,10 @@ export const daemonCommand = {
|
|
|
816
815
|
output.writeln();
|
|
817
816
|
output.writeln(output.bold('Available Workers'));
|
|
818
817
|
output.printList([
|
|
819
|
-
`${output.highlight('map')} - Codebase mapping (
|
|
820
|
-
`${output.highlight('audit')} - Security analysis (10 min interval)`,
|
|
818
|
+
`${output.highlight('map')} - Codebase mapping (15 min interval)`,
|
|
821
819
|
`${output.highlight('optimize')} - Performance optimization (15 min interval)`,
|
|
822
820
|
`${output.highlight('consolidate')} - Memory consolidation (30 min interval)`,
|
|
823
821
|
`${output.highlight('testgaps')} - Test coverage analysis (20 min interval)`,
|
|
824
|
-
`${output.highlight('predict')} - Predictive preloading (2 min, disabled by default)`,
|
|
825
|
-
`${output.highlight('document')} - Auto-documentation (60 min, disabled by default)`,
|
|
826
822
|
`${output.highlight('ultralearn')} - Deep knowledge acquisition (manual trigger)`,
|
|
827
823
|
`${output.highlight('refactor')} - Code refactoring suggestions (manual trigger)`,
|
|
828
824
|
`${output.highlight('benchmark')} - Performance benchmarking (manual trigger)`,
|
|
@@ -1766,14 +1766,14 @@ const workerDispatchCommand = {
|
|
|
1766
1766
|
name: 'dispatch',
|
|
1767
1767
|
description: 'Dispatch a background worker for analysis/optimization',
|
|
1768
1768
|
options: [
|
|
1769
|
-
{ name: 'trigger', short: 't', type: 'string', description: 'Worker type (ultralearn, optimize,
|
|
1769
|
+
{ name: 'trigger', short: 't', type: 'string', description: 'Worker type (ultralearn, optimize, map, testgaps, etc.)', required: true },
|
|
1770
1770
|
{ name: 'context', short: 'c', type: 'string', description: 'Context for the worker (file path, topic)' },
|
|
1771
1771
|
{ name: 'priority', short: 'p', type: 'string', description: 'Priority (low, normal, high, critical)' },
|
|
1772
1772
|
{ name: 'sync', short: 's', type: 'boolean', description: 'Wait for completion (synchronous)' },
|
|
1773
1773
|
],
|
|
1774
1774
|
examples: [
|
|
1775
1775
|
{ command: 'claude-flow hooks worker dispatch -t optimize -c src/', description: 'Dispatch optimize worker' },
|
|
1776
|
-
{ command: 'claude-flow hooks worker dispatch -t
|
|
1776
|
+
{ command: 'claude-flow hooks worker dispatch -t deepdive -p high', description: 'Deep code analysis with high priority' },
|
|
1777
1777
|
{ command: 'claude-flow hooks worker dispatch -t testgaps --sync', description: 'Test coverage analysis (sync)' },
|
|
1778
1778
|
],
|
|
1779
1779
|
action: async (ctx) => {
|
|
@@ -1783,7 +1783,7 @@ const workerDispatchCommand = {
|
|
|
1783
1783
|
const background = !ctx.flags['sync'];
|
|
1784
1784
|
if (!trigger) {
|
|
1785
1785
|
output.printError('--trigger is required');
|
|
1786
|
-
output.writeln('Available triggers: ultralearn, optimize, consolidate,
|
|
1786
|
+
output.writeln('Available triggers: ultralearn, optimize, consolidate, map, preload, deepdive, refactor, benchmark, testgaps');
|
|
1787
1787
|
return { success: false, exitCode: 1 };
|
|
1788
1788
|
}
|
|
1789
1789
|
const spinner = output.createSpinner({ text: `Dispatching ${trigger} worker...`, spinner: 'dots' });
|
|
@@ -2485,12 +2485,9 @@ const workerCommand = {
|
|
|
2485
2485
|
`${output.highlight('ultralearn')} - Deep knowledge acquisition`,
|
|
2486
2486
|
`${output.highlight('optimize')} - Performance optimization`,
|
|
2487
2487
|
`${output.highlight('consolidate')} - Memory consolidation`,
|
|
2488
|
-
`${output.highlight('predict')} - Predictive preloading`,
|
|
2489
|
-
`${output.highlight('audit')} - Security analysis (critical)`,
|
|
2490
2488
|
`${output.highlight('map')} - Codebase mapping`,
|
|
2491
2489
|
`${output.highlight('preload')} - Resource preloading`,
|
|
2492
2490
|
`${output.highlight('deepdive')} - Deep code analysis`,
|
|
2493
|
-
`${output.highlight('document')} - Auto-documentation`,
|
|
2494
2491
|
`${output.highlight('refactor')} - Refactoring suggestions`,
|
|
2495
2492
|
`${output.highlight('benchmark')} - Performance benchmarks`,
|
|
2496
2493
|
`${output.highlight('testgaps')} - Test coverage analysis`,
|
|
@@ -15,8 +15,58 @@ import { callMCPTool } from '../mcp-client.js';
|
|
|
15
15
|
import { TOOL_MEMORY_STORE, TOOL_MEMORY_LIST, TOOL_MEMORY_RETRIEVE } from '../mcp-tools/tool-names.js';
|
|
16
16
|
import { handleMCPError } from '../services/cli-formatters.js';
|
|
17
17
|
import { ensureDaemonForScheduling } from '../services/daemon-readiness.js';
|
|
18
|
+
import { reconcileDaemonAutostart } from '../services/daemon-autostart-lifecycle.js';
|
|
19
|
+
import { isDaemonInstalled } from '../services/daemon-service.js';
|
|
18
20
|
import { validateSchedule, computeNextRun } from '../spells/scheduler/cron-parser.js';
|
|
19
21
|
const NAMESPACE_SCHEDULES = 'scheduled-spells';
|
|
22
|
+
const NAMESPACE_EXECUTIONS = 'schedule-executions';
|
|
23
|
+
const DEFAULT_EXECUTIONS_LIMIT = 10;
|
|
24
|
+
const MAX_NAMESPACE_FETCH = 1000;
|
|
25
|
+
async function loadNamespaceValues(namespace, limit = MAX_NAMESPACE_FETCH) {
|
|
26
|
+
const listResult = await callMCPTool(TOOL_MEMORY_LIST, {
|
|
27
|
+
namespace,
|
|
28
|
+
limit,
|
|
29
|
+
});
|
|
30
|
+
const entries = listResult.entries ?? [];
|
|
31
|
+
// Parallelize retrieves — serial-await over N entries was the cost the
|
|
32
|
+
// reconcile-after-mutation path was paying on every create/cancel.
|
|
33
|
+
const fetched = await Promise.all(entries.map(async (entry) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await callMCPTool(TOOL_MEMORY_RETRIEVE, {
|
|
36
|
+
namespace,
|
|
37
|
+
key: entry.key,
|
|
38
|
+
});
|
|
39
|
+
if (result.value === null || result.value === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
const parsed = typeof result.value === 'string'
|
|
42
|
+
? JSON.parse(result.value)
|
|
43
|
+
: result.value;
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
output.printWarning(`Skipped malformed entry: ${entry.key}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
return fetched.filter((v) => v !== null);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Count enabled schedules in the `scheduled-spells` namespace. Drives the
|
|
55
|
+
* autostart reconcile after a create/cancel — see #960/#961.
|
|
56
|
+
*/
|
|
57
|
+
async function countEnabledSchedules() {
|
|
58
|
+
const records = await loadNamespaceValues(NAMESPACE_SCHEDULES);
|
|
59
|
+
return records.filter(r => r.enabled === true).length;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run the autostart reconcile and surface its message/warning to the user.
|
|
63
|
+
*/
|
|
64
|
+
function emitReconcileResult(result) {
|
|
65
|
+
if (result.message)
|
|
66
|
+
output.printInfo(result.message);
|
|
67
|
+
if (result.warning)
|
|
68
|
+
output.printWarning(result.warning);
|
|
69
|
+
}
|
|
20
70
|
// ── Schedule Create ───────────────────────────────────────────────────────────
|
|
21
71
|
const createCommand = {
|
|
22
72
|
name: 'create',
|
|
@@ -26,11 +76,13 @@ const createCommand = {
|
|
|
26
76
|
{ name: 'cron', short: 'c', description: 'Cron expression (5-field)', type: 'string' },
|
|
27
77
|
{ name: 'interval', short: 'i', description: 'Interval (e.g., "6h", "30m", "1d")', type: 'string' },
|
|
28
78
|
{ name: 'at', short: 'a', description: 'One-time ISO 8601 datetime', type: 'string' },
|
|
79
|
+
{ name: 'no-autostart', description: 'Do not register the daemon as an OS login service', type: 'boolean' },
|
|
29
80
|
],
|
|
30
81
|
examples: [
|
|
31
82
|
{ command: 'moflo spell schedule create -n audit --cron "0 9 * * *"', description: 'Daily at 9am' },
|
|
32
83
|
{ command: 'moflo spell schedule create -n security-audit --interval 6h', description: 'Every 6 hours' },
|
|
33
84
|
{ command: 'moflo spell schedule create -n report --at 2026-04-01T09:00:00Z', description: 'One-time cast' },
|
|
85
|
+
{ command: 'moflo spell schedule create -n audit --interval 6h --no-autostart', description: 'Skip OS login service registration (e.g., container/CI)' },
|
|
34
86
|
],
|
|
35
87
|
action: async (ctx) => {
|
|
36
88
|
const name = ctx.flags.name || ctx.args[0];
|
|
@@ -86,15 +138,41 @@ const createCommand = {
|
|
|
86
138
|
source: 'adhoc',
|
|
87
139
|
};
|
|
88
140
|
try {
|
|
89
|
-
await callMCPTool(TOOL_MEMORY_STORE, {
|
|
141
|
+
const storeResult = await callMCPTool(TOOL_MEMORY_STORE, {
|
|
90
142
|
namespace: NAMESPACE_SCHEDULES,
|
|
91
143
|
key: id,
|
|
92
144
|
value: JSON.stringify(record),
|
|
145
|
+
upsert: true,
|
|
93
146
|
});
|
|
147
|
+
if (storeResult.success === false) {
|
|
148
|
+
output.printError(`Failed to save schedule: ${storeResult.error ?? 'unknown error'}`);
|
|
149
|
+
return { success: false, exitCode: 1 };
|
|
150
|
+
}
|
|
94
151
|
}
|
|
95
152
|
catch (error) {
|
|
96
153
|
return handleMCPError(error, 'save schedule');
|
|
97
154
|
}
|
|
155
|
+
// Reconcile OS-native autostart against the new enabled-schedule count.
|
|
156
|
+
// 0→1 installs the login service; 1→2/2→3/etc. is a no-op (idempotent).
|
|
157
|
+
// Short-circuit: a fresh create can only ever trigger an install (count
|
|
158
|
+
// just went up). If the service is already installed, the reconcile is a
|
|
159
|
+
// guaranteed noop — skip the count fetch entirely.
|
|
160
|
+
// Note: parser normalises --no-autostart to ctx.flags.noAutostart (#787).
|
|
161
|
+
const skipAutostart = ctx.flags.noAutostart === true;
|
|
162
|
+
const alreadyInstalled = readiness.daemonInstalled;
|
|
163
|
+
let reconcileTransition = 'noop';
|
|
164
|
+
if (!skipAutostart && !alreadyInstalled) {
|
|
165
|
+
const reconcile = reconcileDaemonAutostart({
|
|
166
|
+
projectRoot,
|
|
167
|
+
enabledScheduleCount: await countEnabledSchedules(),
|
|
168
|
+
skip: false,
|
|
169
|
+
});
|
|
170
|
+
emitReconcileResult(reconcile);
|
|
171
|
+
reconcileTransition = reconcile.transition;
|
|
172
|
+
}
|
|
173
|
+
const serviceState = reconcileTransition === 'installed' || alreadyInstalled
|
|
174
|
+
? 'installed'
|
|
175
|
+
: 'not installed';
|
|
98
176
|
if (ctx.flags.format === 'json') {
|
|
99
177
|
output.printJson(record);
|
|
100
178
|
return { success: true, data: record };
|
|
@@ -109,7 +187,7 @@ const createCommand = {
|
|
|
109
187
|
at ? `At: ${at}` : null,
|
|
110
188
|
`Next Cast: ${new Date(nextRunAt).toLocaleString()}`,
|
|
111
189
|
`Daemon: ${readiness.daemonRunning ? 'running' : 'not running'}`,
|
|
112
|
-
`Service: ${
|
|
190
|
+
`Service: ${serviceState}`,
|
|
113
191
|
].filter(Boolean).join('\n'), 'Scheduled Spell');
|
|
114
192
|
return { success: true, data: record };
|
|
115
193
|
},
|
|
@@ -127,61 +205,146 @@ const scheduleListCommand = {
|
|
|
127
205
|
aliases: ['ls'],
|
|
128
206
|
description: 'List all scheduled spells',
|
|
129
207
|
action: async (ctx) => {
|
|
208
|
+
let raw;
|
|
130
209
|
try {
|
|
131
|
-
|
|
132
|
-
namespace: NAMESPACE_SCHEDULES,
|
|
133
|
-
});
|
|
134
|
-
// Single-pass: parse + transform for display
|
|
135
|
-
const schedules = [];
|
|
136
|
-
for (const r of result.results ?? []) {
|
|
137
|
-
try {
|
|
138
|
-
const parsed = typeof r.value === 'string' ? JSON.parse(r.value) : r.value;
|
|
139
|
-
if (parsed) {
|
|
140
|
-
schedules.push({
|
|
141
|
-
id: parsed.id,
|
|
142
|
-
spellName: parsed.spellName,
|
|
143
|
-
timing: parsed.cron || parsed.interval || parsed.at || '-',
|
|
144
|
-
nextRun: parsed.nextRunAt ? new Date(parsed.nextRunAt).toLocaleString() : '-',
|
|
145
|
-
enabled: parsed.enabled,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
output.printWarning(`Skipped malformed schedule record: ${r.key}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (ctx.flags.format === 'json') {
|
|
154
|
-
output.printJson(schedules);
|
|
155
|
-
return { success: true, data: schedules };
|
|
156
|
-
}
|
|
157
|
-
if (schedules.length === 0) {
|
|
158
|
-
output.writeln();
|
|
159
|
-
output.printInfo('No scheduled spells');
|
|
160
|
-
return { success: true, data: [] };
|
|
161
|
-
}
|
|
162
|
-
output.writeln();
|
|
163
|
-
output.writeln(output.bold('Scheduled Spells'));
|
|
164
|
-
output.writeln();
|
|
165
|
-
output.printTable({ columns: SCHEDULE_COLUMNS, data: schedules });
|
|
166
|
-
output.writeln();
|
|
167
|
-
output.printInfo(`Total: ${schedules.length} schedule(s)`);
|
|
168
|
-
return { success: true, data: schedules };
|
|
210
|
+
raw = await loadNamespaceValues(NAMESPACE_SCHEDULES);
|
|
169
211
|
}
|
|
170
212
|
catch (error) {
|
|
171
213
|
return handleMCPError(error, 'list schedules');
|
|
172
214
|
}
|
|
215
|
+
const schedules = raw.map(parsed => ({
|
|
216
|
+
id: parsed.id,
|
|
217
|
+
spellName: parsed.spellName,
|
|
218
|
+
timing: parsed.cron || parsed.interval || parsed.at || '-',
|
|
219
|
+
nextRun: parsed.nextRunAt ? new Date(parsed.nextRunAt).toLocaleString() : '-',
|
|
220
|
+
enabled: parsed.enabled,
|
|
221
|
+
}));
|
|
222
|
+
if (ctx.flags.format === 'json') {
|
|
223
|
+
output.printJson(schedules);
|
|
224
|
+
return { success: true, data: schedules };
|
|
225
|
+
}
|
|
226
|
+
if (schedules.length === 0) {
|
|
227
|
+
output.writeln();
|
|
228
|
+
output.printInfo('No scheduled spells');
|
|
229
|
+
return { success: true, data: [] };
|
|
230
|
+
}
|
|
231
|
+
output.writeln();
|
|
232
|
+
output.writeln(output.bold('Scheduled Spells'));
|
|
233
|
+
output.writeln();
|
|
234
|
+
output.printTable({ columns: SCHEDULE_COLUMNS, data: schedules });
|
|
235
|
+
output.writeln();
|
|
236
|
+
output.printInfo(`Total: ${schedules.length} schedule(s)`);
|
|
237
|
+
return { success: true, data: schedules };
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
// ── Schedule Executions ───────────────────────────────────────────────────────
|
|
241
|
+
const EXECUTION_COLUMNS = [
|
|
242
|
+
{ key: 'startedAt', header: 'Started', width: 22 },
|
|
243
|
+
{ key: 'spellName', header: 'Spell', width: 20 },
|
|
244
|
+
{ key: 'status', header: 'Status', width: 10 },
|
|
245
|
+
{ key: 'duration', header: 'Duration', width: 10 },
|
|
246
|
+
{ key: 'manualRun', header: 'Manual', width: 7 },
|
|
247
|
+
{ key: 'scheduleId', header: 'Schedule', width: 30 },
|
|
248
|
+
];
|
|
249
|
+
function formatExecutionRow(parsed) {
|
|
250
|
+
const completed = typeof parsed.completedAt === 'number';
|
|
251
|
+
let status;
|
|
252
|
+
if (!completed) {
|
|
253
|
+
status = output.warning('running');
|
|
254
|
+
}
|
|
255
|
+
else if (parsed.success === true) {
|
|
256
|
+
status = output.success('success');
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
status = output.error('failed');
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
id: String(parsed.id ?? ''),
|
|
263
|
+
scheduleId: String(parsed.scheduleId ?? ''),
|
|
264
|
+
spellName: String(parsed.spellName ?? ''),
|
|
265
|
+
startedAt: typeof parsed.startedAt === 'number'
|
|
266
|
+
? new Date(parsed.startedAt).toLocaleString()
|
|
267
|
+
: '-',
|
|
268
|
+
status,
|
|
269
|
+
duration: typeof parsed.duration === 'number' ? `${parsed.duration}ms` : '-',
|
|
270
|
+
manualRun: parsed.manualRun === true ? 'yes' : '',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const executionsCommand = {
|
|
274
|
+
name: 'executions',
|
|
275
|
+
aliases: ['exec', 'history'],
|
|
276
|
+
description: 'Show recent scheduled spell executions',
|
|
277
|
+
options: [
|
|
278
|
+
{ name: 'schedule', short: 's', description: 'Filter by schedule ID', type: 'string' },
|
|
279
|
+
{ name: 'limit', short: 'l', description: `Max rows to return (default ${DEFAULT_EXECUTIONS_LIMIT})`, type: 'number' },
|
|
280
|
+
],
|
|
281
|
+
examples: [
|
|
282
|
+
{ command: 'moflo spell schedule executions', description: 'Most recent executions across all schedules' },
|
|
283
|
+
{ command: 'moflo spell schedule executions --schedule sched-adhoc-123', description: 'Filter by schedule ID' },
|
|
284
|
+
{ command: 'moflo spell schedule executions --limit 25', description: 'Show last 25 executions' },
|
|
285
|
+
],
|
|
286
|
+
action: async (ctx) => {
|
|
287
|
+
const scheduleFilter = ctx.flags.schedule;
|
|
288
|
+
const rawLimit = ctx.flags.limit;
|
|
289
|
+
const limit = typeof rawLimit === 'number' && rawLimit > 0
|
|
290
|
+
? Math.floor(rawLimit)
|
|
291
|
+
: DEFAULT_EXECUTIONS_LIMIT;
|
|
292
|
+
let raw;
|
|
293
|
+
try {
|
|
294
|
+
raw = await loadNamespaceValues(NAMESPACE_EXECUTIONS);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
return handleMCPError(error, 'list executions');
|
|
298
|
+
}
|
|
299
|
+
const parsed = raw.filter(v => typeof v.startedAt === 'number');
|
|
300
|
+
const filtered = scheduleFilter
|
|
301
|
+
? parsed.filter(e => e.scheduleId === scheduleFilter)
|
|
302
|
+
: parsed;
|
|
303
|
+
filtered.sort((a, b) => b.startedAt - a.startedAt);
|
|
304
|
+
const truncated = filtered.slice(0, limit);
|
|
305
|
+
if (ctx.flags.format === 'json') {
|
|
306
|
+
output.printJson(truncated);
|
|
307
|
+
return { success: true, data: truncated };
|
|
308
|
+
}
|
|
309
|
+
if (truncated.length === 0) {
|
|
310
|
+
output.writeln();
|
|
311
|
+
output.printInfo(scheduleFilter
|
|
312
|
+
? `No executions for schedule ${scheduleFilter}`
|
|
313
|
+
: 'No scheduled spell executions yet');
|
|
314
|
+
return { success: true, data: [] };
|
|
315
|
+
}
|
|
316
|
+
const rows = truncated.map(formatExecutionRow);
|
|
317
|
+
output.writeln();
|
|
318
|
+
output.writeln(output.bold(scheduleFilter
|
|
319
|
+
? `Executions for ${scheduleFilter}`
|
|
320
|
+
: 'Recent Scheduled Executions'));
|
|
321
|
+
output.writeln();
|
|
322
|
+
output.printTable({ columns: EXECUTION_COLUMNS, data: rows });
|
|
323
|
+
output.writeln();
|
|
324
|
+
output.printInfo(filtered.length > truncated.length
|
|
325
|
+
? `Showing ${truncated.length} of ${filtered.length} execution(s)`
|
|
326
|
+
: `Total: ${truncated.length} execution(s)`);
|
|
327
|
+
return { success: true, data: truncated };
|
|
173
328
|
},
|
|
174
329
|
};
|
|
175
330
|
// ── Schedule Cancel ───────────────────────────────────────────────────────────
|
|
176
331
|
const cancelCommand = {
|
|
177
332
|
name: 'cancel',
|
|
178
333
|
description: 'Cancel (disable) a scheduled spell',
|
|
334
|
+
options: [
|
|
335
|
+
{ name: 'keep-autostart', description: 'Keep the OS login service registered even if no schedules remain', type: 'boolean' },
|
|
336
|
+
],
|
|
337
|
+
examples: [
|
|
338
|
+
{ command: 'moflo spell schedule cancel sched-adhoc-123', description: 'Cancel and auto-uninstall daemon service if no schedules remain' },
|
|
339
|
+
{ command: 'moflo spell schedule cancel sched-adhoc-123 --keep-autostart', description: 'Cancel but keep the OS login service registered' },
|
|
340
|
+
],
|
|
179
341
|
action: async (ctx) => {
|
|
180
342
|
const scheduleId = ctx.args[0];
|
|
181
343
|
if (!scheduleId) {
|
|
182
344
|
output.printError('Schedule ID is required');
|
|
183
345
|
return { success: false, exitCode: 1 };
|
|
184
346
|
}
|
|
347
|
+
let updated;
|
|
185
348
|
try {
|
|
186
349
|
// Fetch the current schedule
|
|
187
350
|
const fetchResult = await callMCPTool(TOOL_MEMORY_RETRIEVE, {
|
|
@@ -195,19 +358,42 @@ const cancelCommand = {
|
|
|
195
358
|
const schedule = typeof fetchResult.value === 'string'
|
|
196
359
|
? JSON.parse(fetchResult.value)
|
|
197
360
|
: fetchResult.value;
|
|
198
|
-
// Disable it
|
|
199
|
-
|
|
200
|
-
|
|
361
|
+
// Disable it. upsert:true is critical — the cancel writes back to an
|
|
362
|
+
// existing key, and the historic default (insert-only) silently failed
|
|
363
|
+
// with a UNIQUE constraint violation on (namespace, key) — see #962.
|
|
364
|
+
updated = { ...schedule, enabled: false };
|
|
365
|
+
const storeResult = await callMCPTool(TOOL_MEMORY_STORE, {
|
|
201
366
|
namespace: NAMESPACE_SCHEDULES,
|
|
202
367
|
key: scheduleId,
|
|
203
368
|
value: JSON.stringify(updated),
|
|
369
|
+
upsert: true,
|
|
204
370
|
});
|
|
205
|
-
|
|
206
|
-
|
|
371
|
+
if (storeResult.success === false) {
|
|
372
|
+
output.printError(`Failed to cancel schedule: ${storeResult.error ?? 'unknown error'}`);
|
|
373
|
+
return { success: false, exitCode: 1 };
|
|
374
|
+
}
|
|
207
375
|
}
|
|
208
376
|
catch (error) {
|
|
209
377
|
return handleMCPError(error, 'cancel schedule');
|
|
210
378
|
}
|
|
379
|
+
output.printSuccess(`Schedule ${scheduleId} cancelled`);
|
|
380
|
+
// Reconcile OS-native autostart against the new enabled-schedule count.
|
|
381
|
+
// 1→0 uninstalls the login service; everything else is a no-op.
|
|
382
|
+
// Short-circuit: a fresh cancel can only ever trigger an uninstall (count
|
|
383
|
+
// just went down). If the service isn't currently installed, the reconcile
|
|
384
|
+
// is a guaranteed noop — skip the count fetch entirely.
|
|
385
|
+
// Note: parser normalises --keep-autostart to ctx.flags.keepAutostart (#787).
|
|
386
|
+
const projectRoot = ctx.cwd || process.cwd();
|
|
387
|
+
const skipAutostart = ctx.flags.keepAutostart === true;
|
|
388
|
+
if (!skipAutostart && isDaemonInstalled(projectRoot)) {
|
|
389
|
+
const reconcile = reconcileDaemonAutostart({
|
|
390
|
+
projectRoot,
|
|
391
|
+
enabledScheduleCount: await countEnabledSchedules(),
|
|
392
|
+
skip: false,
|
|
393
|
+
});
|
|
394
|
+
emitReconcileResult(reconcile);
|
|
395
|
+
}
|
|
396
|
+
return { success: true, data: updated };
|
|
211
397
|
},
|
|
212
398
|
};
|
|
213
399
|
// ── Schedule Command (parent) ─────────────────────────────────────────────────
|
|
@@ -215,10 +401,11 @@ const SCHEDULE_DOCS_URL = 'https://github.com/eric-cielo/moflo/blob/main/docs/SP
|
|
|
215
401
|
export const scheduleCommand = {
|
|
216
402
|
name: 'schedule',
|
|
217
403
|
description: `Manage scheduled spells (full reference: ${SCHEDULE_DOCS_URL})`,
|
|
218
|
-
subcommands: [createCommand, scheduleListCommand, cancelCommand],
|
|
404
|
+
subcommands: [createCommand, scheduleListCommand, executionsCommand, cancelCommand],
|
|
219
405
|
examples: [
|
|
220
406
|
{ command: 'moflo spell schedule create -n audit --cron "0 9 * * *"', description: 'Schedule daily audit' },
|
|
221
407
|
{ command: 'moflo spell schedule list', description: 'List all schedules' },
|
|
408
|
+
{ command: 'moflo spell schedule executions --schedule <id>', description: 'Show execution audit trail' },
|
|
222
409
|
{ command: 'moflo spell schedule cancel <id>', description: 'Cancel a schedule' },
|
|
223
410
|
],
|
|
224
411
|
action: async () => {
|
|
@@ -229,9 +416,10 @@ export const scheduleCommand = {
|
|
|
229
416
|
output.writeln();
|
|
230
417
|
output.writeln('Subcommands:');
|
|
231
418
|
output.printList([
|
|
232
|
-
`${output.highlight('create')}
|
|
233
|
-
`${output.highlight('list')}
|
|
234
|
-
`${output.highlight('
|
|
419
|
+
`${output.highlight('create')} - Create a scheduled spell`,
|
|
420
|
+
`${output.highlight('list')} - List all scheduled spells`,
|
|
421
|
+
`${output.highlight('executions')} - Show recent execution history`,
|
|
422
|
+
`${output.highlight('cancel')} - Cancel (disable) a schedule`,
|
|
235
423
|
]);
|
|
236
424
|
output.writeln();
|
|
237
425
|
output.writeln(`Full reference: ${SCHEDULE_DOCS_URL}`);
|
|
@@ -105,9 +105,11 @@ export function generateSettings(options) {
|
|
|
105
105
|
daemon: {
|
|
106
106
|
autoStart: true,
|
|
107
107
|
// Note: this list is documentation for the user — the daemon's actual
|
|
108
|
-
// worker registry lives in src/cli/services/worker-daemon.ts
|
|
109
|
-
//
|
|
110
|
-
//
|
|
108
|
+
// worker registry lives in src/cli/services/worker-daemon.ts
|
|
109
|
+
// DEFAULT_WORKERS. The `audit`/`predict`/`document` workers were
|
|
110
|
+
// removed in #970 (no surfacing layer for findings + dashboard
|
|
111
|
+
// pollution); restore them as opt-in `flo doctor` one-shots if their
|
|
112
|
+
// value is ever re-established.
|
|
111
113
|
workers: [
|
|
112
114
|
'map', // Codebase mapping
|
|
113
115
|
'optimize', // Performance optimization (high priority)
|
|
@@ -115,15 +117,12 @@ export function generateSettings(options) {
|
|
|
115
117
|
'testgaps', // Test coverage gaps
|
|
116
118
|
'ultralearn', // Deep knowledge acquisition
|
|
117
119
|
'deepdive', // Deep code analysis
|
|
118
|
-
'document', // Auto-documentation for ADRs
|
|
119
120
|
'refactor', // Refactoring suggestions (DDD alignment)
|
|
120
121
|
'benchmark', // Performance benchmarking
|
|
121
122
|
],
|
|
122
123
|
schedules: {
|
|
123
|
-
audit: { interval: '1h', priority: 'critical' },
|
|
124
124
|
optimize: { interval: '30m', priority: 'high' },
|
|
125
125
|
consolidate: { interval: '2h', priority: 'low' },
|
|
126
|
-
document: { interval: '1h', priority: 'normal', triggers: ['adr-update', 'api-change'] },
|
|
127
126
|
deepdive: { interval: '4h', priority: 'normal', triggers: ['complex-change'] },
|
|
128
127
|
ultralearn: { interval: '1h', priority: 'normal' },
|
|
129
128
|
},
|
|
@@ -154,7 +154,7 @@ async function ensureInitialized() {
|
|
|
154
154
|
export const memoryTools = [
|
|
155
155
|
{
|
|
156
156
|
name: 'memory_store',
|
|
157
|
-
description: 'Store a value in memory with vector embedding for semantic search (sql.js + HNSW backend).
|
|
157
|
+
description: 'Store a value in memory with vector embedding for semantic search (sql.js + HNSW backend). Upserts by default — pass upsert:false to fail on duplicate keys.',
|
|
158
158
|
category: 'memory',
|
|
159
159
|
inputSchema: {
|
|
160
160
|
type: 'object',
|
|
@@ -168,7 +168,7 @@ export const memoryTools = [
|
|
|
168
168
|
description: 'Optional tags for filtering',
|
|
169
169
|
},
|
|
170
170
|
ttl: { type: 'number', description: 'Time-to-live in seconds (optional)' },
|
|
171
|
-
upsert: { type: 'boolean', description: 'If
|
|
171
|
+
upsert: { type: 'boolean', description: 'If false, fail on duplicate keys instead of replacing (default: true)' },
|
|
172
172
|
},
|
|
173
173
|
required: ['key', 'value'],
|
|
174
174
|
},
|
|
@@ -180,7 +180,9 @@ export const memoryTools = [
|
|
|
180
180
|
const value = typeof input.value === 'string' ? input.value : JSON.stringify(input.value);
|
|
181
181
|
const tags = input.tags || [];
|
|
182
182
|
const ttl = input.ttl;
|
|
183
|
-
|
|
183
|
+
// #962: default upsert=true — silent UNIQUE-constraint failures on update
|
|
184
|
+
// were dropping schedule cancels and similar updates on the floor.
|
|
185
|
+
const upsert = input.upsert === false ? false : true;
|
|
184
186
|
validateMemoryInput(key, value);
|
|
185
187
|
const startTime = performance.now();
|
|
186
188
|
try {
|
|
@@ -364,12 +366,21 @@ export const memoryTools = [
|
|
|
364
366
|
const namespace = input.namespace || 'default';
|
|
365
367
|
try {
|
|
366
368
|
const result = await deleteEntry({ key, namespace });
|
|
369
|
+
// Issue #963: surface the underlying reason when delete fails.
|
|
370
|
+
// `result.success` reflects whether the call itself succeeded; we
|
|
371
|
+
// require `deleted === true` for the MCP-level success boolean,
|
|
372
|
+
// and pass `result.error` through whenever the delete didn't take.
|
|
373
|
+
const deleted = result.deleted === true;
|
|
374
|
+
const errorReason = !deleted
|
|
375
|
+
? (result.error ?? `No entry deleted (key='${key}', namespace='${namespace}'); reason not reported by storage layer`)
|
|
376
|
+
: undefined;
|
|
367
377
|
return {
|
|
368
|
-
success: result.deleted,
|
|
378
|
+
success: result.success === true && deleted,
|
|
369
379
|
key,
|
|
370
380
|
namespace,
|
|
371
|
-
deleted
|
|
381
|
+
deleted,
|
|
372
382
|
backend: 'sql.js + HNSW',
|
|
383
|
+
...(errorReason ? { error: errorReason } : {}),
|
|
373
384
|
};
|
|
374
385
|
}
|
|
375
386
|
catch (error) {
|