@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
@@ -1,17 +1,20 @@
1
1
  ---
2
2
  name: req-assess
3
3
  description: >
4
- Analyze interview transcripts against the agent-aligned engineering standard,
5
- updating skill and behaviour ratings with observed evidence. Produces
6
- per-interview assessments and panel briefs for subsequent interview stages.
4
+ Analyze interview transcripts against the agent-aligned engineering standard.
5
+ Update skill and behaviour ratings with observed evidence. Produces
6
+ per-interview assessments and panel briefs for later interview stages.
7
7
  Use when transcript files appear in a candidate's folder.
8
8
  ---
9
9
 
10
10
  # Assess Interview
11
11
 
12
+ Write tier: `2-Confidential`
13
+ Frontmatter: candidate (stamps `status`, `updated`)
14
+
12
15
  Analyze a candidate's interview transcripts. Update their skill and behaviour
13
16
  profile with **observed** (not claimed) evidence. Interview evidence is
14
- higher-fidelity than CV evidence it confirms or contradicts the screening
17
+ higher-fidelity than CV evidence. It confirms or contradicts the screening
15
18
  assessment.
16
19
 
17
20
  This is **Stage 2** of the three-stage hiring pipeline:
@@ -22,43 +25,46 @@ This is **Stage 2** of the three-stage hiring pipeline:
22
25
 
23
26
  ## Trigger
24
27
 
25
- - A new `transcript-*.md` file appears in `Knowledge/Candidates/{Name}/`.
28
+ - A new `transcript-*.md` file appears in `2-Confidential/Candidates/{Name}/`.
26
29
  - The user asks to analyze an interview or debrief.
27
30
  - The user asks to prepare a panel brief.
28
- - The concierge agent processes a Anarlog interview recording.
31
+ - The concierge agent processes an Anarlog interview recording.
29
32
 
30
33
  ## Prerequisites
31
34
 
32
35
  - `fit-pathway` CLI installed.
33
- - At least one transcript in `Knowledge/Candidates/{Name}/`.
34
- - `screening.md` should exist; if missing, run `req-screen` first (proceed
35
- regardless).
36
+ - At least one transcript in `2-Confidential/Candidates/{Name}/`.
37
+ - `screening.md` should exist. If it is missing, run `req-screen` first.
38
+ Proceed regardless.
36
39
 
37
40
  ## Inputs
38
41
 
39
- - `Knowledge/Candidates/{Name}/transcript-{date}.md`.
40
- - `Knowledge/Candidates/{Name}/screening.md`.
41
- - `Knowledge/Candidates/{Name}/brief.md` — target role.
42
+ - `2-Confidential/Candidates/{Name}/transcript-{date}.md`.
43
+ - `2-Confidential/Candidates/{Name}/screening.md`.
44
+ - `2-Confidential/Candidates/{Name}/brief.md` — target role.
42
45
 
43
46
  ## Outputs
44
47
 
45
- - `Knowledge/Candidates/{Name}/interview-{date}.md`.
46
- - `Knowledge/Candidates/{Name}/panel.md` — only when more interviews are
48
+ - `2-Confidential/Candidates/{Name}/interview-{date}.md`.
49
+ - `2-Confidential/Candidates/{Name}/panel.md` — only when more interviews are
47
50
  planned.
48
- - Updated `Knowledge/Candidates/{Name}/brief.md`.
51
+ - Updated `2-Confidential/Candidates/{Name}/brief.md` — frontmatter `status`
52
+ and `updated` stamped.
49
53
 
50
54
  <do_confirm_checklist goal="Verify the assessment is grounded in transcript
51
55
  evidence">
52
56
 
53
- - [ ] Every skill re-rating cites a specific moment from the transcript.
54
- - [ ] Behaviour assessments reference observed actions, not claimed traits.
55
- - [ ] Level assessment uses standard progression criteria, not gut feel.
56
- - [ ] Interviewer observations are attributed by name.
57
- - [ ] Confirmed strengths and new concerns are distinguished.
58
- - [ ] Panel brief (if created) is written for non-technical readers and ties
59
- suggested questions to remaining gaps.
60
- - [ ] Brief's Pipeline section, links, and Status are updated.
61
- - [ ] Gender field is **not** updated from interview observations.
57
+ - [ ] Cite a specific moment from the transcript for every skill re-rating.
58
+ - [ ] Base behaviour assessments on observed actions. Ignore claimed traits.
59
+ - [ ] Assess the level with standard progression criteria. Ignore gut feel.
60
+ - [ ] Attribute interviewer observations by name.
61
+ - [ ] Distinguish confirmed strengths from new concerns.
62
+ - [ ] Assessment prose about the candidate stays in the candidate's own
63
+ folder. Wider notes record only that the interview happened.
64
+ - [ ] Write the panel brief (if created) for non-technical readers. Tie the
65
+ suggested questions to the remaining gaps.
66
+ - [ ] Update the brief's Pipeline section, links, and Status.
67
+ - [ ] **Never** update the Gender field from interview observations.
62
68
 
63
69
  </do_confirm_checklist>
64
70
 
@@ -79,8 +85,8 @@ bunx fit-pathway skill {skill_id}
79
85
  bunx fit-pathway behaviour --list
80
86
  ```
81
87
 
82
- If screening recommended a different level than originally targeted (e.g. J100
83
- J090), load **both** for comparison.
88
+ If screening recommended a level different from the original target (e.g. J100
89
+ J090), load **both** for comparison.
84
90
 
85
91
  ### 3. Re-rate skills
86
92
 
@@ -90,8 +96,8 @@ moment, quote, or observation per change.
90
96
 
91
97
  ### 4. Re-rate behaviours
92
98
 
93
- Behaviours are better assessed in interviews than CVs they describe how
94
- someone acts, not what they've done. Use the
99
+ Interviews assess behaviours better than CVs. Behaviours describe how someone
100
+ acts. They do not describe what someone did. Use the
95
101
  [behaviour signals](references/rubric.md#behaviour-signals) and the
96
102
  [behaviour maturity scale](references/rubric.md#behaviour-maturity-scale).
97
103
 
@@ -105,33 +111,35 @@ Apply the [level signals](references/rubric.md#level-signals).
105
111
 
106
112
  ### 6. Write the interview assessment
107
113
 
108
- Save to `Knowledge/Candidates/{Name}/interview-{date}.md` using
114
+ Save to `2-Confidential/Candidates/{Name}/interview-{date}.md` with
109
115
  [references/interview-template.md](references/interview-template.md). Include
110
- only skills with new evidence don't repeat the full matrix.
116
+ only skills with new evidence. Do not repeat the full matrix.
111
117
 
112
118
  ### 7. Generate the panel brief (if applicable)
113
119
 
114
- When more interview stages are planned (panel, technical, etc.), pull question
120
+ When you plan more interview stages (panel, technical, etc.), pull question
115
121
  candidates:
116
122
 
117
123
  ```bash
118
124
  bunx fit-pathway interview {discipline} {level} --track={track}
119
125
  ```
120
126
 
121
- Save `Knowledge/Candidates/{Name}/panel.md` using
122
- [references/panel-template.md](references/panel-template.md). Audience:
123
- next-stage interviewers, often non-engineers explain without jargon and tie
124
- suggested questions to remaining gaps.
127
+ Save `2-Confidential/Candidates/{Name}/panel.md` with
128
+ [references/panel-template.md](references/panel-template.md). The audience is
129
+ next-stage interviewers, often non-engineers. Explain without jargon. Tie the
130
+ suggested questions to the remaining gaps.
125
131
 
126
132
  ### 8. Update the candidate brief
127
133
 
128
- Apply targeted Edit operations to `Knowledge/Candidates/{Name}/brief.md`:
134
+ Apply targeted Edit operations to `2-Confidential/Candidates/{Name}/brief.md`:
129
135
 
130
136
  - Append a Pipeline entry with date, type, and outcome.
131
137
  - Add `## Interview Notes` if missing, with key observations.
132
138
  - Append `- [Interview Assessment](./interview-{date}.md)`.
133
139
  - Append `- [Panel Brief](./panel.md)` when one was created.
134
140
  - Update `Status` to reflect the current pipeline stage.
141
+ - Stamp the frontmatter: set `updated` to the interview date and `status` to
142
+ the closest registry value (agents select from `registry.yaml`).
135
143
 
136
144
  Never rewrite the file. Never update the Gender field from interview
137
145
  observations.
@@ -1,8 +1,13 @@
1
1
  # Interview Assessment Template
2
2
 
3
3
  Reference template for `req-assess` Step 6. Save to
4
- `Knowledge/Candidates/{Name}/interview-{date}.md`. Include only skills the
5
- interview produced new evidence for don't repeat the full matrix.
4
+ `2-Confidential/Candidates/{Name}/interview-{date}.md`. Include only skills the
5
+ interview produced new evidence for. Do not repeat the full matrix.
6
+
7
+ Interview-outcome prose about the candidate lives only in the candidate's own
8
+ folder. A wider note (an interviewer's People note, a project note) records
9
+ only that the interview happened. Interviewer names may appear here; write no
10
+ assessment of the interviewers.
6
11
 
7
12
  ```markdown
8
13
  # Interview Assessment — {Full Name}
@@ -1,8 +1,13 @@
1
1
  # Panel Brief Template
2
2
 
3
3
  Reference template for `req-assess` Step 7. Save to
4
- `Knowledge/Candidates/{Name}/panel.md`. Audience: **next-stage interviewers,
5
- often non-engineers** explain without jargon, focus on behaviours and scope.
4
+ `2-Confidential/Candidates/{Name}/panel.md`. The audience is **next-stage
5
+ interviewers, often non-engineers**. Explain without jargon. Focus on
6
+ behaviours and scope.
7
+
8
+ The brief goes to the panel as an export. The note keeps its tier. Assessment
9
+ prose about the candidate stays in this folder; never copy it into panel
10
+ members' People notes.
6
11
 
7
12
  ```markdown
8
13
  # Panel Brief — {Full Name}
@@ -13,17 +13,17 @@ Reference data for `req-assess` Steps 1, 3, 4, and 5.
13
13
  | **Behaviour observations** | How the candidate acted under pressure, with others |
14
14
  | **Self-identified strengths** | What the candidate claims they're good at |
15
15
  | **Self-identified gaps** | What the candidate acknowledges as growth areas |
16
- | **Red flags** | Contradictions with CV, concerning patterns |
16
+ | **Red flags** | Contradictions with CV, patterns of concern |
17
17
  | **Interviewer feedback** | Direct quotes or observations from interviewers |
18
18
  | **Level signals** | Autonomy, scope, complexity demonstrated in responses |
19
19
  | **Track signals** | Business immersion vs. platform thinking in their answers |
20
20
 
21
21
  ## Skill re-rating
22
22
 
23
- **Interview evidence outranks CV evidence.** A candidate who claimed
24
- `practitioner` but demonstrated `foundational` should be re-rated. The reverse
25
- also applies. Every re-rating cites a specific moment, quote, or observation
26
- from the transcript — "seemed strong" is not evidence.
23
+ **Interview evidence outranks CV evidence.** Re-rate a candidate who claimed
24
+ `practitioner` but demonstrated `foundational`. The reverse also applies. Every
25
+ re-rating cites a specific moment, quote, or observation from the transcript.
26
+ The phrase "seemed strong" is not evidence.
27
27
 
28
28
  | Interview evidence | Rating adjustment |
29
29
  | ------------------------------------------- | ------------------------------------- |
@@ -36,13 +36,13 @@ from the transcript — "seemed strong" is not evidence.
36
36
 
37
37
  ## Behaviour signals
38
38
 
39
- | Behaviour | Interview evidence |
40
- | -------------------------- | -------------------------------------------------------------- |
41
- | Own the Outcome | Takes responsibility, doesn't deflect, drives to resolution |
42
- | Think in Systems | Considers second-order effects, trade-offs, system boundaries |
43
- | Communicate with Precision | Clear explanations, appropriate detail, listens actively |
44
- | Be Polymath Oriented | Draws on diverse knowledge, makes unexpected connections |
45
- | Don't Lose Your Curiosity | Asks good questions, explores alternatives, admits not knowing |
39
+ | Behaviour | Interview evidence |
40
+ | -------------------------- | ------------------------------------------------------------------------ |
41
+ | Own the Outcome | Takes responsibility, doesn't deflect, drives to resolution |
42
+ | Think in Systems | Considers second-order effects, trade-offs, system boundaries |
43
+ | Communicate with Precision | Clear explanations, appropriate detail, listens actively |
44
+ | Be Polymath Oriented | Draws on diverse knowledge, makes unexpected connections |
45
+ | Don't Lose Your Curiosity | Asks good questions, explores alternatives, admits what they do not know |
46
46
 
47
47
  ## Behaviour maturity scale
48
48
 
@@ -59,7 +59,7 @@ from the transcript — "seemed strong" is not evidence.
59
59
  | Interview signal | Implication |
60
60
  | --------------------------------------------------------- | ------------------------------ |
61
61
  | Needed guidance to structure their approach | Level may be lower than est. |
62
- | Self-directed; sound trade-offs independently | Level estimate confirmed |
62
+ | Self-directed, made sound trade-offs independently | Level estimate confirmed |
63
63
  | Mentored or coached others during the exercise | Level may be higher than est. |
64
64
  | Struggled with complexity appropriate to the target level | Downgrade |
65
65
  | Interviewer explicitly suggested a different level | Strong signal — weight heavily |
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: req-bundle
3
+ description: >
4
+ Split a Workday CV-bundle PDF into per-candidate CV.pdf files under
5
+ 2-Confidential/Candidates/. Reads the PDF outline (one bookmark per candidate) to
6
+ find each candidate's page range, matches it to the requisition roster so
7
+ folder names agree with req-workday, and extracts each CV losslessly. Use
8
+ when the user provides a Workday CV bundle (a large combined resume PDF, often
9
+ split into `_N_of_M` parts) and asks to split, unbundle, or extract individual
10
+ CVs — run this before req-workday imports the roster.
11
+ ---
12
+
13
+ # Workday CV Bundle Split
14
+
15
+ Write tier: `2-Confidential`
16
+ Frontmatter: none (writes CV assets into candidate folders)
17
+
18
+ Split a Workday CV-**bundle** PDF — one combined file holding every applicant's
19
+ attachments — into per-candidate
20
+ `2-Confidential/Candidates/{Clean Name}/CV.pdf`.
21
+
22
+ This is the **PDF companion** to `req-workday`. `req-workday` imports the roster
23
+ from the `.xlsx` (briefs + `CV.md` from embedded resume text); `req-bundle`
24
+ attaches the real binary CV. Run `req-bundle` **first** so the folders it
25
+ creates carry canonical names that `req-workday` then enriches — no duplicate
26
+ folders.
27
+
28
+ **How the split works.** Workday bundles have
29
+ **no per-candidate separator page** and the name printed on a CV often differs
30
+ from the roster name, so text-based splitting is unreliable. The one dependable
31
+ delimiter is the **PDF outline**: exactly one flat bookmark per candidate, its
32
+ destination being that candidate's first page. Candidate *i* = pages
33
+ `[bookmark(i), bookmark(i+1) − 1]`; the last runs to EOF; the leading Table of
34
+ Contents (before the first bookmark) is skipped. Multiple attachments for one
35
+ candidate sit under one bookmark, so the range captures them all.
36
+ `split-bundle.mjs` reads the outline with `pdfjs-dist` (`getOutline` +
37
+ `getPageIndex`) and extracts each range losslessly with `pdf-lib` (`copyPages`).
38
+ Full anatomy: [references/pdf-structure.md](references/pdf-structure.md).
39
+
40
+ ## Trigger
41
+
42
+ - The user provides a Workday CV bundle PDF (a large combined resume export),
43
+ possibly split into `..._1_of_N.pdf … _N_of_N.pdf` parts.
44
+ - The user asks to split, unbundle, or extract individual CVs from a Workday
45
+ export before importing candidates.
46
+
47
+ ## Prerequisites
48
+
49
+ - PDF libraries for the split (`bun`/`node`), installed via the standard guard:
50
+ `bun pm ls pdfjs-dist 2>/dev/null || bun install pdfjs-dist` and
51
+ `bun pm ls pdf-lib 2>/dev/null || bun install pdf-lib`.
52
+ - The `req-workday` parser dependencies, since the roster is produced by
53
+ `parse-workday.mjs` (see [../req-workday/SKILL.md](../req-workday/SKILL.md)):
54
+ `read-excel-file` and `fflate`.
55
+
56
+ ## Inputs
57
+
58
+ - One or more bundle PDF paths (pass **all** parts of a multi-part export).
59
+ - The requisition `.xlsx` (same req as the bundle) — parsed by
60
+ `req-workday`'s `parse-workday.mjs` into the roster JSON this script consumes,
61
+ so folder names are identical to what `req-workday` will create.
62
+
63
+ ## Outputs
64
+
65
+ - `2-Confidential/Candidates/{Clean Name}/CV.pdf` — the candidate's extracted CV
66
+ (created only after the dry-run manifest is reviewed and confirmed).
67
+ - A JSON manifest at `$HOME/.cache/fit/outpost/state/req-bundle-{Req ID}.json`
68
+ — the reviewable mapping of every bookmark (page range, matched name, status).
69
+ - Never overwrites an existing `CV.pdf`; a second CV for the same person is
70
+ staged as `CV-workday.pdf` / `CV-dup-N.pdf` and flagged for review.
71
+
72
+ <do_confirm_checklist goal="Verify the split is complete, correctly named, and non-destructive">
73
+
74
+ - [ ] Bundle page/bookmark counts sanity-checked; all parts of a multi-part
75
+ export passed together.
76
+ - [ ] Roster parsed from the matching `.xlsx`; bookmark count reconciled against
77
+ roster count.
78
+ - [ ] Dry-run manifest reviewed **before** any write; every `unmatched` bookmark
79
+ resolved to a candidate (or explicitly left staged).
80
+ - [ ] Folder names equal the roster `cleanName` (annotation stripped) so they
81
+ match what `req-workday` produces.
82
+ - [ ] Collisions (duplicate applications) and pre-existing `CV.pdf` files
83
+ handled without overwrite (`CV-dup-N.pdf` / `CV-workday.pdf`), each
84
+ flagged.
85
+ - [ ] After commit, output CV count reconciles with matched bookmarks; a few
86
+ PDFs spot-opened to confirm the right, complete candidate.
87
+
88
+ </do_confirm_checklist>
89
+
90
+ ## Procedure
91
+
92
+ ### 1. Set up
93
+
94
+ Confirm the bundle PDF path(s) and the matching `.xlsx` (glob on the req number,
95
+ not the timestamp — parts can carry different timestamps). Resolve `$HOME`
96
+ (never pass a literal `~` to write tools). Ensure dependencies:
97
+
98
+ ```bash
99
+ bun pm ls pdfjs-dist 2>/dev/null || bun install pdfjs-dist
100
+ bun pm ls pdf-lib 2>/dev/null || bun install pdf-lib
101
+ ```
102
+
103
+ ### 2. Parse the roster
104
+
105
+ Produce the canonical candidate list with `req-workday`'s parser (the single
106
+ source of truth for `cleanName`s), saving the JSON for the split step:
107
+
108
+ ```bash
109
+ node .claude/skills/req-workday/scripts/parse-workday.mjs "<path-to-requisition.xlsx>" --summary
110
+ node .claude/skills/req-workday/scripts/parse-workday.mjs "<path-to-requisition.xlsx>" \
111
+ > "$HOME/.cache/fit/outpost/state/req-bundle-roster-{Req ID}.json"
112
+ ```
113
+
114
+ ### 3. Dry-run the split
115
+
116
+ Compute the mapping and manifest **without writing** any PDF. Pass every part of
117
+ a multi-part bundle in one invocation:
118
+
119
+ ```bash
120
+ node .claude/skills/req-bundle/scripts/split-bundle.mjs \
121
+ "<bundle_1_of_N.pdf>" "<bundle_2_of_N.pdf>" ... \
122
+ --roster "$HOME/.cache/fit/outpost/state/req-bundle-roster-{Req ID}.json" \
123
+ --candidates-dir 2-Confidential/Candidates \
124
+ --manifest "$HOME/.cache/fit/outpost/state/req-bundle-{Req ID}.json" \
125
+ --dry-run
126
+ ```
127
+
128
+ The script prints a summary (bookmarks, matched, unmatched, collisions,
129
+ existing-CV, roster-without-bookmark) and writes the full manifest. Matching
130
+ rules and edge cases: [references/matching.md](references/matching.md).
131
+
132
+ ### 4. Reconcile and resolve
133
+
134
+ Review the manifest. Confirm `bookmarks_total` reconciles with the roster count
135
+ (expect a near-exact match). For each `unmatched` bookmark, use its `hints`
136
+ (nearest roster names by shared tokens) to identify the candidate; note the
137
+ correct `cleanName`. Investigate any `roster_unmatched_names` (applicants with
138
+ no CV in the bundle — usually withdrawn/no-attachment). Do not proceed while
139
+ unexpected mismatches remain unexplained.
140
+
141
+ ### 5. Confirm, then commit
142
+
143
+ Summarize for the user: N CVs to write, M unmatched, K collisions/existing.
144
+ **Get explicit confirmation** before writing. Re-run without `--dry-run` (same
145
+ roster JSON) to create folders and write the CVs:
146
+
147
+ ```bash
148
+ node .claude/skills/req-bundle/scripts/split-bundle.mjs \
149
+ "<bundle parts...>" \
150
+ --roster "$HOME/.cache/fit/outpost/state/req-bundle-roster-{Req ID}.json" \
151
+ --candidates-dir 2-Confidential/Candidates \
152
+ --manifest "$HOME/.cache/fit/outpost/state/req-bundle-{Req ID}.json"
153
+ ```
154
+
155
+ For bookmarks that were `unmatched` but you resolved by hand in Step 4, move the
156
+ staged file into the correct `2-Confidential/Candidates/{Clean Name}/CV.pdf`, or
157
+ add the missing name variant and re-run — never hand-edit PDFs.
158
+
159
+ ### 6. Hand off and report
160
+
161
+ Point `req-workday` at the same `.xlsx` to build briefs/`CV.md` and link the CV
162
+ under each brief's `## CV` section. Flag every candidate that now has a `CV.pdf`
163
+ for `req-screen`. Report `Split {matched}/{bookmarks_total} CVs for {Req ID}`,
164
+ and list any unmatched bookmarks or roster applicants without a CV.
@@ -0,0 +1,70 @@
1
+ # Matching bookmarks to the roster, and edge cases
2
+
3
+ How `split-bundle.mjs` maps each PDF bookmark to a canonical candidate folder,
4
+ and how it handles duplicates, existing files, and misses. The goal: folder
5
+ names identical to what `req-workday` produces, and **never** a destructive
6
+ overwrite.
7
+
8
+ ## Why match at all
9
+
10
+ The bookmark title is a clean name, but folder names must equal the roster
11
+ `cleanName` — the applicant name from the `.xlsx` `Candidates` sheet with any
12
+ trailing annotation (`(Internal)`, `(Prior Worker)`, …) stripped. That
13
+ `cleanName` comes **directly from `req-workday`'s `parse-workday.mjs`** (the
14
+ single source of truth this skill consumes as JSON), so the two skills agree by
15
+ construction and never create duplicate folders. So: bookmark title → (match) →
16
+ roster `cleanName` → folder.
17
+
18
+ ## Normalization (both sides)
19
+
20
+ Before comparing, each name is folded with `normalize()`:
21
+
22
+ 1. Remove parentheticals — `(ΝΙΚΟΣ ΠΑΠΑΔΟΠΟΥΛΟΣ)`, `(Internal)`, `(Referral)`.
23
+ 2. Remove bare annotation tokens — internal / referral / prior worker /
24
+ external.
25
+ 3. Strip diacritics (NFKD) — `Geraño` → `gerano`.
26
+ 4. Lowercase, keep `[a-z0-9 ]`, collapse whitespace.
27
+
28
+ ## Match methods (conservative, deterministic)
29
+
30
+ Applied in order; the method is recorded in the manifest:
31
+
32
+ 1. **`exact`** — normalized strings equal.
33
+ 2. **`token-set`** — same set of tokens regardless of order (handles
34
+ "First Last" ↔ "Last First" and dropped middle names).
35
+ 3. **`unmatched`** — no confident match. The script does **not** guess by
36
+ subset/Levenshtein (that risks merging two different "David …"s). Instead it
37
+ attaches `hints`: the nearest roster names by shared-token count, for a human
38
+ to resolve.
39
+
40
+ ## Edge cases & how the script handles them
41
+
42
+ - **Duplicate applications (collisions).** The same person appears under two
43
+ bookmarks (e.g. across two parts of a multi-part bundle). Both resolve to one
44
+ folder. The first writes `CV.pdf`; the next becomes `CV-dup-2.pdf` (then
45
+ `-3`, …) with status `collision`. Review whether it's a true duplicate (keep
46
+ one) or a distinct newer packet (rename intentionally). Never silently
47
+ overwrite.
48
+ - **Pre-existing `CV.pdf`.** A candidate who already arrived via email
49
+ (`req-track`) may already have `CV.pdf`. The script writes `CV-workday.pdf`
50
+ with status `existing-cv` and flags it, so the richer existing artifact is
51
+ preserved. Decide per candidate which to keep.
52
+ - **Unmatched bookmark.** Staged in a folder named from the sanitized bookmark
53
+ title, status `unmatched`, with `hints`. Resolve by identifying the right
54
+ `cleanName` and moving the staged `CV.pdf` into that folder, or by adding the
55
+ missing name variant and re-running. A genuinely stray bookmark (someone not
56
+ on the roster) is worth flagging to the user.
57
+ - **Roster applicant with no bookmark** (`roster_unmatched_names`). Applied but
58
+ no CV in the bundle — usually withdrawn or never attached a resume. Expected;
59
+ just report it. Not an error.
60
+ - **No outline** (`no-outline` status for a file). The PDF has no bookmarks —
61
+ it is not a standard Workday bundle, or the outline was stripped. Do not
62
+ attempt a blind page-count split; stop and tell the user.
63
+
64
+ ## Reconciliation checklist
65
+
66
+ - `bookmarks_total` ≈ roster count (small gaps explained by
67
+ `roster_unmatched_names`).
68
+ - `unmatched_bookmarks` == 0 after Step 4 resolution (or each is explained).
69
+ - `matched` + resolved unmatched == CVs you expect to write.
70
+ - Known repeat applicants line up with the reported `collisions`.
@@ -0,0 +1,74 @@
1
+ # Workday CV Bundle — PDF anatomy
2
+
3
+ What a Workday "Download resumes / attachments" bundle looks like, and why the
4
+ PDF outline is the only reliable way to split it.
5
+
6
+ ## The shape of a bundle
7
+
8
+ - A bundle is **one PDF that concatenates every applicant's attachments** — CV,
9
+ and often a cover letter and certificates too — back to back.
10
+ - **Front matter:** page 1 (and sometimes 2–3) is a whole-bundle **Table of
11
+ Contents** titled with the req, e.g. `4951493 - Principal Software Engineer`,
12
+ listing `Candidate Name | Candidate ID | Attachments`. The TOC has **no page
13
+ numbers** in its extracted text, so it is not a usable split key — skip it.
14
+ - **No per-candidate separator/cover page.** Each candidate's first attachment
15
+ begins directly on its own page. The TOC is the only inserted page.
16
+
17
+ ## The delimiter: the PDF outline (bookmarks)
18
+
19
+ Every bundle carries a **flat, depth-0 PDF outline with exactly one bookmark per
20
+ candidate**. The bookmark's destination is that candidate's **first page**, and
21
+ its title is the **clean candidate name** (no `(Internal)` / `(Referral)`
22
+ suffix). This is the split key:
23
+
24
+ - Candidate *i* → pages `[bookmark(i).page, bookmark(i+1).page − 1]`.
25
+ - Last candidate → `[bookmark(last).page, EOF]`.
26
+ - A candidate's **multiple attachments are all under their single bookmark**, so
27
+ the page range captures the cover letter + CV + certs together. (This is fine
28
+ — a hiring manager wants the whole packet; `req-screen` reads the CV within.)
29
+ - Bookmark counts match roster counts exactly (verified live: 75↔75 on a
30
+ single-part export; 366↔366 across the four parts of a large export).
31
+
32
+ `split-bundle.mjs` reads this via `pdfjs-dist`: `doc.getOutline()` for the
33
+ items, then `doc.getPageIndex(dest[0])` (resolving named destinations through
34
+ `doc.getDestination()` first) to turn each bookmark into a 0-based page index,
35
+ then sorts by page. Page ranges are extracted losslessly with `pdf-lib`
36
+ (`copyPages`).
37
+
38
+ ## What NOT to rely on
39
+
40
+ - **Separator-page detection** — there are none.
41
+ - **Name text at the top of a page** — a CV's printed name is the candidate's
42
+ own styling and often differs from the roster name (bookmark "Vivian Chen" →
43
+ CV reads "Wei-Ling (Vivian) Chen, PhD"). Match the **bookmark title**, not
44
+ page text.
45
+ - **Page-text extraction** — unnecessary and unreliable for boundary detection:
46
+ the outline gives exact boundaries and the `.xlsx` already carries the resume
47
+ text.
48
+
49
+ ## Multi-part bundles (`_1_of_N` … `_N_of_N`)
50
+
51
+ When a req has many applicants, Workday emits several PDFs. Key facts:
52
+
53
+ - **Each part is self-contained:** its own TOC, its own ~100-candidate subset in
54
+ its own internal order (parts are *not* sequential slices of one ordering).
55
+ - **No candidate spans a part boundary** — every part's first bookmark is a
56
+ fresh candidate after its TOC, and its last candidate is fully contained.
57
+ - Therefore **process each part independently** and union the results; pass all
58
+ parts to `split-bundle.mjs` in one call.
59
+ - A person may legitimately appear in two parts (a genuine duplicate
60
+ application). That surfaces as a **collision** in the manifest (same folder
61
+ targeted twice) — handled per [matching.md](matching.md), not stitched.
62
+
63
+ ## Observed reference numbers (live exports)
64
+
65
+ | Bundle | Pages | Bookmarks |
66
+ | ---------------------------------------- | ----- | --------- |
67
+ | Principal SWE req (single file) | 215 | 75 |
68
+ | Sr. Director req `_1_of_4` | 321 | 100 |
69
+ | Sr. Director req `_2_of_4` | 314 | 100 |
70
+ | Sr. Director req `_3_of_4` | 278 | 100 |
71
+ | Sr. Director req `_4_of_4` | 213 | 66 |
72
+
73
+ Note: one part's filename carried a slightly different timestamp than its
74
+ siblings — glob on the req number, not the timestamp.