@maestrofrontier/frontier 1.5.0 → 1.6.0
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/frontier/progress.cjs +138 -0
- package/hooks/frontier-autorun.cjs +10 -2
- package/package.json +2 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — live progress file for the statusline.
|
|
3
|
+
//
|
|
4
|
+
// The armed autorun runs the panel->judge->synth pipeline inside a blocking
|
|
5
|
+
// UserPromptSubmit hook (silent to the chat until it returns). To give a live
|
|
6
|
+
// signal during that wait, the hook wires makeProgressWriter(scope) as the
|
|
7
|
+
// engine's onProgress callback; it writes frontier-progress.<scope>.json with
|
|
8
|
+
// the current stage as each stage starts. The context-bar statusline reads
|
|
9
|
+
// that file and renders a transient phase (ƒ⠿ fanning 2/3 -> ƒ⚖ judging ->
|
|
10
|
+
// ƒ✶ synth) in place of the static armed badge, then snaps back when the file
|
|
11
|
+
// is cleared. clearProgress removes it on completion; the statusline also
|
|
12
|
+
// ignores a file whose ts is stale (>300s), so a crashed run never pins a
|
|
13
|
+
// phantom phase.
|
|
14
|
+
//
|
|
15
|
+
// Scope + path resolution reuse frontier/config.cjs so the file the engine
|
|
16
|
+
// writes is byte-for-byte the path the statusline computes (cc-<hash>). The
|
|
17
|
+
// statusline only ever renders the whitelisted phase words + clamped integer
|
|
18
|
+
// counts from this file -- never raw bytes -- so the file is presentation
|
|
19
|
+
// data, not a trust boundary.
|
|
20
|
+
//
|
|
21
|
+
// .cjs so Node treats it as CommonJS regardless of a parent "type": "module".
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const { statePath } = require('./config.cjs');
|
|
28
|
+
|
|
29
|
+
// Phases the statusline knows how to render. The writer maps the engine's
|
|
30
|
+
// onProgress events onto exactly these; anything else is dropped.
|
|
31
|
+
const PHASES = ['panel', 'judge', 'synth', 'single'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Progress file path for a scope, derived from statePath so the scope-alias
|
|
35
|
+
* and default/suffix rules match the state file (and the statusline reader)
|
|
36
|
+
* exactly: frontier-state[.scope].json -> frontier-progress[.scope].json.
|
|
37
|
+
* @param {string} [scope]
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function progressPath(scope) {
|
|
41
|
+
const sp = statePath(scope);
|
|
42
|
+
return path.join(path.dirname(sp), path.basename(sp).replace('frontier-state', 'frontier-progress'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Atomic, symlink-refusing, 0600 write of the progress record. Mirrors
|
|
47
|
+
* saveState in config.cjs. Never throws — progress is best-effort telemetry.
|
|
48
|
+
* @param {string} scope
|
|
49
|
+
* @param {{ phase:string, done?:number, total?:number }} rec
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function writeProgress(scope, rec) {
|
|
53
|
+
if (!rec || PHASES.indexOf(rec.phase) === -1) return false;
|
|
54
|
+
const clampInt = (v) => {
|
|
55
|
+
const n = Math.floor(Number(v));
|
|
56
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
57
|
+
return n > 99 ? 99 : n;
|
|
58
|
+
};
|
|
59
|
+
const payload = JSON.stringify({
|
|
60
|
+
phase: rec.phase,
|
|
61
|
+
done: clampInt(rec.done),
|
|
62
|
+
total: clampInt(rec.total),
|
|
63
|
+
ts: Date.now(),
|
|
64
|
+
pid: process.pid,
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
const p = progressPath(scope);
|
|
68
|
+
const dir = path.dirname(p);
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
try { if (fs.lstatSync(dir).isSymbolicLink()) return false; } catch { return false; }
|
|
71
|
+
try {
|
|
72
|
+
if (fs.lstatSync(p).isSymbolicLink()) return false;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.code !== 'ENOENT') return false;
|
|
75
|
+
}
|
|
76
|
+
const tempPath = path.join(dir, '.frontier-progress.' + process.pid + '.' + Date.now() + '.tmp');
|
|
77
|
+
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
78
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
|
|
79
|
+
let fd;
|
|
80
|
+
try {
|
|
81
|
+
if (O_NOFOLLOW === 0) { try { if (fs.lstatSync(tempPath).isSymbolicLink()) return false; } catch {} }
|
|
82
|
+
fd = fs.openSync(tempPath, flags, 0o600);
|
|
83
|
+
fs.writeSync(fd, payload);
|
|
84
|
+
try { fs.fchmodSync(fd, 0o600); } catch {}
|
|
85
|
+
} finally {
|
|
86
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
87
|
+
}
|
|
88
|
+
fs.renameSync(tempPath, p);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove the progress file for a scope. Never throws.
|
|
97
|
+
* @param {string} scope
|
|
98
|
+
*/
|
|
99
|
+
function clearProgress(scope) {
|
|
100
|
+
try { fs.unlinkSync(progressPath(scope)); } catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build an onProgress(event) callback that writes the current stage to the
|
|
105
|
+
* progress file. Maps engine events -> statusline phases; ignores terminal
|
|
106
|
+
* events (panel-done/degraded/done — the caller clears the file on completion).
|
|
107
|
+
* Never throws.
|
|
108
|
+
* @param {string} scope
|
|
109
|
+
* @returns {(event:object)=>void}
|
|
110
|
+
*/
|
|
111
|
+
function makeProgressWriter(scope) {
|
|
112
|
+
return function onProgress(ev) {
|
|
113
|
+
if (!ev || typeof ev.phase !== 'string') return;
|
|
114
|
+
let rec = null;
|
|
115
|
+
switch (ev.phase) {
|
|
116
|
+
case 'panel-start':
|
|
117
|
+
rec = { phase: 'panel', done: 0, total: Array.isArray(ev.models) ? ev.models.length : 0 };
|
|
118
|
+
break;
|
|
119
|
+
case 'panel-progress':
|
|
120
|
+
rec = { phase: 'panel', done: ev.done, total: ev.total };
|
|
121
|
+
break;
|
|
122
|
+
case 'judge-start':
|
|
123
|
+
rec = { phase: 'judge', done: 0, total: 0 };
|
|
124
|
+
break;
|
|
125
|
+
case 'synth-start':
|
|
126
|
+
rec = { phase: 'synth', done: 0, total: 0 };
|
|
127
|
+
break;
|
|
128
|
+
case 'single-start':
|
|
129
|
+
rec = { phase: 'single', done: 0, total: 0 };
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
writeProgress(scope, rec);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { progressPath, writeProgress, clearProgress, makeProgressWriter, PHASES };
|
|
@@ -43,10 +43,11 @@ const fusionDepth = parseInt(process.env.FUSION_DEPTH || '0', 10);
|
|
|
43
43
|
if (Number.isFinite(fusionDepth) && fusionDepth >= 1) noop();
|
|
44
44
|
|
|
45
45
|
let state;
|
|
46
|
+
let scope;
|
|
46
47
|
try {
|
|
47
48
|
const cfg = require('../frontier/config.cjs');
|
|
48
49
|
const cwd = data.cwd || process.env.CLAUDE_PROJECT_DIR || process.env.CODEX_PROJECT_DIR || process.cwd();
|
|
49
|
-
|
|
50
|
+
scope = cfg.resolveScope([], { cwd });
|
|
50
51
|
state = cfg.loadState(scope);
|
|
51
52
|
} catch {
|
|
52
53
|
noop();
|
|
@@ -73,14 +74,21 @@ run().catch((e) => {
|
|
|
73
74
|
async function run() {
|
|
74
75
|
let result;
|
|
75
76
|
const runStart = Date.now();
|
|
77
|
+
// Live statusline progress: write the current stage as each one starts so
|
|
78
|
+
// the context-bar can show ƒ⠿ fanning / ƒ⚖ judging / ƒ✶ synth during the
|
|
79
|
+
// otherwise-silent blocking run. Cleared below on completion or error.
|
|
80
|
+
const progress = require('../frontier/progress.cjs');
|
|
81
|
+
const onProgress = progress.makeProgressWriter(scope);
|
|
76
82
|
try {
|
|
77
83
|
const { runFrontier } = require('../frontier/run.cjs');
|
|
78
|
-
result = await runFrontier({ prompt, state });
|
|
84
|
+
result = await runFrontier({ prompt, state, deps: { onProgress } });
|
|
79
85
|
} catch (e) {
|
|
86
|
+
progress.clearProgress(scope);
|
|
80
87
|
process.stderr.write('frontier-autorun: ' + ((e && e.message) || e) + '\n');
|
|
81
88
|
noop();
|
|
82
89
|
}
|
|
83
90
|
const runMs = Date.now() - runStart;
|
|
91
|
+
progress.clearProgress(scope);
|
|
84
92
|
|
|
85
93
|
if (!result || result.status !== 'ok' || !result.final) {
|
|
86
94
|
if (result && result.status === 'error') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maestrofrontier/frontier",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Achieve Frontier AI performance in your CLI by fusing the model CLIs you already run. Maestro Frontier is an opt-in, zero-dependency local multi-CLI fusion engine for AI coding agents: fan a prompt across a panel of any 1 to 8 local model CLIs you pick, have a judge model and a synthesizer you choose read the answers into a structured analysis and write one grounded synthesis (default Opus 4.8, override either with --judge/--synth). On a 100-task benchmark every fusion panel outscored its individual member models. Three adapters ship today: Opus 4.8, GPT-5.5, Gemini 3.1 Pro, with Kimi, DeepSeek, GLM, and Qwen to follow. Off, single, and fusion modes switch via /maestro:frontier. Built on Maestro orchestration discipline: decision-gated routing, verified done-claims, surgical scope, and structural enforcement hooks.",
|
|
5
5
|
"keywords": ["multi-cli-fusion", "fusion-engine", "frontier", "multi-agent", "orchestration", "claude-code", "gemini", "codex", "agents", "hooks", "doctrine"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"frontier/config.cjs",
|
|
21
21
|
"frontier/dispatch.cjs",
|
|
22
22
|
"frontier/judge.cjs",
|
|
23
|
+
"frontier/progress.cjs",
|
|
23
24
|
"frontier/run.cjs",
|
|
24
25
|
"frontier/schema.cjs",
|
|
25
26
|
"frontier/semaphore.cjs",
|