@forwardimpact/outpost 3.0.3
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/LICENSE +201 -0
- package/README.md +15 -0
- package/bin/fit-outpost.js +3 -0
- package/config/scheduler.json +34 -0
- package/package.json +63 -0
- package/src/agent-runner.js +189 -0
- package/src/index.js +5 -0
- package/src/kb-manager.js +228 -0
- package/src/outpost.js +417 -0
- package/src/scheduler.js +195 -0
- package/src/socket-server.js +336 -0
- package/src/state-manager.js +126 -0
- package/templates/.claude/agents/chief-of-staff.md +58 -0
- package/templates/.claude/agents/concierge.md +46 -0
- package/templates/.claude/agents/head-hunter.md +57 -0
- package/templates/.claude/agents/librarian.md +60 -0
- package/templates/.claude/agents/postman.md +47 -0
- package/templates/.claude/agents/recruiter.md +63 -0
- package/templates/.claude/settings.json +88 -0
- package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
- package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
- package/templates/.claude/skills/candidate-report/references/report.css +438 -0
- package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
- package/templates/.claude/skills/deck-create/SKILL.md +75 -0
- package/templates/.claude/skills/deck-create/references/slide.css +35 -0
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
- package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
- package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
- package/templates/.claude/skills/doc-create/SKILL.md +105 -0
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
- package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
- package/templates/.claude/skills/draft-emails/references/template.md +26 -0
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
- package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
- package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
- package/templates/.claude/skills/extract-entities/references/content.md +104 -0
- package/templates/.claude/skills/extract-entities/references/links.md +48 -0
- package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
- package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
- package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
- package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
- package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
- package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
- package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
- package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
- package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
- package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
- package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
- package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
- package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
- package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
- package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
- package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
- package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
- package/templates/.claude/skills/organize-files/SKILL.md +146 -0
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
- package/templates/.claude/skills/req-assess/SKILL.md +137 -0
- package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
- package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
- package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
- package/templates/.claude/skills/req-decide/SKILL.md +163 -0
- package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
- package/templates/.claude/skills/req-decide/references/template.md +118 -0
- package/templates/.claude/skills/req-forget/SKILL.md +154 -0
- package/templates/.claude/skills/req-forget/references/classify.md +23 -0
- package/templates/.claude/skills/req-forget/references/locations.md +59 -0
- package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
- package/templates/.claude/skills/req-scan/SKILL.md +145 -0
- package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
- package/templates/.claude/skills/req-scan/references/filters.md +83 -0
- package/templates/.claude/skills/req-scan/references/sources.md +76 -0
- package/templates/.claude/skills/req-scan/references/state.md +54 -0
- package/templates/.claude/skills/req-scan/references/template.md +36 -0
- package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
- package/templates/.claude/skills/req-screen/SKILL.md +151 -0
- package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
- package/templates/.claude/skills/req-screen/references/template.md +91 -0
- package/templates/.claude/skills/req-track/SKILL.md +189 -0
- package/templates/.claude/skills/req-track/references/fields.md +114 -0
- package/templates/.claude/skills/req-track/references/signals.md +42 -0
- package/templates/.claude/skills/req-track/references/statuses.md +32 -0
- package/templates/.claude/skills/req-track/references/templates.md +127 -0
- package/templates/.claude/skills/req-workday/SKILL.md +162 -0
- package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
- package/templates/.claude/skills/req-workday/references/templates.md +126 -0
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
- package/templates/.claude/skills/send-chat/SKILL.md +170 -0
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
- package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
- package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
- package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
- package/templates/.claude/skills/weekly-update/references/template.md +55 -0
- package/templates/CLAUDE.md +134 -0
- package/templates/USER.md +3 -0
- package/templates/apm.yml +5 -0
- package/templates/knowledge/Briefings/.gitkeep +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# State Script Reference
|
|
2
|
+
|
|
3
|
+
`req-scan` uses `node scripts/state.mjs <command> [args]` for **all** state-file
|
|
4
|
+
operations. Do **not** write bespoke scripts to update cursor, seen, prospects,
|
|
5
|
+
failures, or log files.
|
|
6
|
+
|
|
7
|
+
## Cursor (source rotation)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
node scripts/state.mjs cursor list
|
|
11
|
+
node scripts/state.mjs cursor get github_open_to_work
|
|
12
|
+
node scripts/state.mjs cursor set github_open_to_work "2026-03-09T22:00:00Z" "UK-done_next:Europe"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Seen (deduplication)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# exit 0 = seen, 1 = new
|
|
19
|
+
node scripts/state.mjs seen check github_open_to_work mxmxmx333
|
|
20
|
+
|
|
21
|
+
node scripts/state.mjs seen add github_open_to_work mxmxmx333
|
|
22
|
+
node scripts/state.mjs seen batch github_open_to_work id1 id2 id3 id4
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Prospects
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node scripts/state.mjs prospect add "Hasan Cam" github_open_to_work strong "J060-J070 platform"
|
|
29
|
+
node scripts/state.mjs prospect list --limit 10
|
|
30
|
+
node scripts/state.mjs prospect count
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Failures
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Source selection — skip if ≥ 3
|
|
37
|
+
node scripts/state.mjs failure get mastodon_hachyderm
|
|
38
|
+
|
|
39
|
+
node scripts/state.mjs failure increment mastodon_hachyderm
|
|
40
|
+
node scripts/state.mjs failure reset github_open_to_work
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Logging
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node scripts/state.mjs log-wake github_open_to_work "Primary query: 'open to work' location:UK — 30 results, 2 new prospects"
|
|
47
|
+
node scripts/state.mjs log "Manual note about source rotation"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Summary
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
node scripts/state.mjs summary
|
|
54
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Prospect Note Template
|
|
2
|
+
|
|
3
|
+
Reference for `req-scan` Step 4. Save to `knowledge/Prospects/{Name}.md`.
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
# {Name}
|
|
7
|
+
|
|
8
|
+
## Info
|
|
9
|
+
**Source:** [{platform}]({permalink})
|
|
10
|
+
**Date found:** {YYYY-MM-DD}
|
|
11
|
+
**Location:** {location or "Not specified"}
|
|
12
|
+
**Estimated level:** {J040–J110} (confidence: {high/medium/low})
|
|
13
|
+
**Best track fit:** {forward_deployed / platform / either}
|
|
14
|
+
**Match strength:** {strong / moderate}
|
|
15
|
+
|
|
16
|
+
## Profile
|
|
17
|
+
{2–4 sentences: who they are, what they do, why they match. Reference
|
|
18
|
+
specific agent-aligned standard skill IDs in parentheses where possible.}
|
|
19
|
+
|
|
20
|
+
## Agent-Aligned Standard Alignment
|
|
21
|
+
**Matching skills:** {comma-separated skill IDs}
|
|
22
|
+
**Key strengths:** {what stands out relative to the standard}
|
|
23
|
+
**Gaps:** {notable missing skills for the estimated role}
|
|
24
|
+
|
|
25
|
+
## Source Post
|
|
26
|
+
> {Brief excerpt or summary of the original post — 2-3 lines max}
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
{Additional observations: polymath signals, AI-tool usage, unusual
|
|
30
|
+
background combinations, anything noteworthy for the user.}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Naming
|
|
34
|
+
|
|
35
|
+
Use the candidate's display name as given. If only a username is available, use
|
|
36
|
+
the username. **Never fabricate real names.**
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Manage head-hunter agent state files.
|
|
4
|
+
*
|
|
5
|
+
* Provides atomic operations on the 5 state files used by the head-hunter agent:
|
|
6
|
+
* cursor.tsv — source rotation state (source, last_checked, position)
|
|
7
|
+
* seen.tsv — deduplication index (source, id, date)
|
|
8
|
+
* prospects.tsv — prospect index (name, source, date, strength, level)
|
|
9
|
+
* failures.tsv — consecutive failure counts (source, count)
|
|
10
|
+
* log.md — append-only activity log
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/state.mjs cursor get <source>
|
|
14
|
+
* node scripts/state.mjs cursor set <source> <timestamp> <position>
|
|
15
|
+
* node scripts/state.mjs seen check <source> <id>
|
|
16
|
+
* node scripts/state.mjs seen add <source> <id> [<date>]
|
|
17
|
+
* node scripts/state.mjs prospect add <name> <source> <strength> <level>
|
|
18
|
+
* node scripts/state.mjs prospect list [--limit N]
|
|
19
|
+
* node scripts/state.mjs failure get <source>
|
|
20
|
+
* node scripts/state.mjs failure increment <source>
|
|
21
|
+
* node scripts/state.mjs failure reset <source>
|
|
22
|
+
* node scripts/state.mjs log <message>
|
|
23
|
+
* node scripts/state.mjs log-wake <source> <summary>
|
|
24
|
+
* node scripts/state.mjs summary
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
appendFileSync,
|
|
29
|
+
existsSync,
|
|
30
|
+
mkdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
writeFileSync,
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
|
|
37
|
+
const HOME = homedir();
|
|
38
|
+
const STATE_DIR = join(HOME, ".cache/fit/outpost/head-hunter");
|
|
39
|
+
|
|
40
|
+
const PATHS = {
|
|
41
|
+
cursor: join(STATE_DIR, "cursor.tsv"),
|
|
42
|
+
seen: join(STATE_DIR, "seen.tsv"),
|
|
43
|
+
prospects: join(STATE_DIR, "prospects.tsv"),
|
|
44
|
+
failures: join(STATE_DIR, "failures.tsv"),
|
|
45
|
+
log: join(STATE_DIR, "log.md"),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
49
|
+
console.log(`state — manage head-hunter agent state
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
node scripts/state.mjs <command> [args]
|
|
53
|
+
|
|
54
|
+
Cursor commands (source rotation):
|
|
55
|
+
cursor get <source> Get current cursor for source
|
|
56
|
+
cursor set <source> <timestamp> <position> Update cursor position
|
|
57
|
+
cursor list List all cursors
|
|
58
|
+
|
|
59
|
+
Seen commands (deduplication):
|
|
60
|
+
seen check <source> <id> Check if ID was already seen (exit 0=seen, 1=new)
|
|
61
|
+
seen add <source> <id> [<date>] Mark ID as seen (date defaults to today)
|
|
62
|
+
seen batch <source> <id1> <id2> ... Mark multiple IDs as seen
|
|
63
|
+
|
|
64
|
+
Prospect commands:
|
|
65
|
+
prospect add <name> <source> <strength> <level> Add a new prospect
|
|
66
|
+
prospect list [--limit N] List recent prospects
|
|
67
|
+
prospect count Count total prospects
|
|
68
|
+
|
|
69
|
+
Failure commands:
|
|
70
|
+
failure get <source> Get failure count for source
|
|
71
|
+
failure increment <source> Increment failure count
|
|
72
|
+
failure reset <source> Reset failure count to 0
|
|
73
|
+
|
|
74
|
+
Log commands:
|
|
75
|
+
log <message> Append raw text to log.md
|
|
76
|
+
log-wake <source> <summary> Append a formatted wake cycle entry
|
|
77
|
+
|
|
78
|
+
Summary:
|
|
79
|
+
summary Print state overview
|
|
80
|
+
|
|
81
|
+
State dir: ~/.cache/fit/outpost/head-hunter/`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Ensure state directory exists ---
|
|
86
|
+
|
|
87
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
88
|
+
|
|
89
|
+
// --- File helpers ---
|
|
90
|
+
|
|
91
|
+
function readTsv(file) {
|
|
92
|
+
if (!existsSync(file)) return [];
|
|
93
|
+
return readFileSync(file, "utf8")
|
|
94
|
+
.split("\n")
|
|
95
|
+
.filter((l) => l.trim());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeTsv(file, lines) {
|
|
99
|
+
writeFileSync(file, lines.join("\n") + (lines.length ? "\n" : ""));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function appendTsv(file, line) {
|
|
103
|
+
appendFileSync(file, line + "\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function today() {
|
|
107
|
+
return new Date().toISOString().slice(0, 10);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Cursor operations ---
|
|
111
|
+
|
|
112
|
+
function cursorGet(source) {
|
|
113
|
+
const lines = readTsv(PATHS.cursor);
|
|
114
|
+
const line = lines.find((l) => l.startsWith(source + "\t"));
|
|
115
|
+
if (!line) {
|
|
116
|
+
console.log(`No cursor for source: ${source}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const [, timestamp, position] = line.split("\t");
|
|
120
|
+
console.log(`${source}\t${timestamp}\t${position}`);
|
|
121
|
+
return { source, timestamp, position };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cursorSet(source, timestamp, position) {
|
|
125
|
+
const lines = readTsv(PATHS.cursor);
|
|
126
|
+
const newLine = `${source}\t${timestamp}\t${position}`;
|
|
127
|
+
const idx = lines.findIndex((l) => l.startsWith(source + "\t"));
|
|
128
|
+
if (idx !== -1) {
|
|
129
|
+
lines[idx] = newLine;
|
|
130
|
+
} else {
|
|
131
|
+
lines.push(newLine);
|
|
132
|
+
}
|
|
133
|
+
writeTsv(PATHS.cursor, lines);
|
|
134
|
+
console.log(`Cursor updated: ${newLine}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cursorList() {
|
|
138
|
+
const lines = readTsv(PATHS.cursor);
|
|
139
|
+
if (lines.length === 0) {
|
|
140
|
+
console.log("No cursors.");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
console.log(line);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Seen operations ---
|
|
149
|
+
|
|
150
|
+
function seenCheck(source, id) {
|
|
151
|
+
const lines = readTsv(PATHS.seen);
|
|
152
|
+
const found = lines.some((l) => {
|
|
153
|
+
const parts = l.split("\t");
|
|
154
|
+
return parts[0] === source && parts[1] === id;
|
|
155
|
+
});
|
|
156
|
+
if (found) {
|
|
157
|
+
console.log(`SEEN: ${source}\t${id}`);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`NEW: ${source}\t${id}`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function seenAdd(source, id, date) {
|
|
166
|
+
appendTsv(PATHS.seen, `${source}\t${id}\t${date || today()}`);
|
|
167
|
+
console.log(`Marked seen: ${source}\t${id}\t${date || today()}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function seenBatch(source, ids) {
|
|
171
|
+
const d = today();
|
|
172
|
+
const lines = ids.map((id) => `${source}\t${id}\t${d}`);
|
|
173
|
+
appendFileSync(PATHS.seen, lines.join("\n") + "\n");
|
|
174
|
+
console.log(`Marked ${ids.length} IDs as seen for ${source}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- Prospect operations ---
|
|
178
|
+
|
|
179
|
+
function prospectAdd(name, source, strength, level) {
|
|
180
|
+
const d = today();
|
|
181
|
+
appendTsv(PATHS.prospects, `${name}\t${source}\t${d}\t${strength}\t${level}`);
|
|
182
|
+
console.log(`Prospect added: ${name} (${strength}, ${level})`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function prospectList(limit) {
|
|
186
|
+
const lines = readTsv(PATHS.prospects);
|
|
187
|
+
if (lines.length === 0) {
|
|
188
|
+
console.log("No prospects.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Show most recent first
|
|
192
|
+
const display = lines.slice(-limit).reverse();
|
|
193
|
+
for (const line of display) {
|
|
194
|
+
console.log(line);
|
|
195
|
+
}
|
|
196
|
+
if (lines.length > limit) {
|
|
197
|
+
console.log(`\n... ${lines.length - limit} more (${lines.length} total)`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function prospectCount() {
|
|
202
|
+
const lines = readTsv(PATHS.prospects);
|
|
203
|
+
console.log(lines.length);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Failure operations ---
|
|
207
|
+
|
|
208
|
+
function failureGet(source) {
|
|
209
|
+
const lines = readTsv(PATHS.failures);
|
|
210
|
+
const line = lines.find((l) => l.startsWith(source + "\t"));
|
|
211
|
+
if (!line) {
|
|
212
|
+
console.log(`0`);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
const count = parseInt(line.split("\t")[1], 10) || 0;
|
|
216
|
+
console.log(`${count}`);
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function failureIncrement(source) {
|
|
221
|
+
const lines = readTsv(PATHS.failures);
|
|
222
|
+
const idx = lines.findIndex((l) => l.startsWith(source + "\t"));
|
|
223
|
+
if (idx !== -1) {
|
|
224
|
+
const count = parseInt(lines[idx].split("\t")[1], 10) || 0;
|
|
225
|
+
lines[idx] = `${source}\t${count + 1}`;
|
|
226
|
+
} else {
|
|
227
|
+
lines.push(`${source}\t1`);
|
|
228
|
+
}
|
|
229
|
+
writeTsv(PATHS.failures, lines);
|
|
230
|
+
const newCount = parseInt(
|
|
231
|
+
lines.find((l) => l.startsWith(source + "\t")).split("\t")[1],
|
|
232
|
+
10,
|
|
233
|
+
);
|
|
234
|
+
console.log(`Failures for ${source}: ${newCount}`);
|
|
235
|
+
if (newCount >= 3) {
|
|
236
|
+
console.log(
|
|
237
|
+
`WARNING: ${source} has ${newCount} consecutive failures (suspended at ≥3)`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function failureReset(source) {
|
|
243
|
+
const lines = readTsv(PATHS.failures);
|
|
244
|
+
const idx = lines.findIndex((l) => l.startsWith(source + "\t"));
|
|
245
|
+
if (idx !== -1) {
|
|
246
|
+
lines[idx] = `${source}\t0`;
|
|
247
|
+
writeTsv(PATHS.failures, lines);
|
|
248
|
+
}
|
|
249
|
+
console.log(`Failures reset for ${source}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- Log operations ---
|
|
253
|
+
|
|
254
|
+
function logAppend(message) {
|
|
255
|
+
appendFileSync(PATHS.log, message + "\n");
|
|
256
|
+
console.log("Log entry appended.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function logWake(source, summary) {
|
|
260
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
261
|
+
const entry = `\n## ${today()} ${timestamp.slice(11)} — Wake (${source})\n\n${summary}\n\n---\n`;
|
|
262
|
+
appendFileSync(PATHS.log, entry);
|
|
263
|
+
console.log(`Wake cycle logged for ${source}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// --- Summary ---
|
|
267
|
+
|
|
268
|
+
function summary() {
|
|
269
|
+
const cursors = readTsv(PATHS.cursor);
|
|
270
|
+
const seen = readTsv(PATHS.seen);
|
|
271
|
+
const prospects = readTsv(PATHS.prospects);
|
|
272
|
+
const failures = readTsv(PATHS.failures);
|
|
273
|
+
|
|
274
|
+
console.log("=== Head-Hunter State Summary ===\n");
|
|
275
|
+
|
|
276
|
+
console.log(`Sources: ${cursors.length}`);
|
|
277
|
+
for (const line of cursors) {
|
|
278
|
+
const [source, timestamp, position] = line.split("\t");
|
|
279
|
+
const failLine = failures.find((l) => l.startsWith(source + "\t"));
|
|
280
|
+
const failCount = failLine ? parseInt(failLine.split("\t")[1], 10) : 0;
|
|
281
|
+
const status = failCount >= 3 ? " [SUSPENDED]" : "";
|
|
282
|
+
console.log(` ${source}: last=${timestamp}, pos=${position}${status}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log(`\nSeen: ${seen.length} entries`);
|
|
286
|
+
console.log(`Prospects: ${prospects.length} total`);
|
|
287
|
+
|
|
288
|
+
// Strength breakdown
|
|
289
|
+
const byStrength = {};
|
|
290
|
+
for (const line of prospects) {
|
|
291
|
+
const strength = line.split("\t")[3] || "unknown";
|
|
292
|
+
byStrength[strength] = (byStrength[strength] || 0) + 1;
|
|
293
|
+
}
|
|
294
|
+
for (const [k, v] of Object.entries(byStrength)) {
|
|
295
|
+
console.log(` ${k}: ${v}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Recent prospects (last 5)
|
|
299
|
+
if (prospects.length > 0) {
|
|
300
|
+
console.log("\nRecent prospects:");
|
|
301
|
+
for (const line of prospects.slice(-5)) {
|
|
302
|
+
const [name, source, date, strength, level] = line.split("\t");
|
|
303
|
+
console.log(
|
|
304
|
+
` ${date} | ${name} | ${strength} | ${level} | via ${source}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- CLI Router ---
|
|
311
|
+
|
|
312
|
+
const cliArgs = process.argv.slice(2);
|
|
313
|
+
const cmd = cliArgs[0];
|
|
314
|
+
const sub = cliArgs[1];
|
|
315
|
+
|
|
316
|
+
switch (cmd) {
|
|
317
|
+
case "cursor":
|
|
318
|
+
if (sub === "get" && cliArgs[2]) cursorGet(cliArgs[2]);
|
|
319
|
+
else if (sub === "set" && cliArgs[2] && cliArgs[3] && cliArgs[4])
|
|
320
|
+
cursorSet(cliArgs[2], cliArgs[3], cliArgs[4]);
|
|
321
|
+
else if (sub === "list") cursorList();
|
|
322
|
+
else {
|
|
323
|
+
console.error(
|
|
324
|
+
"Usage: cursor get|set|list <source> [<timestamp> <position>]",
|
|
325
|
+
);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case "seen":
|
|
331
|
+
if (sub === "check" && cliArgs[2] && cliArgs[3])
|
|
332
|
+
seenCheck(cliArgs[2], cliArgs[3]);
|
|
333
|
+
else if (sub === "add" && cliArgs[2] && cliArgs[3])
|
|
334
|
+
seenAdd(cliArgs[2], cliArgs[3], cliArgs[4]);
|
|
335
|
+
else if (sub === "batch" && cliArgs[2] && cliArgs.length > 3)
|
|
336
|
+
seenBatch(cliArgs[2], cliArgs.slice(3));
|
|
337
|
+
else {
|
|
338
|
+
console.error("Usage: seen check|add|batch <source> <id> [...]");
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case "prospect":
|
|
344
|
+
if (sub === "add" && cliArgs[2] && cliArgs[3] && cliArgs[4] && cliArgs[5])
|
|
345
|
+
prospectAdd(cliArgs[2], cliArgs[3], cliArgs[4], cliArgs[5]);
|
|
346
|
+
else if (sub === "list") {
|
|
347
|
+
const lIdx = cliArgs.indexOf("--limit");
|
|
348
|
+
const lim = lIdx !== -1 ? parseInt(cliArgs[lIdx + 1], 10) || 10 : 10;
|
|
349
|
+
prospectList(lim);
|
|
350
|
+
} else if (sub === "count") prospectCount();
|
|
351
|
+
else {
|
|
352
|
+
console.error(
|
|
353
|
+
"Usage: prospect add|list|count <name> <source> <strength> <level>",
|
|
354
|
+
);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
case "failure":
|
|
360
|
+
if (sub === "get" && cliArgs[2]) failureGet(cliArgs[2]);
|
|
361
|
+
else if (sub === "increment" && cliArgs[2]) failureIncrement(cliArgs[2]);
|
|
362
|
+
else if (sub === "reset" && cliArgs[2]) failureReset(cliArgs[2]);
|
|
363
|
+
else {
|
|
364
|
+
console.error("Usage: failure get|increment|reset <source>");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
|
|
369
|
+
case "log":
|
|
370
|
+
if (cliArgs.length >= 2) logAppend(cliArgs.slice(1).join(" "));
|
|
371
|
+
else {
|
|
372
|
+
console.error("Usage: log <message>");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
|
|
377
|
+
case "log-wake":
|
|
378
|
+
if (cliArgs[1] && cliArgs.length >= 3)
|
|
379
|
+
logWake(cliArgs[1], cliArgs.slice(2).join(" "));
|
|
380
|
+
else {
|
|
381
|
+
console.error("Usage: log-wake <source> <summary>");
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case "summary":
|
|
387
|
+
summary();
|
|
388
|
+
break;
|
|
389
|
+
|
|
390
|
+
default:
|
|
391
|
+
console.error(
|
|
392
|
+
"Unknown command. Run with --help for usage.\n" +
|
|
393
|
+
"Commands: cursor, seen, prospect, failure, log, log-wake, summary",
|
|
394
|
+
);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: req-screen
|
|
3
|
+
description: >
|
|
4
|
+
Screen candidate CVs against the agent-aligned engineering standard to decide
|
|
5
|
+
whether to invest interview time. Produces a structured screening assessment
|
|
6
|
+
with interview/pass recommendation and suggested interview focus areas.
|
|
7
|
+
Use when the user asks to evaluate a CV or when a new CV is detected.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Screen CV
|
|
11
|
+
|
|
12
|
+
Screen a candidate's CV against the agent-aligned engineering standard defined
|
|
13
|
+
in `fit-pathway`. The single question this skill answers: **is this candidate
|
|
14
|
+
worth interviewing?** Every assessment is grounded in the standard — no
|
|
15
|
+
subjective impressions.
|
|
16
|
+
|
|
17
|
+
This is **Stage 1** of a three-stage hiring pipeline:
|
|
18
|
+
|
|
19
|
+
1. **Screen CV** (this skill) — CV arrives → interview or pass.
|
|
20
|
+
2. `req-assess` — interview transcript arrives → updated evidence.
|
|
21
|
+
3. `req-decide` — all stages complete → hire / don't hire.
|
|
22
|
+
|
|
23
|
+
## Trigger
|
|
24
|
+
|
|
25
|
+
- A new CV is added to `knowledge/Candidates/{Name}/`.
|
|
26
|
+
- A CV appears in `~/Downloads/` and is associated with a candidate.
|
|
27
|
+
- The user asks to screen, evaluate, or assess a CV.
|
|
28
|
+
- The user asks "is this person worth interviewing?".
|
|
29
|
+
|
|
30
|
+
## Prerequisites
|
|
31
|
+
|
|
32
|
+
- `fit-pathway` CLI installed (`bunx fit-pathway` works).
|
|
33
|
+
- A CV file (PDF or DOCX) on the filesystem.
|
|
34
|
+
- Optionally a target role: `{discipline} {level} --track={track}`.
|
|
35
|
+
|
|
36
|
+
## Inputs
|
|
37
|
+
|
|
38
|
+
- CV file path (e.g. `knowledge/Candidates/{Name}/CV.pdf`).
|
|
39
|
+
- Target role (optional).
|
|
40
|
+
- Existing `knowledge/Candidates/{Name}/brief.md`, if any.
|
|
41
|
+
- `knowledge/Roles/*.md` matching the candidate's `Req` (provides `Level`,
|
|
42
|
+
`Discipline`, `Hiring manager`, `Domain lead`).
|
|
43
|
+
|
|
44
|
+
## Outputs
|
|
45
|
+
|
|
46
|
+
- `knowledge/Candidates/{Name}/screening.md` — structured assessment.
|
|
47
|
+
- Updated `knowledge/Candidates/{Name}/brief.md` — skills + summary enriched.
|
|
48
|
+
|
|
49
|
+
<do_confirm_checklist goal="Verify the screening is grounded and
|
|
50
|
+
decision-rule-compliant">
|
|
51
|
+
|
|
52
|
+
- [ ] Every claim cites CV evidence or marks "Not evidenced".
|
|
53
|
+
- [ ] Two-level scepticism applied; vague phrases didn't earn levels.
|
|
54
|
+
- [ ] "Not evidenced" skills are counted as gaps in the recommendation.
|
|
55
|
+
- [ ] Recommendation follows the decision rules and threshold rule — match % and
|
|
56
|
+
gap count verified before picking a tier.
|
|
57
|
+
- [ ] "Interview with focus areas" used only for strong candidates with a named
|
|
58
|
+
concern — not as a soft "maybe".
|
|
59
|
+
- [ ] Output file is exactly `screening.md`; any misnamed prior file deleted.
|
|
60
|
+
- [ ] `brief.md` links the screening as `[CV Screening](./screening.md)` and
|
|
61
|
+
Skills/Summary updated via targeted edits.
|
|
62
|
+
- [ ] Gender set only from explicit pronouns/titles.
|
|
63
|
+
- [ ] Recommendation header carries the advisory-only banner.
|
|
64
|
+
|
|
65
|
+
</do_confirm_checklist>
|
|
66
|
+
|
|
67
|
+
## Procedure
|
|
68
|
+
|
|
69
|
+
### 1. Read the CV
|
|
70
|
+
|
|
71
|
+
Extract the fields listed in
|
|
72
|
+
[references/rubric.md](references/rubric.md#what-to-extract-from-the-cv).
|
|
73
|
+
|
|
74
|
+
### 2. Anchor the target role
|
|
75
|
+
|
|
76
|
+
If `brief.md` carries a `Req`, look up the matching Role file:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
ls knowledge/Roles/ | grep "{req_number}"
|
|
80
|
+
cat "knowledge/Roles/{matching file}"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use the Role's `Level` and `Discipline` as the target unless the user specified
|
|
84
|
+
a different target. Capture `Hiring manager` and `Domain lead` for the screening
|
|
85
|
+
header.
|
|
86
|
+
|
|
87
|
+
If no target is available, estimate one using the level heuristics in
|
|
88
|
+
[references/rubric.md](references/rubric.md#level-estimation-heuristics).
|
|
89
|
+
|
|
90
|
+
### 3. Load the standard
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bunx fit-pathway job {discipline} {level} --track={track}
|
|
94
|
+
bunx fit-pathway job {discipline} {level} --track={track} --skills
|
|
95
|
+
bunx fit-pathway track forward_deployed
|
|
96
|
+
bunx fit-pathway track platform
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 4. Map CV → standard skills
|
|
100
|
+
|
|
101
|
+
For each skill in the target job, assess the candidate's likely proficiency.
|
|
102
|
+
Look up nuance with `bunx fit-pathway skill {skill_id}`. Use the proficiency
|
|
103
|
+
mapping and the scepticism rule in
|
|
104
|
+
[references/rubric.md](references/rubric.md#proficiency-mapping).
|
|
105
|
+
|
|
106
|
+
### 5. Assess behaviours
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
bunx fit-pathway behaviour --list
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Map CV evidence using the behaviour signals in
|
|
113
|
+
[references/rubric.md](references/rubric.md#behaviour-signals).
|
|
114
|
+
|
|
115
|
+
### 6. Classify gaps and strengths
|
|
116
|
+
|
|
117
|
+
Optional progression context:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bunx fit-pathway progress {discipline} {level} --track={track}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Classify each skill per
|
|
124
|
+
[references/rubric.md](references/rubric.md#skill-alignment-classification).
|
|
125
|
+
Pick the recommendation using the decision rules and threshold rule in
|
|
126
|
+
[references/rubric.md](references/rubric.md#recommendation-decision-rules).
|
|
127
|
+
|
|
128
|
+
### 7. Write the screening
|
|
129
|
+
|
|
130
|
+
Save to `knowledge/Candidates/{Name}/screening.md` using the template in
|
|
131
|
+
[references/template.md](references/template.md). Include the **Suggested
|
|
132
|
+
Interview Questions** when the recommendation is "Interview" or "Interview with
|
|
133
|
+
focus areas":
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bunx fit-pathway interview {discipline} {level} --track={track}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Pick 3–5 questions most relevant to the gaps; note which gap each targets.
|
|
140
|
+
|
|
141
|
+
### 8. Enrich the brief
|
|
142
|
+
|
|
143
|
+
If `brief.md` exists, apply targeted edits:
|
|
144
|
+
|
|
145
|
+
- Add or update `## Skills` with agent-aligned standard skill IDs.
|
|
146
|
+
- Update `## Summary` if the CV provides better context.
|
|
147
|
+
- Set `**Gender:**` only when explicitly stated and not already set.
|
|
148
|
+
- Append `- [CV Screening](./screening.md)` if missing.
|
|
149
|
+
|
|
150
|
+
If no brief exists, tell the user to run `req-track` first to build the
|
|
151
|
+
candidate profile from email threads.
|