@forwardimpact/outpost 3.1.2 → 3.1.4
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/bin/fit-outpost.js +23 -2
- package/package.json +4 -3
- package/src/agent-runner.js +59 -19
- package/src/index.js +2 -2
- package/src/kb-manager.js +76 -75
- package/src/outpost.js +385 -340
- package/src/scheduler.js +47 -8
- package/src/socket-server.js +76 -45
- package/src/state-manager.js +32 -35
package/src/outpost.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
1
|
// Outpost — CLI and scheduler for autonomous agent teams.
|
|
4
2
|
//
|
|
5
3
|
// Usage:
|
|
@@ -12,406 +10,453 @@
|
|
|
12
10
|
// fit-outpost validate Validate agent definitions exist
|
|
13
11
|
// fit-outpost status Show agent status
|
|
14
12
|
// fit-outpost --help Show this help
|
|
13
|
+
//
|
|
14
|
+
// This module owns the CLI definition and dispatch table. The runtime
|
|
15
|
+
// collaborator bag is constructed once in bin/fit-outpost.js (the sole
|
|
16
|
+
// construction site) and threaded into `run(runtime, version)`; `run` returns
|
|
17
|
+
// the process exit code and the bin translates it to `runtime.proc.exit`.
|
|
15
18
|
|
|
16
|
-
import {
|
|
17
|
-
readFileSync,
|
|
18
|
-
writeFileSync,
|
|
19
|
-
existsSync,
|
|
20
|
-
mkdirSync,
|
|
21
|
-
readdirSync,
|
|
22
|
-
copyFileSync,
|
|
23
|
-
cpSync,
|
|
24
|
-
appendFileSync,
|
|
25
|
-
} from "node:fs";
|
|
26
19
|
import { join, dirname, resolve } from "node:path";
|
|
27
20
|
import { homedir } from "node:os";
|
|
28
|
-
import { fileURLToPath } from "node:url";
|
|
29
21
|
import { createCli } from "@forwardimpact/libcli";
|
|
30
22
|
import { createLogger } from "@forwardimpact/libtelemetry";
|
|
23
|
+
import { isoTimestamp } from "@forwardimpact/libutil";
|
|
31
24
|
|
|
32
25
|
const logger = createLogger("outpost");
|
|
33
26
|
|
|
34
|
-
import * as posixSpawn from "@forwardimpact/libmacos/posix-spawn";
|
|
35
27
|
import { StateManager } from "./state-manager.js";
|
|
36
28
|
import { AgentRunner } from "./agent-runner.js";
|
|
37
|
-
import { Scheduler } from "./scheduler.js";
|
|
29
|
+
import { Scheduler, formatLocalTime } from "./scheduler.js";
|
|
38
30
|
import { KBManager } from "./kb-manager.js";
|
|
39
31
|
import { SocketServer, requestShutdown } from "./socket-server.js";
|
|
40
32
|
|
|
41
|
-
// --- Paths -------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
const HOME = homedir();
|
|
44
|
-
const OUTPOST_HOME = join(HOME, ".fit", "outpost");
|
|
45
|
-
const CONFIG_PATH = join(OUTPOST_HOME, "scheduler.json");
|
|
46
|
-
const STATE_PATH = join(OUTPOST_HOME, "state.json");
|
|
47
|
-
const LOG_DIR = join(OUTPOST_HOME, "logs");
|
|
48
|
-
const CACHE_DIR = join(HOME, ".cache", "fit", "outpost");
|
|
49
|
-
const __dirname =
|
|
50
|
-
import.meta.dirname || dirname(fileURLToPath(import.meta.url));
|
|
51
33
|
const SHARE_DIR = "/usr/local/share/fit-outpost";
|
|
52
|
-
const SOCKET_PATH = join(OUTPOST_HOME, "outpost.sock");
|
|
53
|
-
|
|
54
|
-
// In compiled binaries (bun build --compile), `bun build --define` injects the
|
|
55
|
-
// version string here so the readFileSync branch is eliminated as dead code.
|
|
56
|
-
// Source execution (bun src/outpost.js) falls through to package.json.
|
|
57
|
-
const VERSION =
|
|
58
|
-
process.env.OUTPOST_VERSION ||
|
|
59
|
-
JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"))
|
|
60
|
-
.version;
|
|
61
|
-
|
|
62
|
-
// --- Logging -----------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
function createFileLogger(logDir, fs) {
|
|
65
|
-
if (!logDir) throw new Error("logDir is required");
|
|
66
|
-
if (!fs) throw new Error("fs is required");
|
|
67
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
68
|
-
return function log(msg) {
|
|
69
|
-
const ts = new Date().toISOString();
|
|
70
|
-
const line = `[${ts}] ${msg}`;
|
|
71
|
-
logger.info(line);
|
|
72
|
-
fs.appendFileSync(
|
|
73
|
-
join(logDir, `scheduler-${ts.slice(0, 10)}.log`),
|
|
74
|
-
line + "\n",
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const log = createFileLogger(LOG_DIR, { mkdirSync, appendFileSync });
|
|
80
34
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Build the CLI definition. The object is byte-identical to the libcli
|
|
37
|
+
* definition the goldens were captured against, so `--help` / `--version`
|
|
38
|
+
* output stays stable.
|
|
39
|
+
* @param {string} version
|
|
40
|
+
* @returns {object}
|
|
41
|
+
*/
|
|
42
|
+
function buildDefinition(version) {
|
|
43
|
+
return {
|
|
44
|
+
name: "fit-outpost",
|
|
45
|
+
version,
|
|
46
|
+
description: "Schedule autonomous agents across knowledge bases",
|
|
47
|
+
commands: [
|
|
48
|
+
{ name: "daemon", description: "Run continuously (poll every 60s)" },
|
|
49
|
+
{
|
|
50
|
+
name: "wake",
|
|
51
|
+
args: "<agent>",
|
|
52
|
+
description: "Wake a specific agent immediately",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "init",
|
|
56
|
+
args: "<path>",
|
|
57
|
+
description: "Initialize a new knowledge base",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "update",
|
|
61
|
+
args: "[path]",
|
|
62
|
+
description: "Update KB with latest CLAUDE.md, agents and skills",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "stop",
|
|
66
|
+
description: "Gracefully stop daemon and all running agents",
|
|
67
|
+
},
|
|
68
|
+
{ name: "validate", description: "Validate agent definitions exist" },
|
|
69
|
+
{ name: "status", description: "Show agent status" },
|
|
70
|
+
],
|
|
71
|
+
globalOptions: {
|
|
72
|
+
help: { type: "boolean", short: "h", description: "Show this help" },
|
|
73
|
+
version: { type: "boolean", description: "Show version" },
|
|
74
|
+
json: { type: "boolean", description: "JSON output (with --help)" },
|
|
75
|
+
},
|
|
76
|
+
documentation: [
|
|
77
|
+
{
|
|
78
|
+
title: "Outpost Overview",
|
|
79
|
+
url: "https://www.forwardimpact.team/outpost/index.md",
|
|
80
|
+
description: "Product overview, audience model, and key concepts.",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: "Getting Started: Outpost for Engineers",
|
|
84
|
+
url: "https://www.forwardimpact.team/docs/getting-started/engineers/outpost/index.md",
|
|
85
|
+
description: "From zero to your first daily briefing.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: "Keep Track of Context Without Effort",
|
|
89
|
+
url: "https://www.forwardimpact.team/docs/products/knowledge-systems/index.md",
|
|
90
|
+
description:
|
|
91
|
+
"Maintain continuous awareness of people, projects, and threads.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: "Walk Into Every Meeting Already Oriented",
|
|
95
|
+
url: "https://www.forwardimpact.team/docs/products/knowledge-systems/meeting-prep/index.md",
|
|
96
|
+
description: "Assemble context so you arrive prepared.",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Render an agent's multi-line status block (pure formatting).
|
|
104
|
+
* @param {string} name
|
|
105
|
+
* @param {Object} agent
|
|
106
|
+
* @param {Object} s - The agent's persisted state.
|
|
107
|
+
* @param {boolean} kbMissing - Whether the agent's kb path is absent.
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function renderAgentStatus(name, agent, s, kbMissing) {
|
|
111
|
+
const enabledMark = agent.enabled !== false ? "+" : "-";
|
|
112
|
+
const kbStatus = kbMissing ? " (not found)" : "";
|
|
113
|
+
const lastWake = s.lastWokeAt ? formatLocalTime(s.lastWokeAt) : "never";
|
|
114
|
+
const lines = [
|
|
115
|
+
` ${enabledMark} ${name}`,
|
|
116
|
+
` KB: ${agent.kb || "(none)"}${kbStatus} Schedule: ${JSON.stringify(agent.schedule)}`,
|
|
117
|
+
` Status: ${s.status || "never-woken"} Last wake: ${lastWake} Wakes: ${s.wakeCount || 0}`,
|
|
118
|
+
];
|
|
119
|
+
if (s.lastAction) lines.push(` Last action: ${s.lastAction}`);
|
|
120
|
+
if (s.lastDecision) lines.push(` Last decision: ${s.lastDecision}`);
|
|
121
|
+
if (s.lastError) lines.push(` Error: ${s.lastError.slice(0, 80)}`);
|
|
122
|
+
return lines.join("\n");
|
|
93
123
|
}
|
|
94
124
|
|
|
95
|
-
// --- Wire dependencies -------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
const fsOps = { readFileSync, writeFileSync, mkdirSync };
|
|
98
|
-
const stateManager = new StateManager(STATE_PATH, fsOps);
|
|
99
|
-
const agentRunner = new AgentRunner(posixSpawn, stateManager, log, CACHE_DIR);
|
|
100
|
-
const scheduler = new Scheduler(loadConfig, stateManager, agentRunner, log);
|
|
101
|
-
const kbManager = new KBManager(
|
|
102
|
-
{
|
|
103
|
-
existsSync,
|
|
104
|
-
mkdirSync,
|
|
105
|
-
copyFileSync,
|
|
106
|
-
cpSync,
|
|
107
|
-
readFileSync,
|
|
108
|
-
writeFileSync,
|
|
109
|
-
readdirSync,
|
|
110
|
-
},
|
|
111
|
-
log,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// --- Template dir resolution -------------------------------------------------
|
|
115
|
-
|
|
116
125
|
/**
|
|
117
|
-
*
|
|
118
|
-
* @
|
|
126
|
+
* Run the Outpost CLI.
|
|
127
|
+
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
128
|
+
* Injected collaborator bag (constructed in the bin).
|
|
129
|
+
* @param {string} version - Resolved CLI version string.
|
|
130
|
+
* @returns {Promise<number>} Process exit code.
|
|
119
131
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
132
|
+
export async function run(runtime, version) {
|
|
133
|
+
const { fs, proc, clock } = runtime;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Async existence check via the one fs surface this module uses.
|
|
137
|
+
* @param {string} p
|
|
138
|
+
* @returns {Promise<boolean>}
|
|
139
|
+
*/
|
|
140
|
+
const exists = (p) =>
|
|
141
|
+
fs.access(p).then(
|
|
142
|
+
() => true,
|
|
143
|
+
() => false,
|
|
144
|
+
);
|
|
134
145
|
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
// --- Paths -----------------------------------------------------------------
|
|
147
|
+
const HOME = homedir();
|
|
148
|
+
const OUTPOST_HOME = join(HOME, ".fit", "outpost");
|
|
149
|
+
const CONFIG_PATH = join(OUTPOST_HOME, "scheduler.json");
|
|
150
|
+
const STATE_PATH = join(OUTPOST_HOME, "state.json");
|
|
151
|
+
const LOG_DIR = join(OUTPOST_HOME, "logs");
|
|
152
|
+
const CACHE_DIR = join(HOME, ".cache", "fit", "outpost");
|
|
153
|
+
const SOCKET_PATH = join(OUTPOST_HOME, "outpost.sock");
|
|
154
|
+
const PKG_DIR = dirname(import.meta.dirname);
|
|
155
|
+
|
|
156
|
+
// --- Logging ---------------------------------------------------------------
|
|
157
|
+
await fs.mkdir(LOG_DIR, { recursive: true });
|
|
158
|
+
function log(msg) {
|
|
159
|
+
const ts = isoTimestamp(clock.now());
|
|
160
|
+
const line = `[${ts}] ${msg}`;
|
|
161
|
+
logger.info(line);
|
|
162
|
+
void fs.appendFile(
|
|
163
|
+
join(LOG_DIR, `scheduler-${ts.slice(0, 10)}.log`),
|
|
164
|
+
line + "\n",
|
|
165
|
+
);
|
|
140
166
|
}
|
|
141
|
-
for (const d of [
|
|
142
|
-
join(SHARE_DIR, "templates"),
|
|
143
|
-
join(__dirname, "..", "templates"),
|
|
144
|
-
])
|
|
145
|
-
if (existsSync(d)) return d;
|
|
146
|
-
console.error("Template not found. Reinstall fit-outpost.");
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
167
|
|
|
150
|
-
// ---
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
// --- Config ----------------------------------------------------------------
|
|
169
|
+
async function loadConfig() {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(await fs.readFile(CONFIG_PATH, "utf8"));
|
|
172
|
+
} catch {
|
|
173
|
+
return { agents: {} };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
156
176
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
function expandPath(p) {
|
|
178
|
+
return p.startsWith("~/") ? join(HOME, p.slice(2)) : resolve(p);
|
|
179
|
+
}
|
|
160
180
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
// --- Wire dependencies -----------------------------------------------------
|
|
182
|
+
// posix-spawn is a Bun-FFI module (`bun:ffi`); importing it eagerly would
|
|
183
|
+
// crash plain `node` (e.g. `--help`/`--version`/golden capture). Load it
|
|
184
|
+
// lazily so only an actual agent wake — which only runs under Bun on macOS —
|
|
185
|
+
// pulls it in.
|
|
186
|
+
const loadSpawn = () => import("@forwardimpact/libmacos/posix-spawn");
|
|
187
|
+
const stateManager = new StateManager(STATE_PATH, runtime);
|
|
188
|
+
const agentRunner = new AgentRunner(
|
|
189
|
+
loadSpawn,
|
|
165
190
|
stateManager,
|
|
166
|
-
loadConfig,
|
|
167
191
|
log,
|
|
168
192
|
CACHE_DIR,
|
|
169
|
-
|
|
193
|
+
runtime,
|
|
194
|
+
);
|
|
195
|
+
const scheduler = new Scheduler(
|
|
196
|
+
loadConfig,
|
|
197
|
+
stateManager,
|
|
198
|
+
agentRunner,
|
|
199
|
+
log,
|
|
200
|
+
runtime,
|
|
170
201
|
);
|
|
171
|
-
|
|
202
|
+
const kbManager = new KBManager(runtime, log);
|
|
172
203
|
|
|
173
|
-
|
|
204
|
+
// --- Template dir resolution -----------------------------------------------
|
|
205
|
+
async function getBundlePath() {
|
|
174
206
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
207
|
+
const exe = process.execPath || "";
|
|
208
|
+
const macosDir = dirname(exe);
|
|
209
|
+
const contentsDir = dirname(macosDir);
|
|
210
|
+
const resourcesDir = join(contentsDir, "Resources");
|
|
211
|
+
if (await exists(join(resourcesDir, "config"))) {
|
|
212
|
+
return { bundle: dirname(contentsDir), resources: resourcesDir };
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
/* not in bundle */
|
|
178
216
|
}
|
|
179
|
-
|
|
217
|
+
return null;
|
|
180
218
|
}
|
|
181
|
-
tick();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// --- Update ------------------------------------------------------------------
|
|
185
219
|
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
220
|
+
async function requireTemplateDir() {
|
|
221
|
+
const bundle = await getBundlePath();
|
|
222
|
+
if (bundle) {
|
|
223
|
+
const tpl = join(bundle.resources, "templates");
|
|
224
|
+
if (await exists(tpl)) return tpl;
|
|
225
|
+
}
|
|
226
|
+
for (const d of [join(SHARE_DIR, "templates"), join(PKG_DIR, "templates")])
|
|
227
|
+
if (await exists(d)) return d;
|
|
228
|
+
proc.stderr.write("Template not found. Reinstall fit-outpost.\n");
|
|
229
|
+
return null;
|
|
190
230
|
}
|
|
191
231
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
232
|
+
// --- Daemon ----------------------------------------------------------------
|
|
233
|
+
async function daemon() {
|
|
234
|
+
const daemonStartedAt = clock.now();
|
|
235
|
+
log("Scheduler daemon started. Polling every 60 seconds.");
|
|
236
|
+
log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
|
|
237
|
+
|
|
238
|
+
// Reset any agents left "active" from a previous daemon session.
|
|
239
|
+
const state = await stateManager.load();
|
|
240
|
+
await stateManager.resetStaleAgents(
|
|
241
|
+
state,
|
|
242
|
+
{ reason: "Daemon restarted" },
|
|
243
|
+
log,
|
|
244
|
+
);
|
|
200
245
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
const socketServer = new SocketServer(
|
|
247
|
+
SOCKET_PATH,
|
|
248
|
+
scheduler,
|
|
249
|
+
agentRunner,
|
|
250
|
+
stateManager,
|
|
251
|
+
loadConfig,
|
|
252
|
+
log,
|
|
253
|
+
CACHE_DIR,
|
|
254
|
+
daemonStartedAt,
|
|
255
|
+
runtime,
|
|
205
256
|
);
|
|
206
|
-
|
|
207
|
-
|
|
257
|
+
socketServer.start();
|
|
258
|
+
|
|
259
|
+
let stopped = false;
|
|
260
|
+
let tickHandle;
|
|
261
|
+
void socketServer.whenStopped().then(() => {
|
|
262
|
+
stopped = true;
|
|
263
|
+
// Cancel any pending poll so the armed timer does not keep the event
|
|
264
|
+
// loop alive after shutdown — `run()` returns 0 and the bin exits only
|
|
265
|
+
// on a nonzero code, so a lingering 60s timer would delay exit.
|
|
266
|
+
if (tickHandle !== undefined) clock.clearTimeout(tickHandle);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
async function tick() {
|
|
270
|
+
if (stopped) return;
|
|
271
|
+
try {
|
|
272
|
+
await scheduler.wakeDueAgents();
|
|
273
|
+
} catch (err) {
|
|
274
|
+
log(`Error: ${err.message}`);
|
|
275
|
+
}
|
|
276
|
+
if (!stopped) tickHandle = clock.setTimeout(tick, 60_000);
|
|
277
|
+
}
|
|
278
|
+
void tick();
|
|
208
279
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
280
|
+
// Block until a shutdown is requested via socket or signal; the bin then
|
|
281
|
+
// owns the process-exit call.
|
|
282
|
+
await socketServer.whenStopped();
|
|
283
|
+
return 0;
|
|
212
284
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --- Status ------------------------------------------------------------------
|
|
216
285
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (s.lastDecision) lines.push(` Last decision: ${s.lastDecision}`);
|
|
231
|
-
if (s.lastError) lines.push(` Error: ${s.lastError.slice(0, 80)}`);
|
|
232
|
-
return lines.join("\n");
|
|
233
|
-
}
|
|
286
|
+
// --- Update ----------------------------------------------------------------
|
|
287
|
+
async function runUpdate(args) {
|
|
288
|
+
const tpl = await requireTemplateDir();
|
|
289
|
+
if (tpl === null) return 1;
|
|
290
|
+
|
|
291
|
+
if (args[0]) {
|
|
292
|
+
const result = await kbManager.update(args[0], tpl);
|
|
293
|
+
if (!result.ok) {
|
|
294
|
+
proc.stderr.write(result.error + "\n");
|
|
295
|
+
return result.code;
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
234
299
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
300
|
+
const config = await loadConfig();
|
|
301
|
+
const kbPaths = [
|
|
302
|
+
...new Set(
|
|
303
|
+
Object.values(config.agents)
|
|
304
|
+
.filter((a) => a.kb)
|
|
305
|
+
.map((a) => expandPath(a.kb)),
|
|
306
|
+
),
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
if (kbPaths.length === 0) {
|
|
310
|
+
proc.stderr.write(
|
|
311
|
+
"No knowledge bases configured and no path given.\n" +
|
|
312
|
+
"Usage: fit-outpost update [path]\n",
|
|
313
|
+
);
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
239
316
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
317
|
+
for (const kb of kbPaths) {
|
|
318
|
+
logger.info(`\nUpdating ${kb}...`);
|
|
319
|
+
const result = await kbManager.update(kb, tpl);
|
|
320
|
+
if (!result.ok) {
|
|
321
|
+
proc.stderr.write(result.error + "\n");
|
|
322
|
+
return result.code;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return 0;
|
|
244
326
|
}
|
|
245
327
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
328
|
+
// --- Status ----------------------------------------------------------------
|
|
329
|
+
async function formatAgentStatus(name, agent, s) {
|
|
330
|
+
const kbMissing = agent.kb ? !(await exists(expandPath(agent.kb))) : false;
|
|
331
|
+
return renderAgentStatus(name, agent, s, kbMissing);
|
|
249
332
|
}
|
|
250
|
-
}
|
|
251
333
|
|
|
252
|
-
|
|
334
|
+
async function showStatus() {
|
|
335
|
+
const config = await loadConfig();
|
|
336
|
+
const state = await stateManager.load();
|
|
337
|
+
logger.info("\nOutpost Scheduler\n==================\n");
|
|
253
338
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
339
|
+
const agents = Object.entries(config.agents || {});
|
|
340
|
+
if (agents.length === 0) {
|
|
341
|
+
logger.info(
|
|
342
|
+
`No agents configured.\n\nEdit ${CONFIG_PATH} to add agents.`,
|
|
343
|
+
);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
261
346
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (!agent.kb) {
|
|
270
|
-
logger.info(` [FAIL] ${name}: no "kb" path specified`);
|
|
271
|
-
return false;
|
|
347
|
+
logger.info("Agents:");
|
|
348
|
+
for (const [name, agent] of agents) {
|
|
349
|
+
logger.info(
|
|
350
|
+
await formatAgentStatus(name, agent, state.agents[name] || {}),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return 0;
|
|
272
354
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
355
|
+
|
|
356
|
+
// --- Validate --------------------------------------------------------------
|
|
357
|
+
async function findInLocalOrGlobal(kbPath, subPath) {
|
|
358
|
+
const local = join(kbPath, ".claude", subPath);
|
|
359
|
+
const global = join(HOME, ".claude", subPath);
|
|
360
|
+
if (await exists(local)) return local;
|
|
361
|
+
if (await exists(global)) return global;
|
|
362
|
+
return null;
|
|
277
363
|
}
|
|
278
364
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
365
|
+
async function validateAgent(name, agent) {
|
|
366
|
+
if (!agent.kb) {
|
|
367
|
+
logger.info(` [FAIL] ${name}: no "kb" path specified`);
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const kbPath = expandPath(agent.kb);
|
|
371
|
+
if (!(await exists(kbPath))) {
|
|
372
|
+
logger.info(` [FAIL] ${name}: path not found: ${kbPath}`);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
286
375
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return;
|
|
376
|
+
const agentFile = join("agents", name + ".md");
|
|
377
|
+
const found = await findInLocalOrGlobal(kbPath, agentFile);
|
|
378
|
+
logger.info(
|
|
379
|
+
` [${found ? "OK" : "FAIL"}] ${name}: agent definition${found ? "" : " not found"}`,
|
|
380
|
+
);
|
|
381
|
+
return !!found;
|
|
293
382
|
}
|
|
294
383
|
|
|
295
|
-
|
|
296
|
-
|
|
384
|
+
async function validate() {
|
|
385
|
+
const config = await loadConfig();
|
|
386
|
+
const agents = Object.entries(config.agents || {});
|
|
387
|
+
if (agents.length === 0) {
|
|
388
|
+
logger.info("No agents configured. Nothing to validate.");
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
297
391
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
392
|
+
logger.info("\nValidating agents...\n");
|
|
393
|
+
let errors = 0;
|
|
301
394
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
395
|
+
for (const [name, agent] of agents) {
|
|
396
|
+
if (!(await validateAgent(name, agent))) errors++;
|
|
397
|
+
}
|
|
305
398
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
version: { type: "boolean", description: "Show version" },
|
|
339
|
-
json: { type: "boolean", description: "JSON output (with --help)" },
|
|
340
|
-
},
|
|
341
|
-
documentation: [
|
|
342
|
-
{
|
|
343
|
-
title: "Outpost Overview",
|
|
344
|
-
url: "https://www.forwardimpact.team/outpost/index.md",
|
|
345
|
-
description: "Product overview, audience model, and key concepts.",
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
title: "Getting Started: Outpost for Engineers",
|
|
349
|
-
url: "https://www.forwardimpact.team/docs/getting-started/engineers/outpost/index.md",
|
|
350
|
-
description: "From zero to your first daily briefing.",
|
|
399
|
+
logger.info(errors > 0 ? `\n${errors} error(s).` : "\nAll OK.");
|
|
400
|
+
return errors > 0 ? 1 : 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- CLI entry point -------------------------------------------------------
|
|
404
|
+
const cli = createCli(buildDefinition(version));
|
|
405
|
+
const parsed = cli.parse(proc.argv.slice(2));
|
|
406
|
+
if (!parsed) return 0;
|
|
407
|
+
|
|
408
|
+
const { positionals } = parsed;
|
|
409
|
+
const [command, ...args] = positionals;
|
|
410
|
+
|
|
411
|
+
await fs.mkdir(OUTPOST_HOME, { recursive: true });
|
|
412
|
+
|
|
413
|
+
const COMMANDS = {
|
|
414
|
+
daemon,
|
|
415
|
+
wake: async () => {
|
|
416
|
+
if (!args[0]) {
|
|
417
|
+
cli.usageError("missing required argument <agent>");
|
|
418
|
+
return 2;
|
|
419
|
+
}
|
|
420
|
+
const config = await loadConfig();
|
|
421
|
+
const state = await stateManager.load();
|
|
422
|
+
const agent = config.agents[args[0]];
|
|
423
|
+
if (!agent) {
|
|
424
|
+
cli.error(
|
|
425
|
+
`agent "${args[0]}" not found. Available: ${Object.keys(config.agents).join(", ") || "(none)"}`,
|
|
426
|
+
);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
await agentRunner.wake(args[0], agent, state);
|
|
430
|
+
return 0;
|
|
351
431
|
},
|
|
352
|
-
{
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
432
|
+
init: async () => {
|
|
433
|
+
if (!args[0]) {
|
|
434
|
+
cli.usageError("missing required argument <path>");
|
|
435
|
+
return 2;
|
|
436
|
+
}
|
|
437
|
+
const tpl = await requireTemplateDir();
|
|
438
|
+
if (tpl === null) return 1;
|
|
439
|
+
const result = await kbManager.init(args[0], tpl);
|
|
440
|
+
if (!result.ok) {
|
|
441
|
+
proc.stderr.write(result.error + "\n");
|
|
442
|
+
return result.code;
|
|
443
|
+
}
|
|
444
|
+
return 0;
|
|
357
445
|
},
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
446
|
+
update: () => runUpdate(args),
|
|
447
|
+
stop: async () => {
|
|
448
|
+
const stopped = await requestShutdown(SOCKET_PATH, runtime);
|
|
449
|
+
return stopped ? 0 : 1;
|
|
362
450
|
},
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// --- CLI entry point ---------------------------------------------------------
|
|
367
|
-
|
|
368
|
-
const cli = createCli(definition);
|
|
369
|
-
const parsed = cli.parse(process.argv.slice(2));
|
|
370
|
-
if (!parsed) process.exit(0);
|
|
371
|
-
|
|
372
|
-
const { positionals } = parsed;
|
|
373
|
-
const [command, ...args] = positionals;
|
|
451
|
+
validate,
|
|
452
|
+
status: showStatus,
|
|
453
|
+
};
|
|
374
454
|
|
|
375
|
-
|
|
455
|
+
const handler = COMMANDS[command];
|
|
456
|
+
if (command && !handler) {
|
|
457
|
+
cli.usageError(`unknown command "${command}"`);
|
|
458
|
+
return 2;
|
|
459
|
+
}
|
|
376
460
|
|
|
377
|
-
|
|
378
|
-
daemon,
|
|
379
|
-
wake: async () => {
|
|
380
|
-
if (!args[0]) {
|
|
381
|
-
cli.usageError("missing required argument <agent>");
|
|
382
|
-
process.exit(2);
|
|
383
|
-
}
|
|
384
|
-
const config = loadConfig();
|
|
385
|
-
const state = stateManager.load();
|
|
386
|
-
const agent = config.agents[args[0]];
|
|
387
|
-
if (!agent) {
|
|
388
|
-
cli.error(
|
|
389
|
-
`agent "${args[0]}" not found. Available: ${Object.keys(config.agents).join(", ") || "(none)"}`,
|
|
390
|
-
);
|
|
391
|
-
process.exit(1);
|
|
392
|
-
}
|
|
393
|
-
await agentRunner.wake(args[0], agent, state);
|
|
394
|
-
},
|
|
395
|
-
init: () => {
|
|
396
|
-
if (!args[0]) {
|
|
397
|
-
cli.usageError("missing required argument <path>");
|
|
398
|
-
process.exit(2);
|
|
399
|
-
}
|
|
400
|
-
kbManager.init(args[0], requireTemplateDir());
|
|
401
|
-
},
|
|
402
|
-
update: () => runUpdate(args),
|
|
403
|
-
stop: async () => {
|
|
404
|
-
const stopped = await requestShutdown(SOCKET_PATH);
|
|
405
|
-
if (!stopped) process.exit(1);
|
|
406
|
-
},
|
|
407
|
-
validate,
|
|
408
|
-
status: showStatus,
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const handler = COMMANDS[command];
|
|
412
|
-
if (command && !handler) {
|
|
413
|
-
cli.usageError(`unknown command "${command}"`);
|
|
414
|
-
process.exit(2);
|
|
461
|
+
return (await (handler || (() => scheduler.wakeDueAgents()))()) ?? 0;
|
|
415
462
|
}
|
|
416
|
-
|
|
417
|
-
await (handler || (() => scheduler.wakeDueAgents()))();
|