@forwardimpact/outpost 3.12.0 → 3.12.1

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 (114) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +4 -4
  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 +21 -19
  8. package/src/outpost.js +37 -33
  9. package/src/posture.js +3 -3
  10. package/src/privilege.js +11 -11
  11. package/src/scheduler.js +14 -12
  12. package/src/socket-server.js +19 -18
  13. package/src/spawn-env.js +7 -6
  14. package/src/state-manager.js +2 -2
  15. package/templates/.claude/agents/chief-of-staff.md +22 -22
  16. package/templates/.claude/agents/concierge.md +19 -17
  17. package/templates/.claude/agents/head-hunter.md +27 -26
  18. package/templates/.claude/agents/librarian.md +17 -16
  19. package/templates/.claude/agents/postman.md +19 -18
  20. package/templates/.claude/agents/recruiter.md +31 -27
  21. package/templates/.claude/skills/anarlog-follow/SKILL.md +50 -49
  22. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  23. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +4 -4
  24. package/templates/.claude/skills/anarlog-process/SKILL.md +35 -32
  25. package/templates/.claude/skills/anarlog-process/references/extraction.md +26 -22
  26. package/templates/.claude/skills/anarlog-process/references/sessions.md +10 -10
  27. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +10 -9
  28. package/templates/.claude/skills/anarlog-trim/SKILL.md +43 -40
  29. package/templates/.claude/skills/candidate-report/SKILL.md +30 -28
  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 +55 -50
  35. package/templates/.claude/skills/deck-create/SKILL.md +30 -30
  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 +59 -57
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +49 -43
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +4 -3
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +25 -25
  44. package/templates/.claude/skills/doc-create/SKILL.md +21 -20
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +37 -33
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +5 -4
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +11 -9
  50. package/templates/.claude/skills/extract-entities/SKILL.md +65 -65
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +5 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +14 -13
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +5 -5
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +19 -18
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +13 -13
  57. package/templates/.claude/skills/extract-entities/references/sources.md +8 -8
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +2 -2
  59. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +2 -2
  60. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  61. package/templates/.claude/skills/meeting-prep/SKILL.md +25 -24
  62. package/templates/.claude/skills/organize-files/SKILL.md +24 -22
  63. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  64. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  65. package/templates/.claude/skills/person-identify/SKILL.md +24 -22
  66. package/templates/.claude/skills/person-identify/scripts/identify.sh +17 -17
  67. package/templates/.claude/skills/person-lookup/SKILL.md +28 -29
  68. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +19 -16
  69. package/templates/.claude/skills/req-assess/SKILL.md +27 -27
  70. package/templates/.claude/skills/req-assess/references/interview-template.md +1 -1
  71. package/templates/.claude/skills/req-assess/references/panel-template.md +3 -2
  72. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  73. package/templates/.claude/skills/req-decide/SKILL.md +28 -25
  74. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  75. package/templates/.claude/skills/req-forget/SKILL.md +28 -26
  76. package/templates/.claude/skills/req-forget/references/classify.md +13 -13
  77. package/templates/.claude/skills/req-forget/references/locations.md +2 -2
  78. package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
  79. package/templates/.claude/skills/req-scan/SKILL.md +21 -22
  80. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  81. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  82. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  83. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  84. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  85. package/templates/.claude/skills/req-screen/SKILL.md +31 -29
  86. package/templates/.claude/skills/req-screen/references/rubric.md +18 -18
  87. package/templates/.claude/skills/req-screen/references/template.md +4 -4
  88. package/templates/.claude/skills/req-track/SKILL.md +40 -40
  89. package/templates/.claude/skills/req-track/references/fields.md +19 -18
  90. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  91. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  92. package/templates/.claude/skills/req-track/references/templates.md +4 -4
  93. package/templates/.claude/skills/req-workday/SKILL.md +24 -24
  94. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  95. package/templates/.claude/skills/req-workday/references/templates.md +13 -13
  96. package/templates/.claude/skills/req-workday/references/xlsx-format.md +7 -7
  97. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +19 -17
  98. package/templates/.claude/skills/send-chat/SKILL.md +47 -43
  99. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +20 -19
  100. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  101. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  102. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  103. package/templates/.claude/skills/sync-apple-mail/SKILL.md +17 -14
  104. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  105. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  106. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  107. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  108. package/templates/.claude/skills/sync-teams/SKILL.md +43 -43
  109. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  110. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  111. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  112. package/templates/.claude/skills/upstream-instructions/SKILL.md +39 -40
  113. package/templates/.claude/skills/upstream-instructions/references/examples.md +2 -2
  114. package/templates/CLAUDE.md +40 -37
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  <!-- BEGIN:description — Do not edit. Generated from package.json. -->
4
4
 
5
- Personal operations center — context from email, calendar, and knowledge
6
- assembled so preparation is continuous, not a morning scramble.
5
+ Personal operations center — context assembled from email, calendar, and
6
+ knowledge so preparation is continuous instead of a morning scramble.
7
7
 
8
8
  <!-- END:description -->
9
9
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // Thin entry point the sole construction site for the runtime collaborator
3
- // bag, threaded into src/outpost.js's dispatch via run(runtime, version).
2
+ // This file is the thin entry point. It is the sole construction site for the
3
+ // runtime collaborator bag. It threads that bag into the dispatch in
4
+ // src/outpost.js through run(runtime, version).
4
5
  import "@forwardimpact/libpreflight/node22";
5
6
 
6
7
  import { resolveVersion } from "@forwardimpact/libcli";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@forwardimpact/outpost",
3
- "version": "3.12.0",
4
- "description": "Personal operations center — context from email, calendar, and knowledge assembled so preparation is continuous, not a morning scramble.",
3
+ "version": "3.12.1",
4
+ "description": "Personal operations center — context assembled from email, calendar, and knowledge so preparation is continuous instead of a morning scramble.",
5
5
  "homepage": "https://www.forwardimpact.team",
6
6
  "repository": {
7
7
  "type": "git",
@@ -14,12 +14,12 @@
14
14
  {
15
15
  "user": "Empowered Engineers",
16
16
  "goal": "Be Prepared and Productive",
17
- "trigger": "Walking into a meeting cold because context was scattered across email, Slack, and last week's notes.",
17
+ "trigger": "The engineer walks into a meeting cold because context is scattered across email, Slack, and last week's notes.",
18
18
  "bigHire": "keep track of people, projects, and threads without depending on memory.",
19
19
  "littleHire": "walk into every meeting already oriented.",
20
20
  "competesWith": "morning email scan; re-reading calendar invites; personal notes; asking colleagues for context",
21
21
  "forces": {
22
- "push": "Context-switching compounds; starting cold wastes everyone's time.",
22
+ "push": "Context-switching compounds. Starting cold wastes everyone's time.",
23
23
  "pull": "Continuous awareness without continuous effort — context assembled.",
24
24
  "habit": "Morning routines and tracking systems that feel 'good enough.'",
25
25
  "anxiety": "Delegating awareness to a system feels like losing control."
package/src/agent-path.js CHANGED
@@ -1,15 +1,19 @@
1
1
  /**
2
- * agent-path — validate a config-supplied agent name before using it as a
2
+ * agent-path — validate a config-supplied agent name before you use it as a
3
3
  * filesystem path component.
4
4
  *
5
5
  * The agent-state writer maps an agent name to a per-agent state filename.
6
- * A name carrying `/` or `..` segments would let a rewritten `scheduler.json`
7
- * direct writes outside `~/.cache/fit/outpost/state/`. This module validates
8
- * and rejects rather than silently sanitising, so an unexpected path segment
9
- * surfaces as an intrusion signal instead of a quietly rewritten filename.
6
+ * A name that carries `/` or `..` segments would let a rewritten
7
+ * `scheduler.json` direct writes outside `~/.cache/fit/outpost/state/`. This
8
+ * module validates the name and rejects an unsafe one. It never sanitises the
9
+ * name silently. So an unexpected path segment surfaces as an intrusion
10
+ * signal. It does not become a quietly rewritten filename.
10
11
  */
11
12
 
12
- /** Raised when an agent name cannot map to a safe state-file prefix. */
13
+ /**
14
+ * `agentNameToStatePrefix` raises this when an agent name cannot map to a
15
+ * safe state-file prefix.
16
+ */
13
17
  export class UnsafeAgentNameError extends Error {
14
18
  /** @param {string} name */
15
19
  constructor(name) {
@@ -15,14 +15,14 @@ import { resolvePrivilege, disclaimFor } from "./privilege.js";
15
15
  import { buildSpawnEnv } from "./spawn-env.js";
16
16
 
17
17
  /**
18
- * System-prompt directive injected under the `brief` posture. Neutralises any
19
- * draft-side prose in a materialised agent definition and forbids writing the
20
- * posture record.
18
+ * The system-prompt directive the runner injects under the `brief` posture. It
19
+ * neutralises any draft-side prose in a materialised agent definition. It also
20
+ * forbids a write to the posture record.
21
21
  */
22
22
  const BRIEF_DIRECTIVE =
23
23
  "Adoption posture: brief. Run only read-and-brief work. Do not draft or " +
24
- "send content on the user's behalf for delivery to anyone else, do not move " +
25
- "or write files outside the knowledge base, and never write the posture " +
24
+ "send content on the user's behalf for delivery to anyone else. Do not " +
25
+ "move or write files outside the knowledge base. Never write the posture " +
26
26
  "record (~/.fit/outpost/posture.json).";
27
27
 
28
28
  /** Spawn agent CLI processes, capture their output, and update agent state. */
@@ -41,11 +41,11 @@ export class AgentRunner {
41
41
 
42
42
  /**
43
43
  * @param {Object | (() => Object | Promise<{default?: Object}>)} spawn -
44
- * The posix-spawn module, or a (possibly async) loader returning it. A
45
- * loader lets the Bun-FFI module load lazily so plain `node` invocations
46
- * that never wake an agent don't pull in `bun:ffi`.
44
+ * The posix-spawn module, or a loader that returns it. The loader may be
45
+ * async. A loader lets the Bun-FFI module load lazily. So a plain `node`
46
+ * invocation that never wakes an agent does not pull in `bun:ffi`.
47
47
  * @param {import('./state-manager.js').StateManager} stateManager
48
- * @param {Function} logFn - Logging function
48
+ * @param {Function} logFn - The function that writes a log line
49
49
  * @param {string} cacheDir - Cache directory for state files
50
50
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
51
51
  * Injected runtime bag (uses `fs` (async), `proc`, `clock`).
@@ -84,8 +84,8 @@ export class AgentRunner {
84
84
  }
85
85
 
86
86
  /**
87
- * Resolve the injected spawn collaborator to the posix-spawn module,
88
- * invoking and unwrapping a loader thunk on first use.
87
+ * Resolve the injected spawn collaborator to the posix-spawn module. On
88
+ * first use, invoke a loader thunk and unwrap it.
89
89
  * @returns {Promise<Object>}
90
90
  */
91
91
  async #resolveSpawn() {
@@ -97,7 +97,7 @@ export class AgentRunner {
97
97
  }
98
98
 
99
99
  /**
100
- * Test whether a path exists, via the async fs surface.
100
+ * Test whether a path exists, through the async fs surface.
101
101
  * @param {string} p
102
102
  * @returns {Promise<boolean>}
103
103
  */
@@ -148,11 +148,12 @@ export class AgentRunner {
148
148
  }
149
149
 
150
150
  /**
151
- * Resolve the spawn flags the recorded posture adds. Under `brief` (also the
152
- * default when no posture is recorded), deny every draft-side skill by name
153
- * and inject the brief directive a deterministic gate the woken agent
154
- * cannot override under `bypassPermissions`. Under `brief+draft`, add
155
- * nothing. The posture record is read here, never written.
151
+ * Resolve the spawn flags the recorded posture adds. Under `brief`, deny
152
+ * every draft-side skill by name and inject the brief directive. `brief` is
153
+ * also the default when no posture is recorded. The directive is a
154
+ * deterministic gate. The woken agent cannot override it under
155
+ * `bypassPermissions`. Under `brief+draft`, add nothing. This method reads
156
+ * the posture record. It never writes the record.
156
157
  * @returns {Promise<string[]>} Extra args to append to the spawn argv.
157
158
  */
158
159
  async #postureArgs() {
@@ -170,17 +171,20 @@ export class AgentRunner {
170
171
  }
171
172
 
172
173
  /**
173
- * Validate the agent's kb path exists, spawn `claude --agent` with the prompt "Observe and act.", and update agent state to active/idle/failed.
174
+ * Validate that the agent's kb path exists. Spawn `claude --agent` with the
175
+ * prompt "Observe and act." Then update the agent state to active, idle, or
176
+ * failed.
174
177
  * @param {string} agentName
175
178
  * @param {Object} agent
176
179
  * @param {Object} state
177
180
  * @param {Record<string, string>} [configEnv] - Extra env vars from config
178
181
  */
179
182
  async wake(agentName, agent, state, configEnv) {
180
- // Resolve the mandatory privilege level before any work. A missing or
181
- // invalid level is fail-closed: log and skip the wake no agent process is
182
- // spawned with a guessed privilege. The level lives in the user-only trust
183
- // root, so a spawned agent cannot raise its own.
183
+ // Resolve the mandatory privilege level before any work. The runner is
184
+ // fail-closed on a missing or invalid level. It logs the problem and skips
185
+ // the wake. It never spawns an agent process with a guessed privilege. The
186
+ // level lives in the user-only trust root, so a spawned agent cannot raise
187
+ // its own.
184
188
  let level;
185
189
  try {
186
190
  level = resolvePrivilege(agent);
@@ -196,13 +200,13 @@ export class AgentRunner {
196
200
  }
197
201
 
198
202
  if (!agent.kb) {
199
- this.#log(`Agent ${agentName}: no "kb" specified, skipping.`);
203
+ this.#log(`Agent ${agentName}: no "kb" specified. Skipped.`);
200
204
  return;
201
205
  }
202
206
  const kbPath = this.#expandPath(agent.kb);
203
207
  if (!(await this.#exists(kbPath))) {
204
208
  this.#log(
205
- `Agent ${agentName}: path "${kbPath}" does not exist, skipping.`,
209
+ `Agent ${agentName}: path "${kbPath}" does not exist. Skipped.`,
206
210
  );
207
211
  return;
208
212
  }
package/src/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // Public entry point for @forwardimpact/outpost.
2
- // Outpost is primarily a CLI this file exists so the package conforms
3
- // to the repo-wide layout contract. The runtime CLI dispatch lives in
4
- // src/outpost.js.
2
+ // Outpost is primarily a CLI. This file exists so the package conforms to the
3
+ // repo-wide layout contract. The runtime CLI dispatch lives in src/outpost.js.
5
4
  export * from "./outpost.js";
package/src/kb-manager.js CHANGED
@@ -6,7 +6,7 @@ import { join, dirname, resolve, basename } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { createLogger } from "@forwardimpact/libtelemetry";
8
8
 
9
- /** Manage knowledge base lifecycle including initialization, updates, and settings merging. */
9
+ /** Manage the knowledge base lifecycle: init, update, and settings merge. */
10
10
  export class KBManager {
11
11
  #fs;
12
12
  #logger;
@@ -25,12 +25,13 @@ export class KBManager {
25
25
 
26
26
  /**
27
27
  * Resolve a knowledge-base name to its path under the XDG data home,
28
- * `~/.local/share/fit/outpost/<name>`. The argument is a single path segment,
29
- * never an arbitrary filesystem path, so a provisioned KB always lands outside
30
- * TCC-protected folders. The name is validated — not sanitised — against the
31
- * same rule as `agent-path.js`: a name carrying `/`, `\`, `..`, NUL, or a
32
- * leading `~` could steer the KB back inside `~/Documents`, reopening the TCC
33
- * hole, so it is rejected rather than rewritten.
28
+ * `~/.local/share/fit/outpost/<name>`. The argument is a single path
29
+ * segment. It is never an arbitrary filesystem path. So a provisioned KB
30
+ * always lands outside TCC-protected folders. This method validates the name
31
+ * against the same rule as `agent-path.js`. It does not sanitise the name. A
32
+ * name that carries `/`, `\`, `..`, NUL, or a leading `~` could steer the KB
33
+ * back inside `~/Documents` and reopen the TCC hole. So this method rejects
34
+ * the name. It never rewrites the name.
34
35
  * @param {string} name - The KB name (e.g. `Team`, `personal`).
35
36
  * @returns {string} Absolute path under the data home.
36
37
  * @throws {Error} when `name` is empty, non-string, or an unsafe segment.
@@ -51,7 +52,7 @@ export class KBManager {
51
52
  }
52
53
 
53
54
  /**
54
- * Test whether a path exists, via the async fs surface.
55
+ * Test whether a path exists, through the async fs surface.
55
56
  * @param {string} p
56
57
  * @returns {Promise<boolean>}
57
58
  */
@@ -94,7 +95,7 @@ export class KBManager {
94
95
  }
95
96
 
96
97
  /**
97
- * Copy bundled files (CLAUDE.md, skills, agents) from template to a KB.
98
+ * Copy bundled files (CLAUDE.md, skills, agents) from the template to a KB.
98
99
  * @param {string} tpl - Path to the template directory
99
100
  * @param {string} dest - Path to the target knowledge base
100
101
  * @returns {Promise<void>}
@@ -156,7 +157,7 @@ export class KBManager {
156
157
  }
157
158
 
158
159
  /**
159
- * Merge template settings.json into the destination's settings.json.
160
+ * Merge the template settings.json into the destination's settings.json.
160
161
  * @param {string} tpl - Template directory
161
162
  * @param {string} dest - Knowledge base directory
162
163
  * @returns {Promise<void>}
@@ -206,10 +207,10 @@ export class KBManager {
206
207
  }
207
208
 
208
209
  await this.#ensureDir(dest);
209
- // Create the top-level roots only. `Knowledge/` is the shared graph (synced
210
- // with the team); `Drafts/` and `Briefings/` are personal and local. Entity
211
- // subdirectories (People, Organizations, ...) are created on demand by the
212
- // skills that write into them.
210
+ // Create the top-level roots only. `Knowledge/` is the shared graph, and
211
+ // the team syncs it. `Drafts/` and `Briefings/` are personal and local.
212
+ // The skills that write into the entity subdirectories (People,
213
+ // Organizations, ...) create them on demand.
213
214
  for (const d of ["Knowledge", "Drafts", "Briefings"])
214
215
  await this.#ensureDir(join(dest, d));
215
216
 
@@ -223,12 +224,13 @@ export class KBManager {
223
224
  }
224
225
 
225
226
  /**
226
- * Create a navigation symlink at `~/Documents/<name>` pointing to the KB.
227
+ * Create a navigation symlink at `~/Documents/<name>` that points to the KB.
227
228
  * The KB data itself stays under the XDG data home, outside TCC-protected
228
- * folders this is only a convenience pointer so the KB is easy to find and
229
- * open from Finder. Best-effort: a pre-existing entry is left untouched, and
230
- * any failure (e.g. macOS denying write access to `~/Documents`) is logged,
231
- * never fatal, because the KB is already provisioned at `dest`.
229
+ * folders. The symlink is only a convenience pointer. It makes the KB easy
230
+ * to find and open from Finder. The method is best-effort. It leaves a
231
+ * pre-existing entry untouched. It logs any failure, for example when macOS
232
+ * denies write access to `~/Documents`. A failure is never fatal, because
233
+ * the KB is already provisioned at `dest`.
232
234
  * @param {string} dest - Absolute path to the provisioned KB.
233
235
  * @returns {Promise<void>}
234
236
  */
package/src/outpost.js CHANGED
@@ -11,10 +11,11 @@
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";
@@ -38,9 +39,9 @@ import {
38
39
  const SHARE_DIR = "/usr/local/share/fit-outpost";
39
40
 
40
41
  /**
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.
42
+ * Build the CLI definition. This object is byte-identical to the libcli
43
+ * definition that the golden capture used. So `--help` and `--version` output
44
+ * stays stable.
44
45
  * @param {string} version
45
46
  * @returns {object}
46
47
  */
@@ -111,7 +112,7 @@ function buildDefinition(version) {
111
112
  }
112
113
 
113
114
  /**
114
- * Render an agent's multi-line status block (pure formatting).
115
+ * Render an agent's multi-line status block. This is a pure format step.
115
116
  * @param {string} name
116
117
  * @param {Object} agent
117
118
  * @param {Object} s - The agent's persisted state.
@@ -145,7 +146,8 @@ export async function run(runtime, version) {
145
146
  const logger = createLogger("outpost", runtime);
146
147
 
147
148
  /**
148
- * Async existence check via the one fs surface this module uses.
149
+ * Test whether a path exists, through the one async fs surface this module
150
+ * uses.
149
151
  * @param {string} p
150
152
  * @returns {Promise<boolean>}
151
153
  */
@@ -193,10 +195,10 @@ export async function run(runtime, version) {
193
195
  }
194
196
 
195
197
  // --- 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.
198
+ // posix-spawn is a Bun-FFI module (`bun:ffi`). An eager import would crash
199
+ // plain `node` (e.g. `--help`/`--version`/golden capture). So load it
200
+ // lazily. Then only an actual agent wake pulls it in. That wake only runs
201
+ // under Bun on macOS.
200
202
  const loadSpawn = () => import("@forwardimpact/libmacos/posix-spawn");
201
203
  const stateManager = new StateManager(STATE_PATH, runtime);
202
204
  const agentRunner = new AgentRunner(
@@ -247,7 +249,7 @@ export async function run(runtime, version) {
247
249
  // --- Daemon ----------------------------------------------------------------
248
250
  async function daemon() {
249
251
  const daemonStartedAt = clock.now();
250
- log("Scheduler daemon started. Polling every 60 seconds.");
252
+ log("Scheduler daemon started. It polls every 60 seconds.");
251
253
  log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
252
254
 
253
255
  // Reset any agents left "active" from a previous daemon session.
@@ -276,8 +278,8 @@ export async function run(runtime, version) {
276
278
  void socketServer.whenStopped().then(() => {
277
279
  stopped = true;
278
280
  // 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.
281
+ // loop alive after shutdown. `run()` returns 0, and the bin exits only
282
+ // on a nonzero code. So a 60s timer that stays armed would delay exit.
281
283
  if (tickHandle !== undefined) clock.clearTimeout(tickHandle);
282
284
  });
283
285
 
@@ -292,8 +294,8 @@ export async function run(runtime, version) {
292
294
  }
293
295
  void tick();
294
296
 
295
- // Block until a shutdown is requested via socket or signal; the bin then
296
- // owns the process-exit call.
297
+ // Block until a socket or a signal requests a shutdown. The bin then owns
298
+ // the process-exit call.
297
299
  await socketServer.whenStopped();
298
300
  return 0;
299
301
  }
@@ -323,9 +325,9 @@ export async function run(runtime, version) {
323
325
  const state = await stateManager.load();
324
326
  const posture = await readPosture(fs, POSTURE_PATH);
325
327
  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.
328
+ // The posture must be observable on a line that matches
329
+ // `^posture: (brief|brief+draft|unset)$`. So write it as plain text to
330
+ // stdout. Do not write it through the RFC5424-prefixed logger.
329
331
  proc.stdout.write(`posture: ${posture ?? "unset"}\n`);
330
332
 
331
333
  const agents = Object.entries(config.agents || {});
@@ -409,13 +411,14 @@ export async function run(runtime, version) {
409
411
  cli.usageError("missing required argument <agent>");
410
412
  return 2;
411
413
  }
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.
414
+ // Always route the wake through the daemon that already runs. The daemon
415
+ // is the only spawn site that descends from fit-outpost.app. So a
416
+ // `claude` spawned there inherits the app as its TCC responsible
417
+ // process, and a single grant to the app covers it. A spawn from this
418
+ // CLI process would attribute the access to the terminal instead. That
419
+ // breaks the single-grant model. When no daemon runs, there is nowhere
420
+ // to wake with correct attribution. So this command errors. It does not
421
+ // spawn locally.
419
422
  const result = await requestWake(SOCKET_PATH, args[0], runtime);
420
423
  if (result.ok) {
421
424
  log(`Wake dispatched to daemon for "${args[0]}".`);
@@ -423,7 +426,7 @@ export async function run(runtime, version) {
423
426
  }
424
427
  if (result.reason === "not-running") {
425
428
  cli.error(
426
- "daemon not running. Start fit-outpost.app (or run `fit-outpost daemon`) before waking an agent.",
429
+ "daemon not running. Start fit-outpost.app (or run `fit-outpost daemon`) before you wake an agent.",
427
430
  );
428
431
  } else if (result.reason === "timeout") {
429
432
  cli.error("daemon did not respond to the wake request.");
@@ -434,8 +437,9 @@ export async function run(runtime, version) {
434
437
  },
435
438
  init: async () => {
436
439
  // `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.
440
+ // `Team`). It never accepts an arbitrary path. So nobody can steer the
441
+ // substrate back into a TCC-protected folder. This command refuses an
442
+ // unsafe name.
439
443
  const name = args[0] ?? "Team";
440
444
  let target;
441
445
  try {
@@ -452,8 +456,8 @@ export async function run(runtime, version) {
452
456
  return result.code;
453
457
  }
454
458
  // 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.
459
+ // contract. Only write when no posture is recorded. A later run then
460
+ // never flips an existing posture.
457
461
  if ((await readPosture(fs, POSTURE_PATH)) === null) {
458
462
  await writePosture(fs, POSTURE_PATH, "brief");
459
463
  }
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();