moflo 4.10.13 → 4.10.15

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.
@@ -145,6 +145,10 @@ For the full `moflo.yaml` schema, gate toggles, model routing, and sandbox confi
145
145
  | Every 5+ file changes | `map` | Update codebase map |
146
146
  | Complex debugging | `deepdive` | Deep code analysis |
147
147
 
148
+ ### Worker Report Location
149
+
150
+ Headless workers (`optimize`, `testgaps`, `ultralearn`, `refactor`, `deepdive`) write the latest run's full output to `.moflo/reports/<workerType>.<ext>` (`.md` for markdown workers, `.json` for `ultralearn`). The path is overwritten each run; for history, see `.moflo/logs/headless/`. The directory is gitignored by `flo init`, so reports never reach a consumer's commit. When the user asks "what did testgaps find?" or "where's the optimize report?", read `.moflo/reports/<workerType>.md` directly — do NOT re-run the worker.
151
+
148
152
  ### Memory-Enhanced Development
149
153
 
150
154
  | Action | When |
package/README.md CHANGED
@@ -419,7 +419,7 @@ flo daemon status # shows whether the service is registered AND running
419
419
 
420
420
  `flo spell schedule create` warns when the daemon isn't installed so you don't quietly miss runs.
421
421
 
422
- **Monitoring.** **[The Luminarium](#the-luminarium)** — moflo's localhost daemon dashboard — surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now), alongside worker health, memory stats, and Claude Code session stats. Each project gets its own deterministic port (33000–33999) recorded in `.moflo/daemon.lock`; ask `/luminarium` in your Claude session and it'll print the link.
422
+ **Monitoring.** **[The Luminarium](#the-luminarium)** — moflo's localhost daemon dashboard — surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now), alongside worker health, memory stats, and Claude Code session stats. Ask `/luminarium` in your Claude session and it'll print the link.
423
423
 
424
424
  For full configuration (`scheduler:` block in `moflo.yaml`), event types, and the catch-up window after restarts, see [docs/SPELLS.md#scheduling](docs/SPELLS.md#scheduling).
425
425
 
@@ -465,13 +465,7 @@ The Luminarium is moflo's localhost daemon dashboard. It boots automatically wit
465
465
 
466
466
  ### Finding the URL
467
467
 
468
- Each project gets a deterministic port in the range 33000–33999, derived from a hash of the project root so two projects never collide on the same machine. The actual bound port is written to `.moflo/daemon.lock` when the daemon starts if the deterministic port is already taken the daemon scans forward, so the lock file is the source of truth, not the hash.
469
-
470
- Three ways to get the URL:
471
-
472
- - **`/luminarium`** — inside a Claude Code session in a moflo project, this skill reads `.moflo/daemon.lock` and prints `http://localhost:<port>`. Fastest path.
473
- - **`flo daemon status`** — prints the URL alongside the health summary.
474
- - **`cat .moflo/daemon.lock`** — read the JSON directly: `{ "pid": ..., "port": 33421, ... }`.
468
+ Inside a Claude Code session in a moflo project, ask **`/luminarium`** the skill prints the dashboard URL for the current project. `flo daemon status` prints the same URL alongside the health summary. Each project binds its own port so two projects on the same machine never collide.
475
469
 
476
470
  ### What it shows
477
471
 
@@ -486,7 +480,7 @@ Three ways to get the URL:
486
480
  ### Flags
487
481
 
488
482
  - `flo daemon start --no-dashboard` — disable the HTTP server entirely (the daemon itself still runs)
489
- - `flo daemon start --dashboard-port <N>` — pin to a specific port, overriding the deterministic resolver. Also accepts the `MOFLO_DAEMON_PORT` env var, which the rest of moflo respects when talking to the daemon
483
+ - `flo daemon start --dashboard-port <N>` — pin to a specific port, overriding the per-project default. Also accepts the `MOFLO_DAEMON_PORT` env var, which the rest of moflo respects when talking to the daemon
490
484
 
491
485
  ## Commands
492
486
 
@@ -425,13 +425,10 @@ export async function checkNestedMofloIslands(cwd) {
425
425
  }
426
426
  const { islands, truncated } = scan;
427
427
  if (islands.length === 0) {
428
- const baseMsg = 'No nested .moflo/ directories detected';
429
428
  return {
430
429
  name: 'Nested .moflo/ Islands',
431
- status: truncated ? 'warn' : 'pass',
432
- message: truncated
433
- ? `${baseMsg} within depth-5 walk — deeper subtrees not inspected`
434
- : baseMsg,
430
+ status: 'pass',
431
+ message: 'No nested .moflo/ directories detected',
435
432
  };
436
433
  }
437
434
  const rels = islands.map(p => relative(root, p) || '.');
@@ -90,7 +90,7 @@ Provide actionable suggestions with code examples.`,
90
90
  - Check for missing error handling tests
91
91
  - Identify integration test gaps
92
92
 
93
- For each gap, provide a test skeleton.`,
93
+ For each gap, include a test skeleton inline in the report as a fenced code block — DO NOT create separate test files; the consumer will copy the skeletons into their test tree by hand.`,
94
94
  sandbox: 'permissive',
95
95
  model: 'sonnet',
96
96
  outputFormat: 'markdown',
@@ -300,11 +300,13 @@ export class HeadlessWorkerExecutor extends EventEmitter {
300
300
  maxContextFiles: options?.maxContextFiles ?? 20,
301
301
  maxCharsPerFile: options?.maxCharsPerFile ?? 5000,
302
302
  logDir: options?.logDir ?? join(projectRoot, '.moflo', 'logs', 'headless'),
303
+ reportsDir: options?.reportsDir ?? join(projectRoot, '.moflo', 'reports'),
303
304
  cacheContext: options?.cacheContext ?? true,
304
305
  cacheTtlMs: options?.cacheTtlMs ?? 60000, // 1 minute default
305
306
  };
306
- // Ensure log directory exists
307
+ // Ensure log + reports directories exist
307
308
  this.ensureLogDir();
309
+ this.ensureReportsDir();
308
310
  // Register for process-exit cleanup via the shared listener.
309
311
  ensureExitListener();
310
312
  liveExecutors.add(this);
@@ -546,6 +548,29 @@ export class HeadlessWorkerExecutor extends EventEmitter {
546
548
  this.emit('warning', { message: 'Failed to create log directory', error });
547
549
  }
548
550
  }
551
+ /**
552
+ * Ensure the reports directory exists. Reports land under `.moflo/reports/`
553
+ * (gitignored by `flo init`); without this directory the post-spawn write
554
+ * would no-op and the consumer would lose the report entirely.
555
+ */
556
+ ensureReportsDir() {
557
+ try {
558
+ if (!existsSync(this.config.reportsDir)) {
559
+ mkdirSync(this.config.reportsDir, { recursive: true });
560
+ }
561
+ }
562
+ catch (error) {
563
+ this.emit('warning', { message: 'Failed to create reports directory', error });
564
+ }
565
+ }
566
+ /**
567
+ * Resolve the on-disk report path for a worker. Extension matches the
568
+ * declared outputFormat so tools reading the report don't have to sniff.
569
+ */
570
+ getReportPath(workerType, outputFormat) {
571
+ const ext = outputFormat === 'json' ? 'json' : outputFormat === 'text' ? 'txt' : 'md';
572
+ return join(this.config.reportsDir, `${workerType}.${ext}`);
573
+ }
549
574
  /**
550
575
  * Internal execution logic
551
576
  */
@@ -558,8 +583,11 @@ export class HeadlessWorkerExecutor extends EventEmitter {
558
583
  try {
559
584
  // Build context from file patterns
560
585
  const context = await this.buildContext(headless.contextPatterns || []);
586
+ // Resolve the on-disk report path before prompt assembly so the prompt
587
+ // can quote the exact absolute path Claude should write to.
588
+ const reportPath = this.getReportPath(workerType, headless.outputFormat);
561
589
  // Build the full prompt
562
- const fullPrompt = this.buildPrompt(headless.promptTemplate, context);
590
+ const fullPrompt = this.buildPrompt(headless.promptTemplate, context, reportPath);
563
591
  // Log prompt for debugging
564
592
  this.logExecution(executionId, 'prompt', fullPrompt);
565
593
  // Execute Claude Code headlessly
@@ -571,6 +599,13 @@ export class HeadlessWorkerExecutor extends EventEmitter {
571
599
  workerType,
572
600
  signal,
573
601
  });
602
+ // Persist the spawn output to the canonical report path. Belt-and-braces
603
+ // against Claude ignoring the in-prompt instruction — this guarantees
604
+ // the consumer ends up with a report at a deterministic location no
605
+ // matter what the model chose to do with its Write tool.
606
+ if (result.success && result.output && result.output.trim().length > 0) {
607
+ this.writeReport(reportPath, result.output);
608
+ }
574
609
  // Parse output based on format
575
610
  let parsedOutput;
576
611
  if (headless.outputFormat === 'json' && result.output) {
@@ -768,15 +803,23 @@ export class HeadlessWorkerExecutor extends EventEmitter {
768
803
  return name === pattern;
769
804
  }
770
805
  /**
771
- * Build full prompt with context
806
+ * Build full prompt with context. The report path is injected so Claude
807
+ * saves output to the canonical `.moflo/reports/` location instead of
808
+ * dropping `*-analysis.md` / `*-report.md` files at the project root — the
809
+ * behaviour consumers were seeing before this change.
772
810
  */
773
- buildPrompt(template, context) {
811
+ buildPrompt(template, context, reportPath) {
812
+ const ioInstructions = `## Output
813
+
814
+ Save the full report to \`${reportPath}\` using the Write tool. Overwrite any prior content at that path. DO NOT create any other files anywhere in the project; if you need to suggest test skeletons or code samples, include them inline in the report as fenced code blocks. Moflo persists the same output to that path after you finish, so the location is authoritative.`;
774
815
  if (!context) {
775
816
  return `${template}
776
817
 
777
818
  ## Instructions
778
819
 
779
- Analyze the codebase and provide your response following the format specified in the task.`;
820
+ Analyze the codebase and provide your response following the format specified in the task.
821
+
822
+ ${ioInstructions}`;
780
823
  }
781
824
  return `${template}
782
825
 
@@ -786,7 +829,9 @@ ${context}
786
829
 
787
830
  ## Instructions
788
831
 
789
- Analyze the above codebase context and provide your response following the format specified in the task.`;
832
+ Analyze the above codebase context and provide your response following the format specified in the task.
833
+
834
+ ${ioInstructions}`;
790
835
  }
791
836
  /**
792
837
  * Execute Claude Code in headless mode
@@ -1011,6 +1056,22 @@ Analyze the above codebase context and provide your response following the forma
1011
1056
  // Ignore log write errors
1012
1057
  }
1013
1058
  }
1059
+ /**
1060
+ * Persist a worker's report to the canonical `.moflo/reports/` location.
1061
+ * The directory was created at construction time; we recreate it here as a
1062
+ * safety net in case it was removed between construction and execution.
1063
+ */
1064
+ writeReport(reportPath, content) {
1065
+ try {
1066
+ if (!existsSync(this.config.reportsDir)) {
1067
+ mkdirSync(this.config.reportsDir, { recursive: true });
1068
+ }
1069
+ writeFileSync(reportPath, content);
1070
+ }
1071
+ catch (error) {
1072
+ this.emit('warning', { message: 'Failed to write worker report', reportPath, error });
1073
+ }
1074
+ }
1014
1075
  }
1015
1076
  // Export default
1016
1077
  export default HeadlessWorkerExecutor;
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.10.13';
5
+ export const VERSION = '4.10.15';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.13",
3
+ "version": "4.10.15",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.10.12",
98
+ "moflo": "^4.10.14",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"