@forwardimpact/outpost 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/scheduler.json +12 -6
- package/package.json +1 -1
- package/src/agent-runner.js +27 -0
- package/src/kb-manager.js +27 -0
- package/src/outpost.js +12 -5
- package/src/privilege.js +45 -0
- package/templates/.claude/agents/recruiter.md +1 -1
- package/templates/.claude/skills/candidate-report/SKILL.md +1 -1
- package/templates/.claude/skills/req-assess/references/rubric.md +1 -1
- package/templates/.claude/skills/req-decide/references/template.md +1 -1
- package/templates/.claude/skills/req-scan/references/template.md +1 -1
- package/templates/.claude/skills/req-screen/SKILL.md +1 -1
- package/templates/.claude/skills/req-screen/references/template.md +2 -2
- package/templates/.claude/skills/req-track/SKILL.md +1 -1
- package/templates/.claude/skills/req-workday/references/templates.md +1 -1
package/config/scheduler.json
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"agents": {
|
|
3
3
|
"postman": {
|
|
4
|
-
"kb": "
|
|
4
|
+
"kb": "~/.local/share/fit/outpost/Team",
|
|
5
|
+
"privilege": "full",
|
|
5
6
|
"schedule": { "type": "cron", "expression": "*/15 8-18 * * 1-5" },
|
|
6
7
|
"enabled": true
|
|
7
8
|
},
|
|
8
9
|
"concierge": {
|
|
9
|
-
"kb": "
|
|
10
|
+
"kb": "~/.local/share/fit/outpost/Team",
|
|
11
|
+
"privilege": "full",
|
|
10
12
|
"schedule": { "type": "cron", "expression": "*/30 8-18 * * 1-5" },
|
|
11
13
|
"enabled": true
|
|
12
14
|
},
|
|
13
15
|
"librarian": {
|
|
14
|
-
"kb": "
|
|
16
|
+
"kb": "~/.local/share/fit/outpost/Team",
|
|
17
|
+
"privilege": "restricted",
|
|
15
18
|
"schedule": { "type": "cron", "expression": "0 9,12,15,18 * * 1-5" },
|
|
16
19
|
"enabled": true
|
|
17
20
|
},
|
|
18
21
|
"chief-of-staff": {
|
|
19
|
-
"kb": "
|
|
22
|
+
"kb": "~/.local/share/fit/outpost/Team",
|
|
23
|
+
"privilege": "restricted",
|
|
20
24
|
"schedule": { "type": "cron", "expression": "0 7,18 * * 1-5" },
|
|
21
25
|
"enabled": true
|
|
22
26
|
},
|
|
23
27
|
"recruiter": {
|
|
24
|
-
"kb": "
|
|
28
|
+
"kb": "~/.local/share/fit/outpost/Team",
|
|
29
|
+
"privilege": "restricted",
|
|
25
30
|
"schedule": { "type": "cron", "expression": "0 8,12,17 * * 1-5" },
|
|
26
31
|
"enabled": true
|
|
27
32
|
},
|
|
28
33
|
"head-hunter": {
|
|
29
|
-
"kb": "
|
|
34
|
+
"kb": "~/.local/share/fit/outpost/Team",
|
|
35
|
+
"privilege": "restricted",
|
|
30
36
|
"schedule": { "type": "cron", "expression": "0 9 * * 1-5" },
|
|
31
37
|
"enabled": true
|
|
32
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/outpost",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Personal operations center — context from email, calendar, and knowledge assembled so preparation is continuous, not a morning scramble.",
|
|
5
5
|
"homepage": "https://www.forwardimpact.team",
|
|
6
6
|
"repository": {
|
package/src/agent-runner.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadManifest,
|
|
12
12
|
draftSkills,
|
|
13
13
|
} from "./posture.js";
|
|
14
|
+
import { resolvePrivilege, disclaimFor } from "./privilege.js";
|
|
14
15
|
import { buildSpawnEnv } from "./spawn-env.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -176,6 +177,24 @@ export class AgentRunner {
|
|
|
176
177
|
* @param {Record<string, string>} [configEnv] - Extra env vars from config
|
|
177
178
|
*/
|
|
178
179
|
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.
|
|
184
|
+
let level;
|
|
185
|
+
try {
|
|
186
|
+
level = resolvePrivilege(agent);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
this.#log(
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
event: "outpost.privilege.rejected",
|
|
191
|
+
agent: agentName,
|
|
192
|
+
error: err.message,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
179
198
|
if (!agent.kb) {
|
|
180
199
|
this.#log(`Agent ${agentName}: no "kb" specified, skipping.`);
|
|
181
200
|
return;
|
|
@@ -191,6 +210,13 @@ export class AgentRunner {
|
|
|
191
210
|
const claude = await this.#findClaude();
|
|
192
211
|
|
|
193
212
|
this.#log(`Waking agent: ${agentName} (kb: ${agent.kb})`);
|
|
213
|
+
this.#log(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
event: "outpost.privilege.resolved",
|
|
216
|
+
agent: agentName,
|
|
217
|
+
level,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
194
220
|
|
|
195
221
|
const as = (state.agents[agentName] ||= {});
|
|
196
222
|
as.status = "active";
|
|
@@ -226,6 +252,7 @@ export class AgentRunner {
|
|
|
226
252
|
env,
|
|
227
253
|
kbPath,
|
|
228
254
|
this.#runtime,
|
|
255
|
+
disclaimFor(level),
|
|
229
256
|
);
|
|
230
257
|
this.#activeChildren.add(pid);
|
|
231
258
|
|
package/src/kb-manager.js
CHANGED
|
@@ -23,6 +23,33 @@ export class KBManager {
|
|
|
23
23
|
this.#logger = createLogger("outpost", runtime);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
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.
|
|
34
|
+
* @param {string} name - The KB name (e.g. `Team`, `personal`).
|
|
35
|
+
* @returns {string} Absolute path under the data home.
|
|
36
|
+
* @throws {Error} when `name` is empty, non-string, or an unsafe segment.
|
|
37
|
+
*/
|
|
38
|
+
static kbPathForName(name) {
|
|
39
|
+
if (
|
|
40
|
+
typeof name !== "string" ||
|
|
41
|
+
name.length === 0 ||
|
|
42
|
+
name.includes("/") ||
|
|
43
|
+
name.includes("\\") ||
|
|
44
|
+
name.includes("..") ||
|
|
45
|
+
name.includes("\0") ||
|
|
46
|
+
name.startsWith("~")
|
|
47
|
+
) {
|
|
48
|
+
throw new Error(`unsafe KB name: ${JSON.stringify(name)}`);
|
|
49
|
+
}
|
|
50
|
+
return join(homedir(), ".local/share/fit/outpost", name);
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
/**
|
|
27
54
|
* Test whether a path exists, via the async fs surface.
|
|
28
55
|
* @param {string} p
|
package/src/outpost.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// fit-outpost Wake due agents once and exit
|
|
5
5
|
// fit-outpost daemon Run continuously (poll every 60s)
|
|
6
6
|
// fit-outpost wake <agent> Wake a specific agent immediately
|
|
7
|
-
// fit-outpost init
|
|
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
10
|
// fit-outpost validate Validate agent definitions exist
|
|
@@ -58,7 +58,7 @@ function buildDefinition(version) {
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
name: "init",
|
|
61
|
-
args: "
|
|
61
|
+
args: "[name]",
|
|
62
62
|
description: "Initialize a new knowledge base",
|
|
63
63
|
},
|
|
64
64
|
{
|
|
@@ -433,13 +433,20 @@ export async function run(runtime, version) {
|
|
|
433
433
|
return 1;
|
|
434
434
|
},
|
|
435
435
|
init: async () => {
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
// `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.
|
|
439
|
+
const name = args[0] ?? "Team";
|
|
440
|
+
let target;
|
|
441
|
+
try {
|
|
442
|
+
target = KBManager.kbPathForName(name);
|
|
443
|
+
} catch {
|
|
444
|
+
cli.usageError(`invalid KB name "${name}"`);
|
|
438
445
|
return 2;
|
|
439
446
|
}
|
|
440
447
|
const tpl = await requireTemplateDir();
|
|
441
448
|
if (tpl === null) return 1;
|
|
442
|
-
const result = await kbManager.init(
|
|
449
|
+
const result = await kbManager.init(target, tpl);
|
|
443
450
|
if (!result.ok) {
|
|
444
451
|
proc.stderr.write(result.error + "\n");
|
|
445
452
|
return result.code;
|
package/src/privilege.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privilege — resolve an agent's least-privilege execution level and map it to
|
|
3
|
+
* the hop-2 spawn disclaim flag.
|
|
4
|
+
*
|
|
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.
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The two privilege levels, in declaration order. */
|
|
18
|
+
export const PRIVILEGE_LEVELS = ["full", "restricted"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve an agent's declared privilege level. The level is mandatory: a
|
|
22
|
+
* missing or unrecognised value throws — there is no default.
|
|
23
|
+
* @param {{ privilege?: string }} agent - One agent's config.
|
|
24
|
+
* @returns {"full"|"restricted"} The declared level.
|
|
25
|
+
* @throws {Error} when `agent.privilege` is not one of {@link PRIVILEGE_LEVELS}.
|
|
26
|
+
*/
|
|
27
|
+
export function resolvePrivilege(agent) {
|
|
28
|
+
const level = agent?.privilege;
|
|
29
|
+
if (!PRIVILEGE_LEVELS.includes(level)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`invalid privilege "${level}"; expected one of ${PRIVILEGE_LEVELS.join(", ")}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return level;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map a level to the hop-2 disclaim flag: `restricted` self-disclaims (`1`),
|
|
39
|
+
* `full` keeps the inherited responsible process (`0`).
|
|
40
|
+
* @param {"full"|"restricted"} level
|
|
41
|
+
* @returns {0|1}
|
|
42
|
+
*/
|
|
43
|
+
export function disclaimFor(level) {
|
|
44
|
+
return level === "restricted" ? 1 : 0;
|
|
45
|
+
}
|
|
@@ -66,7 +66,7 @@ candidates, pipeline totals by stage/track, aggregate diversity, retention flags
|
|
|
66
66
|
|
|
67
67
|
```
|
|
68
68
|
Decision: {observation and chosen action}
|
|
69
|
-
Action: {e.g. "req-screen for John Smith against J060
|
|
69
|
+
Action: {e.g. "req-screen for John Smith against J060 forward-deployed"}
|
|
70
70
|
Stage: {1 | 2 | sync | erasure}
|
|
71
71
|
Priority Watch: {priority at risk + one-line why, or "none"}
|
|
72
72
|
```
|
|
@@ -31,7 +31,7 @@ before deciding whether to invest interview time.
|
|
|
31
31
|
|
|
32
32
|
- **Candidate name** — locates `Knowledge/Candidates/{Name}/brief.md`.
|
|
33
33
|
- **Target role** — discipline, level, track (e.g.
|
|
34
|
-
`
|
|
34
|
+
`software-engineering J070 forward-deployed`). If not given, infer from the
|
|
35
35
|
candidate's `Req` field → Role file. Ask the user if it can't be inferred.
|
|
36
36
|
- **Recipient** — pod lead or hiring manager the report is for.
|
|
37
37
|
- **CV file** (optional) — read directly when no `screening.md` exists.
|
|
@@ -51,7 +51,7 @@ from the transcript — "seemed strong" is not evidence.
|
|
|
51
51
|
| `emerging` | Behaviour not observed or only when prompted |
|
|
52
52
|
| `developing` | Behaviour present but inconsistent or surface-level |
|
|
53
53
|
| `practicing` | Behaviour consistent and natural throughout the interview |
|
|
54
|
-
| `
|
|
54
|
+
| `role-modeling` | Behaviour demonstrated at high level, influenced the room |
|
|
55
55
|
| `exemplifying` | Behaviour exceptional, set the standard for the conversation |
|
|
56
56
|
|
|
57
57
|
## Level signals
|
|
@@ -10,7 +10,7 @@ Reference for `req-scan` Step 4. Save to `Knowledge/Prospects/{Name}.md`.
|
|
|
10
10
|
**Date found:** {YYYY-MM-DD}
|
|
11
11
|
**Location:** {location or "Not specified"}
|
|
12
12
|
**Estimated level:** {J040–J110} (confidence: {high/medium/low})
|
|
13
|
-
**Best track fit:** {
|
|
13
|
+
**Best track fit:** {forward-deployed / platform / either}
|
|
14
14
|
**Match strength:** {strong / moderate}
|
|
15
15
|
|
|
16
16
|
## Profile
|
|
@@ -92,7 +92,7 @@ If no target is available, estimate one using the level heuristics in
|
|
|
92
92
|
```bash
|
|
93
93
|
bunx fit-pathway job {discipline} {level} --track={track}
|
|
94
94
|
bunx fit-pathway job {discipline} {level} --track={track} --skills
|
|
95
|
-
bunx fit-pathway track
|
|
95
|
+
bunx fit-pathway track forward-deployed
|
|
96
96
|
bunx fit-pathway track platform
|
|
97
97
|
```
|
|
98
98
|
|
|
@@ -26,7 +26,7 @@ around the screening question — is this worth an interview?}
|
|
|
26
26
|
| Dimension | Assessment |
|
|
27
27
|
| -------------- | -------------------------------------- |
|
|
28
28
|
| **Level** | {estimated level and confidence} |
|
|
29
|
-
| **Track fit** | {
|
|
29
|
+
| **Track fit** | {forward-deployed / platform / either} |
|
|
30
30
|
| **Discipline** | {best discipline match} |
|
|
31
31
|
| **Gender** | {Woman / Man / —} |
|
|
32
32
|
|
|
@@ -54,7 +54,7 @@ Standard reference: `{discipline} {level} --track={track}`
|
|
|
54
54
|
|
|
55
55
|
## Track Fit Analysis
|
|
56
56
|
|
|
57
|
-
{Paragraph explaining why the candidate fits
|
|
57
|
+
{Paragraph explaining why the candidate fits forward-deployed,
|
|
58
58
|
platform, or either. Reference specific CV evidence.}
|
|
59
59
|
|
|
60
60
|
## Screening Recommendation
|
|
@@ -186,7 +186,7 @@ either skill until it changes.
|
|
|
186
186
|
bunx fit-pathway skill --list
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
Use agent-aligned engineering standard IDs (e.g. `
|
|
189
|
+
Use agent-aligned engineering standard IDs (e.g. `data-integration`,
|
|
190
190
|
`full_stack_development`, `architecture_and_design`) in the `## Skills` section
|
|
191
191
|
instead of free-form tags. Flag any candidate with a CV attachment for
|
|
192
192
|
`req-screen`.
|
|
@@ -15,7 +15,7 @@ Use when no Role file exists for the requisition. Filename:
|
|
|
15
15
|
**Title:** {Full title from export}
|
|
16
16
|
**Level:** {Infer from title: "Principal" → J100, "Staff" → J090, "Director" → J100 M-track, "Senior" → J070}
|
|
17
17
|
**Track:** {P-track for IC roles, M-track for Director/Manager roles}
|
|
18
|
-
**Discipline:** {Infer: "Software Engineer" →
|
|
18
|
+
**Discipline:** {Infer: "Software Engineer" → software-engineering, "Data Engineer" → data-engineering, "Data Scientist" → data-science}
|
|
19
19
|
**Domain lead:** —
|
|
20
20
|
**Hiring manager:** {From export metadata if available, or "—"}
|
|
21
21
|
**Locations:** {Primary Location from export}
|