moflo 4.12.4-rc.2 → 4.12.4-rc.3

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.
@@ -649,17 +649,32 @@ function getUpgradeNotice() {
649
649
  };
650
650
  }
651
651
 
652
+ // #1363: an 'upgrade' notice that still reads 'in-progress' when we render is
653
+ // never live work. Claude Code paints the statusline only AFTER the SessionStart
654
+ // hook returns, and the launcher flips the notice to 'completed' the moment the
655
+ // upgrade functionally lands (commitVersionStamp) — so reaching this code with
656
+ // 'in-progress' means the launcher died before finishing its sync: killed by the
657
+ // 5s hook timeout via SIGKILL, or Windows TerminateProcess, neither of which runs
658
+ // the launcher's cleanup handlers. Rendering "(updating…)" there advertises
659
+ // progress nothing is making, and because the TTL is only evaluated when Claude
660
+ // Code re-invokes this script, that badge stays frozen on screen until the next
661
+ // repaint. Report the stall instead, and point at the fix.
662
+ //
663
+ // 'repair' notices are exempt: §0's bootstrap sentinel deliberately holds an
664
+ // in-progress one open to keep the healer prompt in front of the user until §3h
665
+ // resolves it, so there the in-flight state is the intended signal.
652
666
  function formatUpgradeNoticeSegment(notice) {
653
667
  if (!notice) return '';
654
668
  const inFlight = notice.status === 'in-progress';
655
- const suffix = inFlight ? ` ${c.dim}(updating…)${c.reset}` : '';
656
- // Pick body text: repair > in-flight version range > completed "upgraded to"
669
+ const stalledUpgrade = inFlight && notice.kind !== 'repair';
670
+ const suffix = inFlight && !stalledUpgrade ? ` ${c.dim}(updating…)${c.reset}` : '';
671
+ // Pick body text: repair > stalled upgrade > completed "upgraded to"
657
672
  // > bare "upgraded" fallback when no version is known.
658
673
  let body;
659
674
  if (notice.kind === 'repair') {
660
675
  body = 'install repaired';
661
- } else if (inFlight) {
662
- body = notice.from && notice.to ? `${notice.from} → ${notice.to}` : (notice.to || 'upgraded');
676
+ } else if (stalledUpgrade) {
677
+ return `${c.brightRed}📦 upgrade interrupted${c.reset} ${c.dim}(run /healer)${c.reset}`;
663
678
  } else {
664
679
  const target = notice.to || notice.from || '';
665
680
  body = target ? `upgraded to ${target}` : 'upgraded';
@@ -277,6 +277,18 @@ let upgradeNoticeContext = null;
277
277
  // after the sync that installs this version's files succeeds; every section
278
278
  // after the §3 sync block runs unconditionally + idempotently each session, so
279
279
  // an abort past this point strands no upgrade work.
280
+ // #1363: this is also the moment the statusline notice flips to 'completed'.
281
+ // The flip used to live at the end of §3 (old §3f), after every best-effort
282
+ // stage — hook-drift, CLAUDE.md injection, embeddings migration, the memory
283
+ // re-index that advertises 30-60s. A launcher killed by the 5s SessionStart
284
+ // hook-timeout never reached it, so the notice stayed 'in-progress' and the
285
+ // statusline painted "(updating…)" for the full 5-minute in-progress TTL with
286
+ // nothing actually running. Marking completion here — where the upgrade has
287
+ // functionally landed — means a kill during the best-effort tail leaves a
288
+ // truthful terminal badge instead. A kill BEFORE this point deliberately
289
+ // leaves 'in-progress': that upgrade really didn't land, and both the exit
290
+ // handler (graceful) and the statusline's stalled-upgrade rendering (SIGKILL)
291
+ // surface it honestly rather than claiming success.
280
292
  function commitVersionStamp(stampPath, version) {
281
293
  try {
282
294
  mkdirSync(dirname(stampPath), { recursive: true });
@@ -284,6 +296,12 @@ function commitVersionStamp(stampPath, version) {
284
296
  } catch (err) {
285
297
  emitWarning(`version stamp write failed (${errMessage(err)}) — next launcher will re-detect the upgrade`);
286
298
  }
299
+ // Written even when the stamp write above failed: #1173's recovery path keys
300
+ // off a prior session's 'completed' notice to repair exactly that miss.
301
+ if (upgradeNoticeContext) {
302
+ writeUpgradeNotice('completed');
303
+ upgradeNoticeFinalized = true;
304
+ }
287
305
  }
288
306
 
289
307
  // 5-min TTL is a safety net for zombie launchers (statusline ignores past-TTL
@@ -977,10 +995,14 @@ try {
977
995
  };
978
996
  emitMutation('repaired stale install', 'manifest drift detected');
979
997
  }
980
- // Surface a transient "(updating…)" badge in the statusline before the
981
- // long-running upgrade work (manifest sync, daemon recycle, embeddings
982
- // migration). See #738 — section 3f flips this to a 2-min "completed"
983
- // badge once work finishes (TTL rationale at the constants above).
998
+ // Open the statusline notice before the long-running upgrade work
999
+ // (manifest sync, daemon recycle, embeddings migration). commitVersionStamp
1000
+ // flips it to a 2-min "completed" badge the moment the upgrade lands
1001
+ // (#1363; TTL rationale at the constants above). Nothing paints while
1002
+ // this value is live — Claude Code renders the statusline only after the
1003
+ // hook returns — so 'in-progress' surviving to paint time means the
1004
+ // launcher died before the sync finished, which is what the statusline's
1005
+ // stalled-upgrade rendering reports.
984
1006
  writeUpgradeNotice('in-progress');
985
1007
 
986
1008
  // Stop the daemon BEFORE any DB writes (#851). It was started under the
@@ -2425,19 +2447,16 @@ try {
2425
2447
  } catch { /* stderr write must not throw */ }
2426
2448
  }
2427
2449
 
2428
- // ── 3f. Flip the upgrade notice to "completed" (#636, #738) ─────────────────
2429
- // See the TTL rationale at the constants above for why we switch to a
2430
- // short-TTL completed badge instead of clearing the file.
2431
- //
2432
- // #1173: setting upgradeNoticeFinalized signals the exit handler (Option D
2433
- // above) that the notice reached its terminal 'completed' state cleanly, so
2434
- // the handler should NOT clear it on launcher exit. Without this flag the
2435
- // exit cleanup would race with the statusline reader and drop the short-TTL
2436
- // 'completed' badge the user is supposed to see.
2437
- if (upgradeNoticeContext) {
2438
- writeUpgradeNotice('completed');
2439
- upgradeNoticeFinalized = true;
2440
- }
2450
+ // ── 3f. (moved) Upgrade notice now completes at the version-stamp commit ────
2451
+ // The flip to 'completed' — and the upgradeNoticeFinalized flag that tells the
2452
+ // exit handler not to clear it — used to run here, at the very end of §3.
2453
+ // Everything between the sync block and this point is best-effort work that
2454
+ // routinely outlives the 5s SessionStart hook timeout, so on a slow upgrade
2455
+ // the launcher was killed before reaching it and the notice stayed
2456
+ // 'in-progress'. commitVersionStamp now owns the flip (#1363); see the
2457
+ // rationale there. Deliberately NOT reinstated as a fallback: a run that never
2458
+ // reached the stamp did not complete its upgrade, and flipping to 'completed'
2459
+ // here would paint success over exactly that failure.
2441
2460
 
2442
2461
  // ── 3g. (removed) Version stamp now commits eagerly on sync success ──────────
2443
2462
  // The stamp used to be deferred to here ("written LAST", #730). That made it
@@ -8,14 +8,11 @@ import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { errorDetail } from '../shared/utils/error-detail.js';
11
+ import { isProjectInitialized } from '../shared/utils/project-initialized.js';
11
12
  // Default configuration
12
13
  const DEFAULT_TOPOLOGY = 'hierarchical-mesh';
13
14
  const DEFAULT_MAX_AGENTS = 15;
14
- // Check if project is initialized
15
- function isInitialized(cwd) {
16
- const configPath = path.join(cwd, '.moflo', 'config.yaml');
17
- return fs.existsSync(configPath);
18
- }
15
+ // Check if project is initialized — shared with `flo status` (#1363).
19
16
  // Simple YAML parser for config (basic implementation)
20
17
  function parseSimpleYaml(content) {
21
18
  const result = {};
@@ -87,7 +84,7 @@ const startAction = async (ctx) => {
87
84
  const skipMcp = ctx.flags.skipMcp;
88
85
  const cwd = ctx.cwd;
89
86
  // Check initialization
90
- if (!isInitialized(cwd)) {
87
+ if (!isProjectInitialized(cwd)) {
91
88
  output.printError('MoFlo is not initialized in this directory');
92
89
  output.printInfo('Run "flo init" first to initialize');
93
90
  return { success: false, exitCode: 1 };
@@ -365,7 +362,7 @@ const quickCommand = {
365
362
  description: 'Quick start with default settings',
366
363
  action: async (ctx) => {
367
364
  // Initialize if needed
368
- if (!isInitialized(ctx.cwd)) {
365
+ if (!isProjectInitialized(ctx.cwd)) {
369
366
  output.printInfo('Project not initialized, running init first...');
370
367
  output.writeln();
371
368
  // Call init with minimal settings
@@ -4,9 +4,8 @@
4
4
  */
5
5
  import { output } from '../output.js';
6
6
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
7
  import * as os from 'os';
8
+ import { isProjectInitialized } from '../shared/utils/project-initialized.js';
10
9
  // Status refresh interval (ms)
11
10
  const DEFAULT_WATCH_INTERVAL = 2000;
12
11
  // Track CPU usage over time
@@ -32,11 +31,7 @@ function getProcessMemoryUsage() {
32
31
  const usedMemory = memoryUsage.heapUsed + memoryUsage.external;
33
32
  return (usedMemory / totalMemory) * 100;
34
33
  }
35
- // Check if project is initialized
36
- function isInitialized(cwd) {
37
- const configPath = path.join(cwd, '.moflo', 'config.yaml');
38
- return fs.existsSync(configPath);
39
- }
34
+ // Check if project is initialized — shared with `flo start` (#1363).
40
35
  // Format uptime
41
36
  function formatUptime(ms) {
42
37
  const seconds = Math.floor(ms / 1000);
@@ -257,7 +252,7 @@ const statusAction = async (ctx) => {
257
252
  const healthCheck = ctx.flags.healthCheck;
258
253
  const cwd = ctx.cwd;
259
254
  // Check initialization
260
- if (!isInitialized(cwd)) {
255
+ if (!isProjectInitialized(cwd)) {
261
256
  output.printError('MoFlo is not initialized in this directory');
262
257
  output.printInfo('Run "flo init" to initialize');
263
258
  return { success: false, exitCode: 1 };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared "is this project moflo-initialized?" predicate (#1363).
3
+ *
4
+ * `flo status` and `flo start` each carried their own copy that tested for
5
+ * `.moflo/config.yaml` and nothing else. That file is written by
6
+ * `writeRuntimeConfig()` in `src/cli/init/executor.ts` only under
7
+ * `if (options.components.runtime)`, so any consumer who inits with a component
8
+ * subset never receives it — and both commands then hard-failed with
9
+ * "MoFlo is not initialized in this directory" on an install with a healthy
10
+ * `.moflo/`, a running daemon, and a working MCP server.
11
+ *
12
+ * Nothing load-bears on `.moflo/config.yaml`: `src/cli/config/cli-config-store.ts`
13
+ * treats it as one candidate among several. So the gate is widened to any
14
+ * genuine init marker, and lives in one place rather than two.
15
+ *
16
+ * Cross-platform: `path.join` throughout, no separator or case assumptions.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ /**
21
+ * Config files that, alongside a `.moflo/` directory, mark a project as
22
+ * initialized. Paths are relative to the project root and joined per-platform.
23
+ */
24
+ const INIT_MARKERS = [
25
+ ['moflo.yaml'],
26
+ ['moflo.config.json'],
27
+ ['.moflo', 'config.yaml'],
28
+ ['.moflo', 'config.json'],
29
+ ];
30
+ function isDirectory(target) {
31
+ try {
32
+ return fs.statSync(target).isDirectory();
33
+ }
34
+ catch {
35
+ // ENOENT, ENOTDIR, EACCES — treat every failure as "not a directory"
36
+ // rather than letting a permissions edge case crash the command.
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * True when `cwd` looks like a moflo-initialized project.
42
+ *
43
+ * Requires the `.moflo/` state directory AND at least one recognized config
44
+ * marker — the directory alone can be left behind by a partial teardown, and a
45
+ * stray `moflo.yaml` alone means init never ran.
46
+ */
47
+ export function isProjectInitialized(cwd) {
48
+ if (!isDirectory(path.join(cwd, '.moflo')))
49
+ return false;
50
+ return INIT_MARKERS.some((segments) => fs.existsSync(path.join(cwd, ...segments)));
51
+ }
52
+ //# sourceMappingURL=project-initialized.js.map
@@ -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.12.4-rc.2';
5
+ export const VERSION = '4.12.4-rc.3';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.12.4-rc.2",
3
+ "version": "4.12.4-rc.3",
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",
@@ -98,7 +98,7 @@
98
98
  "@typescript-eslint/parser": "^8.65.0",
99
99
  "eslint": "^10.8.0",
100
100
  "glob": "^11.1.0",
101
- "moflo": "^4.12.4-rc.1",
101
+ "moflo": "^4.12.4-rc.2",
102
102
  "tsx": "^4.21.0",
103
103
  "typescript": "^5.9.3",
104
104
  "vitest": "^4.0.0"