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.
- package/.claude/helpers/statusline.cjs +19 -4
- package/bin/session-start-launcher.mjs +36 -17
- package/dist/src/cli/commands/start.js +4 -7
- package/dist/src/cli/commands/status.js +3 -8
- package/dist/src/cli/shared/utils/project-initialized.js +52 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
656
|
-
|
|
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 (
|
|
662
|
-
|
|
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
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
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.
|
|
2429
|
-
//
|
|
2430
|
-
//
|
|
2431
|
-
//
|
|
2432
|
-
//
|
|
2433
|
-
//
|
|
2434
|
-
//
|
|
2435
|
-
//
|
|
2436
|
-
//
|
|
2437
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.12.4-rc.
|
|
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.
|
|
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"
|