@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.
Files changed (129) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +6 -5
  4. package/src/agent-path.js +10 -6
  5. package/src/agent-runner.js +28 -24
  6. package/src/index.js +2 -3
  7. package/src/kb-manager.js +75 -20
  8. package/src/kb-validator.js +762 -0
  9. package/src/outpost.js +108 -39
  10. package/src/posture.js +3 -3
  11. package/src/privilege.js +11 -11
  12. package/src/scheduler.js +14 -12
  13. package/src/socket-server.js +19 -18
  14. package/src/spawn-env.js +7 -6
  15. package/src/state-manager.js +2 -2
  16. package/templates/.claude/agents/chief-of-staff.md +32 -24
  17. package/templates/.claude/agents/concierge.md +26 -17
  18. package/templates/.claude/agents/head-hunter.md +34 -26
  19. package/templates/.claude/agents/librarian.md +26 -18
  20. package/templates/.claude/agents/postman.md +26 -18
  21. package/templates/.claude/agents/recruiter.md +38 -27
  22. package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
  23. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  24. package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
  25. package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
  26. package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
  27. package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
  28. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
  29. package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
  30. package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
  31. package/templates/.claude/skills/candidate-report/references/report.css +4 -4
  32. package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
  33. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
  34. package/templates/.claude/skills/changelog/SKILL.md +122 -67
  35. package/templates/.claude/skills/deck-create/SKILL.md +90 -32
  36. package/templates/.claude/skills/deck-create/references/slide.css +4 -4
  37. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
  38. package/templates/.claude/skills/deck-review/SKILL.md +63 -58
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
  44. package/templates/.claude/skills/doc-create/SKILL.md +27 -23
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
  50. package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +37 -13
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
  57. package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
  59. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
  60. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
  61. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
  62. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
  64. package/templates/.claude/skills/organize-files/SKILL.md +27 -22
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  67. package/templates/.claude/skills/person-identify/SKILL.md +77 -27
  68. package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
  69. package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
  70. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
  71. package/templates/.claude/skills/req-assess/SKILL.md +44 -36
  72. package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
  73. package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
  74. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  75. package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
  76. package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
  77. package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
  78. package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
  79. package/templates/.claude/skills/req-decide/SKILL.md +52 -41
  80. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  81. package/templates/.claude/skills/req-decide/references/template.md +2 -2
  82. package/templates/.claude/skills/req-forget/SKILL.md +58 -39
  83. package/templates/.claude/skills/req-forget/references/classify.md +15 -13
  84. package/templates/.claude/skills/req-forget/references/locations.md +27 -22
  85. package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
  86. package/templates/.claude/skills/req-scan/SKILL.md +27 -25
  87. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  88. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  89. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  90. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  91. package/templates/.claude/skills/req-scan/references/template.md +11 -1
  92. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  93. package/templates/.claude/skills/req-screen/SKILL.md +96 -50
  94. package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
  95. package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
  96. package/templates/.claude/skills/req-screen/references/template.md +29 -17
  97. package/templates/.claude/skills/req-track/SKILL.md +76 -76
  98. package/templates/.claude/skills/req-track/references/fields.md +31 -27
  99. package/templates/.claude/skills/req-track/references/overlays.md +42 -0
  100. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  101. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  102. package/templates/.claude/skills/req-track/references/templates.md +28 -27
  103. package/templates/.claude/skills/req-workday/SKILL.md +69 -40
  104. package/templates/.claude/skills/req-workday/references/brief.md +81 -0
  105. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  106. package/templates/.claude/skills/req-workday/references/templates.md +11 -78
  107. package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
  108. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
  109. package/templates/.claude/skills/send-chat/SKILL.md +58 -54
  110. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
  111. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  112. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  113. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  114. package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
  115. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  116. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  117. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  118. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  119. package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
  120. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  121. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  122. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  123. package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
  124. package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
  125. package/templates/CLAUDE.md +97 -85
  126. package/templates/MIGRATION.md +359 -0
  127. package/templates/registry.yaml +25 -0
  128. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
  129. 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 Validate agent definitions exist
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. 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`.
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. The object is byte-identical to the libcli
42
- * definition the goldens were captured against, so `--help` / `--version`
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
- { name: "validate", description: "Validate agent definitions exist" },
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: { type: "boolean", description: "JSON output (with --help)" },
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 (pure formatting).
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
- * Async existence check via the one fs surface this module uses.
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`); importing it eagerly would
197
- // crash plain `node` (e.g. `--help`/`--version`/golden capture). Load it
198
- // lazily so only an actual agent wake which only runs under Bun on macOS —
199
- // pulls it in.
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. Polling every 60 seconds.");
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 `run()` returns 0 and the bin exits only
280
- // on a nonzero code, so a lingering 60s timer would delay exit.
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 shutdown is requested via socket or signal; the bin then
296
- // owns the process-exit call.
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 matching
327
- // `^posture: (brief|brief+draft|unset)$`, so write it as plain text to
328
- // stdout rather than through the RFC5424-prefixed logger.
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
- return errors > 0 ? 1 : 0;
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 running daemon. The daemon is the
413
- // only spawn site that descends from fit-outpost.app, so a `claude`
414
- // spawned there inherits the app as its TCC responsible process and a
415
- // single grant to the app covers it. Spawning from this CLI process
416
- // would attribute the access to the terminal instead, breaking the
417
- // single-grant model. If no daemon is running there is nowhere to wake
418
- // with correct attribution, so this errors rather than spawning locally.
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 waking an agent.",
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`), never an arbitrary path so the substrate cannot be
438
- // steered back into a TCC-protected folder. An unsafe name is refused.
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 none is recorded so re-running never flips
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; they appear
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, applying the default when no posture has been
50
- * recorded so a posture-less install behaves as `brief`.
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 (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 held responsible for itself, so those grants are not
9
- * extended and it can reach only non-TCC-protected substrate.
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 and 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. Patterned on `posture.js` but with no `effective*` coercion and no
14
- * default a missing or unrecognised value throws.
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: a
22
- * missing or unrecognised value throws there is no default.
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: `restricted` self-disclaims (`1`),
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` and
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()`); the AST checker cannot distinguish the two, so this file is
10
- * allow-listed in .coaligned/invariants/ambient-deps.allow.yml with that reason.
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 being considered stale (35 min). */
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; this is the single seam that turns the
21
- * wall-clock read (`runtime.clock.now()`) into one, so callers in other
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. Parses
32
- * an explicit value (never the wall clock); lives here so the only `new Date`
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 by evaluating schedules and delegating to AgentRunner. */
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 agents exceeding max runtime, reload config, then wake each agent whose schedule is due.
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();
@@ -75,8 +75,9 @@ export class SocketServer {
75
75
  }
76
76
 
77
77
  /**
78
- * Resolves once a shutdown has been requested (via socket or signal). The
79
- * daemon awaits this, then the bin translates it to `runtime.proc.exit(0)`.
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 a directory matching a filter.
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 via socket.");
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 listening socket, then signal the daemon
262
- * (via `whenStopped`) that it is safe to exit. The bin owns the actual
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, bind the server, and register
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 running daemon and ask it to wake an agent.
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, which is the only spawn site that
330
- * descends from fit-outpost.app so the spawned `claude` inherits the app as
331
- * its TCC responsible process. Routing every wake through the daemon is what
332
- * keeps the single-grant model intact; a wake spawned from this CLI process
333
- * would be attributed to the terminal instead. The daemon acknowledges
334
- * (`ack`) once it has accepted the request and then runs the wake
335
- * asynchronously, so this resolves on the ack rather than on completion.
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; treat it
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 are honored.
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 whose mutators are neutralised. `Object.freeze` alone does not
14
- * stop `Set.prototype.add`/`delete`/`clear` from mutating internal state, so
15
- * the allow-set is only a durable trust contract if the mutators themselves
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`. Keys outside the allow-set are dropped and returned in
42
- * `rejections`. Tilde-prefixed values are home-expanded. Pure; the caller logs.
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[] }}
@@ -27,8 +27,8 @@ export class StateManager {
27
27
  }
28
28
 
29
29
  /**
30
- * Read and parse state from disk; on any read or parse error, write a fresh
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
- Woken at key moments (morning, evening) by the Outpost scheduler.
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 the user's executive assistant. Each wake:
12
- synthesize what matters across email, calendar, and the knowledge graph into a
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
- `Knowledge/Priorities/` is the backbone of every briefing. Read it and
18
- `Knowledge/Conditions/` (the live constraints that shape how priorities are
19
- pursued see Operating Context in CLAUDE.md) each wake (both are also listed
20
- under Inputs) and frame the whole briefing around what advances or threatens the
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 plus anything you find in your own reads — into a
27
- `## Priority Watch` section in the briefing, each item naming the priority,
28
- the evidence, and the risk. A signal that could contradict, block, or slow a
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 writing these are the
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
- Plus directly: `Knowledge/Priorities/`, `Knowledge/Conditions/`, `Drafts/`,
43
- `~/.cache/fit/outpost/apple_calendar/`, and unchecked `- [ ]` items in
44
- `Knowledge/`.
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 to `[[Priorities/...]]`, priority progress, a **Priority Watch** section
55
- consolidating priority risks flagged by the agents, inbox snapshot (urgent /
56
- awaiting reply), open commitments, recruitment pipeline summary, and a heads-up
57
- section. Evening briefings replace "Priority Actions" with "What Happened Today"
58
- and "Still Outstanding".
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** never duplicate work the other agents have
63
- already triaged. Cite their findings, don't re-derive them.
64
- - Do not act on email, candidates, or transcripts directly those belong to the
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