@forwardimpact/outpost 3.12.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +6 -5
- 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 +75 -20
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +108 -39
- 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 +32 -24
- package/templates/.claude/agents/concierge.md +26 -17
- package/templates/.claude/agents/head-hunter.md +34 -26
- package/templates/.claude/agents/librarian.md +26 -18
- package/templates/.claude/agents/postman.md +26 -18
- package/templates/.claude/agents/recruiter.md +38 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
- package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
- package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
- 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 +122 -67
- package/templates/.claude/skills/deck-create/SKILL.md +90 -32
- 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 +63 -58
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
- package/templates/.claude/skills/doc-create/SKILL.md +27 -23
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
- package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +37 -13
- package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
- package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
- package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
- package/templates/.claude/skills/organize-files/SKILL.md +27 -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 +77 -27
- package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
- package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
- package/templates/.claude/skills/req-assess/SKILL.md +44 -36
- package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
- package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
- package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
- package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
- package/templates/.claude/skills/req-decide/SKILL.md +52 -41
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +58 -39
- package/templates/.claude/skills/req-forget/references/classify.md +15 -13
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
- package/templates/.claude/skills/req-scan/SKILL.md +27 -25
- 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/references/template.md +11 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +96 -50
- package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +29 -17
- package/templates/.claude/skills/req-track/SKILL.md +76 -76
- package/templates/.claude/skills/req-track/references/fields.md +31 -27
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- 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 +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +69 -40
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +11 -78
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
- package/templates/.claude/skills/send-chat/SKILL.md +58 -54
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -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 +20 -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 +46 -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 +43 -41
- package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
- package/templates/CLAUDE.md +97 -85
- package/templates/MIGRATION.md +359 -0
- package/templates/registry.yaml +25 -0
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
- package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -183
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Split a Workday CV-bundle PDF into per-candidate CV.pdf files.
|
|
4
|
+
*
|
|
5
|
+
* Workday's "bundle" export concatenates every candidate's attachments (CV,
|
|
6
|
+
* cover letter, certificates) into one PDF with NO per-candidate separator page.
|
|
7
|
+
* The only reliable delimiter is the PDF outline (bookmarks): exactly one flat,
|
|
8
|
+
* depth-0 bookmark per candidate whose destination is that candidate's first
|
|
9
|
+
* page. Candidate i occupies pages [bookmark(i), bookmark(i+1) - 1]; the last
|
|
10
|
+
* candidate runs to the end of the file. Everything before the first bookmark is
|
|
11
|
+
* the front-matter Table of Contents and is skipped.
|
|
12
|
+
*
|
|
13
|
+
* This script reads the outline (pdfjs-dist), matches each bookmark title to a
|
|
14
|
+
* canonical candidate name from the parsed Workday roster (so folder names agree
|
|
15
|
+
* with req-workday), extracts the page range losslessly (pdf-lib), and writes it
|
|
16
|
+
* to 2-Confidential/Candidates/{Clean Name}/CV.pdf.
|
|
17
|
+
*
|
|
18
|
+
* It ALWAYS emits a JSON manifest describing every mapping and never overwrites
|
|
19
|
+
* an existing CV.pdf. With --dry-run it computes the manifest without touching
|
|
20
|
+
* disk. Multi-part bundles (`..._1_of_4.pdf` … `_4_of_4.pdf`) are self-contained:
|
|
21
|
+
* pass all parts as positional arguments; each part is processed independently.
|
|
22
|
+
*
|
|
23
|
+
* The roster is the stdout JSON of req-workday/scripts/parse-workday.mjs
|
|
24
|
+
* ({ requisition, candidates: [{ name, cleanName, ... }] }) — the single source
|
|
25
|
+
* of truth for candidate names, so this script needs no XLSX parser of its own.
|
|
26
|
+
*
|
|
27
|
+
* Requires (bun install): pdfjs-dist, pdf-lib.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* node split-bundle.mjs <bundle.pdf> [<bundle2.pdf> ...] \
|
|
31
|
+
* --roster <roster.json> \
|
|
32
|
+
* --candidates-dir <2-Confidential/Candidates> \
|
|
33
|
+
* --manifest <out.json> \
|
|
34
|
+
* [--dry-run]
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { basename, join } from "node:path";
|
|
39
|
+
|
|
40
|
+
let PDFDocument, pdfjs;
|
|
41
|
+
try {
|
|
42
|
+
({ PDFDocument } = await import("pdf-lib"));
|
|
43
|
+
pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
44
|
+
} catch {
|
|
45
|
+
console.error(
|
|
46
|
+
"Error: pdfjs-dist and/or pdf-lib not found. Install them first:\n bun install pdfjs-dist pdf-lib",
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- CLI parsing --------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
process.argv.includes("-h") ||
|
|
55
|
+
process.argv.includes("--help") ||
|
|
56
|
+
process.argv.length < 3
|
|
57
|
+
) {
|
|
58
|
+
console.log(`split-bundle — split a Workday CV bundle into per-candidate CV.pdf by PDF bookmarks
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
node split-bundle.mjs <bundle.pdf> [<bundle2.pdf> ...] --roster <roster.json> \\
|
|
62
|
+
--candidates-dir <dir> --manifest <out.json> [--dry-run]
|
|
63
|
+
|
|
64
|
+
<roster.json> is the stdout of req-workday/scripts/parse-workday.mjs.`);
|
|
65
|
+
process.exit(process.argv.length < 3 ? 1 : 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function optValue(name) {
|
|
69
|
+
const i = process.argv.indexOf(name);
|
|
70
|
+
return i >= 0 && i + 1 < process.argv.length
|
|
71
|
+
? process.argv[i + 1]
|
|
72
|
+
: undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
76
|
+
const rosterPath = optValue("--roster");
|
|
77
|
+
const candidatesDir = optValue("--candidates-dir");
|
|
78
|
+
const manifestPath = optValue("--manifest");
|
|
79
|
+
const FLAGS_WITH_VALUE = new Set([
|
|
80
|
+
"--roster",
|
|
81
|
+
"--candidates-dir",
|
|
82
|
+
"--manifest",
|
|
83
|
+
]);
|
|
84
|
+
const bundles = process.argv.slice(2).filter((a, i, arr) => {
|
|
85
|
+
if (a.startsWith("--")) return false;
|
|
86
|
+
const prev = arr[i - 1];
|
|
87
|
+
return !(prev && FLAGS_WITH_VALUE.has(prev)); // drop values consumed by flags
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!candidatesDir || !manifestPath || bundles.length === 0) {
|
|
91
|
+
console.error(
|
|
92
|
+
"Error: need <bundle.pdf...>, --candidates-dir, and --manifest.",
|
|
93
|
+
);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Name normalization & matching -------------------------------------------
|
|
98
|
+
|
|
99
|
+
const SUFFIX_RE = /\b(internal|referral|prior worker|external|contractor)\b/gi;
|
|
100
|
+
const PAREN_RE = /\([^)]*\)/g;
|
|
101
|
+
|
|
102
|
+
/** Fold a display name to a comparable key: no parentheticals, no diacritics,
|
|
103
|
+
* lowercase alphanumerics only, whitespace collapsed. */
|
|
104
|
+
function normalize(name) {
|
|
105
|
+
let s = name || "";
|
|
106
|
+
s = s.replace(PAREN_RE, " ").replace(SUFFIX_RE, " ");
|
|
107
|
+
s = s.normalize("NFKD").replace(/\p{M}/gu, ""); // strip combining accents
|
|
108
|
+
s = s.toLowerCase().replace(/[^a-z0-9\s]/g, " ");
|
|
109
|
+
return s.replace(/\s+/g, " ").trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Make a bookmark title safe as a directory name (used only when unmatched). */
|
|
113
|
+
function sanitizeFolder(title) {
|
|
114
|
+
const s = (title || "").replace(/[/\\]/g, "-").replace(/\s+/g, " ").trim();
|
|
115
|
+
return s || "UNKNOWN";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class Roster {
|
|
119
|
+
constructor(candidates) {
|
|
120
|
+
this.byExact = new Map(); // normalized name -> canonical cleanName
|
|
121
|
+
this.byTokens = new Map(); // sorted-token key -> canonical cleanName
|
|
122
|
+
this.all = []; // [{ key, clean }]
|
|
123
|
+
for (const c of candidates) {
|
|
124
|
+
const clean = (c.cleanName || c.name || "").trim();
|
|
125
|
+
if (!clean) continue;
|
|
126
|
+
const key = normalize(clean);
|
|
127
|
+
if (!key) continue;
|
|
128
|
+
if (!this.byExact.has(key)) this.byExact.set(key, clean);
|
|
129
|
+
const tkey = key.split(" ").sort().join(" ");
|
|
130
|
+
if (!this.byTokens.has(tkey)) this.byTokens.set(tkey, clean);
|
|
131
|
+
this.all.push({ key, clean });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Return { clean, method } — conservative: exact key, then token-set equality. */
|
|
136
|
+
match(title) {
|
|
137
|
+
const key = normalize(title);
|
|
138
|
+
if (!key) return { clean: null, method: "unmatched" };
|
|
139
|
+
if (this.byExact.has(key))
|
|
140
|
+
return { clean: this.byExact.get(key), method: "exact" };
|
|
141
|
+
const tkey = key.split(" ").sort().join(" ");
|
|
142
|
+
if (this.byTokens.has(tkey))
|
|
143
|
+
return { clean: this.byTokens.get(tkey), method: "token-set" };
|
|
144
|
+
return { clean: null, method: "unmatched" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Nearest roster names by shared-token count — helps a human resolve a miss. */
|
|
148
|
+
hints(title, limit = 3) {
|
|
149
|
+
const tokens = new Set(normalize(title).split(" ").filter(Boolean));
|
|
150
|
+
const scored = [];
|
|
151
|
+
for (const { key, clean } of this.all) {
|
|
152
|
+
const shared = key.split(" ").filter((t) => tokens.has(t)).length;
|
|
153
|
+
if (shared) scored.push({ shared, clean });
|
|
154
|
+
}
|
|
155
|
+
scored.sort(
|
|
156
|
+
(a, b) => b.shared - a.shared || a.clean.localeCompare(b.clean),
|
|
157
|
+
);
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const { clean } of scored) {
|
|
160
|
+
if (!out.includes(clean)) out.push(clean);
|
|
161
|
+
if (out.length >= limit) break;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- PDF outline handling -----------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/** Return [{ title, page }] (0-based page index) for every outline destination,
|
|
170
|
+
* in document order. pdfjs resolves both named and explicit destinations. */
|
|
171
|
+
async function readOutline(doc) {
|
|
172
|
+
const outline = (await doc.getOutline()) || [];
|
|
173
|
+
const items = [];
|
|
174
|
+
|
|
175
|
+
async function pageIndex(dest) {
|
|
176
|
+
const explicit =
|
|
177
|
+
typeof dest === "string" ? await doc.getDestination(dest) : dest;
|
|
178
|
+
if (!Array.isArray(explicit) || !explicit[0]) return null;
|
|
179
|
+
try {
|
|
180
|
+
return await doc.getPageIndex(explicit[0]);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Flatten defensively (Workday bundles are flat, depth-0).
|
|
187
|
+
const stack = [...outline];
|
|
188
|
+
while (stack.length) {
|
|
189
|
+
const node = stack.shift();
|
|
190
|
+
const page = await pageIndex(node.dest);
|
|
191
|
+
if (node.title != null && page != null)
|
|
192
|
+
items.push({ title: String(node.title), page });
|
|
193
|
+
if (Array.isArray(node.items) && node.items.length)
|
|
194
|
+
stack.unshift(...node.items);
|
|
195
|
+
}
|
|
196
|
+
items.sort((a, b) => a.page - b.page);
|
|
197
|
+
return items;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function partLabel(path) {
|
|
201
|
+
const m = basename(path).match(/_(\d+)_of_(\d+)\b/);
|
|
202
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Main ---------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
let requisition = {};
|
|
208
|
+
let rosterCandidates = [];
|
|
209
|
+
if (rosterPath && existsSync(rosterPath)) {
|
|
210
|
+
const data = JSON.parse(readFileSync(rosterPath, "utf8"));
|
|
211
|
+
rosterCandidates = data.candidates || [];
|
|
212
|
+
requisition = data.requisition || {};
|
|
213
|
+
}
|
|
214
|
+
const roster = new Roster(rosterCandidates);
|
|
215
|
+
|
|
216
|
+
const entries = [];
|
|
217
|
+
const matchedClean = new Set(); // normalized cleanNames that got a bookmark
|
|
218
|
+
const folderWritten = new Map(); // folder -> count (within this run, for collisions)
|
|
219
|
+
|
|
220
|
+
for (const bundle of bundles) {
|
|
221
|
+
// pdfjs detaches the ArrayBuffer it's handed, so give each library its own copy.
|
|
222
|
+
const doc = await pdfjs.getDocument({
|
|
223
|
+
data: new Uint8Array(readFileSync(bundle)),
|
|
224
|
+
useSystemFonts: true,
|
|
225
|
+
verbosity: 0,
|
|
226
|
+
}).promise;
|
|
227
|
+
const outline = await readOutline(doc);
|
|
228
|
+
const part = partLabel(bundle);
|
|
229
|
+
|
|
230
|
+
if (outline.length === 0) {
|
|
231
|
+
entries.push({
|
|
232
|
+
bundle,
|
|
233
|
+
part,
|
|
234
|
+
status: "no-outline",
|
|
235
|
+
note: "No PDF bookmarks found — cannot split this file by candidate.",
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// pdf-lib source (only loaded when we actually extract).
|
|
241
|
+
let src = null;
|
|
242
|
+
let total = 0;
|
|
243
|
+
if (!dryRun) {
|
|
244
|
+
src = await PDFDocument.load(new Uint8Array(readFileSync(bundle)));
|
|
245
|
+
total = src.getPageCount();
|
|
246
|
+
} else {
|
|
247
|
+
total = doc.numPages;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < outline.length; i++) {
|
|
251
|
+
const start = outline[i].page;
|
|
252
|
+
const end = i + 1 < outline.length ? outline[i + 1].page - 1 : total - 1;
|
|
253
|
+
const title = outline[i].title;
|
|
254
|
+
const { clean, method } = roster.match(title);
|
|
255
|
+
const matched = clean != null;
|
|
256
|
+
let folder, status, hints;
|
|
257
|
+
if (matched) {
|
|
258
|
+
matchedClean.add(normalize(clean));
|
|
259
|
+
folder = clean;
|
|
260
|
+
status = "matched";
|
|
261
|
+
hints = [];
|
|
262
|
+
} else {
|
|
263
|
+
folder = sanitizeFolder(title);
|
|
264
|
+
status = "unmatched";
|
|
265
|
+
hints = roster.hints(title);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const destDir = join(candidatesDir, folder);
|
|
269
|
+
const cvPath = join(destDir, "CV.pdf");
|
|
270
|
+
const seen = folderWritten.get(folder) || 0;
|
|
271
|
+
const existingCv = existsSync(cvPath);
|
|
272
|
+
let outName;
|
|
273
|
+
if (seen > 0) {
|
|
274
|
+
outName = `CV-dup-${seen + 1}.pdf`;
|
|
275
|
+
if (matched) status = "collision";
|
|
276
|
+
} else if (existingCv) {
|
|
277
|
+
outName = "CV-workday.pdf";
|
|
278
|
+
if (matched) status = "existing-cv";
|
|
279
|
+
} else {
|
|
280
|
+
outName = "CV.pdf";
|
|
281
|
+
}
|
|
282
|
+
folderWritten.set(folder, seen + 1);
|
|
283
|
+
const outPath = join(destDir, outName);
|
|
284
|
+
|
|
285
|
+
let action = dryRun ? "would-write" : "pending";
|
|
286
|
+
if (!dryRun) {
|
|
287
|
+
try {
|
|
288
|
+
mkdirSync(destDir, { recursive: true });
|
|
289
|
+
const out = await PDFDocument.create();
|
|
290
|
+
const idxs = [];
|
|
291
|
+
for (let p = start; p <= end; p++) idxs.push(p);
|
|
292
|
+
const copied = await out.copyPages(src, idxs);
|
|
293
|
+
for (const pg of copied) out.addPage(pg);
|
|
294
|
+
writeFileSync(outPath, await out.save());
|
|
295
|
+
action = "wrote";
|
|
296
|
+
} catch (exc) {
|
|
297
|
+
action = `error: ${exc.message || exc}`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
entries.push({
|
|
302
|
+
bundle,
|
|
303
|
+
part,
|
|
304
|
+
order: i + 1,
|
|
305
|
+
title,
|
|
306
|
+
page_start: start + 1, // 1-based for humans
|
|
307
|
+
page_end: end + 1,
|
|
308
|
+
page_count: end - start + 1,
|
|
309
|
+
matched,
|
|
310
|
+
match_method: method,
|
|
311
|
+
clean_name: clean || "",
|
|
312
|
+
folder,
|
|
313
|
+
status,
|
|
314
|
+
out_path: outPath,
|
|
315
|
+
action,
|
|
316
|
+
hints,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const rosterUnmatched = [
|
|
322
|
+
...new Set(
|
|
323
|
+
rosterCandidates
|
|
324
|
+
.map((c) => c.cleanName || c.name)
|
|
325
|
+
.filter((n) => n && !matchedClean.has(normalize(n))),
|
|
326
|
+
),
|
|
327
|
+
].sort();
|
|
328
|
+
|
|
329
|
+
const bookmarkEntries = entries.filter((e) => "title" in e);
|
|
330
|
+
const summary = {
|
|
331
|
+
bundles: bundles.length,
|
|
332
|
+
bookmarks_total: bookmarkEntries.length,
|
|
333
|
+
matched: bookmarkEntries.filter((e) => e.matched).length,
|
|
334
|
+
unmatched_bookmarks: bookmarkEntries.filter((e) => !e.matched).length,
|
|
335
|
+
collisions: bookmarkEntries.filter((e) => e.status === "collision").length,
|
|
336
|
+
existing_cv: bookmarkEntries.filter((e) => e.status === "existing-cv").length,
|
|
337
|
+
roster_total: rosterCandidates.length,
|
|
338
|
+
roster_unmatched: rosterUnmatched.length,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const manifest = {
|
|
342
|
+
generated_from: bundles,
|
|
343
|
+
requisition,
|
|
344
|
+
candidates_dir: candidatesDir,
|
|
345
|
+
dry_run: dryRun,
|
|
346
|
+
summary,
|
|
347
|
+
entries,
|
|
348
|
+
roster_unmatched_names: rosterUnmatched,
|
|
349
|
+
};
|
|
350
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
351
|
+
|
|
352
|
+
// Human summary to stdout.
|
|
353
|
+
const mode = dryRun ? "DRY RUN — nothing written" : "COMMIT";
|
|
354
|
+
console.log(
|
|
355
|
+
`[${mode}] ${requisition.id || "?"} ${requisition.title || ""}`.trimEnd(),
|
|
356
|
+
);
|
|
357
|
+
console.log(` bundles: ${summary.bundles}`);
|
|
358
|
+
console.log(` bookmarks: ${summary.bookmarks_total}`);
|
|
359
|
+
console.log(` matched → roster: ${summary.matched}`);
|
|
360
|
+
console.log(` unmatched bookmark: ${summary.unmatched_bookmarks}`);
|
|
361
|
+
console.log(` collisions (dupes): ${summary.collisions}`);
|
|
362
|
+
console.log(` existing CV.pdf: ${summary.existing_cv}`);
|
|
363
|
+
console.log(
|
|
364
|
+
` roster w/o bookmark:${summary.roster_unmatched} (of ${summary.roster_total})`,
|
|
365
|
+
);
|
|
366
|
+
console.log(` manifest: ${manifestPath}`);
|
|
367
|
+
if (summary.unmatched_bookmarks) {
|
|
368
|
+
console.log(" ⚠ unmatched bookmarks (resolve in the manifest):");
|
|
369
|
+
for (const e of bookmarkEntries) {
|
|
370
|
+
if (!e.matched) {
|
|
371
|
+
const hint = e.hints.length ? ` ~ ${e.hints.join(", ")}` : "";
|
|
372
|
+
console.log(
|
|
373
|
+
` - "${e.title}" (p${e.page_start}-${e.page_end})${hint}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -9,8 +9,11 @@ description: >
|
|
|
9
9
|
|
|
10
10
|
# Hiring Decision
|
|
11
11
|
|
|
12
|
+
Write tier: `2-Confidential`
|
|
13
|
+
Frontmatter: candidate (stamps `status`, `updated`)
|
|
14
|
+
|
|
12
15
|
Synthesize all available evidence for a candidate into a final hiring
|
|
13
|
-
recommendation. This is the culmination of the hiring pipeline
|
|
16
|
+
recommendation. This is the culmination of the hiring pipeline. Every prior
|
|
14
17
|
assessment feeds in.
|
|
15
18
|
|
|
16
19
|
This is **Stage 3** of the three-stage pipeline:
|
|
@@ -29,41 +32,46 @@ This is **Stage 3** of the three-stage pipeline:
|
|
|
29
32
|
## Prerequisites
|
|
30
33
|
|
|
31
34
|
- `fit-pathway` CLI installed.
|
|
32
|
-
- `
|
|
33
|
-
- At least one `
|
|
34
|
-
- `
|
|
35
|
+
- `2-Confidential/Candidates/{Name}/screening.md` exists.
|
|
36
|
+
- At least one `2-Confidential/Candidates/{Name}/interview-*.md`.
|
|
37
|
+
- `2-Confidential/Candidates/{Name}/brief.md` exists.
|
|
35
38
|
|
|
36
39
|
## Inputs
|
|
37
40
|
|
|
38
|
-
- All artifacts in `
|
|
41
|
+
- All artifacts in `2-Confidential/Candidates/{Name}/`.
|
|
39
42
|
- Standard data via `fit-pathway`.
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
43
|
+
- `2-Confidential/Candidates/Insights.md` — cross-candidate context.
|
|
44
|
+
- `2-Confidential/Roles/*.md` — the candidate's requisition (positions, hiring
|
|
42
45
|
manager, domain lead). Check the `**Status:**` field to distinguish active
|
|
43
46
|
from historical reqs.
|
|
44
|
-
- `
|
|
45
|
-
- Other active candidates at the same level — relative
|
|
47
|
+
- `3-Team/Priorities/*.md` — strategic context.
|
|
48
|
+
- Other active candidates at the same level — relative position.
|
|
46
49
|
|
|
47
50
|
## Outputs
|
|
48
51
|
|
|
49
|
-
- `
|
|
50
|
-
- Updated `
|
|
51
|
-
|
|
52
|
+
- `2-Confidential/Candidates/{Name}/recommendation.md` — final recommendation.
|
|
53
|
+
- Updated `2-Confidential/Candidates/{Name}/brief.md` — status, link, and
|
|
54
|
+
frontmatter `status` and `updated` stamped.
|
|
55
|
+
- Updated `2-Confidential/Candidates/Insights.md` — cross-candidate
|
|
56
|
+
observations.
|
|
52
57
|
|
|
53
58
|
<do_confirm_checklist goal="Verify the recommendation is grounded and
|
|
54
59
|
audit-ready">
|
|
55
60
|
|
|
56
|
-
- [ ]
|
|
57
|
-
- [ ]
|
|
58
|
-
- [ ]
|
|
59
|
-
- [ ]
|
|
60
|
-
|
|
61
|
-
- [ ]
|
|
62
|
-
- [ ]
|
|
63
|
-
|
|
64
|
-
- [ ]
|
|
65
|
-
|
|
66
|
-
- [ ]
|
|
61
|
+
- [ ] Trace every skill rating to a specific evidence source (stage + detail).
|
|
62
|
+
- [ ] Respect the evidence hierarchy. Interview evidence outranks CV.
|
|
63
|
+
- [ ] Ground the level recommendation in standard progression criteria.
|
|
64
|
+
- [ ] Cite interview evidence for the track recommendation. A CV alone is not
|
|
65
|
+
enough.
|
|
66
|
+
- [ ] Apply the decision rules strictly. Verify the match % and the gap counts.
|
|
67
|
+
- [ ] Keep the risk assessment honest. Do not minimise a concern to justify a
|
|
68
|
+
hire.
|
|
69
|
+
- [ ] Write "Do not hire" with the same rigour as "Hire" (GDPR Article 15 lets
|
|
70
|
+
the candidate request access).
|
|
71
|
+
- [ ] Update the brief with `[Hiring Recommendation](./recommendation.md)` and
|
|
72
|
+
a final pipeline entry.
|
|
73
|
+
- [ ] Write a recommendation you could show to the candidate without
|
|
74
|
+
embarrassment.
|
|
67
75
|
|
|
68
76
|
</do_confirm_checklist>
|
|
69
77
|
|
|
@@ -72,7 +80,7 @@ audit-ready">
|
|
|
72
80
|
### 1. Gather all evidence
|
|
73
81
|
|
|
74
82
|
```bash
|
|
75
|
-
ls
|
|
83
|
+
ls 2-Confidential/Candidates/{Name}/
|
|
76
84
|
```
|
|
77
85
|
|
|
78
86
|
Read in order: `brief.md`, `screening.md`, every `interview-*.md`, relevant
|
|
@@ -86,9 +94,10 @@ bunx fit-pathway job {discipline} {level} --track={track} --skills
|
|
|
86
94
|
bunx fit-pathway progress {discipline} {level} --track={track}
|
|
87
95
|
```
|
|
88
96
|
|
|
89
|
-
For each skill in the target job,
|
|
90
|
-
[evidence hierarchy](references/rubric.md#evidence-hierarchy)
|
|
91
|
-
rating, best evidence source,
|
|
97
|
+
For each skill in the target job, use the
|
|
98
|
+
[evidence hierarchy](references/rubric.md#evidence-hierarchy) to pick the
|
|
99
|
+
highest-fidelity evidence. Record final rating, best evidence source,
|
|
100
|
+
trajectory, confidence.
|
|
92
101
|
|
|
93
102
|
### 3. Build the final behaviour profile
|
|
94
103
|
|
|
@@ -115,21 +124,20 @@ Apply the level-confirmation criteria in
|
|
|
115
124
|
If `brief.md` carries a `Req`:
|
|
116
125
|
|
|
117
126
|
```bash
|
|
118
|
-
ls
|
|
119
|
-
cat "
|
|
127
|
+
ls 2-Confidential/Roles/ | grep "{req_number}"
|
|
128
|
+
cat "2-Confidential/Roles/{matching file}"
|
|
120
129
|
```
|
|
121
130
|
|
|
122
131
|
Check the `**Status:**` field. Capture remaining positions, hiring manager,
|
|
123
|
-
domain lead, goal alignment, other
|
|
124
|
-
|
|
125
|
-
in terms of strategic impact.
|
|
132
|
+
domain lead, goal alignment, other candidates on the same req, and channel
|
|
133
|
+
(hr / vendor). Frame the recommendation in terms of strategic impact.
|
|
126
134
|
|
|
127
135
|
### 6. Assess against the active pipeline
|
|
128
136
|
|
|
129
137
|
```bash
|
|
130
|
-
cat
|
|
138
|
+
cat 2-Confidential/Candidates/Insights.md
|
|
131
139
|
|
|
132
|
-
for dir in
|
|
140
|
+
for dir in 2-Confidential/Candidates/*/; do
|
|
133
141
|
if [ -f "$dir/screening.md" ]; then
|
|
134
142
|
head -5 "$dir/screening.md"
|
|
135
143
|
fi
|
|
@@ -146,9 +154,10 @@ Apply the [decision rules](references/rubric.md#decision-rules) and run the
|
|
|
146
154
|
|
|
147
155
|
### 8. Write the recommendation
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
banner. Cite specific evidence in skill, behaviour, level, and
|
|
157
|
+
Use the template in [references/template.md](references/template.md). Save to
|
|
158
|
+
`2-Confidential/Candidates/{Name}/recommendation.md`. Carry the
|
|
159
|
+
**advisory-only** banner. Cite specific evidence in skill, behaviour, level, and
|
|
160
|
+
track sections.
|
|
152
161
|
|
|
153
162
|
### 9. Update brief and insights
|
|
154
163
|
|
|
@@ -157,9 +166,11 @@ Update `brief.md`:
|
|
|
157
166
|
- Set `Status` to `recommended` or `not-recommended`.
|
|
158
167
|
- Append `- [Hiring Recommendation](./recommendation.md)`.
|
|
159
168
|
- Add the final pipeline entry with date and outcome.
|
|
169
|
+
- Stamp the frontmatter: set `updated` to the decision date and `status` to
|
|
170
|
+
the closest registry value (agents select from `registry.yaml`).
|
|
160
171
|
|
|
161
|
-
Update `
|
|
162
|
-
observation
|
|
163
|
-
level-adjustment implications).
|
|
172
|
+
Update `2-Confidential/Candidates/Insights.md` only when a cross-candidate
|
|
173
|
+
observation deserves a record (strongest at level, a pattern in the sourcing
|
|
174
|
+
channel, level-adjustment implications).
|
|
164
175
|
|
|
165
|
-
Use targeted Edit operations
|
|
176
|
+
Use targeted Edit operations. Never rewrite entire files.
|
|
@@ -13,8 +13,9 @@ applies and use it as the source of the final rating.
|
|
|
13
13
|
answers.
|
|
14
14
|
3. **Interviewer observation** — an interviewer noted strength or concern.
|
|
15
15
|
4. **CV evidence with quantification** — concrete metrics, named projects.
|
|
16
|
-
5. **CV evidence without quantification** —
|
|
17
|
-
|
|
16
|
+
5. **CV evidence without quantification** — the CV describes it but stays
|
|
17
|
+
vague.
|
|
18
|
+
6. **Not evidenced** — no stage surfaced it.
|
|
18
19
|
|
|
19
20
|
For each skill record: **final rating**, **best evidence source**,
|
|
20
21
|
**trajectory** (↑ / ↓ / —), **confidence** (High = demonstrated live; Medium =
|
|
@@ -22,9 +23,9 @@ discussed well; Low = CV only or thin).
|
|
|
22
23
|
|
|
23
24
|
## Behaviour evidence
|
|
24
25
|
|
|
25
|
-
Same hierarchy.
|
|
26
|
-
|
|
27
|
-
pattern), **consistency** (once vs. throughout).
|
|
26
|
+
Same hierarchy. A behaviour that an interview assessed carries far more weight
|
|
27
|
+
than a CV signal. For each: **final maturity**, **best evidence** (specific
|
|
28
|
+
moment or pattern), **consistency** (once vs. throughout).
|
|
28
29
|
|
|
29
30
|
## Level confirmation criteria
|
|
30
31
|
|
|
@@ -41,7 +42,7 @@ pattern), **consistency** (once vs. throughout).
|
|
|
41
42
|
| Recommendation | Criteria |
|
|
42
43
|
| -------------------------- | ----------------------------------------------------------------------------------------- |
|
|
43
44
|
| **Hire** | ≥ 70 % Strong match at the final level, no unresolved core-skill gaps, strong behaviours |
|
|
44
|
-
| **Hire at adjusted level** | Strong candidate
|
|
45
|
+
| **Hire at adjusted level** | Strong candidate. The evidence supports a level other than the original target |
|
|
45
46
|
| **Do not hire** | Unresolved core-skill gaps, behaviour concerns, or insufficient evidence after interviews |
|
|
46
47
|
|
|
47
48
|
### Hire criteria check
|
|
@@ -50,14 +51,14 @@ pattern), **consistency** (once vs. throughout).
|
|
|
50
51
|
- No core skill gaps remain unresolved (core = top-tier skills for the
|
|
51
52
|
discipline/track combo).
|
|
52
53
|
- All agent-aligned standard behaviours at or above expected maturity.
|
|
53
|
-
-
|
|
54
|
-
-
|
|
54
|
+
- Interview evidence confirms the level. The CV alone does not.
|
|
55
|
+
- Interview evidence confirms the track fit.
|
|
55
56
|
- No red flags from any interview stage.
|
|
56
57
|
|
|
57
58
|
## Pipeline context (Step 5)
|
|
58
59
|
|
|
59
|
-
This is **not** a ranking exercise
|
|
60
|
+
This is **not** a ranking exercise. It gives context. Note:
|
|
60
61
|
|
|
61
62
|
- Whether this candidate fills a gap in the current pipeline.
|
|
62
63
|
- Whether stronger candidates exist for the same role.
|
|
63
|
-
- Whether
|
|
64
|
+
- Whether a different open position suits this candidate better.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Recommendation Template
|
|
2
2
|
|
|
3
|
-
Reference template for `req-decide` Step
|
|
4
|
-
`
|
|
3
|
+
Reference template for `req-decide` Step 8. Save to
|
|
4
|
+
`2-Confidential/Candidates/{Name}/recommendation.md`.
|
|
5
5
|
|
|
6
6
|
```markdown
|
|
7
7
|
# Hiring Recommendation — {Full Name}
|