moflo 4.9.22 → 4.9.23

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.
Files changed (29) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +19 -16
  2. package/.claude/guidance/shipped/moflo-core-guidance.md +0 -2
  3. package/.claude/guidance/shipped/moflo-spell-runner.md +1 -0
  4. package/.claude/guidance/shipped/moflo-spell-scheduling.md +225 -0
  5. package/.claude/guidance/shipped/moflo-spell-troubleshooting.md +1 -0
  6. package/.claude/skills/fl/phases.md +67 -0
  7. package/.claude/skills/spell-schedule/SKILL.md +18 -5
  8. package/README.md +1 -1
  9. package/bin/index-guidance.mjs +32 -6
  10. package/bin/session-start-launcher.mjs +15 -8
  11. package/dist/src/cli/commands/daemon.js +13 -17
  12. package/dist/src/cli/commands/hooks.js +3 -6
  13. package/dist/src/cli/commands/spell-schedule.js +237 -49
  14. package/dist/src/cli/init/settings-generator.js +5 -6
  15. package/dist/src/cli/mcp-tools/memory-tools.js +16 -5
  16. package/dist/src/cli/memory/bridge-embedder.js +26 -6
  17. package/dist/src/cli/memory/bridge-entries.js +33 -15
  18. package/dist/src/cli/services/daemon-autostart-lifecycle.js +62 -0
  19. package/dist/src/cli/services/daemon-dashboard.js +187 -18
  20. package/dist/src/cli/services/daemon-readiness.js +19 -31
  21. package/dist/src/cli/services/ephemeral-namespace-purge.js +61 -33
  22. package/dist/src/cli/services/headless-worker-executor.js +7 -94
  23. package/dist/src/cli/services/worker-daemon.js +40 -66
  24. package/dist/src/cli/spells/core/runner.js +12 -0
  25. package/dist/src/cli/spells/scheduler/scheduler.js +24 -9
  26. package/dist/src/cli/spells/schema/validator.js +2 -1
  27. package/dist/src/cli/spells/schema/validators/top-level.js +18 -0
  28. package/dist/src/cli/version.js +1 -1
  29. 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,audit,optimize,consolidate,testgaps)' },
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,audit,optimize', description: 'Start with specific workers' },
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 ? [`Dashboard: http://localhost:${dashboard.port}`] : []),
131
+ ...(dashboard ? [`The Arcane Console: 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(`Dashboard: http://localhost:${dashboard.port}`);
218
+ output.printSuccess(`The Arcane Console: http://localhost:${dashboard.port}`);
219
219
  }
220
220
  catch (err) {
221
- logWarn(`Dashboard failed to start: ${errorDetail(err)}`);
221
+ logWarn(`The Arcane Console 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(`Dashboard: http://localhost:${dashboardPort ?? DEFAULT_DASHBOARD_PORT}`);
381
+ output.printInfo(`The Arcane Console: 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 audit', description: 'Trigger security audit' },
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, audit, optimize, consolidate, testgaps, predict, document, ultralearn, refactor, benchmark, deepdive, preload');
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 predict', description: 'Enable predict worker' },
672
- { command: 'claude-flow daemon enable -w document --disable', description: 'Disable document worker' },
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 audit', description: 'Run security audit' },
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 12 specialized workers for continuous optimization and monitoring.');
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 (5 min interval)`,
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, audit, map, etc.)', required: true },
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 audit -p critical', description: 'Security audit with critical priority' },
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, predict, audit, map, preload, deepdive, document, refactor, benchmark, testgaps');
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: ${readiness.daemonInstalled ? 'installed' : 'not installed'}`,
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
- const result = await callMCPTool(TOOL_MEMORY_LIST, {
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
- const updated = { ...schedule, enabled: false };
200
- await callMCPTool(TOOL_MEMORY_STORE, {
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
- output.printSuccess(`Schedule ${scheduleId} cancelled`);
206
- return { success: true, data: updated };
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')} - Create a scheduled spell`,
233
- `${output.highlight('list')} - List all scheduled spells`,
234
- `${output.highlight('cancel')} - Cancel (disable) a schedule`,
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 DEFAULT_WORKERS.
109
- // 'audit' is intentionally absent here because it's default-disabled
110
- // pending the perf fix in #631.
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). Use upsert=true to update existing keys.',
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 true, update existing key instead of failing (default: false)' },
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
- const upsert = input.upsert || false;
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: result.deleted,
381
+ deleted,
372
382
  backend: 'sql.js + HNSW',
383
+ ...(errorReason ? { error: errorReason } : {}),
373
384
  };
374
385
  }
375
386
  catch (error) {