@forwardimpact/outpost 3.11.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.
- package/README.md +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +4 -4
- 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 +21 -19
- package/src/outpost.js +37 -33
- 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 +22 -22
- package/templates/.claude/agents/concierge.md +19 -17
- package/templates/.claude/agents/head-hunter.md +27 -26
- package/templates/.claude/agents/librarian.md +17 -16
- package/templates/.claude/agents/postman.md +19 -18
- package/templates/.claude/agents/recruiter.md +31 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +50 -49
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +4 -4
- package/templates/.claude/skills/anarlog-process/SKILL.md +35 -32
- package/templates/.claude/skills/anarlog-process/references/extraction.md +26 -22
- package/templates/.claude/skills/anarlog-process/references/sessions.md +10 -10
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +10 -9
- package/templates/.claude/skills/anarlog-trim/SKILL.md +43 -40
- package/templates/.claude/skills/candidate-report/SKILL.md +30 -28
- 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 +55 -50
- package/templates/.claude/skills/deck-create/SKILL.md +30 -30
- 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 +59 -57
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +49 -43
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +4 -3
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +25 -25
- package/templates/.claude/skills/doc-create/SKILL.md +21 -20
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +37 -33
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +5 -4
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +11 -9
- package/templates/.claude/skills/extract-entities/SKILL.md +65 -65
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +5 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +14 -13
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +5 -5
- package/templates/.claude/skills/extract-entities/references/recruitment.md +19 -18
- package/templates/.claude/skills/extract-entities/references/resolution.md +13 -13
- package/templates/.claude/skills/extract-entities/references/sources.md +8 -8
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +2 -2
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +2 -2
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +25 -24
- package/templates/.claude/skills/organize-files/SKILL.md +24 -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 +24 -22
- package/templates/.claude/skills/person-identify/scripts/identify.sh +17 -17
- package/templates/.claude/skills/person-lookup/SKILL.md +28 -29
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +19 -16
- package/templates/.claude/skills/req-assess/SKILL.md +27 -27
- package/templates/.claude/skills/req-assess/references/interview-template.md +1 -1
- package/templates/.claude/skills/req-assess/references/panel-template.md +3 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-decide/SKILL.md +28 -25
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-forget/SKILL.md +28 -26
- package/templates/.claude/skills/req-forget/references/classify.md +13 -13
- package/templates/.claude/skills/req-forget/references/locations.md +2 -2
- package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
- package/templates/.claude/skills/req-scan/SKILL.md +21 -22
- 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/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +31 -29
- package/templates/.claude/skills/req-screen/references/rubric.md +18 -18
- package/templates/.claude/skills/req-screen/references/template.md +4 -4
- package/templates/.claude/skills/req-track/SKILL.md +40 -40
- package/templates/.claude/skills/req-track/references/fields.md +19 -18
- 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 +4 -4
- package/templates/.claude/skills/req-workday/SKILL.md +24 -24
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +13 -13
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +7 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +19 -17
- package/templates/.claude/skills/send-chat/SKILL.md +47 -43
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +20 -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 +17 -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 +43 -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 +39 -40
- package/templates/.claude/skills/upstream-instructions/references/examples.md +2 -2
- 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
|
|
6
|
-
|
|
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
|
|
package/bin/fit-outpost.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// bag
|
|
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.
|
|
4
|
-
"description": "Personal operations center — context from email, calendar, and knowledge
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
7
|
-
* direct writes outside `~/.cache/fit/outpost/state/`. This
|
|
8
|
-
* and rejects
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
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) {
|
package/src/agent-runner.js
CHANGED
|
@@ -15,14 +15,14 @@ import { resolvePrivilege, disclaimFor } from "./privilege.js";
|
|
|
15
15
|
import { buildSpawnEnv } from "./spawn-env.js";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* draft-side prose in a materialised agent definition
|
|
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
|
|
25
|
-
"or write files outside the knowledge base
|
|
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
|
|
45
|
-
* loader lets the Bun-FFI module load lazily
|
|
46
|
-
* that never
|
|
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 -
|
|
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
|
-
*
|
|
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,
|
|
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
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
|
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.
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// root, so a spawned agent cannot raise
|
|
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
|
|
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
|
|
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
|
|
3
|
-
//
|
|
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
|
|
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
|
|
29
|
-
* never an arbitrary filesystem path
|
|
30
|
-
* TCC-protected folders.
|
|
31
|
-
* same rule as `agent-path.js
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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,
|
|
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
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
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>`
|
|
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
|
|
229
|
-
* open from Finder.
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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.
|
|
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";
|
|
@@ -38,9 +39,9 @@ import {
|
|
|
38
39
|
const SHARE_DIR = "/usr/local/share/fit-outpost";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* Build the CLI definition.
|
|
42
|
-
* definition the
|
|
43
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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`)
|
|
197
|
-
//
|
|
198
|
-
// lazily
|
|
199
|
-
//
|
|
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.
|
|
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
|
|
280
|
-
// on a nonzero code
|
|
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
|
|
296
|
-
//
|
|
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
|
|
327
|
-
// `^posture: (brief|brief+draft|unset)
|
|
328
|
-
// stdout
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
438
|
-
//
|
|
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
|
|
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
|
|
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();
|