@forwardimpact/outpost 3.0.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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +15 -0
  3. package/bin/fit-outpost.js +3 -0
  4. package/config/scheduler.json +34 -0
  5. package/package.json +63 -0
  6. package/src/agent-runner.js +189 -0
  7. package/src/index.js +5 -0
  8. package/src/kb-manager.js +228 -0
  9. package/src/outpost.js +417 -0
  10. package/src/scheduler.js +195 -0
  11. package/src/socket-server.js +336 -0
  12. package/src/state-manager.js +126 -0
  13. package/templates/.claude/agents/chief-of-staff.md +58 -0
  14. package/templates/.claude/agents/concierge.md +46 -0
  15. package/templates/.claude/agents/head-hunter.md +57 -0
  16. package/templates/.claude/agents/librarian.md +60 -0
  17. package/templates/.claude/agents/postman.md +47 -0
  18. package/templates/.claude/agents/recruiter.md +63 -0
  19. package/templates/.claude/settings.json +88 -0
  20. package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
  21. package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
  22. package/templates/.claude/skills/candidate-report/references/report.css +438 -0
  23. package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
  24. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
  25. package/templates/.claude/skills/deck-create/SKILL.md +75 -0
  26. package/templates/.claude/skills/deck-create/references/slide.css +35 -0
  27. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
  28. package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
  29. package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
  30. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
  31. package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
  32. package/templates/.claude/skills/doc-create/SKILL.md +105 -0
  33. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
  34. package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
  35. package/templates/.claude/skills/draft-emails/references/template.md +26 -0
  36. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
  37. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
  38. package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
  39. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
  40. package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
  41. package/templates/.claude/skills/extract-entities/references/content.md +104 -0
  42. package/templates/.claude/skills/extract-entities/references/links.md +48 -0
  43. package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
  44. package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
  45. package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
  46. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
  47. package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
  48. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
  49. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
  50. package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
  51. package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
  52. package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
  53. package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
  54. package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
  55. package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
  56. package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
  57. package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
  58. package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
  59. package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
  60. package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
  61. package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
  62. package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
  64. package/templates/.claude/skills/organize-files/SKILL.md +146 -0
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
  67. package/templates/.claude/skills/req-assess/SKILL.md +137 -0
  68. package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
  69. package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
  70. package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
  71. package/templates/.claude/skills/req-decide/SKILL.md +163 -0
  72. package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
  73. package/templates/.claude/skills/req-decide/references/template.md +118 -0
  74. package/templates/.claude/skills/req-forget/SKILL.md +154 -0
  75. package/templates/.claude/skills/req-forget/references/classify.md +23 -0
  76. package/templates/.claude/skills/req-forget/references/locations.md +59 -0
  77. package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
  78. package/templates/.claude/skills/req-scan/SKILL.md +145 -0
  79. package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
  80. package/templates/.claude/skills/req-scan/references/filters.md +83 -0
  81. package/templates/.claude/skills/req-scan/references/sources.md +76 -0
  82. package/templates/.claude/skills/req-scan/references/state.md +54 -0
  83. package/templates/.claude/skills/req-scan/references/template.md +36 -0
  84. package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
  85. package/templates/.claude/skills/req-screen/SKILL.md +151 -0
  86. package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
  87. package/templates/.claude/skills/req-screen/references/template.md +91 -0
  88. package/templates/.claude/skills/req-track/SKILL.md +189 -0
  89. package/templates/.claude/skills/req-track/references/fields.md +114 -0
  90. package/templates/.claude/skills/req-track/references/signals.md +42 -0
  91. package/templates/.claude/skills/req-track/references/statuses.md +32 -0
  92. package/templates/.claude/skills/req-track/references/templates.md +127 -0
  93. package/templates/.claude/skills/req-workday/SKILL.md +162 -0
  94. package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
  95. package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
  96. package/templates/.claude/skills/req-workday/references/templates.md +126 -0
  97. package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
  98. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
  99. package/templates/.claude/skills/send-chat/SKILL.md +170 -0
  100. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
  101. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
  102. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
  103. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
  104. package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
  105. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
  106. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
  107. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
  108. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
  109. package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
  110. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
  111. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
  112. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
  113. package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
  114. package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
  115. package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
  116. package/templates/.claude/skills/weekly-update/references/template.md +55 -0
  117. package/templates/CLAUDE.md +134 -0
  118. package/templates/USER.md +3 -0
  119. package/templates/apm.yml +5 -0
  120. package/templates/knowledge/Briefings/.gitkeep +0 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * KBManager — knowledge base init/update operations.
3
+ */
4
+
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ copyFileSync,
9
+ cpSync,
10
+ readFileSync,
11
+ writeFileSync,
12
+ readdirSync,
13
+ } from "node:fs";
14
+ import { join, dirname, resolve } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import { createLogger } from "@forwardimpact/libtelemetry";
17
+
18
+ const logger = createLogger("outpost");
19
+
20
+ /** Manage knowledge base lifecycle including initialization, updates, and settings merging. */
21
+ export class KBManager {
22
+ #fs;
23
+
24
+ /**
25
+ * @param {{ existsSync: Function, mkdirSync: Function, copyFileSync: Function, cpSync: Function, readFileSync: Function, writeFileSync: Function, readdirSync: Function }} fs
26
+ * @param {Function} logFn
27
+ */
28
+ constructor(fs, logFn) {
29
+ if (!fs) throw new Error("fs is required");
30
+ if (!logFn) throw new Error("logFn is required");
31
+ this.#fs = fs;
32
+ }
33
+
34
+ /**
35
+ * @param {string} dir
36
+ */
37
+ #ensureDir(dir) {
38
+ this.#fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+
41
+ /**
42
+ * @param {string} path
43
+ * @param {*} fallback
44
+ * @returns {*}
45
+ */
46
+ #readJSON(path, fallback) {
47
+ try {
48
+ return JSON.parse(this.#fs.readFileSync(path, "utf8"));
49
+ } catch {
50
+ return fallback;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {string} path
56
+ * @param {*} data
57
+ */
58
+ #writeJSON(path, data) {
59
+ this.#ensureDir(dirname(path));
60
+ this.#fs.writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
61
+ }
62
+
63
+ /**
64
+ * Copy bundled files (CLAUDE.md, skills, agents) from template to a KB.
65
+ * @param {string} tpl - Path to the template directory
66
+ * @param {string} dest - Path to the target knowledge base
67
+ */
68
+ copyBundledFiles(tpl, dest) {
69
+ this.#fs.copyFileSync(join(tpl, "CLAUDE.md"), join(dest, "CLAUDE.md"));
70
+ logger.info(` Updated CLAUDE.md`);
71
+
72
+ const apmSrc = join(tpl, "apm.yml");
73
+ if (this.#fs.existsSync(apmSrc)) {
74
+ this.#fs.copyFileSync(apmSrc, join(dest, "apm.yml"));
75
+ logger.info(` Updated apm.yml`);
76
+ }
77
+
78
+ this.mergeSettings(tpl, dest);
79
+
80
+ for (const sub of ["skills", "agents"]) {
81
+ const src = join(tpl, ".claude", sub);
82
+ if (!this.#fs.existsSync(src)) continue;
83
+ this.#fs.cpSync(src, join(dest, ".claude", sub), { recursive: true });
84
+ const entries = this.#fs
85
+ .readdirSync(src, { withFileTypes: true })
86
+ .filter((d) =>
87
+ sub === "skills" ? d.isDirectory() : d.name.endsWith(".md"),
88
+ );
89
+ const names = entries.map((d) =>
90
+ sub === "agents" ? d.name.replace(".md", "") : d.name,
91
+ );
92
+ logger.info(` Updated ${names.length} ${sub}: ${names.join(", ")}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Merge new entries from a template permission list into an existing one.
98
+ * @param {Object} templatePerms - Template permissions object
99
+ * @param {Object} existingPerms - Existing permissions object (mutated)
100
+ * @returns {number} Number of entries added
101
+ */
102
+ #mergePermissionLists(templatePerms, existingPerms) {
103
+ let added = 0;
104
+ for (const key of ["allow", "deny", "additionalDirectories"]) {
105
+ if (!templatePerms[key]?.length) continue;
106
+ const set = new Set((existingPerms[key] ||= []));
107
+ for (const entry of templatePerms[key]) {
108
+ if (!set.has(entry)) {
109
+ existingPerms[key].push(entry);
110
+ set.add(entry);
111
+ added++;
112
+ }
113
+ }
114
+ }
115
+ if (templatePerms.defaultMode && !existingPerms.defaultMode) {
116
+ existingPerms.defaultMode = templatePerms.defaultMode;
117
+ added++;
118
+ }
119
+ return added;
120
+ }
121
+
122
+ /**
123
+ * Merge template settings.json into the destination's settings.json.
124
+ * @param {string} tpl - Template directory
125
+ * @param {string} dest - Knowledge base directory
126
+ */
127
+ mergeSettings(tpl, dest) {
128
+ const src = join(tpl, ".claude", "settings.json");
129
+ if (!this.#fs.existsSync(src)) return;
130
+
131
+ const destPath = join(dest, ".claude", "settings.json");
132
+
133
+ if (!this.#fs.existsSync(destPath)) {
134
+ this.#ensureDir(join(dest, ".claude"));
135
+ this.#fs.copyFileSync(src, destPath);
136
+ logger.info(` Created settings.json`);
137
+ return;
138
+ }
139
+
140
+ const template = this.#readJSON(src, {});
141
+ const existing = this.#readJSON(destPath, {});
142
+ const added = this.#mergePermissionLists(
143
+ template.permissions || {},
144
+ (existing.permissions ||= {}),
145
+ );
146
+
147
+ if (added > 0) {
148
+ this.#writeJSON(destPath, existing);
149
+ logger.info(` Updated settings.json (${added} new entries)`);
150
+ } else {
151
+ logger.info(` Settings up to date`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Initialize a new knowledge base
157
+ * @param {string} targetPath
158
+ * @param {string} templateDir
159
+ */
160
+ init(targetPath, templateDir) {
161
+ const dest = this.#expandPath(targetPath);
162
+ if (this.#fs.existsSync(join(dest, "CLAUDE.md"))) {
163
+ console.error(`Knowledge base already exists at ${dest}`);
164
+ process.exit(1);
165
+ }
166
+
167
+ this.#ensureDir(dest);
168
+ for (const d of [
169
+ "knowledge/People",
170
+ "knowledge/Organizations",
171
+ "knowledge/Projects",
172
+ "knowledge/Topics",
173
+ "knowledge/Briefings",
174
+ ])
175
+ this.#ensureDir(join(dest, d));
176
+
177
+ this.#fs.copyFileSync(join(templateDir, "USER.md"), join(dest, "USER.md"));
178
+
179
+ this.copyBundledFiles(templateDir, dest);
180
+
181
+ logger.info(
182
+ `Knowledge base initialized at ${dest}\n\nNext steps:\n 1. Edit ${dest}/USER.md with your name, email, and domain\n 2. cd ${dest} && npx apm install\n 3. claude`,
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Update an existing knowledge base with the latest bundled files.
188
+ * @param {string} targetPath
189
+ * @param {string} templateDir
190
+ */
191
+ update(targetPath, templateDir) {
192
+ const dest = this.#expandPath(targetPath);
193
+ if (!this.#fs.existsSync(join(dest, "CLAUDE.md"))) {
194
+ console.error(`No knowledge base found at ${dest}`);
195
+ process.exit(1);
196
+ }
197
+ this.copyBundledFiles(templateDir, dest);
198
+ logger.info(`\nKnowledge base updated: ${dest}`);
199
+ }
200
+
201
+ /**
202
+ * @param {string} p
203
+ * @returns {string}
204
+ */
205
+ #expandPath(p) {
206
+ return p.startsWith("~/") ? join(homedir(), p.slice(2)) : resolve(p);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Create a KBManager with real fs dependencies
212
+ * @param {Function} logFn
213
+ * @returns {KBManager}
214
+ */
215
+ export function createKBManager(logFn) {
216
+ return new KBManager(
217
+ {
218
+ existsSync,
219
+ mkdirSync,
220
+ copyFileSync,
221
+ cpSync,
222
+ readFileSync,
223
+ writeFileSync,
224
+ readdirSync,
225
+ },
226
+ logFn,
227
+ );
228
+ }
package/src/outpost.js ADDED
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Outpost — CLI and scheduler for autonomous agent teams.
4
+ //
5
+ // Usage:
6
+ // fit-outpost Wake due agents once and exit
7
+ // fit-outpost daemon Run continuously (poll every 60s)
8
+ // fit-outpost wake <agent> Wake a specific agent immediately
9
+ // fit-outpost init <path> Initialize a new knowledge base
10
+ // fit-outpost update [path] Update KB with latest CLAUDE.md, agents and skills
11
+ // fit-outpost stop Gracefully stop daemon and all running agents
12
+ // fit-outpost validate Validate agent definitions exist
13
+ // fit-outpost status Show agent status
14
+ // fit-outpost --help Show this help
15
+
16
+ import {
17
+ readFileSync,
18
+ writeFileSync,
19
+ existsSync,
20
+ mkdirSync,
21
+ readdirSync,
22
+ copyFileSync,
23
+ cpSync,
24
+ appendFileSync,
25
+ } from "node:fs";
26
+ import { join, dirname, resolve } from "node:path";
27
+ import { homedir } from "node:os";
28
+ import { fileURLToPath } from "node:url";
29
+ import { createCli } from "@forwardimpact/libcli";
30
+ import { createLogger } from "@forwardimpact/libtelemetry";
31
+
32
+ const logger = createLogger("outpost");
33
+
34
+ import * as posixSpawn from "@forwardimpact/libmacos/posix-spawn";
35
+ import { StateManager } from "./state-manager.js";
36
+ import { AgentRunner } from "./agent-runner.js";
37
+ import { Scheduler } from "./scheduler.js";
38
+ import { KBManager } from "./kb-manager.js";
39
+ import { SocketServer, requestShutdown } from "./socket-server.js";
40
+
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
+ 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
+
81
+ // --- Config ------------------------------------------------------------------
82
+
83
+ function loadConfig() {
84
+ try {
85
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
86
+ } catch {
87
+ return { agents: {} };
88
+ }
89
+ }
90
+
91
+ function expandPath(p) {
92
+ return p.startsWith("~/") ? join(HOME, p.slice(2)) : resolve(p);
93
+ }
94
+
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
+ /**
117
+ * Detect if running from inside a macOS .app bundle.
118
+ * @returns {{ bundle: string, resources: string } | null}
119
+ */
120
+ function getBundlePath() {
121
+ try {
122
+ const exe = process.execPath || "";
123
+ const macosDir = dirname(exe);
124
+ const contentsDir = dirname(macosDir);
125
+ const resourcesDir = join(contentsDir, "Resources");
126
+ if (existsSync(join(resourcesDir, "config"))) {
127
+ return { bundle: dirname(contentsDir), resources: resourcesDir };
128
+ }
129
+ } catch {
130
+ /* not in bundle */
131
+ }
132
+ return null;
133
+ }
134
+
135
+ function requireTemplateDir() {
136
+ const bundle = getBundlePath();
137
+ if (bundle) {
138
+ const tpl = join(bundle.resources, "templates");
139
+ if (existsSync(tpl)) return tpl;
140
+ }
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
+
150
+ // --- Daemon ------------------------------------------------------------------
151
+
152
+ function daemon() {
153
+ const daemonStartedAt = Date.now();
154
+ log("Scheduler daemon started. Polling every 60 seconds.");
155
+ log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
156
+
157
+ // Reset any agents left "active" from a previous daemon session.
158
+ const state = stateManager.load();
159
+ stateManager.resetStaleAgents(state, { reason: "Daemon restarted" }, log);
160
+
161
+ const socketServer = new SocketServer(
162
+ SOCKET_PATH,
163
+ scheduler,
164
+ agentRunner,
165
+ stateManager,
166
+ loadConfig,
167
+ log,
168
+ CACHE_DIR,
169
+ daemonStartedAt,
170
+ );
171
+ socketServer.start();
172
+
173
+ async function tick() {
174
+ try {
175
+ await scheduler.wakeDueAgents();
176
+ } catch (err) {
177
+ log(`Error: ${err.message}`);
178
+ }
179
+ setTimeout(tick, 60_000);
180
+ }
181
+ tick();
182
+ }
183
+
184
+ // --- Update ------------------------------------------------------------------
185
+
186
+ function runUpdate(args) {
187
+ if (args[0]) {
188
+ kbManager.update(args[0], requireTemplateDir());
189
+ return;
190
+ }
191
+
192
+ const config = loadConfig();
193
+ const kbPaths = [
194
+ ...new Set(
195
+ Object.values(config.agents)
196
+ .filter((a) => a.kb)
197
+ .map((a) => expandPath(a.kb)),
198
+ ),
199
+ ];
200
+
201
+ if (kbPaths.length === 0) {
202
+ console.error(
203
+ "No knowledge bases configured and no path given.\n" +
204
+ "Usage: fit-outpost update [path]",
205
+ );
206
+ process.exit(1);
207
+ }
208
+
209
+ for (const kb of kbPaths) {
210
+ logger.info(`\nUpdating ${kb}...`);
211
+ kbManager.update(kb, requireTemplateDir());
212
+ }
213
+ }
214
+
215
+ // --- Status ------------------------------------------------------------------
216
+
217
+ function formatAgentStatus(name, agent, s) {
218
+ const enabledMark = agent.enabled !== false ? "+" : "-";
219
+ const kbStatus =
220
+ agent.kb && !existsSync(expandPath(agent.kb)) ? " (not found)" : "";
221
+ const lastWake = s.lastWokeAt
222
+ ? new Date(s.lastWokeAt).toLocaleString()
223
+ : "never";
224
+ const lines = [
225
+ ` ${enabledMark} ${name}`,
226
+ ` KB: ${agent.kb || "(none)"}${kbStatus} Schedule: ${JSON.stringify(agent.schedule)}`,
227
+ ` Status: ${s.status || "never-woken"} Last wake: ${lastWake} Wakes: ${s.wakeCount || 0}`,
228
+ ];
229
+ if (s.lastAction) lines.push(` Last action: ${s.lastAction}`);
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
+ }
234
+
235
+ function showStatus() {
236
+ const config = loadConfig();
237
+ const state = stateManager.load();
238
+ logger.info("\nOutpost Scheduler\n==================\n");
239
+
240
+ const agents = Object.entries(config.agents || {});
241
+ if (agents.length === 0) {
242
+ logger.info(`No agents configured.\n\nEdit ${CONFIG_PATH} to add agents.`);
243
+ return;
244
+ }
245
+
246
+ logger.info("Agents:");
247
+ for (const [name, agent] of agents) {
248
+ logger.info(formatAgentStatus(name, agent, state.agents[name] || {}));
249
+ }
250
+ }
251
+
252
+ // --- Validate ----------------------------------------------------------------
253
+
254
+ function findInLocalOrGlobal(kbPath, subPath) {
255
+ const local = join(kbPath, ".claude", subPath);
256
+ const global = join(HOME, ".claude", subPath);
257
+ if (existsSync(local)) return local;
258
+ if (existsSync(global)) return global;
259
+ return null;
260
+ }
261
+
262
+ /**
263
+ * Validate a single agent's configuration.
264
+ * @param {string} name
265
+ * @param {Object} agent
266
+ * @returns {boolean} true if valid
267
+ */
268
+ function validateAgent(name, agent) {
269
+ if (!agent.kb) {
270
+ logger.info(` [FAIL] ${name}: no "kb" path specified`);
271
+ return false;
272
+ }
273
+ const kbPath = expandPath(agent.kb);
274
+ if (!existsSync(kbPath)) {
275
+ logger.info(` [FAIL] ${name}: path not found: ${kbPath}`);
276
+ return false;
277
+ }
278
+
279
+ const agentFile = join("agents", name + ".md");
280
+ const found = findInLocalOrGlobal(kbPath, agentFile);
281
+ logger.info(
282
+ ` [${found ? "OK" : "FAIL"}] ${name}: agent definition${found ? "" : " not found"}`,
283
+ );
284
+ return !!found;
285
+ }
286
+
287
+ function validate() {
288
+ const config = loadConfig();
289
+ const agents = Object.entries(config.agents || {});
290
+ if (agents.length === 0) {
291
+ logger.info("No agents configured. Nothing to validate.");
292
+ return;
293
+ }
294
+
295
+ logger.info("\nValidating agents...\n");
296
+ let errors = 0;
297
+
298
+ for (const [name, agent] of agents) {
299
+ if (!validateAgent(name, agent)) errors++;
300
+ }
301
+
302
+ logger.info(errors > 0 ? `\n${errors} error(s).` : "\nAll OK.");
303
+ if (errors > 0) process.exit(1);
304
+ }
305
+
306
+ // --- CLI definition ----------------------------------------------------------
307
+
308
+ const definition = {
309
+ name: "fit-outpost",
310
+ version: VERSION,
311
+ description: "Schedule autonomous agents across knowledge bases",
312
+ commands: [
313
+ { name: "daemon", description: "Run continuously (poll every 60s)" },
314
+ {
315
+ name: "wake",
316
+ args: "<agent>",
317
+ description: "Wake a specific agent immediately",
318
+ },
319
+ {
320
+ name: "init",
321
+ args: "<path>",
322
+ description: "Initialize a new knowledge base",
323
+ },
324
+ {
325
+ name: "update",
326
+ args: "[path]",
327
+ description: "Update KB with latest CLAUDE.md, agents and skills",
328
+ },
329
+ {
330
+ name: "stop",
331
+ description: "Gracefully stop daemon and all running agents",
332
+ },
333
+ { name: "validate", description: "Validate agent definitions exist" },
334
+ { name: "status", description: "Show agent status" },
335
+ ],
336
+ globalOptions: {
337
+ help: { type: "boolean", short: "h", description: "Show this help" },
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.",
351
+ },
352
+ {
353
+ title: "Keep Track of Context Without Effort",
354
+ url: "https://www.forwardimpact.team/docs/products/knowledge-systems/index.md",
355
+ description:
356
+ "Maintain continuous awareness of people, projects, and threads.",
357
+ },
358
+ {
359
+ title: "Walk Into Every Meeting Already Oriented",
360
+ url: "https://www.forwardimpact.team/docs/products/knowledge-systems/meeting-prep/index.md",
361
+ description: "Assemble context so you arrive prepared.",
362
+ },
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;
374
+
375
+ mkdirSync(OUTPOST_HOME, { recursive: true });
376
+
377
+ const COMMANDS = {
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);
415
+ }
416
+
417
+ await (handler || (() => scheduler.wakeDueAgents()))();