@forwardimpact/outpost 3.12.0 → 3.13.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/README.md +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +6 -5
- package/src/agent-path.js +10 -6
- package/src/agent-runner.js +28 -24
- package/src/index.js +2 -3
- package/src/kb-manager.js +75 -20
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +108 -39
- package/src/posture.js +3 -3
- package/src/privilege.js +11 -11
- package/src/scheduler.js +14 -12
- package/src/socket-server.js +19 -18
- package/src/spawn-env.js +7 -6
- package/src/state-manager.js +2 -2
- package/templates/.claude/agents/chief-of-staff.md +32 -24
- package/templates/.claude/agents/concierge.md +26 -17
- package/templates/.claude/agents/head-hunter.md +34 -26
- package/templates/.claude/agents/librarian.md +26 -18
- package/templates/.claude/agents/postman.md +26 -18
- package/templates/.claude/agents/recruiter.md +38 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
- package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
- package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
- package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
- package/templates/.claude/skills/candidate-report/references/report.css +4 -4
- package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
- package/templates/.claude/skills/changelog/SKILL.md +122 -67
- package/templates/.claude/skills/deck-create/SKILL.md +90 -32
- package/templates/.claude/skills/deck-create/references/slide.css +4 -4
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/deck-review/SKILL.md +63 -58
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
- package/templates/.claude/skills/doc-create/SKILL.md +27 -23
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
- package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +37 -13
- package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
- package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
- package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
- package/templates/.claude/skills/organize-files/SKILL.md +27 -22
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
- package/templates/.claude/skills/person-identify/SKILL.md +77 -27
- package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
- package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
- package/templates/.claude/skills/req-assess/SKILL.md +44 -36
- package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
- package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
- package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
- package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
- package/templates/.claude/skills/req-decide/SKILL.md +52 -41
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +58 -39
- package/templates/.claude/skills/req-forget/references/classify.md +15 -13
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
- package/templates/.claude/skills/req-scan/SKILL.md +27 -25
- package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
- package/templates/.claude/skills/req-scan/references/filters.md +13 -13
- package/templates/.claude/skills/req-scan/references/sources.md +7 -7
- package/templates/.claude/skills/req-scan/references/state.md +1 -1
- package/templates/.claude/skills/req-scan/references/template.md +11 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +96 -50
- package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +29 -17
- package/templates/.claude/skills/req-track/SKILL.md +76 -76
- package/templates/.claude/skills/req-track/references/fields.md +31 -27
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- package/templates/.claude/skills/req-track/references/signals.md +12 -12
- package/templates/.claude/skills/req-track/references/statuses.md +3 -2
- package/templates/.claude/skills/req-track/references/templates.md +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +69 -40
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +11 -78
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
- package/templates/.claude/skills/send-chat/SKILL.md +58 -54
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
- package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
- package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
- package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
- package/templates/CLAUDE.md +97 -85
- package/templates/MIGRATION.md +359 -0
- package/templates/registry.yaml +25 -0
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
- package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -183
package/src/outpost.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
// fit-outpost init [name] Initialize a knowledge base by name (default: Team)
|
|
8
8
|
// fit-outpost update [path] Update KB with latest CLAUDE.md, agents and skills (defaults to current directory)
|
|
9
9
|
// fit-outpost stop Gracefully stop daemon and all running agents
|
|
10
|
-
// fit-outpost validate
|
|
10
|
+
// fit-outpost validate [path] Validate agent definitions and knowledge bases
|
|
11
11
|
// fit-outpost status Show agent status
|
|
12
12
|
// fit-outpost --help Show this help
|
|
13
13
|
//
|
|
14
|
-
// This module owns the CLI definition and dispatch table.
|
|
15
|
-
// collaborator bag
|
|
16
|
-
// construction site
|
|
17
|
-
// the process exit code
|
|
14
|
+
// This module owns the CLI definition and dispatch table. bin/fit-outpost.js
|
|
15
|
+
// constructs the runtime collaborator bag once. That file is the sole
|
|
16
|
+
// construction site. It threads the bag into `run(runtime, version)`. `run`
|
|
17
|
+
// returns the process exit code. The bin translates that code to
|
|
18
|
+
// `runtime.proc.exit`.
|
|
18
19
|
|
|
19
20
|
import { join, dirname, resolve } from "node:path";
|
|
20
21
|
import { homedir } from "node:os";
|
|
@@ -26,6 +27,7 @@ import { StateManager } from "./state-manager.js";
|
|
|
26
27
|
import { AgentRunner } from "./agent-runner.js";
|
|
27
28
|
import { Scheduler, formatLocalTime } from "./scheduler.js";
|
|
28
29
|
import { KBManager } from "./kb-manager.js";
|
|
30
|
+
import { validateKnowledgeBase } from "./kb-validator.js";
|
|
29
31
|
import { SocketServer, requestShutdown, requestWake } from "./socket-server.js";
|
|
30
32
|
import {
|
|
31
33
|
readPosture,
|
|
@@ -38,13 +40,13 @@ import {
|
|
|
38
40
|
const SHARE_DIR = "/usr/local/share/fit-outpost";
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
* Build the CLI definition.
|
|
42
|
-
* definition the
|
|
43
|
-
* output stays stable.
|
|
43
|
+
* Build the CLI definition. This object is byte-identical to the libcli
|
|
44
|
+
* definition that the golden capture used, and `test/golden.test.js`
|
|
45
|
+
* enforces that contract. So `--help` and `--version` output stays stable.
|
|
44
46
|
* @param {string} version
|
|
45
47
|
* @returns {object}
|
|
46
48
|
*/
|
|
47
|
-
function buildDefinition(version) {
|
|
49
|
+
export function buildDefinition(version) {
|
|
48
50
|
return {
|
|
49
51
|
name: "fit-outpost",
|
|
50
52
|
version,
|
|
@@ -71,7 +73,11 @@ function buildDefinition(version) {
|
|
|
71
73
|
name: "stop",
|
|
72
74
|
description: "Gracefully stop daemon and all running agents",
|
|
73
75
|
},
|
|
74
|
-
{
|
|
76
|
+
{
|
|
77
|
+
name: "validate",
|
|
78
|
+
args: "[path]",
|
|
79
|
+
description: "Validate agent definitions and knowledge bases",
|
|
80
|
+
},
|
|
75
81
|
{ name: "status", description: "Show agent status" },
|
|
76
82
|
{
|
|
77
83
|
name: "posture",
|
|
@@ -82,7 +88,10 @@ function buildDefinition(version) {
|
|
|
82
88
|
globalOptions: {
|
|
83
89
|
help: { type: "boolean", short: "h", description: "Show this help" },
|
|
84
90
|
version: { type: "boolean", description: "Show version" },
|
|
85
|
-
json: {
|
|
91
|
+
json: {
|
|
92
|
+
type: "boolean",
|
|
93
|
+
description: "JSON output (with --help and validate)",
|
|
94
|
+
},
|
|
86
95
|
},
|
|
87
96
|
documentation: [
|
|
88
97
|
{
|
|
@@ -111,7 +120,7 @@ function buildDefinition(version) {
|
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/**
|
|
114
|
-
* Render an agent's multi-line status block
|
|
123
|
+
* Render an agent's multi-line status block. This is a pure format step.
|
|
115
124
|
* @param {string} name
|
|
116
125
|
* @param {Object} agent
|
|
117
126
|
* @param {Object} s - The agent's persisted state.
|
|
@@ -145,7 +154,8 @@ export async function run(runtime, version) {
|
|
|
145
154
|
const logger = createLogger("outpost", runtime);
|
|
146
155
|
|
|
147
156
|
/**
|
|
148
|
-
*
|
|
157
|
+
* Test whether a path exists, through the one async fs surface this module
|
|
158
|
+
* uses.
|
|
149
159
|
* @param {string} p
|
|
150
160
|
* @returns {Promise<boolean>}
|
|
151
161
|
*/
|
|
@@ -193,10 +203,10 @@ export async function run(runtime, version) {
|
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
// --- Wire dependencies -----------------------------------------------------
|
|
196
|
-
// posix-spawn is a Bun-FFI module (`bun:ffi`)
|
|
197
|
-
//
|
|
198
|
-
// lazily
|
|
199
|
-
//
|
|
206
|
+
// posix-spawn is a Bun-FFI module (`bun:ffi`). An eager import would crash
|
|
207
|
+
// plain `node` (e.g. `--help`/`--version`/golden capture). So load it
|
|
208
|
+
// lazily. Then only an actual agent wake pulls it in. That wake only runs
|
|
209
|
+
// under Bun on macOS.
|
|
200
210
|
const loadSpawn = () => import("@forwardimpact/libmacos/posix-spawn");
|
|
201
211
|
const stateManager = new StateManager(STATE_PATH, runtime);
|
|
202
212
|
const agentRunner = new AgentRunner(
|
|
@@ -247,7 +257,7 @@ export async function run(runtime, version) {
|
|
|
247
257
|
// --- Daemon ----------------------------------------------------------------
|
|
248
258
|
async function daemon() {
|
|
249
259
|
const daemonStartedAt = clock.now();
|
|
250
|
-
log("Scheduler daemon started.
|
|
260
|
+
log("Scheduler daemon started. It polls every 60 seconds.");
|
|
251
261
|
log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
|
|
252
262
|
|
|
253
263
|
// Reset any agents left "active" from a previous daemon session.
|
|
@@ -276,8 +286,8 @@ export async function run(runtime, version) {
|
|
|
276
286
|
void socketServer.whenStopped().then(() => {
|
|
277
287
|
stopped = true;
|
|
278
288
|
// Cancel any pending poll so the armed timer does not keep the event
|
|
279
|
-
// loop alive after shutdown
|
|
280
|
-
// on a nonzero code
|
|
289
|
+
// loop alive after shutdown. `run()` returns 0, and the bin exits only
|
|
290
|
+
// on a nonzero code. So a 60s timer that stays armed would delay exit.
|
|
281
291
|
if (tickHandle !== undefined) clock.clearTimeout(tickHandle);
|
|
282
292
|
});
|
|
283
293
|
|
|
@@ -292,8 +302,8 @@ export async function run(runtime, version) {
|
|
|
292
302
|
}
|
|
293
303
|
void tick();
|
|
294
304
|
|
|
295
|
-
// Block until a
|
|
296
|
-
//
|
|
305
|
+
// Block until a socket or a signal requests a shutdown. The bin then owns
|
|
306
|
+
// the process-exit call.
|
|
297
307
|
await socketServer.whenStopped();
|
|
298
308
|
return 0;
|
|
299
309
|
}
|
|
@@ -323,9 +333,9 @@ export async function run(runtime, version) {
|
|
|
323
333
|
const state = await stateManager.load();
|
|
324
334
|
const posture = await readPosture(fs, POSTURE_PATH);
|
|
325
335
|
logger.info("\nOutpost Scheduler\n==================\n");
|
|
326
|
-
// The posture must be observable on a line
|
|
327
|
-
// `^posture: (brief|brief+draft|unset)
|
|
328
|
-
// stdout
|
|
336
|
+
// The posture must be observable on a line that matches
|
|
337
|
+
// `^posture: (brief|brief+draft|unset)$`. So write it as plain text to
|
|
338
|
+
// stdout. Do not write it through the RFC5424-prefixed logger.
|
|
329
339
|
proc.stdout.write(`posture: ${posture ?? "unset"}\n`);
|
|
330
340
|
|
|
331
341
|
const agents = Object.entries(config.agents || {});
|
|
@@ -373,7 +383,59 @@ export async function run(runtime, version) {
|
|
|
373
383
|
return !!found;
|
|
374
384
|
}
|
|
375
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Render one knowledge finding as a report line. Baselined findings warn.
|
|
388
|
+
* @param {import("./kb-validator.js").Finding} f
|
|
389
|
+
* @returns {string}
|
|
390
|
+
*/
|
|
391
|
+
function formatFinding(f) {
|
|
392
|
+
const prefix = f.baselined ? "warn: " : "";
|
|
393
|
+
if (f.path !== undefined) {
|
|
394
|
+
return `${prefix}${f.path} ${f.kind}${f.message ? ` — ${f.message}` : ""}`;
|
|
395
|
+
}
|
|
396
|
+
return `${prefix}${f.file}:${f.line} ${f.kind} ${f.link ?? f.property}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Render one KB root's findings as report lines through the logger.
|
|
401
|
+
* @param {string} root - Absolute KB root path.
|
|
402
|
+
* @param {import("./kb-validator.js").Finding[]} findings
|
|
403
|
+
* @returns {void}
|
|
404
|
+
*/
|
|
405
|
+
function reportFindings(root, findings) {
|
|
406
|
+
logger.info(`\nKnowledge base: ${root}`);
|
|
407
|
+
for (const f of findings) logger.info(` ${formatFinding(f)}`);
|
|
408
|
+
if (findings.length === 0) logger.info(" OK");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Run the knowledge checks over each KB root. With `--json` the merged
|
|
413
|
+
* findings array is the only stdout, so tooling can parse it. A root the
|
|
414
|
+
* validator cannot read (a mistyped path, a malformed registry or
|
|
415
|
+
* baseline) fails with one clean error line instead of a stack trace.
|
|
416
|
+
* @param {string[]} roots - Absolute KB root paths.
|
|
417
|
+
* @returns {Promise<number>} 1 when any finding is not baselined, else 0.
|
|
418
|
+
*/
|
|
419
|
+
async function runKnowledgeChecks(roots) {
|
|
420
|
+
const all = [];
|
|
421
|
+
for (const root of roots) {
|
|
422
|
+
let result;
|
|
423
|
+
try {
|
|
424
|
+
result = await validateKnowledgeBase(root, runtime);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
cli.error(`validate failed for ${root}: ${err.message}`);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
if (!values.json) reportFindings(root, result.findings);
|
|
430
|
+
all.push(...result.findings);
|
|
431
|
+
}
|
|
432
|
+
if (values.json) proc.stdout.write(`${JSON.stringify(all)}\n`);
|
|
433
|
+
return all.some((f) => !f.baselined) ? 1 : 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
376
436
|
async function validate() {
|
|
437
|
+
if (args[0]) return runKnowledgeChecks([expandPath(args[0])]);
|
|
438
|
+
|
|
377
439
|
const config = await loadConfig();
|
|
378
440
|
const agents = Object.entries(config.agents || {});
|
|
379
441
|
if (agents.length === 0) {
|
|
@@ -389,7 +451,12 @@ export async function run(runtime, version) {
|
|
|
389
451
|
}
|
|
390
452
|
|
|
391
453
|
logger.info(errors > 0 ? `\n${errors} error(s).` : "\nAll OK.");
|
|
392
|
-
|
|
454
|
+
// The logger writes to stderr, so with --json the findings array stays
|
|
455
|
+
// the only stdout even on the no-path form.
|
|
456
|
+
const kbRoots = [
|
|
457
|
+
...new Set(agents.map(([, a]) => a.kb).filter(Boolean)),
|
|
458
|
+
].map(expandPath);
|
|
459
|
+
return Math.max(errors ? 1 : 0, await runKnowledgeChecks(kbRoots));
|
|
393
460
|
}
|
|
394
461
|
|
|
395
462
|
// --- CLI entry point -------------------------------------------------------
|
|
@@ -397,7 +464,7 @@ export async function run(runtime, version) {
|
|
|
397
464
|
const parsed = cli.parse(proc.argv.slice(2));
|
|
398
465
|
if (!parsed) return 0;
|
|
399
466
|
|
|
400
|
-
const { positionals } = parsed;
|
|
467
|
+
const { positionals, values } = parsed;
|
|
401
468
|
const [command, ...args] = positionals;
|
|
402
469
|
|
|
403
470
|
await fs.mkdir(OUTPOST_HOME, { recursive: true });
|
|
@@ -409,13 +476,14 @@ export async function run(runtime, version) {
|
|
|
409
476
|
cli.usageError("missing required argument <agent>");
|
|
410
477
|
return 2;
|
|
411
478
|
}
|
|
412
|
-
// Always route the wake through the
|
|
413
|
-
// only spawn site that descends from fit-outpost.app
|
|
414
|
-
// spawned there inherits the app as its TCC responsible
|
|
415
|
-
// single grant to the app covers it.
|
|
416
|
-
// would attribute the access to the terminal instead
|
|
417
|
-
// single-grant model.
|
|
418
|
-
// with correct attribution
|
|
479
|
+
// Always route the wake through the daemon that already runs. The daemon
|
|
480
|
+
// is the only spawn site that descends from fit-outpost.app. So a
|
|
481
|
+
// `claude` spawned there inherits the app as its TCC responsible
|
|
482
|
+
// process, and a single grant to the app covers it. A spawn from this
|
|
483
|
+
// CLI process would attribute the access to the terminal instead. That
|
|
484
|
+
// breaks the single-grant model. When no daemon runs, there is nowhere
|
|
485
|
+
// to wake with correct attribution. So this command errors. It does not
|
|
486
|
+
// spawn locally.
|
|
419
487
|
const result = await requestWake(SOCKET_PATH, args[0], runtime);
|
|
420
488
|
if (result.ok) {
|
|
421
489
|
log(`Wake dispatched to daemon for "${args[0]}".`);
|
|
@@ -423,7 +491,7 @@ export async function run(runtime, version) {
|
|
|
423
491
|
}
|
|
424
492
|
if (result.reason === "not-running") {
|
|
425
493
|
cli.error(
|
|
426
|
-
"daemon not running. Start fit-outpost.app (or run `fit-outpost daemon`) before
|
|
494
|
+
"daemon not running. Start fit-outpost.app (or run `fit-outpost daemon`) before you wake an agent.",
|
|
427
495
|
);
|
|
428
496
|
} else if (result.reason === "timeout") {
|
|
429
497
|
cli.error("daemon did not respond to the wake request.");
|
|
@@ -434,8 +502,9 @@ export async function run(runtime, version) {
|
|
|
434
502
|
},
|
|
435
503
|
init: async () => {
|
|
436
504
|
// `init [name]` provisions a KB by name under the data home (default
|
|
437
|
-
// `Team`)
|
|
438
|
-
//
|
|
505
|
+
// `Team`). It never accepts an arbitrary path. So nobody can steer the
|
|
506
|
+
// substrate back into a TCC-protected folder. This command refuses an
|
|
507
|
+
// unsafe name.
|
|
439
508
|
const name = args[0] ?? "Team";
|
|
440
509
|
let target;
|
|
441
510
|
try {
|
|
@@ -452,8 +521,8 @@ export async function run(runtime, version) {
|
|
|
452
521
|
return result.code;
|
|
453
522
|
}
|
|
454
523
|
// A fresh init defaults the posture to `brief`, the opted-into trust
|
|
455
|
-
// contract. Only write when
|
|
456
|
-
// an existing posture.
|
|
524
|
+
// contract. Only write when no posture is recorded. A later run then
|
|
525
|
+
// never flips an existing posture.
|
|
457
526
|
if ((await readPosture(fs, POSTURE_PATH)) === null) {
|
|
458
527
|
await writePosture(fs, POSTURE_PATH, "brief");
|
|
459
528
|
}
|
package/src/posture.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Posture — read/write the adoption-posture record and resolve the
|
|
3
3
|
* draft-side deny set from the skill-posture manifest.
|
|
4
4
|
*
|
|
5
|
-
* The two committed posture strings are the only valid values
|
|
5
|
+
* The two committed posture strings are the only valid values. They appear
|
|
6
6
|
* verbatim in CLI flags, `status` output, and landing-page copy.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -46,8 +46,8 @@ export async function writePosture(fs, posturePath, value) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Resolve the effective posture
|
|
50
|
-
* recorded
|
|
49
|
+
* Resolve the effective posture. Apply the default when no posture is
|
|
50
|
+
* recorded. A posture-less install then behaves as `brief`.
|
|
51
51
|
* @param {"brief"|"brief+draft"|null} stored
|
|
52
52
|
* @returns {"brief"|"brief+draft"}
|
|
53
53
|
*/
|
package/src/privilege.js
CHANGED
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
* the hop-2 spawn disclaim flag.
|
|
4
4
|
*
|
|
5
5
|
* The level governs the macOS reach the daemon grants a woken agent. `full`
|
|
6
|
-
* keeps today's single-grant model
|
|
7
|
-
* responsible process, so Full Disk Access and Automation flow to it
|
|
8
|
-
* `restricted` agent is
|
|
9
|
-
*
|
|
6
|
+
* keeps today's single-grant model. The child inherits `fit-outpost.app` as its
|
|
7
|
+
* responsible process, so Full Disk Access and Automation flow to it. A
|
|
8
|
+
* `restricted` agent is responsible for itself. Those grants do not extend to
|
|
9
|
+
* it, so it can reach only non-TCC-protected substrate.
|
|
10
10
|
*
|
|
11
|
-
* The level is mandatory
|
|
12
|
-
* spawn-env allow-set and the state roots
|
|
13
|
-
* level.
|
|
14
|
-
* default
|
|
11
|
+
* The level is mandatory. It lives in the same user-only trust root as the
|
|
12
|
+
* spawn-env allow-set and the state roots. So an agent cannot raise its own
|
|
13
|
+
* level. This module follows the `posture.js` pattern, with no `effective*`
|
|
14
|
+
* coercion and no default. A missing or unrecognised value throws.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/** The two privilege levels, in declaration order. */
|
|
18
18
|
export const PRIVILEGE_LEVELS = ["full", "restricted"];
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Resolve an agent's declared privilege level. The level is mandatory
|
|
22
|
-
* missing or unrecognised value throws
|
|
21
|
+
* Resolve an agent's declared privilege level. The level is mandatory. A
|
|
22
|
+
* missing or unrecognised value throws. There is no default.
|
|
23
23
|
* @param {{ privilege?: string }} agent - One agent's config.
|
|
24
24
|
* @returns {"full"|"restricted"} The declared level.
|
|
25
25
|
* @throws {Error} when `agent.privilege` is not one of {@link PRIVILEGE_LEVELS}.
|
|
@@ -35,7 +35,7 @@ export function resolvePrivilege(agent) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Map a level to the hop-2 disclaim flag
|
|
38
|
+
* Map a level to the hop-2 disclaim flag. `restricted` self-disclaims (`1`).
|
|
39
39
|
* `full` keeps the inherited responsible process (`0`).
|
|
40
40
|
* @param {"full"|"restricted"} level
|
|
41
41
|
* @returns {0|1}
|
package/src/scheduler.js
CHANGED
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The genuine wall-clock read ("what time is it now?") routes through the
|
|
5
5
|
* injected `runtime.clock` (see `Scheduler#wakeDueAgents` and `failAgent`).
|
|
6
|
-
* The pure cron helpers below receive that `now` as an explicit `Date
|
|
6
|
+
* The pure cron helpers below receive that `now` as an explicit `Date`. They
|
|
7
7
|
* construct further `Date` objects only for deterministic date *arithmetic*
|
|
8
8
|
* over caller-supplied or parsed-from-state inputs (never an ambient no-arg
|
|
9
|
-
* `new Date()`)
|
|
10
|
-
*
|
|
9
|
+
* `new Date()`). The AST checker cannot distinguish the two, so
|
|
10
|
+
* .jidoka/invariants/ambient-deps.allow.yml allow-lists this file with that
|
|
11
|
+
* reason.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { isoTimestamp } from "@forwardimpact/libutil";
|
|
14
15
|
|
|
15
|
-
/** Maximum time an agent can be "active" before
|
|
16
|
+
/** Maximum time an agent can be "active" before it becomes stale (35 min). */
|
|
16
17
|
const MAX_AGENT_RUNTIME_MS = 35 * 60_000;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Build a `Date` for "now" from the injected clock's milliseconds. The cron
|
|
20
|
-
* helpers operate on `Date` objects
|
|
21
|
-
* wall-clock read (`runtime.clock.now()`) into one
|
|
22
|
-
* modules never construct an ambient `new Date()` themselves.
|
|
21
|
+
* helpers operate on `Date` objects. This function is the single seam that
|
|
22
|
+
* turns the wall-clock read (`runtime.clock.now()`) into one. So callers in
|
|
23
|
+
* other modules never construct an ambient `new Date()` themselves.
|
|
23
24
|
* @param {{now: () => number}} clock
|
|
24
25
|
* @returns {Date}
|
|
25
26
|
*/
|
|
@@ -28,9 +29,9 @@ export function nowFromClock(clock) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* Format a stored ISO timestamp as a human-readable local time string.
|
|
32
|
-
* an explicit value (never the wall clock)
|
|
33
|
-
* construction sites stay in this allow-listed module.
|
|
32
|
+
* Format a stored ISO timestamp as a human-readable local time string. This
|
|
33
|
+
* function parses an explicit value (never the wall clock). It lives here so
|
|
34
|
+
* the only `new Date` construction sites stay in this allow-listed module.
|
|
34
35
|
* @param {string} iso
|
|
35
36
|
* @returns {string}
|
|
36
37
|
*/
|
|
@@ -179,7 +180,7 @@ export function failAgent(agentState, error, nowMs) {
|
|
|
179
180
|
|
|
180
181
|
// --- Scheduler class ---------------------------------------------------------
|
|
181
182
|
|
|
182
|
-
/** Orchestrate periodic agent wakes
|
|
183
|
+
/** Orchestrate periodic agent wakes. Evaluate schedules and delegate to AgentRunner. */
|
|
183
184
|
export class Scheduler {
|
|
184
185
|
#loadConfig;
|
|
185
186
|
#stateManager;
|
|
@@ -209,7 +210,8 @@ export class Scheduler {
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
/**
|
|
212
|
-
* Reset any
|
|
213
|
+
* Reset any agent that exceeds the maximum runtime. Reload the config. Then
|
|
214
|
+
* wake each agent whose schedule is due.
|
|
213
215
|
*/
|
|
214
216
|
async wakeDueAgents() {
|
|
215
217
|
const config = await this.#loadConfig();
|
package/src/socket-server.js
CHANGED
|
@@ -75,8 +75,9 @@ export class SocketServer {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
* daemon awaits this
|
|
78
|
+
* The returned promise resolves once the daemon receives a shutdown request
|
|
79
|
+
* through the socket or a signal. The daemon awaits this. Then the bin
|
|
80
|
+
* translates it to `runtime.proc.exit(0)`.
|
|
80
81
|
* @returns {Promise<void>}
|
|
81
82
|
*/
|
|
82
83
|
whenStopped() {
|
|
@@ -102,7 +103,7 @@ export class SocketServer {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
|
-
* Find the most recently modified file in
|
|
106
|
+
* Find the most recently modified file in `dir` whose name passes `filter`.
|
|
106
107
|
* @param {string} dir
|
|
107
108
|
* @param {(name: string) => boolean} filter
|
|
108
109
|
* @returns {string|null}
|
|
@@ -124,7 +125,7 @@ export class SocketServer {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
|
-
* Resolve briefing file for an agent
|
|
128
|
+
* Resolve the briefing file for an agent
|
|
128
129
|
* @param {string} agentName
|
|
129
130
|
* @param {Object} agentConfig
|
|
130
131
|
* @returns {string|null}
|
|
@@ -218,7 +219,7 @@ export class SocketServer {
|
|
|
218
219
|
if (request.type === "status") return this.#handleStatusRequest(socket);
|
|
219
220
|
|
|
220
221
|
if (request.type === "shutdown") {
|
|
221
|
-
this.#log("Shutdown requested
|
|
222
|
+
this.#log("Shutdown requested through the socket.");
|
|
222
223
|
this.#send(socket, { type: "ack", command: "shutdown" });
|
|
223
224
|
socket.end();
|
|
224
225
|
this.#requestShutdown();
|
|
@@ -258,9 +259,9 @@ export class SocketServer {
|
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
/**
|
|
261
|
-
* Tear down active children and the
|
|
262
|
-
*
|
|
263
|
-
* `runtime.proc.exit` call (design Decision 4).
|
|
262
|
+
* Tear down active children and the socket that listens. Then signal the
|
|
263
|
+
* daemon through `whenStopped` that it is safe to exit. The bin owns the
|
|
264
|
+
* actual `runtime.proc.exit` call (design Decision 4).
|
|
264
265
|
*/
|
|
265
266
|
#requestShutdown() {
|
|
266
267
|
this.#agentRunner.killActiveChildren();
|
|
@@ -272,7 +273,7 @@ export class SocketServer {
|
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
/**
|
|
275
|
-
* Remove any existing socket file
|
|
276
|
+
* Remove any existing socket file. Bind the server. Register
|
|
276
277
|
* SIGTERM/SIGINT handlers that request a graceful shutdown.
|
|
277
278
|
* @returns {import('node:net').Server}
|
|
278
279
|
*/
|
|
@@ -324,15 +325,15 @@ export class SocketServer {
|
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
/**
|
|
327
|
-
* Connect to the
|
|
328
|
+
* Connect to the daemon that already runs and ask it to wake an agent.
|
|
328
329
|
*
|
|
329
|
-
* The wake runs inside the daemon process
|
|
330
|
-
* descends from fit-outpost.app
|
|
331
|
-
* its TCC responsible process.
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
330
|
+
* The wake runs inside the daemon process. That process is the only spawn site
|
|
331
|
+
* that descends from fit-outpost.app. So the spawned `claude` inherits the app
|
|
332
|
+
* as its TCC responsible process. Every wake routes through the daemon to keep
|
|
333
|
+
* the single-grant model intact. macOS would attribute a wake spawned from
|
|
334
|
+
* this CLI process to the terminal instead. The daemon acknowledges (`ack`)
|
|
335
|
+
* once it accepts the request. It then runs the wake asynchronously. So this
|
|
336
|
+
* function resolves on the ack. It does not wait for the wake to finish.
|
|
336
337
|
*
|
|
337
338
|
* @param {string} socketPath
|
|
338
339
|
* @param {string} agent - Agent name to wake.
|
|
@@ -379,7 +380,7 @@ export async function requestWake(socketPath, agent, runtime) {
|
|
|
379
380
|
}
|
|
380
381
|
});
|
|
381
382
|
|
|
382
|
-
// A stale socket file (daemon crashed) refuses the connection
|
|
383
|
+
// A stale socket file (daemon crashed) refuses the connection. Treat it
|
|
383
384
|
// the same as a missing daemon.
|
|
384
385
|
socket.on("error", () => {
|
|
385
386
|
runtime.clock.clearTimeout(timeout);
|
package/src/spawn-env.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The daemon-mediated wake paths forward `config.env` from
|
|
5
5
|
* `~/.fit/outpost/scheduler.json` into spawned `claude` processes. This module
|
|
6
|
-
* is the single trust contract that decides which keys
|
|
6
|
+
* is the single trust contract that decides which keys the daemon honors.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Build a Set
|
|
14
|
-
*
|
|
15
|
-
* the allow-set is
|
|
13
|
+
* Build a Set with neutralised mutators. `Object.freeze` alone does not stop
|
|
14
|
+
* `Set.prototype.add`/`delete`/`clear`. They still mutate internal state. So
|
|
15
|
+
* the allow-set is a durable trust contract only when the mutators themselves
|
|
16
16
|
* throw.
|
|
17
17
|
* @param {string[]} keys
|
|
18
18
|
* @returns {ReadonlySet<string>}
|
|
@@ -38,8 +38,9 @@ export const AGENT_ENV_ALLOWSET = frozenSet(["ANTHROPIC_API_KEY"]);
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Build the spawn environment from a base env plus allow-set members of
|
|
41
|
-
* `configEnv`.
|
|
42
|
-
* `rejections`.
|
|
41
|
+
* `configEnv`. This function drops keys outside the allow-set and returns them
|
|
42
|
+
* in `rejections`. It expands tilde-prefixed values to the home directory. It
|
|
43
|
+
* is pure, so the caller logs.
|
|
43
44
|
* @param {Record<string,string>=} configEnv
|
|
44
45
|
* @param {NodeJS.ProcessEnv} baseEnv
|
|
45
46
|
* @returns {{ env: Record<string,string>, rejections: string[] }}
|
package/src/state-manager.js
CHANGED
|
@@ -27,8 +27,8 @@ export class StateManager {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Read and parse state from disk
|
|
31
|
-
* empty state and return it.
|
|
30
|
+
* Read and parse the state from disk. On any read or parse error, write a
|
|
31
|
+
* fresh empty state and return it.
|
|
32
32
|
* @returns {Promise<Object>}
|
|
33
33
|
*/
|
|
34
34
|
async load() {
|
|
@@ -3,34 +3,41 @@ name: chief-of-staff
|
|
|
3
3
|
description: >
|
|
4
4
|
The user's executive assistant. Creates daily briefings that synthesize
|
|
5
5
|
email, calendar, and knowledge graph state into actionable priorities.
|
|
6
|
-
|
|
6
|
+
The Outpost scheduler wakes it at key moments (morning, evening).
|
|
7
7
|
model: sonnet
|
|
8
8
|
permissionMode: bypassPermissions
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
You are the chief of staff
|
|
12
|
-
synthesize what matters across email, calendar, and the knowledge graph
|
|
13
|
-
single briefing.
|
|
11
|
+
You are the chief of staff. You are the user's executive assistant. On each
|
|
12
|
+
wake, synthesize what matters across email, calendar, and the knowledge graph
|
|
13
|
+
into a single briefing.
|
|
14
|
+
|
|
15
|
+
## Tiers
|
|
16
|
+
|
|
17
|
+
Read: every tier present
|
|
18
|
+
Write: none (the output is personal `Briefings/`)
|
|
19
|
+
Stamp the frontmatter standard per CLAUDE.md on every note you write.
|
|
20
|
+
Aggregate outputs over narrower-tier sources go to `0-Draft/`.
|
|
14
21
|
|
|
15
22
|
## Priorities
|
|
16
23
|
|
|
17
|
-
`
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
user's priorities.
|
|
24
|
+
`Priorities/` notes in every tier present are the backbone of every briefing.
|
|
25
|
+
Read them and `Conditions/` on each wake. Conditions hold the live
|
|
26
|
+
constraints that shape how you pursue the priorities. See Operating Context in
|
|
27
|
+
CLAUDE.md. The Inputs section below also lists both folders. Frame the whole
|
|
28
|
+
briefing around what advances or threatens the user's priorities.
|
|
22
29
|
|
|
23
30
|
- **Always consider them.** Tie the schedule, the top actions, and the pipeline
|
|
24
31
|
back to the priority each one serves.
|
|
25
32
|
- **Always escalate risks.** Consolidate every `## Priority Watch` flag from the
|
|
26
|
-
sibling triage files
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
sibling triage files into a `## Priority Watch` section in the briefing. Add
|
|
34
|
+
anything you find in your own reads. Each item names the priority, the
|
|
35
|
+
evidence, and the risk. A signal that could contradict, block, or slow a
|
|
29
36
|
priority is the most important thing the briefing surfaces.
|
|
30
37
|
|
|
31
38
|
## Inputs
|
|
32
39
|
|
|
33
|
-
Read all five sibling agents' triage files before
|
|
40
|
+
Read all five sibling agents' triage files before you write. These files are the
|
|
34
41
|
authoritative current-state summaries:
|
|
35
42
|
|
|
36
43
|
- `~/.cache/fit/outpost/state/postman_triage.md`
|
|
@@ -39,9 +46,9 @@ authoritative current-state summaries:
|
|
|
39
46
|
- `~/.cache/fit/outpost/state/recruiter_triage.md`
|
|
40
47
|
- `~/.cache/fit/outpost/state/head_hunter_triage.md`
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
`~/.cache/fit/outpost/apple_calendar/`, and unchecked `- [ ]`
|
|
44
|
-
|
|
49
|
+
Also read these directly: `Priorities/` and `Conditions/` in every tier,
|
|
50
|
+
`0-Draft/`, `~/.cache/fit/outpost/apple_calendar/`, and unchecked `- [ ]`
|
|
51
|
+
items across the tiers.
|
|
45
52
|
|
|
46
53
|
## Routing
|
|
47
54
|
|
|
@@ -51,17 +58,18 @@ Plus directly: `Knowledge/Priorities/`, `Knowledge/Conditions/`, `Drafts/`,
|
|
|
51
58
|
| Noon or later | `Briefings/{YYYY-MM-DD}-evening.md` |
|
|
52
59
|
|
|
53
60
|
A briefing covers: today's schedule with prep status, top three priority actions
|
|
54
|
-
linked
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
linked with tier-prefixed links (`[[3-Team/Priorities/...]]`), priority
|
|
62
|
+
progress, a **Priority Watch** section
|
|
63
|
+
that consolidates the priority risks the agents flagged, inbox snapshot
|
|
64
|
+
(urgent / awaiting reply), open commitments, recruitment pipeline summary, and a
|
|
65
|
+
heads-up section. Evening briefings replace "Priority Actions" with "What
|
|
66
|
+
Happened Today" and "Still Outstanding".
|
|
59
67
|
|
|
60
68
|
## Scope
|
|
61
69
|
|
|
62
|
-
- This agent **synthesizes
|
|
63
|
-
|
|
64
|
-
- Do not act on email, candidates, or transcripts directly
|
|
70
|
+
- This agent **synthesizes**. Never duplicate work the other agents already
|
|
71
|
+
triaged. Cite their findings. Do not re-derive them.
|
|
72
|
+
- Do not act on email, candidates, or transcripts directly. Those belong to the
|
|
65
73
|
postman, recruiter, and concierge.
|
|
66
74
|
|
|
67
75
|
## Output
|