moflo 4.9.4 → 4.9.5

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.
@@ -44,6 +44,10 @@ flo-search "[query]" --namespace guidance # Semantic search
44
44
  flo doctor --fix # Health check
45
45
  \`\`\`
46
46
 
47
+ ### After upgrading MoFlo
48
+
49
+ After running \`npm install moflo@*\` (or \`npm install\` that touches moflo), check \`.moflo/restart-pending.json\`. If it exists, surface the \`message\` field to the user verbatim (Claude Code can't pick up new hooks/MCP tools/launcher mid-session — they're only loaded at session-start), then delete the file.
50
+
47
51
  ### Full Reference
48
52
 
49
53
  - **Subagents protocol:** \`.claude/guidance/shipped/moflo-subagents.md\`
@@ -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.9.4';
5
+ export const VERSION = '4.9.5';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.4",
3
+ "version": "4.9.5",
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",
@@ -80,7 +80,7 @@
80
80
  "@typescript-eslint/eslint-plugin": "^7.18.0",
81
81
  "@typescript-eslint/parser": "^7.18.0",
82
82
  "eslint": "^8.0.0",
83
- "moflo": "^4.9.3",
83
+ "moflo": "^4.9.4",
84
84
  "tsx": "^4.21.0",
85
85
  "typescript": "^5.9.3",
86
86
  "vitest": "^4.0.0"
@@ -1,25 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Postinstall restart-nudge banner.
3
+ * Postinstall restart-nudge — drops a notice file Claude reads after upgrade.
4
4
  *
5
- * When `npm install` runs inside Claude Code (typically because the user
6
- * asked Claude to upgrade moflo), the just-installed bits are sitting on
7
- * disk but the running session still has the OLD launcher, hooks, MCP
8
- * server, and statusline loaded. The session-start launcher only re-reads
9
- * them on the NEXT session-start so the upgrade is inert until the user
10
- * exits and reopens Claude Code.
5
+ * Problem this solves:
6
+ * When `npm install` runs inside Claude Code (typically because the user
7
+ * asked Claude to upgrade moflo), the just-installed bits are on disk but
8
+ * the running session still has the OLD launcher, hooks, MCP server, and
9
+ * statusline loaded. The launcher only re-reads them on the NEXT
10
+ * session-start the upgrade is inert until the user restarts.
11
11
  *
12
- * This script prints a banner that npm relays back to Claude as the install
13
- * stdout. The phrasing names Claude Code explicitly so the assistant
14
- * surfaces it to the user as a restart prompt.
12
+ * The original v4.9.4 design printed a banner to stdout, expecting npm to
13
+ * relay it. It does not: npm 7+ defaults to `foreground-scripts: false`
14
+ * and captures install-script stdout/stderr into log files. The banner
15
+ * never reached Claude. (#856.)
16
+ *
17
+ * Fix:
18
+ * This script drops `<project>/.moflo/restart-pending.json` on every
19
+ * relevant install. Claude is instructed (via the moflo CLAUDE.md
20
+ * injection) to read + surface + delete the file after running
21
+ * `npm install moflo@*`. No reliance on npm cooperating with stdout.
22
+ *
23
+ * The banner is still printed to stdout for the rare `--foreground-scripts`
24
+ * user, and the dedup tracker is preserved so repeat postinstalls of the
25
+ * same version don't double-write the notice.
26
+ *
27
+ * Files written:
28
+ * - .moflo/restart-pending.json (the payload Claude reads)
29
+ * - .moflo/last-install-banner.json (dedup tracker, version-stamped)
15
30
  *
16
31
  * Gating:
17
- * - Only prints when CLAUDE_PROJECT_DIR or CLAUDECODE is set (avoids
18
- * noise on CI and non-Claude installs).
19
- * - Dedupes by version: only prints once per (consumer-project, version)
20
- * pair, so unrelated `npm install` runs that re-trigger postinstall
21
- * don't re-spam the banner. Tracker lives at
22
- * `<project>/.moflo/last-install-banner.json`.
32
+ * - Only fires when CLAUDE_PROJECT_DIR or CLAUDECODE is set; non-Claude
33
+ * installs and CI stay silent.
34
+ * - Dedupes by version: same (project, version) pair won't re-write.
23
35
  *
24
36
  * Failure posture: never blocks an install. Errors are swallowed; exit 0.
25
37
  */
@@ -52,7 +64,7 @@ function installedVersion() {
52
64
  }
53
65
  }
54
66
 
55
- function readLastBanner(trackerPath) {
67
+ function readTrackedVersion(trackerPath) {
56
68
  if (!existsSync(trackerPath)) return null;
57
69
  try {
58
70
  return JSON.parse(readFileSync(trackerPath, 'utf-8')).version || null;
@@ -61,33 +73,36 @@ function readLastBanner(trackerPath) {
61
73
  }
62
74
  }
63
75
 
64
- function writeLastBanner(trackerPath, version) {
76
+ function writeJson(filePath, payload) {
65
77
  try {
66
- mkdirSync(dirname(trackerPath), { recursive: true });
67
- writeFileSync(trackerPath, JSON.stringify({
68
- version,
69
- shownAt: new Date().toISOString(),
70
- }, null, 2));
71
- } catch { /* non-fatal */ }
78
+ mkdirSync(dirname(filePath), { recursive: true });
79
+ writeFileSync(filePath, JSON.stringify(payload, null, 2));
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
72
84
  }
73
85
 
74
- function printBanner(version) {
75
- // Phrased to name Claude Code by name so the assistant surfaces it as a
76
- // restart instruction rather than a generic install log line.
77
- const lines = [
78
- '',
79
- '═══════════════════════════════════════════════════════════════════',
80
- ` MoFlo ${version} installed.`,
86
+ function buildMessage(version) {
87
+ // Plain-text payload Claude relays verbatim. Names Claude Code explicitly
88
+ // so the assistant frames it as a restart instruction, not a log line.
89
+ return [
90
+ `MoFlo ${version} installed.`,
81
91
  '',
82
- ' Claude Code: please restart your session to load the new MoFlo.',
92
+ 'Please restart Claude Code to load the new MoFlo.',
83
93
  '',
84
- ' Hooks, MCP tools, statusline, and the session-start launcher are',
85
- ' loaded once at session-start — the running session is still on',
86
- ' the previous moflo until you exit and reopen Claude Code.',
87
- '═══════════════════════════════════════════════════════════════════',
88
- '',
89
- ];
90
- process.stdout.write(lines.join('\n'));
94
+ 'Hooks, MCP tools, statusline, and the session-start launcher are',
95
+ 'loaded once at session-start — the running session is still on the',
96
+ 'previous moflo until you exit and reopen Claude Code.',
97
+ ].join('\n');
98
+ }
99
+
100
+ function printBanner(version, message) {
101
+ // Stdout fallback for --foreground-scripts users. With npm's default
102
+ // config this output is captured and never seen — that's why the notice
103
+ // file exists. Kept anyway because it costs nothing.
104
+ const border = '═'.repeat(67);
105
+ process.stdout.write(`\n${border}\n MoFlo ${version} installed.\n\n ⚠ ${message.split('\n')[2]}\n${border}\n\n`);
91
106
  }
92
107
 
93
108
  function run() {
@@ -98,12 +113,19 @@ function run() {
98
113
 
99
114
  const projectRoot = consumerProjectRoot();
100
115
  const trackerPath = join(projectRoot, '.moflo', 'last-install-banner.json');
101
- const lastShown = readLastBanner(trackerPath);
116
+ const noticePath = join(projectRoot, '.moflo', 'restart-pending.json');
117
+
118
+ const lastShown = readTrackedVersion(trackerPath);
102
119
  if (lastShown === version) return { fired: false, reason: 'already-shown' };
103
120
 
104
- printBanner(version);
105
- writeLastBanner(trackerPath, version);
106
- return { fired: true, version };
121
+ const writtenAt = new Date().toISOString();
122
+ const message = buildMessage(version);
123
+
124
+ writeJson(noticePath, { version, writtenAt, message });
125
+ writeJson(trackerPath, { version, shownAt: writtenAt });
126
+
127
+ printBanner(version, message);
128
+ return { fired: true, version, noticePath };
107
129
  }
108
130
 
109
131
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {