@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.
Files changed (129) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +6 -5
  4. package/src/agent-path.js +10 -6
  5. package/src/agent-runner.js +28 -24
  6. package/src/index.js +2 -3
  7. package/src/kb-manager.js +75 -20
  8. package/src/kb-validator.js +762 -0
  9. package/src/outpost.js +108 -39
  10. package/src/posture.js +3 -3
  11. package/src/privilege.js +11 -11
  12. package/src/scheduler.js +14 -12
  13. package/src/socket-server.js +19 -18
  14. package/src/spawn-env.js +7 -6
  15. package/src/state-manager.js +2 -2
  16. package/templates/.claude/agents/chief-of-staff.md +32 -24
  17. package/templates/.claude/agents/concierge.md +26 -17
  18. package/templates/.claude/agents/head-hunter.md +34 -26
  19. package/templates/.claude/agents/librarian.md +26 -18
  20. package/templates/.claude/agents/postman.md +26 -18
  21. package/templates/.claude/agents/recruiter.md +38 -27
  22. package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
  23. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  24. package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
  25. package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
  26. package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
  27. package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
  28. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
  29. package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
  30. package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
  31. package/templates/.claude/skills/candidate-report/references/report.css +4 -4
  32. package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
  33. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
  34. package/templates/.claude/skills/changelog/SKILL.md +122 -67
  35. package/templates/.claude/skills/deck-create/SKILL.md +90 -32
  36. package/templates/.claude/skills/deck-create/references/slide.css +4 -4
  37. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
  38. package/templates/.claude/skills/deck-review/SKILL.md +63 -58
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
  44. package/templates/.claude/skills/doc-create/SKILL.md +27 -23
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
  50. package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +37 -13
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
  57. package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
  59. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
  60. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
  61. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
  62. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
  64. package/templates/.claude/skills/organize-files/SKILL.md +27 -22
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  67. package/templates/.claude/skills/person-identify/SKILL.md +77 -27
  68. package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
  69. package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
  70. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
  71. package/templates/.claude/skills/req-assess/SKILL.md +44 -36
  72. package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
  73. package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
  74. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  75. package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
  76. package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
  77. package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
  78. package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
  79. package/templates/.claude/skills/req-decide/SKILL.md +52 -41
  80. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  81. package/templates/.claude/skills/req-decide/references/template.md +2 -2
  82. package/templates/.claude/skills/req-forget/SKILL.md +58 -39
  83. package/templates/.claude/skills/req-forget/references/classify.md +15 -13
  84. package/templates/.claude/skills/req-forget/references/locations.md +27 -22
  85. package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
  86. package/templates/.claude/skills/req-scan/SKILL.md +27 -25
  87. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  88. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  89. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  90. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  91. package/templates/.claude/skills/req-scan/references/template.md +11 -1
  92. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  93. package/templates/.claude/skills/req-screen/SKILL.md +96 -50
  94. package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
  95. package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
  96. package/templates/.claude/skills/req-screen/references/template.md +29 -17
  97. package/templates/.claude/skills/req-track/SKILL.md +76 -76
  98. package/templates/.claude/skills/req-track/references/fields.md +31 -27
  99. package/templates/.claude/skills/req-track/references/overlays.md +42 -0
  100. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  101. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  102. package/templates/.claude/skills/req-track/references/templates.md +28 -27
  103. package/templates/.claude/skills/req-workday/SKILL.md +69 -40
  104. package/templates/.claude/skills/req-workday/references/brief.md +81 -0
  105. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  106. package/templates/.claude/skills/req-workday/references/templates.md +11 -78
  107. package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
  108. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
  109. package/templates/.claude/skills/send-chat/SKILL.md +58 -54
  110. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
  111. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  112. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  113. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  114. package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
  115. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  116. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  117. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  118. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  119. package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
  120. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  121. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  122. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  123. package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
  124. package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
  125. package/templates/CLAUDE.md +97 -85
  126. package/templates/MIGRATION.md +359 -0
  127. package/templates/registry.yaml +25 -0
  128. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
  129. 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 every prior
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
- - `Knowledge/Candidates/{Name}/screening.md` exists.
33
- - At least one `Knowledge/Candidates/{Name}/interview-*.md`.
34
- - `Knowledge/Candidates/{Name}/brief.md` exists.
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 `Knowledge/Candidates/{Name}/`.
41
+ - All artifacts in `2-Confidential/Candidates/{Name}/`.
39
42
  - Standard data via `fit-pathway`.
40
- - `Knowledge/Candidates/Insights.md` — cross-candidate context.
41
- - `Knowledge/Roles/*.md` — the candidate's requisition (positions, hiring
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
- - `Knowledge/Priorities/*.md` — strategic context.
45
- - Other active candidates at the same level — relative positioning.
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
- - `Knowledge/Candidates/{Name}/recommendation.md` — final recommendation.
50
- - Updated `Knowledge/Candidates/{Name}/brief.md` — status and link.
51
- - Updated `Knowledge/Candidates/Insights.md` cross-candidate observations.
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
- - [ ] Every skill rating traces to a specific evidence source (stage + detail).
57
- - [ ] Evidence hierarchy respected interview evidence outranks CV.
58
- - [ ] Level recommendation grounded in standard progression criteria.
59
- - [ ] Track recommendation cites interview evidence, not just CV.
60
- - [ ] Decision rules applied strictly; match % and gap counts verified.
61
- - [ ] Risk assessment is honest concerns not minimised to justify a hire.
62
- - [ ] "Do not hire" written with the same rigour as "Hire" (GDPR Article 15:
63
- candidate may request access).
64
- - [ ] Brief updated with `[Hiring Recommendation](./recommendation.md)` and a
65
- final pipeline entry.
66
- - [ ] Recommendation could be shown to the candidate without embarrassment.
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 Knowledge/Candidates/{Name}/
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, pick the highest-fidelity evidence using the
90
- [evidence hierarchy](references/rubric.md#evidence-hierarchy). Record final
91
- rating, best evidence source, trajectory, confidence.
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 Knowledge/Roles/ | grep "{req_number}"
119
- cat "Knowledge/Roles/{matching file}"
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
- candidates on the same req, and channel (hr / vendor). Frame the recommendation
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 Knowledge/Candidates/Insights.md
138
+ cat 2-Confidential/Candidates/Insights.md
131
139
 
132
- for dir in Knowledge/Candidates/*/; do
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
- Save to `Knowledge/Candidates/{Name}/recommendation.md` using the template in
150
- [references/template.md](references/template.md). Carry the **advisory-only**
151
- banner. Cite specific evidence in skill, behaviour, level, and track sections.
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 `Knowledge/Candidates/Insights.md` only when there's a cross-candidate
162
- observation worth keeping (strongest at level, sourcing channel pattern,
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 never rewrite entire files.
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** — described but vague.
17
- 6. **Not evidenced** — never surfaced across any stage.
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. Behaviours assessed in interviews carry far more weight than CV
26
- signals. For each: **final maturity**, **best evidence** (specific moment or
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; evidence supports a different level than originally targeted |
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
- - Level confirmed by interview evidence (not just CV).
54
- - Track fit confirmed by interview evidence.
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 it provides context. Note:
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 this candidate is better suited to a different open position.
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 6. Save to
4
- `Knowledge/Candidates/{Name}/recommendation.md`.
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}