@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
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * Parse a Workday requisition export (.xlsx) and output structured JSON.
4
4
  *
5
- * Reads Sheet1 for requisition metadata and the "Candidates" sheet for
6
- * candidate data. Outputs a JSON object to stdout with:
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
- /** Read a sheet by number (1-indexed) or name, returning rows as arrays of strings. */
58
- async function readSheet(file, sheet) {
59
- const rows = await readXlsxFile(file, { sheet });
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 = await readSheet(filePath, 1);
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 = await readSheet(filePath, candSheetName);
159
+ const candRows = readSheet(filePath, candSheetName);
124
160
 
125
- // Find the header row dynamically look for a row containing "Stage"
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 (extra columns like "Jobs Applied to"
138
- // or "Referred by" shift indices). Map by header name to be resilient.
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) always take the latest
178
- // occurrence so we end up with the second one (index 1) which has the name
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: if "name" wasn't mapped, use index 0 (new format) or 1 (old format)
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's only one "Job Application" column (index 0) — the
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 by stripping annotations like (Prior Worker),
197
- * (Internal), etc. Returns { cleanName, internalExternal }.
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
- const name = String(raw).trim();
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
- const match = name.match(/^(.+?)\s*\(([^)]+)\)\s*$/);
204
- if (match) {
205
- const annotation = match[2].trim();
206
- let ie;
207
- if (/prior\s*worker/i.test(annotation)) ie = "External (Prior Worker)";
208
- else if (/internal/i.test(annotation)) ie = "Internal";
209
- else ie = annotation;
210
- return { cleanName: match[1].trim(), internalExternal: ie };
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
- return { cleanName: name, internalExternal: "" };
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 to avoid UTC offset shifting the day
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; stop at stage-summary rows (name present but no stage)
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 via chat platforms (e.g. Microsoft Teams, Slack) using browser automation. Resolves people by name using the knowledge graph, drafts messages for approval, and sends via the web app. Use when the user asks to message, ping, or chat with someone.
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
- Send chat messages to people using browser automation against a web-based chat
12
- platform (Microsoft Teams, Slack, or similar). Resolves recipients by name from
13
- the knowledge graph so the user can say "message Sarah about the standup"
14
- without needing exact display names.
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 via chat
25
+ - Follow up with someone through chat
23
26
  - Send a message about a topic
24
27
 
25
28
  ## Prerequisites
26
29
 
27
- - Chat platform web app open and authenticated in the browser
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 messaging anyone, you MUST look up the person in the knowledge base.**
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}" Knowledge/People/`
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, what you've been working on together
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
- This context is essential for:
47
+ You need this context to:
44
48
 
45
- - Finding the right person if the name is ambiguous
46
- - Drafting an appropriate message if the user gave a loose prompt
47
- - Knowing the person's role and relationship for tone
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
- ## Resolving People
53
+ ## Resolve People
50
54
 
51
- The user will refer to people by first name, last name, or nickname. Resolve to
52
- a full name using the knowledge graph:
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}" Knowledge/People/
60
+ rg -l -i "{name}" 3-Team/People/
57
61
 
58
62
  # If ambiguous, read candidates to disambiguate
59
- cat "Knowledge/People/{Candidate}.md"
63
+ cat "3-Team/People/{Candidate}.md"
60
64
  ```
61
65
 
62
- **If ambiguous** (multiple matches), ask the user which person they mean — list
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 you don't have this person in the knowledge base
66
- and ask for their full name as it appears in the chat platform.
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
- ## Composing the Message
72
+ ## Compose the Message
69
73
 
70
- **Every message MUST be drafted as a text file first.** This ensures the user
71
- can review and edit the exact message before it's sent.
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 `Drafts/chat-{recipient-slug}-{date}.md`
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 (everything below the `---` separator) is what gets pasted into
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 casual for peers, professional for leadership
101
- - Keep it concise — chat is informal, not email
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 provides exact wording, use it verbatim
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 don't offer multiple options
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 via browser automation.
113
+ formatting is unreliable through browser automation.
109
114
 
110
115
  ## Browser Automation Flow
111
116
 
112
- Once the user has approved the draft, send it as a **single submission** paste
113
- the entire message at once rather than typing line by line.
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 matching the configured chat URL
120
- - If no tab is open, ask the user which platform to use and navigate to it
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 doesn't appear in search, inform the user they may not be in the
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: Update Knowledge Graph (Optional)
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
- ```markdown
146
- - {YYYY-MM-DD}: Messaged on {Platform} re: {topic}
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 they may be external or using a
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. Don't
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 sending.** Never send a message without explicit user
163
- approval this is a hard requirement
164
- - **One message at a time.** Don't batch-send to multiple people without
165
- confirming each one
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.** Targets 1:1 chats only
168
- - **No message deletion or editing.** Once sent, it's sent
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 per the knowledge base ethics policy
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 data
11
- pipeline skill it ingests raw calendar data that other skills (like
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) and handles all data
42
- transformation in one pass:
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 including table structures, column names, and important caveats
64
- (e.g., Identity uses `address` not `email`, Participant has no `display_name`).
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
- ## Querying Events
105
+ ## Query Events
102
106
 
103
- After syncing, use the query script to filter events by date or time window.
104
- **Agents should use this script instead of writing bespoke calendar parsers.**
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 starting today (default if no filter given) |
115
- | `--tomorrow` | Events starting tomorrow |
116
- | `--upcoming 2h` | Events starting within interval (e.g., `2h`, `30m`, `1d`) |
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 always queries the current sliding window
148
- - All-day events may have null end times use start date as end date
149
- - All-day events have timezone `_float` omit timezone from output
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 check description too) |
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 `display_name` is the reliable field |
32
- | `last_name` | TEXT | Usually null `display_name` is the reliable field |
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 (not
36
- `first_name`/`last_name`, which are typically null).
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 via
53
- `identity_id`. There is NO `Attendee` table only use `Participant`.
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. Designed to eliminate
7
- * the need for agents to write bespoke calendar-parsing scripts.
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 starting today
35
- --tomorrow Events starting tomorrow
36
- --upcoming <interval> Events starting within interval (e.g., 2h, 30m, 1d)
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 if no time filter is specified.`);
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 (via node:sqlite) for events in a
6
- * sliding window N days in the past through 14 days in the future. Writes one
7
- * JSON file per event and removes files for events that fall outside the window.
8
- * Attendee details (name, email, status, role) are batch-fetched and included.
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. Is Calendar configured?",
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 with retry on lock.
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"