@forwardimpact/outpost 3.12.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +6 -5
- package/src/agent-path.js +10 -6
- package/src/agent-runner.js +28 -24
- package/src/index.js +2 -3
- package/src/kb-manager.js +75 -20
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +108 -39
- package/src/posture.js +3 -3
- package/src/privilege.js +11 -11
- package/src/scheduler.js +14 -12
- package/src/socket-server.js +19 -18
- package/src/spawn-env.js +7 -6
- package/src/state-manager.js +2 -2
- package/templates/.claude/agents/chief-of-staff.md +32 -24
- package/templates/.claude/agents/concierge.md +26 -17
- package/templates/.claude/agents/head-hunter.md +34 -26
- package/templates/.claude/agents/librarian.md +26 -18
- package/templates/.claude/agents/postman.md +26 -18
- package/templates/.claude/agents/recruiter.md +38 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
- package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
- package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
- package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
- package/templates/.claude/skills/candidate-report/references/report.css +4 -4
- package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
- package/templates/.claude/skills/changelog/SKILL.md +122 -67
- package/templates/.claude/skills/deck-create/SKILL.md +90 -32
- package/templates/.claude/skills/deck-create/references/slide.css +4 -4
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/deck-review/SKILL.md +63 -58
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
- package/templates/.claude/skills/doc-create/SKILL.md +27 -23
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
- package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +37 -13
- package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
- package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
- package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
- package/templates/.claude/skills/organize-files/SKILL.md +27 -22
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
- package/templates/.claude/skills/person-identify/SKILL.md +77 -27
- package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
- package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
- package/templates/.claude/skills/req-assess/SKILL.md +44 -36
- package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
- package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
- package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
- package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
- package/templates/.claude/skills/req-decide/SKILL.md +52 -41
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +58 -39
- package/templates/.claude/skills/req-forget/references/classify.md +15 -13
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
- package/templates/.claude/skills/req-scan/SKILL.md +27 -25
- package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
- package/templates/.claude/skills/req-scan/references/filters.md +13 -13
- package/templates/.claude/skills/req-scan/references/sources.md +7 -7
- package/templates/.claude/skills/req-scan/references/state.md +1 -1
- package/templates/.claude/skills/req-scan/references/template.md +11 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +96 -50
- package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +29 -17
- package/templates/.claude/skills/req-track/SKILL.md +76 -76
- package/templates/.claude/skills/req-track/references/fields.md +31 -27
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- package/templates/.claude/skills/req-track/references/signals.md +12 -12
- package/templates/.claude/skills/req-track/references/statuses.md +3 -2
- package/templates/.claude/skills/req-track/references/templates.md +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +69 -40
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +11 -78
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
- package/templates/.claude/skills/send-chat/SKILL.md +58 -54
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
- package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
- package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
- package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
- package/templates/CLAUDE.md +97 -85
- package/templates/MIGRATION.md +359 -0
- package/templates/registry.yaml +25 -0
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
- package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -183
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Parse a Workday requisition export (.xlsx) and output structured JSON.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* candidate data.
|
|
5
|
+
* The script reads Sheet1 for requisition metadata. It reads the "Candidates"
|
|
6
|
+
* sheet for candidate data. It outputs a JSON object to stdout with:
|
|
7
7
|
* - requisition: { id, title, startDate, targetHireDate, location,
|
|
8
8
|
* hiringManager, recruiter }
|
|
9
9
|
* - candidates: [ { name, cleanName, stage, step, resumeFile, dateApplied,
|
|
@@ -54,20 +54,56 @@ try {
|
|
|
54
54
|
const filePath = process.argv[2];
|
|
55
55
|
const summaryMode = process.argv.includes("--summary");
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Read a workbook. Tolerate Workday's ZIP container.
|
|
59
|
+
*
|
|
60
|
+
* Workday exports .xlsx via Apache POI's streaming writer. That writer sets
|
|
61
|
+
* the ZIP "data descriptor" flag (bit 0x08) on every entry, so the sizes live
|
|
62
|
+
* in a trailer after each entry, not in the local header. read-excel-file's
|
|
63
|
+
* streaming unzipper trusts the (zeroed) local-header sizes, loses alignment,
|
|
64
|
+
* and throws `invalid signature: 0x…`. fflate reads the authoritative ZIP
|
|
65
|
+
* central directory, so on failure we re-pack into a clean container (sizes
|
|
66
|
+
* in the local headers, no data descriptors) and retry. Files that already
|
|
67
|
+
* parse take the original path unchanged.
|
|
68
|
+
*/
|
|
69
|
+
async function readWorkbook(path) {
|
|
70
|
+
try {
|
|
71
|
+
return await readXlsxFile(path);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
let fflate;
|
|
74
|
+
try {
|
|
75
|
+
fflate = await import("fflate");
|
|
76
|
+
} catch {
|
|
77
|
+
throw err; // fflate unavailable — surface the original parse error
|
|
78
|
+
}
|
|
79
|
+
const { readFileSync } = await import("node:fs");
|
|
80
|
+
const raw = readFileSync(path);
|
|
81
|
+
const normalized = Buffer.from(
|
|
82
|
+
fflate.zipSync(fflate.unzipSync(new Uint8Array(raw))),
|
|
83
|
+
);
|
|
84
|
+
return await readXlsxFile(normalized);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// read-excel-file v9 returns the whole workbook as [{ sheet, data }] —
|
|
89
|
+
// read it once and index sheets from that.
|
|
90
|
+
const workbook = await readWorkbook(filePath);
|
|
91
|
+
const sheetNames = workbook.map((s) => s.sheet);
|
|
92
|
+
|
|
93
|
+
/** Read a sheet by number (1-indexed) or name. Rows are arrays of strings. */
|
|
94
|
+
function readSheet(_file, sheet) {
|
|
95
|
+
const entry =
|
|
96
|
+
typeof sheet === "number"
|
|
97
|
+
? workbook[sheet - 1]
|
|
98
|
+
: workbook.find((s) => s.sheet === sheet);
|
|
99
|
+
const rows = entry ? entry.data : [];
|
|
60
100
|
// Normalise null cells to empty strings to match previous behaviour
|
|
61
101
|
return rows.map((row) => row.map((cell) => (cell == null ? "" : cell)));
|
|
62
102
|
}
|
|
63
103
|
|
|
64
|
-
// Get sheet names to find the candidates sheet
|
|
65
|
-
const sheets = await readXlsxFile(filePath, { getSheets: true });
|
|
66
|
-
const sheetNames = sheets.map((s) => s.name);
|
|
67
|
-
|
|
68
104
|
// --- Sheet 1: Requisition metadata ---
|
|
69
105
|
|
|
70
|
-
const sheet1Rows =
|
|
106
|
+
const sheet1Rows = readSheet(filePath, 1);
|
|
71
107
|
|
|
72
108
|
/** Extract the requisition ID and title from the header row. */
|
|
73
109
|
function parseReqHeader(headerText) {
|
|
@@ -120,9 +156,9 @@ const requisition = {
|
|
|
120
156
|
const candSheetName =
|
|
121
157
|
sheetNames.find((n) => n.toLowerCase() === "candidates") ||
|
|
122
158
|
sheetNames[Math.min(2, sheetNames.length - 1)];
|
|
123
|
-
const candRows =
|
|
159
|
+
const candRows = readSheet(filePath, candSheetName);
|
|
124
160
|
|
|
125
|
-
// Find the header row dynamically
|
|
161
|
+
// Find the header row dynamically. Look for a row that contains "Stage".
|
|
126
162
|
// Old format: row 3 (index 2). New format: row 8 (index 7).
|
|
127
163
|
let HEADER_ROW = 2;
|
|
128
164
|
for (let i = 0; i < Math.min(15, candRows.length); i++) {
|
|
@@ -134,8 +170,9 @@ for (let i = 0; i < Math.min(15, candRows.length); i++) {
|
|
|
134
170
|
const DATA_START = HEADER_ROW + 1;
|
|
135
171
|
|
|
136
172
|
// --- Build header-driven column index map ---
|
|
137
|
-
// Column layout varies between Workday exports
|
|
138
|
-
// or "Referred by" shift indices
|
|
173
|
+
// Column layout varies between Workday exports. Extra columns like "Jobs
|
|
174
|
+
// Applied to" or "Referred by" shift the indices. Map by header name to be
|
|
175
|
+
// resilient.
|
|
139
176
|
|
|
140
177
|
const headerRow = candRows[HEADER_ROW] || [];
|
|
141
178
|
const colMap = {};
|
|
@@ -174,16 +211,17 @@ for (let i = 0; i < headerRow.length; i++) {
|
|
|
174
211
|
const hdr = String(headerRow[i]).trim().toLowerCase();
|
|
175
212
|
const field = HEADER_ALIASES[hdr];
|
|
176
213
|
if (field) {
|
|
177
|
-
// "Job Application" appears twice (cols A and B)
|
|
178
|
-
// occurrence
|
|
214
|
+
// "Job Application" appears twice (cols A and B). Always take the latest
|
|
215
|
+
// occurrence. We then end up with the second one (index 1), which has the
|
|
216
|
+
// name.
|
|
179
217
|
colMap[field] = i;
|
|
180
218
|
}
|
|
181
219
|
}
|
|
182
220
|
|
|
183
|
-
// Fallback:
|
|
221
|
+
// Fallback: with no "name" column, use index 0 (new format) or 1 (old format)
|
|
184
222
|
if (colMap.name === undefined) colMap.name = 1;
|
|
185
|
-
// In new format there
|
|
186
|
-
// "always take latest" logic already handles this correctly.
|
|
223
|
+
// In the new format there is only one "Job Application" column (index 0).
|
|
224
|
+
// The "always take latest" logic already handles this correctly.
|
|
187
225
|
|
|
188
226
|
/** Get a cell value by field name, with fallback to empty string. */
|
|
189
227
|
function col(row, field) {
|
|
@@ -192,38 +230,78 @@ function col(row, field) {
|
|
|
192
230
|
return row[idx] ?? "";
|
|
193
231
|
}
|
|
194
232
|
|
|
233
|
+
// Any character outside the Latin script blocks (Basic Latin + Latin-1
|
|
234
|
+
// Supplement + Latin Extended-A/B + Latin Extended Additional). Accented Latin
|
|
235
|
+
// (é, ñ, ø, ç, Vietnamese, …) stays; Greek, Cyrillic, CJK, Arabic, Hebrew,
|
|
236
|
+
// etc. count as native-alphabet text to drop.
|
|
237
|
+
const NON_LATIN = /[^\u0020-\u024F\u1E00-\u1EFF]/;
|
|
238
|
+
const HAS_LATIN_LETTER = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/;
|
|
239
|
+
|
|
195
240
|
/**
|
|
196
|
-
* Clean a candidate name
|
|
197
|
-
*
|
|
241
|
+
* Clean a candidate name into the canonical Latin name.
|
|
242
|
+
*
|
|
243
|
+
* Workday encodes several things in the name cell as parentheticals:
|
|
244
|
+
* - Employment annotations: `(Internal)`, `(Prior Worker)`, `(External)`.
|
|
245
|
+
* - A native-alphabet transliteration of the name, e.g.
|
|
246
|
+
* `Nikos Papadopoulos (ΝΙΚΟΣ ΠΑΠΑΔΟΠΟΥΛΟΣ)` or `Wei Zhang (张伟)`.
|
|
247
|
+
* A name may carry both, in either paren style. Normalise on the Latin name:
|
|
248
|
+
* strip every trailing parenthetical (classify employment ones into
|
|
249
|
+
* `internalExternal`, discard native-alphabet ones), then drop any residual
|
|
250
|
+
* non-Latin-script tokens. Returns { cleanName, internalExternal }.
|
|
198
251
|
*/
|
|
252
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: annotation peel loop classifies employment vs transliteration cases
|
|
199
253
|
function parseName(raw) {
|
|
200
|
-
|
|
254
|
+
// Normalise full-width parens () to ASCII so both styles strip uniformly.
|
|
255
|
+
let name = String(raw).replace(/(/g, "(").replace(/)/g, ")").trim();
|
|
201
256
|
if (!name) return { cleanName: "", internalExternal: "" };
|
|
202
257
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
258
|
+
let internalExternal = "";
|
|
259
|
+
// Repeatedly peel a trailing parenthetical group (allowing one level of
|
|
260
|
+
// nesting so `(External (Prior Worker))` is captured whole).
|
|
261
|
+
const trailing = /\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)\s*$/;
|
|
262
|
+
let m;
|
|
263
|
+
while ((m = name.match(trailing)) !== null) {
|
|
264
|
+
const annotation = m[1].trim();
|
|
265
|
+
if (/prior\s*worker/i.test(annotation)) {
|
|
266
|
+
if (!internalExternal) internalExternal = "External (Prior Worker)";
|
|
267
|
+
} else if (/internal/i.test(annotation)) {
|
|
268
|
+
if (!internalExternal) internalExternal = "Internal";
|
|
269
|
+
} else if (/external/i.test(annotation)) {
|
|
270
|
+
if (!internalExternal) internalExternal = "External";
|
|
271
|
+
} else if (NON_LATIN.test(annotation)) {
|
|
272
|
+
// Native-alphabet transliteration of the name — drop, no IE signal.
|
|
273
|
+
} else if (!internalExternal && annotation) {
|
|
274
|
+
// Unknown Latin annotation — preserve prior behaviour (first one wins).
|
|
275
|
+
internalExternal = annotation;
|
|
276
|
+
}
|
|
277
|
+
name = name.slice(0, name.length - m[0].length).trim();
|
|
211
278
|
}
|
|
212
|
-
|
|
279
|
+
|
|
280
|
+
// Drop any residual whole-word tokens that are purely non-Latin script
|
|
281
|
+
// (e.g. a native name appended without parentheses). Keep Latin tokens
|
|
282
|
+
// and pure punctuation/digits.
|
|
283
|
+
name = name
|
|
284
|
+
.split(/\s+/)
|
|
285
|
+
.filter((tok) => HAS_LATIN_LETTER.test(tok) || !NON_LATIN.test(tok))
|
|
286
|
+
.join(" ")
|
|
287
|
+
.replace(/\s{2,}/g, " ")
|
|
288
|
+
.trim();
|
|
289
|
+
|
|
290
|
+
return { cleanName: name, internalExternal };
|
|
213
291
|
}
|
|
214
292
|
|
|
215
|
-
/** Detect source-based internal/external when name annotation is absent. */
|
|
293
|
+
/** Detect source-based internal/external when the name annotation is absent. */
|
|
216
294
|
function inferInternalExternal(source, nameAnnotation) {
|
|
217
295
|
if (nameAnnotation) return nameAnnotation;
|
|
218
296
|
if (/internal/i.test(source)) return "Internal";
|
|
219
297
|
return "External";
|
|
220
298
|
}
|
|
221
299
|
|
|
222
|
-
/** Format a date value (may be Date object or string). */
|
|
300
|
+
/** Format a date value (it may be a Date object or a string). */
|
|
223
301
|
function fmtDate(val) {
|
|
224
302
|
if (!val) return "";
|
|
225
303
|
if (val instanceof Date) {
|
|
226
|
-
// Use local date parts
|
|
304
|
+
// Use local date parts so a UTC offset does not shift the day
|
|
227
305
|
const y = val.getFullYear();
|
|
228
306
|
const m = String(val.getMonth() + 1).padStart(2, "0");
|
|
229
307
|
const d = String(val.getDate()).padStart(2, "0");
|
|
@@ -253,7 +331,7 @@ for (let i = DATA_START; i < candRows.length; i++) {
|
|
|
253
331
|
const rawName = String(col(row, "name") || "").trim();
|
|
254
332
|
const stage = String(col(row, "stage") || "").trim();
|
|
255
333
|
|
|
256
|
-
// Skip empty rows
|
|
334
|
+
// Skip empty rows. Stop at stage-summary rows (name present but no stage)
|
|
257
335
|
if (!rawName) continue;
|
|
258
336
|
if (!stage) break;
|
|
259
337
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: send-chat
|
|
3
|
-
description: Send messages to people
|
|
3
|
+
description: Send messages to people through chat platforms (e.g. Microsoft Teams, Slack) with browser automation. Resolves people by name from the knowledge graph, drafts messages for approval, and sends them through the web app. Use when the user asks to message, ping, or chat with someone.
|
|
4
4
|
compatibility:
|
|
5
5
|
requires:
|
|
6
6
|
- browser-automation
|
|
@@ -8,10 +8,13 @@ compatibility:
|
|
|
8
8
|
|
|
9
9
|
# Send Chat
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
Write tier: `0-Draft` (sending is an export)
|
|
12
|
+
Frontmatter: none
|
|
13
|
+
|
|
14
|
+
Send chat messages to people with browser automation against a web-based chat
|
|
15
|
+
platform (Microsoft Teams, Slack, or similar). This skill resolves recipients by
|
|
16
|
+
name from the knowledge graph. The user can say "message Sarah about the
|
|
17
|
+
standup" without an exact display name.
|
|
15
18
|
|
|
16
19
|
## Trigger
|
|
17
20
|
|
|
@@ -19,61 +22,62 @@ Run when the user asks to:
|
|
|
19
22
|
|
|
20
23
|
- Send a message on Teams / Slack / chat
|
|
21
24
|
- Ping / chat / DM someone
|
|
22
|
-
- Follow up with someone
|
|
25
|
+
- Follow up with someone through chat
|
|
23
26
|
- Send a message about a topic
|
|
24
27
|
|
|
25
28
|
## Prerequisites
|
|
26
29
|
|
|
27
|
-
-
|
|
30
|
+
- The web app for the chat platform, open and authenticated in the browser
|
|
28
31
|
- Browser automation available (e.g. Chrome MCP, Playwright)
|
|
29
32
|
- Knowledge base populated with people notes
|
|
30
33
|
|
|
31
34
|
## Critical: Always Look Up Context First
|
|
32
35
|
|
|
33
|
-
**BEFORE
|
|
36
|
+
**BEFORE you message anyone, you MUST look up the person in the knowledge
|
|
37
|
+
base.**
|
|
34
38
|
|
|
35
39
|
When the user mentions ANY person:
|
|
36
40
|
|
|
37
41
|
1. **STOP** — Do not open the chat platform yet
|
|
38
|
-
2. **SEARCH** — Look them up: `rg -l "{name}"
|
|
42
|
+
2. **SEARCH** — Look them up: `rg -l "{name}" 3-Team/People/`
|
|
39
43
|
3. **READ** — Read their note to understand context, role, recent interactions
|
|
40
|
-
4. **UNDERSTAND** — Know who they are
|
|
44
|
+
4. **UNDERSTAND** — Know who they are and what you work on together
|
|
41
45
|
5. **THEN PROCEED** — Only now compose the message and use browser automation
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
You need this context to:
|
|
44
48
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
49
|
+
- Find the right person if the name is ambiguous
|
|
50
|
+
- Draft an appropriate message if the user gave a loose prompt
|
|
51
|
+
- Know the person's role and relationship for tone
|
|
48
52
|
|
|
49
|
-
##
|
|
53
|
+
## Resolve People
|
|
50
54
|
|
|
51
|
-
The user will refer to people by first name, last name, or nickname. Resolve
|
|
52
|
-
a full name
|
|
55
|
+
The user will refer to people by first name, last name, or nickname. Resolve the
|
|
56
|
+
reference to a full name with the knowledge graph:
|
|
53
57
|
|
|
54
58
|
```bash
|
|
55
59
|
# Find person by partial name
|
|
56
|
-
rg -l -i "{name}"
|
|
60
|
+
rg -l -i "{name}" 3-Team/People/
|
|
57
61
|
|
|
58
62
|
# If ambiguous, read candidates to disambiguate
|
|
59
|
-
cat "
|
|
63
|
+
cat "3-Team/People/{Candidate}.md"
|
|
60
64
|
```
|
|
61
65
|
|
|
62
|
-
**If ambiguous** (multiple matches), ask the user which person they mean
|
|
66
|
+
**If ambiguous** (multiple matches), ask the user which person they mean. List
|
|
63
67
|
the matches with roles/orgs to help them pick.
|
|
64
68
|
|
|
65
|
-
**If no match**, tell the user
|
|
66
|
-
|
|
69
|
+
**If no match**, tell the user this person is not in the knowledge base. Ask for
|
|
70
|
+
their full name as it appears in the chat platform.
|
|
67
71
|
|
|
68
|
-
##
|
|
72
|
+
## Compose the Message
|
|
69
73
|
|
|
70
|
-
**
|
|
71
|
-
can review and edit the exact message before it
|
|
74
|
+
**You MUST draft every message as a text file first.** This makes sure the user
|
|
75
|
+
can review and edit the exact message before you send it.
|
|
72
76
|
|
|
73
77
|
### Draft Workflow
|
|
74
78
|
|
|
75
79
|
1. **Compose the message** based on context and user intent.
|
|
76
|
-
2. **Write it to a draft file** at `
|
|
80
|
+
2. **Write it to a draft file** at `0-Draft/chat-{recipient-slug}-{date}.md`
|
|
77
81
|
- `{recipient-slug}` = lowercase, hyphenated full name (e.g. `sarah-chen`)
|
|
78
82
|
- `{date}` = ISO date (e.g. `2026-02-19`)
|
|
79
83
|
3. **Show the user the draft** — display the file path and contents.
|
|
@@ -92,32 +96,33 @@ Date: {YYYY-MM-DD}
|
|
|
92
96
|
{message body}
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
The message body
|
|
99
|
+
The message body is everything below the `---` separator. Paste that body into
|
|
96
100
|
the chat.
|
|
97
101
|
|
|
98
102
|
**Message guidelines:**
|
|
99
103
|
|
|
100
|
-
- Match the user's usual tone
|
|
101
|
-
|
|
104
|
+
- Match the user's usual tone. Use casual tone for peers and professional tone
|
|
105
|
+
for leadership
|
|
106
|
+
- Keep it concise. Chat is informal. It is not email
|
|
102
107
|
- Reference specific context naturally (project names, recent decisions)
|
|
103
|
-
- If the user
|
|
108
|
+
- If the user gives exact words, use them verbatim
|
|
104
109
|
- If the user said "ping {name}" without detail, ask what they want to say
|
|
105
|
-
- Draft one message based on context
|
|
110
|
+
- Draft one message based on context. Do not offer multiple options
|
|
106
111
|
- **Keep messages on a single line with no formatting.** No line breaks, no
|
|
107
112
|
markdown. Use inline separators (e.g. `•`, `—`) to keep structure. Multi-line
|
|
108
|
-
formatting is unreliable
|
|
113
|
+
formatting is unreliable through browser automation.
|
|
109
114
|
|
|
110
115
|
## Browser Automation Flow
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
After the user approves the draft, send it as a **single submission**. Paste the
|
|
118
|
+
entire message at once. Do not type it line by line.
|
|
114
119
|
|
|
115
120
|
### Step 1: Identify the Chat Platform
|
|
116
121
|
|
|
117
122
|
Check which platform is available:
|
|
118
123
|
|
|
119
|
-
- Look for an open tab
|
|
120
|
-
- If no tab is open, ask the user which platform to use
|
|
124
|
+
- Look for an open tab that matches the configured chat URL
|
|
125
|
+
- If no tab is open, ask the user which platform to use. Then navigate to it
|
|
121
126
|
|
|
122
127
|
### Step 2: Open a Chat with the Recipient
|
|
123
128
|
|
|
@@ -126,7 +131,7 @@ Check which platform is available:
|
|
|
126
131
|
3. Wait for search results to populate (take a screenshot to verify)
|
|
127
132
|
4. Click the correct person from the results
|
|
128
133
|
|
|
129
|
-
If the person
|
|
134
|
+
If the person does not appear in search, tell the user. They may not be in the
|
|
130
135
|
same organization.
|
|
131
136
|
|
|
132
137
|
### Step 3: Send the Approved Message
|
|
@@ -137,34 +142,33 @@ same organization.
|
|
|
137
142
|
4. Press Enter or click Send
|
|
138
143
|
5. Take a screenshot to confirm the message was sent
|
|
139
144
|
|
|
140
|
-
### Step 4:
|
|
141
|
-
|
|
142
|
-
If the message is substantive (not just "hey" or "thanks"), note the interaction
|
|
143
|
-
on the person's knowledge note:
|
|
145
|
+
### Step 4: Keep the Draft as the Record
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
-
|
|
147
|
-
|
|
147
|
+
Do not write the interaction to the person's note. This skill writes only to
|
|
148
|
+
`0-Draft/`. The chat sync (`sync-teams`) and `extract-entities` record the
|
|
149
|
+
interaction when the messages sync.
|
|
148
150
|
|
|
149
151
|
## Error Handling
|
|
150
152
|
|
|
151
153
|
- **Platform not loaded / auth required:** Tell the user to sign in first, then
|
|
152
154
|
retry
|
|
153
|
-
- **Person not found in search:** Report back
|
|
154
|
-
different display name. Ask the user for the exact name
|
|
155
|
+
- **Person not found in search:** Report back. They may be external, or they may
|
|
156
|
+
use a different display name. Ask the user for the exact name
|
|
155
157
|
- **Chat already open:** If a chat with this person is already visible, use it
|
|
156
158
|
directly
|
|
157
|
-
- **UI not as expected:** Take a screenshot and describe what you see.
|
|
159
|
+
- **UI not as expected:** Take a screenshot and describe what you see. Do not
|
|
158
160
|
click blindly
|
|
159
161
|
|
|
160
162
|
## Constraints
|
|
161
163
|
|
|
162
|
-
- **Always confirm before
|
|
163
|
-
approval
|
|
164
|
-
- **One message at a time.**
|
|
165
|
-
|
|
164
|
+
- **Always confirm before you send.** Never send a message without explicit user
|
|
165
|
+
approval. This is a hard requirement
|
|
166
|
+
- **One message at a time.** Do not batch-send to multiple people. Confirm each
|
|
167
|
+
one first
|
|
166
168
|
- **No file attachments.** This skill handles text messages only
|
|
167
|
-
- **No group chats.**
|
|
168
|
-
- **No message deletion or editing.**
|
|
169
|
+
- **No group chats.** This skill targets 1:1 chats only
|
|
170
|
+
- **No message deletion or editing.** After you send a message, you cannot
|
|
171
|
+
delete it or edit it
|
|
169
172
|
- **Respect ethics rules.** Never send messages that contain personal judgments,
|
|
170
|
-
gossip, or sensitive information
|
|
173
|
+
gossip, or sensitive information. The ethics policy of the knowledge base
|
|
174
|
+
requires this
|
|
@@ -6,9 +6,12 @@ compatibility: Requires macOS with Apple Calendar configured and Full Disk Acces
|
|
|
6
6
|
|
|
7
7
|
# Sync Apple Calendar
|
|
8
8
|
|
|
9
|
+
Write tier: none (cache only)
|
|
10
|
+
Frontmatter: none
|
|
11
|
+
|
|
9
12
|
Sync calendar events from the macOS Calendar app's local SQLite database into
|
|
10
|
-
`~/.cache/fit/outpost/apple_calendar/` as JSON files. This is an automated
|
|
11
|
-
|
|
13
|
+
`~/.cache/fit/outpost/apple_calendar/` as JSON files. This is an automated skill
|
|
14
|
+
in the data pipeline. It ingests raw calendar data that other skills (like
|
|
12
15
|
`extract-entities` and `meeting-prep`) consume downstream.
|
|
13
16
|
|
|
14
17
|
## Trigger
|
|
@@ -38,8 +41,8 @@ their calendar.
|
|
|
38
41
|
## Implementation
|
|
39
42
|
|
|
40
43
|
Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
|
|
41
|
-
process invocations (one per event for attendees)
|
|
42
|
-
|
|
44
|
+
process invocations (one per event for attendees). It also transforms all the
|
|
45
|
+
data in one pass:
|
|
43
46
|
|
|
44
47
|
```text
|
|
45
48
|
node scripts/sync.mjs [--days N]
|
|
@@ -60,8 +63,9 @@ The script:
|
|
|
60
63
|
## Database Schema
|
|
61
64
|
|
|
62
65
|
See [references/SCHEMA.md](references/SCHEMA.md) for the complete Apple Calendar
|
|
63
|
-
SQLite schema
|
|
64
|
-
|
|
66
|
+
SQLite schema. It gives the table structures, the column names, and the
|
|
67
|
+
important caveats. For example, Identity uses `address` and has no `email`
|
|
68
|
+
column. Participant has no `display_name`.
|
|
65
69
|
|
|
66
70
|
## Output Format
|
|
67
71
|
|
|
@@ -98,10 +102,10 @@ Each `{event_id}.json` file:
|
|
|
98
102
|
- Database locked → wait 2 seconds, retry once
|
|
99
103
|
- Skip events with no summary (likely cancelled or placeholder)
|
|
100
104
|
|
|
101
|
-
##
|
|
105
|
+
## Query Events
|
|
102
106
|
|
|
103
|
-
After
|
|
104
|
-
**Agents should use this script
|
|
107
|
+
After the sync, use the query script to filter events by date or time window.
|
|
108
|
+
**Agents should use this script. Do not write bespoke calendar parsers.**
|
|
105
109
|
|
|
106
110
|
```text
|
|
107
111
|
node scripts/query.mjs [options]
|
|
@@ -109,13 +113,13 @@ node scripts/query.mjs [options]
|
|
|
109
113
|
|
|
110
114
|
### Time filters (combinable)
|
|
111
115
|
|
|
112
|
-
| Flag | Description
|
|
113
|
-
| ------------------------------- |
|
|
114
|
-
| `--today` | Events
|
|
115
|
-
| `--tomorrow` | Events
|
|
116
|
-
| `--upcoming 2h` | Events
|
|
117
|
-
| `--date 2026-03-09` | Events on a specific date
|
|
118
|
-
| `--range 2026-03-09 2026-03-11` | Events between two dates (inclusive)
|
|
116
|
+
| Flag | Description |
|
|
117
|
+
| ------------------------------- | --------------------------------------------------------------- |
|
|
118
|
+
| `--today` | Events that start today (default if no filter given) |
|
|
119
|
+
| `--tomorrow` | Events that start tomorrow |
|
|
120
|
+
| `--upcoming 2h` | Events that start within the interval (e.g., `2h`, `30m`, `1d`) |
|
|
121
|
+
| `--date 2026-03-09` | Events on a specific date |
|
|
122
|
+
| `--range 2026-03-09 2026-03-11` | Events between two dates (inclusive) |
|
|
119
123
|
|
|
120
124
|
### Output options
|
|
121
125
|
|
|
@@ -144,6 +148,6 @@ node scripts/query.mjs --today --json | node -e "process.stdin.on('data',d=>cons
|
|
|
144
148
|
## Constraints
|
|
145
149
|
|
|
146
150
|
- Open database read-only (`readOnly: true`)
|
|
147
|
-
- This sync is stateless
|
|
148
|
-
- All-day events may have null end times
|
|
149
|
-
- All-day events have timezone `_float
|
|
151
|
+
- This sync is stateless. It always queries the current sliding window
|
|
152
|
+
- All-day events may have null end times. Use the start date as the end date
|
|
153
|
+
- All-day events have timezone `_float`. Omit the timezone from the output
|
|
@@ -19,7 +19,7 @@ column names (verified on macOS Sonoma+).
|
|
|
19
19
|
| `organizer_id` | INTEGER | FK → Identity.ROWID |
|
|
20
20
|
| `calendar_id` | INTEGER | FK → Calendar.ROWID |
|
|
21
21
|
| `has_attendees` | INTEGER | 1 = event has attendees |
|
|
22
|
-
| `conference_url` | TEXT | Video call URL (often null
|
|
22
|
+
| `conference_url` | TEXT | Video call URL (often null). Check description too |
|
|
23
23
|
| `entity_type` | INTEGER | 2 = calendar event |
|
|
24
24
|
|
|
25
25
|
## Identity (organizer info)
|
|
@@ -28,12 +28,12 @@ column names (verified on macOS Sonoma+).
|
|
|
28
28
|
| -------------- | ---- | ------------------------------------------------------------- |
|
|
29
29
|
| `display_name` | TEXT | Full name (e.g., `"Chen, Sarah"`) |
|
|
30
30
|
| `address` | TEXT | Email with `mailto:` prefix (e.g., `"mailto:sarah@acme.com"`) |
|
|
31
|
-
| `first_name` | TEXT | Usually null
|
|
32
|
-
| `last_name` | TEXT | Usually null
|
|
31
|
+
| `first_name` | TEXT | Usually null. `display_name` is the reliable field |
|
|
32
|
+
| `last_name` | TEXT | Usually null. `display_name` is the reliable field |
|
|
33
33
|
|
|
34
34
|
**IMPORTANT:** Identity does NOT have an `email` column. Use `address` and strip
|
|
35
|
-
the `mailto:` prefix. Use `display_name` for the name
|
|
36
|
-
`
|
|
35
|
+
the `mailto:` prefix. Use `display_name` for the name. Do not use `first_name`
|
|
36
|
+
or `last_name`. They are typically null.
|
|
37
37
|
|
|
38
38
|
## Participant (attendees and organizer)
|
|
39
39
|
|
|
@@ -49,8 +49,8 @@ the `mailto:` prefix. Use `display_name` for the name (not
|
|
|
49
49
|
| `is_self` | INTEGER | 1 = this is the calendar owner |
|
|
50
50
|
|
|
51
51
|
**IMPORTANT:** Participant does NOT have `display_name`, `first_name`, or
|
|
52
|
-
`last_name` columns. To get the attendee's name, JOIN with Identity
|
|
53
|
-
`identity_id`. There is NO `Attendee` table
|
|
52
|
+
`last_name` columns. To get the attendee's name, JOIN with Identity through
|
|
53
|
+
`identity_id`. There is NO `Attendee` table. Use `Participant` only.
|
|
54
54
|
|
|
55
55
|
### EKParticipantStatus mapping
|
|
56
56
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Query synced Apple Calendar events by date/time window.
|
|
4
4
|
*
|
|
5
5
|
* Reads JSON event files from ~/.cache/fit/outpost/apple_calendar/ and filters
|
|
6
|
-
* them by date range, time window, or upcoming interval.
|
|
7
|
-
*
|
|
6
|
+
* them by date range, time window, or upcoming interval. Agents then do not
|
|
7
|
+
* need to write their own calendar-parsing scripts.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* node scripts/query.mjs --today Today's events
|
|
@@ -31,9 +31,9 @@ Usage:
|
|
|
31
31
|
node scripts/query.mjs [options]
|
|
32
32
|
|
|
33
33
|
Time filters (combinable):
|
|
34
|
-
--today Events
|
|
35
|
-
--tomorrow Events
|
|
36
|
-
--upcoming <interval> Events
|
|
34
|
+
--today Events that start today
|
|
35
|
+
--tomorrow Events that start tomorrow
|
|
36
|
+
--upcoming <interval> Events that start within interval (e.g., 2h, 30m, 1d)
|
|
37
37
|
--date <YYYY-MM-DD> Events on a specific date
|
|
38
38
|
--range <start> <end> Events between two dates (inclusive)
|
|
39
39
|
|
|
@@ -42,7 +42,7 @@ Output options:
|
|
|
42
42
|
--include-all-day Include all-day events (excluded by default)
|
|
43
43
|
--no-attendees Omit attendee names from output
|
|
44
44
|
|
|
45
|
-
Defaults to --today
|
|
45
|
+
Defaults to --today when you give no time filter.`);
|
|
46
46
|
process.exit(0);
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Sync Apple Calendar events to ~/.cache/fit/outpost/apple_calendar/ as JSON.
|
|
4
4
|
*
|
|
5
|
-
* Queries the macOS Calendar SQLite database (
|
|
6
|
-
* sliding window
|
|
7
|
-
* JSON file per event
|
|
8
|
-
*
|
|
5
|
+
* Queries the macOS Calendar SQLite database (through node:sqlite) for events
|
|
6
|
+
* in a sliding window. The window runs from N days in the past to 14 days in
|
|
7
|
+
* the future. Writes one JSON file per event. Removes the files for events
|
|
8
|
+
* outside the window. Fetches attendee details (name, email, status, role) in
|
|
9
|
+
* one batch and includes them.
|
|
9
10
|
*
|
|
10
11
|
* Requires macOS with Calendar app configured and Full Disk Access granted.
|
|
11
12
|
*/
|
|
@@ -74,7 +75,7 @@ function findDb() {
|
|
|
74
75
|
const db = DB_PATHS.find((p) => existsSync(p));
|
|
75
76
|
if (!db) {
|
|
76
77
|
console.error(
|
|
77
|
-
"Error: Apple Calendar database not found.
|
|
78
|
+
"Error: Apple Calendar database not found. Check that Calendar is configured.",
|
|
78
79
|
);
|
|
79
80
|
process.exit(1);
|
|
80
81
|
}
|
|
@@ -82,7 +83,7 @@ function findDb() {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
|
-
* Open the database in read-only mode
|
|
86
|
+
* Open the database in read-only mode. Retry once if the database is locked.
|
|
86
87
|
* @param {string} dbPath
|
|
87
88
|
* @returns {import("node:sqlite").DatabaseSync}
|
|
88
89
|
*/
|
|
@@ -114,7 +115,7 @@ function query(db, sql) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
* Convert Core Data timestamp to ISO 8601.
|
|
118
|
+
* Convert a Core Data timestamp to ISO 8601.
|
|
118
119
|
* @param {number | null} ts - Seconds since 2001-01-01
|
|
119
120
|
* @param {string | null} tzName
|
|
120
121
|
* @returns {string | null}
|
|
@@ -138,7 +139,7 @@ function coredataToIso(ts, tzName) {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/**
|
|
141
|
-
* Get UTC offset string for a timezone at a given instant.
|
|
142
|
+
* Get the UTC offset string for a timezone at a given instant.
|
|
142
143
|
* @param {Date} dt
|
|
143
144
|
* @param {string} tzName
|
|
144
145
|
* @returns {string} e.g. "+02:00" or "-05:00"
|