@forwardimpact/basecamp 2.3.1 → 2.4.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 CHANGED
@@ -165,6 +165,25 @@ vi ~/.fit/basecamp/scheduler.json
165
165
  { "type": "once", "runAt": "2025-02-12T10:00:00Z" }
166
166
  ```
167
167
 
168
+ ## Updating
169
+
170
+ When you upgrade Basecamp (install a new `.pkg`), the installer automatically
171
+ runs `--update` on all configured knowledge bases. This pushes the latest
172
+ `CLAUDE.md`, skills, and agents into each KB without touching your data.
173
+
174
+ You can also run it manually at any time:
175
+
176
+ ```bash
177
+ # Update all configured knowledge bases
178
+ /Applications/Basecamp.app/Contents/MacOS/fit-basecamp --update
179
+
180
+ # Update a specific knowledge base
181
+ /Applications/Basecamp.app/Contents/MacOS/fit-basecamp --update ~/Documents/Personal
182
+ ```
183
+
184
+ The update merges `.claude/settings.json` non-destructively — new entries are
185
+ added but your existing permissions are preserved.
186
+
168
187
  ## CLI Reference
169
188
 
170
189
  ```
@@ -172,6 +191,7 @@ fit-basecamp Run due tasks once and exit
172
191
  fit-basecamp --daemon Run continuously (poll every 60s)
173
192
  fit-basecamp --run <task> Run a specific task immediately
174
193
  fit-basecamp --init <path> Initialize a new knowledge base
194
+ fit-basecamp --update [path] Update KB skills, agents, and CLAUDE.md
175
195
  fit-basecamp --status Show knowledge bases and task status
176
196
  fit-basecamp --validate Validate agents and skills exist
177
197
  fit-basecamp --help Show this help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/basecamp",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "Claude Code-native personal knowledge system with autonomous agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,5 +28,8 @@
28
28
  ],
29
29
  "engines": {
30
30
  "node": ">=18.0.0"
31
+ },
32
+ "dependencies": {
33
+ "xlsx": "^0.18.5"
31
34
  }
32
35
  }
@@ -0,0 +1,341 @@
1
+ ---
2
+ name: workday-requisition
3
+ description: >
4
+ Import candidates from a Workday requisition export (.xlsx) into
5
+ knowledge/Candidates/. Parses requisition metadata and candidate data,
6
+ creates candidate briefs and CV.md files from resume text, and integrates
7
+ with the existing track-candidates pipeline. Use when the user provides a
8
+ Workday export file or asks to import candidates from an XLSX requisition
9
+ export.
10
+ ---
11
+
12
+ # Workday Requisition Import
13
+
14
+ Import candidates from a Workday requisition export (`.xlsx`) into
15
+ `knowledge/Candidates/`. Extracts requisition metadata and candidate profiles,
16
+ creates standardized candidate briefs and `CV.md` files from the embedded resume
17
+ text, and integrates with the existing `track-candidates` pipeline format.
18
+
19
+ ## Trigger
20
+
21
+ Run this skill:
22
+
23
+ - When the user provides a Workday requisition export file (`.xlsx`)
24
+ - When the user asks to import candidates from Workday or an XLSX export
25
+ - When the user mentions a requisition ID and asks to process the export
26
+
27
+ ## Prerequisites
28
+
29
+ - A Workday requisition export file (`.xlsx`) accessible on the filesystem
30
+ (typically in `~/Downloads/`)
31
+ - The `xlsx` npm package installed in the KB root:
32
+ ```bash
33
+ npm install xlsx
34
+ ```
35
+ - User identity configured in `USER.md`
36
+
37
+ ## Inputs
38
+
39
+ - Path to the `.xlsx` file (e.g.
40
+ `~/Downloads/4951493_Principal_Software_Engineer_–_Forward_Deployed_(Open).xlsx`)
41
+
42
+ ## Outputs
43
+
44
+ - `knowledge/Candidates/{Full Name}/brief.md` — candidate profile note
45
+ - `knowledge/Candidates/{Full Name}/CV.md` — resume text rendered as markdown
46
+ - Updated existing candidate briefs if candidate already exists
47
+
48
+ ---
49
+
50
+ ## Workday Export Format
51
+
52
+ The Workday requisition export contains multiple sheets. This skill uses:
53
+
54
+ ### Sheet 1 — Requisition Metadata
55
+
56
+ Key-value pairs, one per row:
57
+
58
+ | Row | Field | Example |
59
+ | --- | --------------------- | -------------------------------------- |
60
+ | 1 | Title header | `4951493 Principal Software Engineer…` |
61
+ | 2 | Recruiting Start Date | `02/10/2026` |
62
+ | 3 | Target Hire Date | `02/10/2026` |
63
+ | 4 | Primary Location | `USA - NY - Headquarters` |
64
+ | 5 | Hiring Manager Title | `Hiring Manager` |
65
+ | 6 | Hiring Manager | Name |
66
+ | 7 | Recruiter Title | `Recruiter` |
67
+ | 8 | Recruiter | Name |
68
+
69
+ ### Sheet 3 — Candidates
70
+
71
+ Row 3 contains column headers. Data rows start at row 4. After the last
72
+ candidate, stage-summary rows appear (these are not candidates).
73
+
74
+ | Column | Field | Maps to brief field… |
75
+ | ------ | ---------------------- | ------------------------ |
76
+ | B | Candidate name | `# {Name}` |
77
+ | C | Stage | Status derivation |
78
+ | D | Step / Disposition | Status derivation |
79
+ | G | Resume filename | Reference only (no file) |
80
+ | H | Date Applied | **First seen** |
81
+ | I | Current Job Title | **Current title**, Title |
82
+ | J | Current Company | **Current title** suffix |
83
+ | K | Source | **Source** |
84
+ | L | Referred by | **Source** suffix |
85
+ | N | Availability Date | **Availability** |
86
+ | O | Visa Requirement | Notes |
87
+ | P | Eligible to Work | Notes |
88
+ | Q | Relocation | Notes |
89
+ | R | Salary Expectations | **Rate** |
90
+ | S | Non-Compete | Notes |
91
+ | T | Candidate Location | **Location** |
92
+ | U | Phone | **Phone** |
93
+ | V | Email | **Email** |
94
+ | W | Total Years Experience | Summary context |
95
+ | X | All Job Titles | Work History context |
96
+ | Y | Companies | Work History context |
97
+ | Z | Degrees | Education |
98
+ | AA | Fields of Study | Education |
99
+ | AB | Language | **English** / Language |
100
+ | AC | Resume Text | `CV.md` content |
101
+
102
+ #### Name Annotations
103
+
104
+ Names may include parenthetical annotations:
105
+
106
+ - `(Prior Worker)` → Internal/External = `External (Prior Worker)`
107
+ - `(Internal)` → Internal/External = `Internal`
108
+ - No annotation + source contains "Internal" → `Internal`
109
+ - Otherwise → `External`
110
+
111
+ ## Before Starting
112
+
113
+ 1. Read `USER.md` to get the user's name, email, and domain.
114
+ 2. Confirm the XLSX file path with the user (or use the provided path).
115
+ 3. Ensure the `xlsx` package is installed:
116
+ ```bash
117
+ npm list xlsx 2>/dev/null || npm install xlsx
118
+ ```
119
+
120
+ ## Step 1: Parse the Export
121
+
122
+ Run the parse script to extract structured data:
123
+
124
+ ```bash
125
+ node .claude/skills/workday-requisition/scripts/parse-workday.mjs "<path-to-xlsx>" --summary
126
+ ```
127
+
128
+ This prints a summary of the requisition and all candidates. Review the output
129
+ to confirm the file parsed correctly and note the total candidate count.
130
+
131
+ For the full JSON output (used in subsequent steps):
132
+
133
+ ```bash
134
+ node .claude/skills/workday-requisition/scripts/parse-workday.mjs "<path-to-xlsx>"
135
+ ```
136
+
137
+ The full output is a JSON object with:
138
+
139
+ - `requisition` — metadata (id, title, location, hiringManager, recruiter)
140
+ - `candidates` — array of candidate objects with all extracted fields
141
+
142
+ ## Step 2: Build Candidate Index
143
+
144
+ Scan existing candidate notes to avoid duplicates:
145
+
146
+ ```bash
147
+ ls -d knowledge/Candidates/*/ 2>/dev/null
148
+ ```
149
+
150
+ For each existing candidate, check if they match any imported candidate by name.
151
+ Use fuzzy matching — the Workday name may differ slightly from an existing note
152
+ (e.g. middle names, accents, spelling variations).
153
+
154
+ ## Step 3: Determine Pipeline Status
155
+
156
+ Map Workday stage and step/disposition to the `track-candidates` pipeline
157
+ status:
158
+
159
+ | Workday Step / Disposition | Pipeline Status |
160
+ | ---------------------------- | ------------------ |
161
+ | `Considered` | `new` |
162
+ | `Manager Resume Screen` | `screening` |
163
+ | `Assessment` | `screening` |
164
+ | `Interview` / `Phone Screen` | `first-interview` |
165
+ | `Second Interview` | `second-interview` |
166
+ | `Reference Check` | `second-interview` |
167
+ | `Offer` | `offer` |
168
+ | `Employment Agreement` | `offer` |
169
+ | `Background Check` | `hired` |
170
+ | `Ready for Hire` | `hired` |
171
+ | `Rejected` / `Declined` | `rejected` |
172
+
173
+ If the step is identical to the stage (e.g. both "Considered"), default to
174
+ `new`.
175
+
176
+ ## Step 4: Create CV.md from Resume Text
177
+
178
+ For each candidate with resume text, create
179
+ `knowledge/Candidates/{Clean Name}/CV.md`:
180
+
181
+ ```markdown
182
+ # {Clean Name} — Resume
183
+
184
+ > Extracted from Workday requisition export {Req ID} on {today's date}.
185
+ > Original file: {Resume filename from column G}
186
+
187
+ ---
188
+
189
+ {Resume text from column AC, preserving original formatting}
190
+ ```
191
+
192
+ **Formatting rules for resume text:**
193
+
194
+ - Preserve paragraph breaks (double newlines)
195
+ - Convert ALL-CAPS section headers to `## Heading` format
196
+ - Preserve bullet points and lists
197
+ - Clean up excessive whitespace but keep structure
198
+ - Do not rewrite or summarize — reproduce faithfully
199
+
200
+ If a candidate has no resume text, skip the CV.md file.
201
+
202
+ ## Step 5: Write Candidate Brief
203
+
204
+ ### For NEW candidates
205
+
206
+ Create the candidate directory and brief:
207
+
208
+ ```bash
209
+ mkdir -p "knowledge/Candidates/{Clean Name}"
210
+ ```
211
+
212
+ Then create `knowledge/Candidates/{Clean Name}/brief.md` using the
213
+ `track-candidates` format:
214
+
215
+ ```markdown
216
+ # {Clean Name}
217
+
218
+ ## Info
219
+ **Title:** {Current Job Title or "—"}
220
+ **Rate:** {Salary Expectations or "—"}
221
+ **Availability:** {Availability Date or "—"}
222
+ **English:** {Language field or "—"}
223
+ **Location:** {Candidate Location or "—"}
224
+ **Gender:** —
225
+ **Source:** {Source} {via Referred by, if present}
226
+ **Status:** {pipeline status from Step 3}
227
+ **First seen:** {Date Applied, YYYY-MM-DD}
228
+ **Last activity:** {Date Applied, YYYY-MM-DD}
229
+ **Req:** {Req ID} — {Req Title}
230
+ **Internal/External:** {Internal / External / External (Prior Worker)}
231
+ **Current title:** {Current Job Title at Current Company}
232
+ **Email:** {Email or "—"}
233
+ **Phone:** {Phone or "—"}
234
+
235
+ ## Summary
236
+ {2-3 sentences based on resume text: role focus, years of experience, key
237
+ strengths. If no resume text, use Current Job Title + Total Years Experience.}
238
+
239
+ ## CV
240
+ - [CV.md](./CV.md)
241
+
242
+ ## Connected to
243
+ - {Referred by person, if present}
244
+
245
+ ## Pipeline
246
+ - **{Date Applied}**: Applied via {Source}
247
+
248
+ ## Skills
249
+ {Extract key technical skills from resume text — use framework IDs where
250
+ possible via `npx fit-pathway skill --list`}
251
+
252
+ ## Education
253
+ {Degrees and Fields of Study from the export columns}
254
+
255
+ ## Work History
256
+ {All Job Titles and Companies from the export columns, formatted as a list}
257
+
258
+ ## Notes
259
+ {Include any noteworthy fields here:}
260
+ {- Visa requirement (if present)}
261
+ {- Eligible to work (if present)}
262
+ {- Relocation willingness (if present)}
263
+ {- Non-compete status (if present)}
264
+ {- Total years of experience}
265
+ ```
266
+
267
+ **Extra fields** (after Last activity, in order): Req, Internal/External,
268
+ Current title, Email, Phone, LinkedIn — include only when available. Follow the
269
+ order defined in the `track-candidates` skill.
270
+
271
+ ### For EXISTING candidates
272
+
273
+ Read `knowledge/Candidates/{Name}/brief.md`, then apply targeted edits:
274
+
275
+ - Add or update **Req** field with this requisition's ID
276
+ - Update **Status** if the Workday stage is more advanced
277
+ - Update **Last activity** date if this application is more recent
278
+ - Add a new **Pipeline** entry:
279
+ `**{Date Applied}**: Applied to {Req ID} — {Req Title} via {Source}`
280
+ - Update any missing fields (Email, Phone, Location) from the export
281
+ - Do NOT overwrite existing richer data with sparser Workday data
282
+
283
+ **Use precise edits — don't rewrite the entire file.**
284
+
285
+ ## Step 6: Process in Batches
286
+
287
+ Workday exports can contain many candidates. Process in batches of **10
288
+ candidates per run** to stay within context limits.
289
+
290
+ For each batch:
291
+
292
+ 1. Parse the JSON output (or re-run the parse script)
293
+ 2. Process 10 candidates: create/update brief + CV.md
294
+ 3. Report progress: `Processed {N}/{Total} candidates`
295
+
296
+ If the export has more than 10 candidates, tell the user how many remain and
297
+ offer to continue.
298
+
299
+ ## Step 7: Capture Key Insights
300
+
301
+ After processing all candidates, review the batch for strategic observations and
302
+ add them to `knowledge/Candidates/Insights.md`:
303
+
304
+ - Candidates who stand out as strong matches
305
+ - Candidates better suited for a different role
306
+ - Notable patterns (source quality, experience distribution, skill gaps)
307
+
308
+ Follow the `track-candidates` Insights format: one bullet per insight under
309
+ `## Placement Notes` with `[[Candidates/Name/brief|Name]]` links.
310
+
311
+ ## Step 8: Tag Skills with Framework IDs
312
+
313
+ When resume text mentions technical skills, map them to the engineering
314
+ framework:
315
+
316
+ ```bash
317
+ npx fit-pathway skill --list
318
+ ```
319
+
320
+ Use framework skill IDs in the **Skills** section of each brief. If a candidate
321
+ has a CV.md, flag them for the `analyze-cv` skill for a full framework-aligned
322
+ assessment.
323
+
324
+ ## Quality Checklist
325
+
326
+ - [ ] XLSX parsed correctly — verify candidate count matches summary
327
+ - [ ] Requisition metadata extracted (ID, title, hiring manager, recruiter)
328
+ - [ ] Each candidate has a directory under `knowledge/Candidates/{Clean Name}/`
329
+ - [ ] CV.md created for every candidate with resume text
330
+ - [ ] CV.md faithfully reproduces resume text (no rewriting or summarizing)
331
+ - [ ] Brief follows `track-candidates` format exactly
332
+ - [ ] Info fields in standard order (Title → Rate → Availability → English →
333
+ Location → Gender → Source → Status → First seen → Last activity → extras)
334
+ - [ ] Pipeline status correctly mapped from Workday stage/step
335
+ - [ ] Internal/External correctly derived from name annotations and source
336
+ - [ ] Name annotations stripped from directory names and headings
337
+ - [ ] Existing candidates updated (not duplicated) with precise edits
338
+ - [ ] Skills tagged using framework skill IDs where possible
339
+ - [ ] Gender field set to `—` (Workday exports don't include gender signals)
340
+ - [ ] Insights.md updated with strategic observations
341
+ - [ ] No duplicate candidate directories created
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Parse a Workday requisition export (.xlsx) and output structured JSON.
4
+ *
5
+ * Reads Sheet1 for requisition metadata and the "Candidates" sheet for
6
+ * candidate data. Outputs a JSON object to stdout with:
7
+ * - requisition: { id, title, startDate, targetHireDate, location,
8
+ * hiringManager, recruiter }
9
+ * - candidates: [ { name, cleanName, stage, step, resumeFile, dateApplied,
10
+ * currentTitle, currentCompany, source, referredBy,
11
+ * availabilityDate, visaRequirement, eligibleToWork,
12
+ * relocation, salaryExpectations, nonCompete, location,
13
+ * phone, email, totalYearsExperience, allJobTitles,
14
+ * companies, degrees, fieldsOfStudy, language,
15
+ * resumeText, internalExternal } ]
16
+ *
17
+ * Usage:
18
+ * node scripts/parse-workday.mjs <path-to-xlsx>
19
+ * node scripts/parse-workday.mjs <path-to-xlsx> --summary
20
+ * node scripts/parse-workday.mjs -h|--help
21
+ *
22
+ * Requires: npm install xlsx
23
+ */
24
+
25
+ import { readFileSync } from "node:fs";
26
+
27
+ if (
28
+ process.argv.includes("-h") ||
29
+ process.argv.includes("--help") ||
30
+ process.argv.length < 3
31
+ ) {
32
+ console.log(`parse-workday — extract candidates from a Workday requisition export
33
+
34
+ Usage:
35
+ node scripts/parse-workday.mjs <path-to-xlsx> Full JSON output
36
+ node scripts/parse-workday.mjs <path-to-xlsx> --summary Name + status only
37
+ node scripts/parse-workday.mjs -h|--help Show this help
38
+
39
+ Output (JSON):
40
+ { requisition: { id, title, ... }, candidates: [ { name, ... }, ... ] }
41
+
42
+ Requires: npm install xlsx`);
43
+ process.exit(process.argv.length < 3 ? 1 : 0);
44
+ }
45
+
46
+ let XLSX;
47
+ try {
48
+ XLSX = await import("xlsx");
49
+ } catch {
50
+ console.error(
51
+ "Error: xlsx package not found. Install it first:\n npm install xlsx",
52
+ );
53
+ process.exit(1);
54
+ }
55
+
56
+ const filePath = process.argv[2];
57
+ const summaryMode = process.argv.includes("--summary");
58
+
59
+ const data = readFileSync(filePath);
60
+ const wb = XLSX.read(data, { type: "buffer", cellDates: true });
61
+
62
+ // --- Sheet 1: Requisition metadata ---
63
+
64
+ const ws1 = wb.Sheets[wb.SheetNames[0]];
65
+ const sheet1Rows = XLSX.utils.sheet_to_json(ws1, { header: 1, defval: "" });
66
+
67
+ /** Extract the requisition ID and title from the header row. */
68
+ function parseReqHeader(headerText) {
69
+ // Format: "4951493 Principal Software Engineer – Forward Deployed: 4951493 ..."
70
+ const text = String(headerText).split(":")[0].trim();
71
+ const match = text.match(/^(\d+)\s+(.+)$/);
72
+ if (match) return { id: match[1], title: match[2] };
73
+ return { id: "", title: text };
74
+ }
75
+
76
+ /** Build a key-value map from Sheet1 rows (column A = label, column B = value). */
77
+ function buildReqMetadata(rows) {
78
+ const meta = {};
79
+ for (const row of rows) {
80
+ const key = String(row[0] || "").trim();
81
+ const val = String(row[1] || "").trim();
82
+ if (key && val) meta[key] = val;
83
+ }
84
+ return meta;
85
+ }
86
+
87
+ const reqHeader = parseReqHeader(sheet1Rows[0]?.[0] || "");
88
+ const reqMeta = buildReqMetadata(sheet1Rows.slice(1));
89
+
90
+ /** Clean a metadata date string (e.g. "02/10/2026 - 22 days ago" → "2026-02-10"). */
91
+ function cleanMetaDate(val) {
92
+ if (!val) return "";
93
+ const clean = val.replace(/\s*-\s*\d+\s+days?\s+ago$/i, "").trim();
94
+ // Convert MM/DD/YYYY → YYYY-MM-DD
95
+ const match = clean.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
96
+ if (match) return `${match[3]}-${match[1]}-${match[2]}`;
97
+ return clean;
98
+ }
99
+
100
+ const requisition = {
101
+ id: reqHeader.id,
102
+ title: reqHeader.title,
103
+ startDate: cleanMetaDate(reqMeta["Recruiting Start Date"]),
104
+ targetHireDate: cleanMetaDate(reqMeta["Target Hire Date"]),
105
+ location: reqMeta["Primary Location"] || "",
106
+ hiringManager: reqMeta["Hiring Manager"] || "",
107
+ recruiter: reqMeta["Recruiter"] || "",
108
+ };
109
+
110
+ // --- Sheet 3: Candidates ---
111
+
112
+ // Find the "Candidates" sheet (usually index 2, but search by name to be safe)
113
+ const candSheetName =
114
+ wb.SheetNames.find((n) => n.toLowerCase() === "candidates") ||
115
+ wb.SheetNames[2];
116
+ const ws3 = wb.Sheets[candSheetName];
117
+ const candRows = XLSX.utils.sheet_to_json(ws3, { header: 1, defval: "" });
118
+
119
+ // Row 3 (index 2) has column headers. Data starts at row 4 (index 3).
120
+ // Stage summary rows start when column A has a non-empty value that looks like
121
+ // a label or number — detect by checking if column C (Stage) is empty and
122
+ // column A has a value.
123
+ const DATA_START = 3;
124
+
125
+ /**
126
+ * Clean a candidate name by stripping annotations like (Prior Worker),
127
+ * (Internal), etc. Returns { cleanName, internalExternal }.
128
+ */
129
+ function parseName(raw) {
130
+ const name = String(raw).trim();
131
+ if (!name) return { cleanName: "", internalExternal: "" };
132
+
133
+ const match = name.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
134
+ if (match) {
135
+ const annotation = match[2].trim();
136
+ let ie = "";
137
+ if (/prior\s*worker/i.test(annotation)) ie = "External (Prior Worker)";
138
+ else if (/internal/i.test(annotation)) ie = "Internal";
139
+ else ie = annotation;
140
+ return { cleanName: match[1].trim(), internalExternal: ie };
141
+ }
142
+ return { cleanName: name, internalExternal: "" };
143
+ }
144
+
145
+ /** Detect source-based internal/external when name annotation is absent. */
146
+ function inferInternalExternal(source, nameAnnotation) {
147
+ if (nameAnnotation) return nameAnnotation;
148
+ if (/internal/i.test(source)) return "Internal";
149
+ return "External";
150
+ }
151
+
152
+ /** Format a date value (may be Date object or string). */
153
+ function fmtDate(val) {
154
+ if (!val) return "";
155
+ if (val instanceof Date) {
156
+ // Use local date parts to avoid UTC offset shifting the day
157
+ const y = val.getFullYear();
158
+ const m = String(val.getMonth() + 1).padStart(2, "0");
159
+ const d = String(val.getDate()).padStart(2, "0");
160
+ return `${y}-${m}-${d}`;
161
+ }
162
+ const s = String(val).trim();
163
+ // Strip trailing " 00:00:00" and relative text like " - 22 days ago"
164
+ return s
165
+ .replace(/\s+\d{2}:\d{2}:\d{2}$/, "")
166
+ .replace(/\s*-\s*\d+\s+days?\s+ago$/i, "");
167
+ }
168
+
169
+ /** Normalise multiline cell values into clean lists. */
170
+ function multiline(val) {
171
+ if (!val) return "";
172
+ return String(val)
173
+ .split("\n")
174
+ .map((l) => l.trim())
175
+ .filter(Boolean)
176
+ .join(", ");
177
+ }
178
+
179
+ const candidates = [];
180
+
181
+ for (let i = DATA_START; i < candRows.length; i++) {
182
+ const row = candRows[i];
183
+ const rawName = String(row[1] || "").trim(); // Column B (index 1)
184
+ const stage = String(row[2] || "").trim(); // Column C (index 2)
185
+
186
+ // Stop at stage-summary rows: column A has a value, column C (stage) is empty
187
+ if (!rawName || (!stage && String(row[0] || "").trim())) break;
188
+ if (!rawName) continue;
189
+
190
+ const { cleanName, internalExternal: nameIE } = parseName(rawName);
191
+ const source = String(row[10] || "").trim();
192
+
193
+ candidates.push({
194
+ name: rawName,
195
+ cleanName,
196
+ stage,
197
+ step: String(row[3] || "").trim(),
198
+ awaitingMe: String(row[4] || "").trim(),
199
+ awaitingAction: String(row[5] || "").trim(),
200
+ resumeFile: String(row[6] || "").trim(),
201
+ dateApplied: fmtDate(row[7]),
202
+ currentTitle: String(row[8] || "").trim(),
203
+ currentCompany: String(row[9] || "").trim(),
204
+ source,
205
+ referredBy: String(row[11] || "").trim(),
206
+ availabilityDate: fmtDate(row[13]),
207
+ visaRequirement: String(row[14] || "").trim(),
208
+ eligibleToWork: String(row[15] || "").trim(),
209
+ relocation: String(row[16] || "").trim(),
210
+ salaryExpectations: String(row[17] || "").trim(),
211
+ nonCompete: String(row[18] || "").trim(),
212
+ location: String(row[19] || "").trim(),
213
+ phone: String(row[20] || "").trim(),
214
+ email: String(row[21] || "").trim(),
215
+ totalYearsExperience: String(row[22] || "").trim(),
216
+ allJobTitles: multiline(row[23]),
217
+ companies: multiline(row[24]),
218
+ degrees: multiline(row[25]),
219
+ fieldsOfStudy: multiline(row[26]),
220
+ language: multiline(row[27]),
221
+ resumeText: String(row[28] || "").trim(),
222
+ internalExternal: inferInternalExternal(source, nameIE),
223
+ });
224
+ }
225
+
226
+ // --- Output ---
227
+
228
+ if (summaryMode) {
229
+ console.log(`Requisition: ${requisition.id} — ${requisition.title}`);
230
+ console.log(`Location: ${requisition.location}`);
231
+ console.log(`Hiring Manager: ${requisition.hiringManager}`);
232
+ console.log(`Recruiter: ${requisition.recruiter}`);
233
+ console.log(`Candidates: ${candidates.length}`);
234
+ console.log();
235
+ for (const c of candidates) {
236
+ const resume = c.resumeText ? "has resume" : "no resume";
237
+ console.log(
238
+ ` ${c.cleanName} — ${c.step || c.stage} (${c.internalExternal}, ${resume})`,
239
+ );
240
+ }
241
+ } else {
242
+ console.log(JSON.stringify({ requisition, candidates }, null, 2));
243
+ }
@@ -81,13 +81,13 @@ This knowledge base is maintained by a team of agents, each defined in
81
81
  `.claude/agents/`. They are woken on a schedule by the Basecamp scheduler. Each
82
82
  wake, they observe KB state, decide the most valuable action, and execute.
83
83
 
84
- | Agent | Domain | Schedule | Skills |
85
- | ------------------ | ------------------------------ | --------------- | -------------------------------------------------------------- |
86
- | **postman** | Email triage and drafts | Every 5 min | sync-apple-mail, draft-emails |
87
- | **concierge** | Meeting prep and transcripts | Every 10 min | sync-apple-calendar, meeting-prep, process-hyprnote |
88
- | **librarian** | Knowledge graph maintenance | Every 15 min | extract-entities, organize-files, manage-tasks |
89
- | **recruiter** | Engineering recruitment | Every 30 min | track-candidates, analyze-cv, right-to-be-forgotten, fit-pathway, fit-map |
90
- | **chief-of-staff** | Daily briefings and priorities | 7am, Mon 7:30am | weekly-update _(Mon)_, _(reads all state for daily briefings)_ |
84
+ | Agent | Domain | Schedule | Skills |
85
+ | ------------------ | ------------------------------ | --------------- | ---------------------------------------------------------------------------------------------- |
86
+ | **postman** | Email triage and drafts | Every 5 min | sync-apple-mail, draft-emails |
87
+ | **concierge** | Meeting prep and transcripts | Every 10 min | sync-apple-calendar, meeting-prep, process-hyprnote |
88
+ | **librarian** | Knowledge graph maintenance | Every 15 min | extract-entities, organize-files, manage-tasks |
89
+ | **recruiter** | Engineering recruitment | Every 30 min | track-candidates, analyze-cv, workday-requisition, right-to-be-forgotten, fit-pathway, fit-map |
90
+ | **chief-of-staff** | Daily briefings and priorities | 7am, Mon 7:30am | weekly-update _(Mon)_, _(reads all state for daily briefings)_ |
91
91
 
92
92
  Each agent writes a triage file to `~/.cache/fit/basecamp/state/` every wake
93
93
  cycle. The naming convention is `{agent}_triage.md`:
@@ -200,16 +200,17 @@ Available skills (grouped by function):
200
200
 
201
201
  **Knowledge graph** — build and maintain structured notes:
202
202
 
203
- | Skill | Purpose |
204
- | ------------------ | ---------------------------------------- |
205
- | `extract-entities` | Process synced data into knowledge notes |
206
- | `manage-tasks` | Per-person task boards with lifecycle |
207
- | `track-candidates` | Recruitment pipeline from email threads |
208
- | `analyze-cv` | CV assessment against career framework |
209
- | `right-to-be-forgotten` | GDPR data erasure with audit trail |
210
- | `weekly-update` | Weekly priorities from tasks + calendar |
211
- | `process-hyprnote` | Extract entities from Hyprnote sessions |
212
- | `organize-files` | Tidy Desktop/Downloads, chain to extract |
203
+ | Skill | Purpose |
204
+ | ----------------------- | ---------------------------------------- |
205
+ | `extract-entities` | Process synced data into knowledge notes |
206
+ | `manage-tasks` | Per-person task boards with lifecycle |
207
+ | `track-candidates` | Recruitment pipeline from email threads |
208
+ | `workday-requisition` | Import candidates from Workday XLSX |
209
+ | `analyze-cv` | CV assessment against career framework |
210
+ | `right-to-be-forgotten` | GDPR data erasure with audit trail |
211
+ | `weekly-update` | Weekly priorities from tasks + calendar |
212
+ | `process-hyprnote` | Extract entities from Hyprnote sessions |
213
+ | `organize-files` | Tidy Desktop/Downloads, chain to extract |
213
214
 
214
215
  **Communication** — draft, send, and present:
215
216