@forwardimpact/outpost 3.11.0 → 3.12.1

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 (114) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +4 -4
  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 +21 -19
  8. package/src/outpost.js +37 -33
  9. package/src/posture.js +3 -3
  10. package/src/privilege.js +11 -11
  11. package/src/scheduler.js +14 -12
  12. package/src/socket-server.js +19 -18
  13. package/src/spawn-env.js +7 -6
  14. package/src/state-manager.js +2 -2
  15. package/templates/.claude/agents/chief-of-staff.md +22 -22
  16. package/templates/.claude/agents/concierge.md +19 -17
  17. package/templates/.claude/agents/head-hunter.md +27 -26
  18. package/templates/.claude/agents/librarian.md +17 -16
  19. package/templates/.claude/agents/postman.md +19 -18
  20. package/templates/.claude/agents/recruiter.md +31 -27
  21. package/templates/.claude/skills/anarlog-follow/SKILL.md +50 -49
  22. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  23. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +4 -4
  24. package/templates/.claude/skills/anarlog-process/SKILL.md +35 -32
  25. package/templates/.claude/skills/anarlog-process/references/extraction.md +26 -22
  26. package/templates/.claude/skills/anarlog-process/references/sessions.md +10 -10
  27. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +10 -9
  28. package/templates/.claude/skills/anarlog-trim/SKILL.md +43 -40
  29. package/templates/.claude/skills/candidate-report/SKILL.md +30 -28
  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 +55 -50
  35. package/templates/.claude/skills/deck-create/SKILL.md +30 -30
  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 +59 -57
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +49 -43
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +4 -3
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +25 -25
  44. package/templates/.claude/skills/doc-create/SKILL.md +21 -20
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +37 -33
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +5 -4
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +11 -9
  50. package/templates/.claude/skills/extract-entities/SKILL.md +65 -65
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +5 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +14 -13
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +5 -5
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +19 -18
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +13 -13
  57. package/templates/.claude/skills/extract-entities/references/sources.md +8 -8
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +2 -2
  59. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +2 -2
  60. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  61. package/templates/.claude/skills/meeting-prep/SKILL.md +25 -24
  62. package/templates/.claude/skills/organize-files/SKILL.md +24 -22
  63. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  64. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  65. package/templates/.claude/skills/person-identify/SKILL.md +24 -22
  66. package/templates/.claude/skills/person-identify/scripts/identify.sh +17 -17
  67. package/templates/.claude/skills/person-lookup/SKILL.md +28 -29
  68. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +19 -16
  69. package/templates/.claude/skills/req-assess/SKILL.md +27 -27
  70. package/templates/.claude/skills/req-assess/references/interview-template.md +1 -1
  71. package/templates/.claude/skills/req-assess/references/panel-template.md +3 -2
  72. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  73. package/templates/.claude/skills/req-decide/SKILL.md +28 -25
  74. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  75. package/templates/.claude/skills/req-forget/SKILL.md +28 -26
  76. package/templates/.claude/skills/req-forget/references/classify.md +13 -13
  77. package/templates/.claude/skills/req-forget/references/locations.md +2 -2
  78. package/templates/.claude/skills/req-forget/references/report-template.md +1 -1
  79. package/templates/.claude/skills/req-scan/SKILL.md +21 -22
  80. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  81. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  82. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  83. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  84. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  85. package/templates/.claude/skills/req-screen/SKILL.md +31 -29
  86. package/templates/.claude/skills/req-screen/references/rubric.md +18 -18
  87. package/templates/.claude/skills/req-screen/references/template.md +4 -4
  88. package/templates/.claude/skills/req-track/SKILL.md +40 -40
  89. package/templates/.claude/skills/req-track/references/fields.md +19 -18
  90. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  91. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  92. package/templates/.claude/skills/req-track/references/templates.md +4 -4
  93. package/templates/.claude/skills/req-workday/SKILL.md +24 -24
  94. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  95. package/templates/.claude/skills/req-workday/references/templates.md +13 -13
  96. package/templates/.claude/skills/req-workday/references/xlsx-format.md +7 -7
  97. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +19 -17
  98. package/templates/.claude/skills/send-chat/SKILL.md +47 -43
  99. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +20 -19
  100. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  101. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  102. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  103. package/templates/.claude/skills/sync-apple-mail/SKILL.md +17 -14
  104. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  105. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  106. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  107. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  108. package/templates/.claude/skills/sync-teams/SKILL.md +43 -43
  109. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  110. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  111. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  112. package/templates/.claude/skills/upstream-instructions/SKILL.md +39 -40
  113. package/templates/.claude/skills/upstream-instructions/references/examples.md +2 -2
  114. package/templates/CLAUDE.md +40 -37
@@ -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,7 +54,7 @@ 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. */
57
+ /** Read a sheet by number (1-indexed) or name. Rows are arrays of strings. */
58
58
  async function readSheet(file, sheet) {
59
59
  const rows = await readXlsxFile(file, { sheet });
60
60
  // Normalise null cells to empty strings to match previous behaviour
@@ -122,7 +122,7 @@ const candSheetName =
122
122
  sheetNames[Math.min(2, sheetNames.length - 1)];
123
123
  const candRows = await readSheet(filePath, candSheetName);
124
124
 
125
- // Find the header row dynamically look for a row containing "Stage"
125
+ // Find the header row dynamically. Look for a row that contains "Stage".
126
126
  // Old format: row 3 (index 2). New format: row 8 (index 7).
127
127
  let HEADER_ROW = 2;
128
128
  for (let i = 0; i < Math.min(15, candRows.length); i++) {
@@ -134,8 +134,9 @@ for (let i = 0; i < Math.min(15, candRows.length); i++) {
134
134
  const DATA_START = HEADER_ROW + 1;
135
135
 
136
136
  // --- 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.
137
+ // Column layout varies between Workday exports. Extra columns like "Jobs
138
+ // Applied to" or "Referred by" shift the indices. Map by header name to be
139
+ // resilient.
139
140
 
140
141
  const headerRow = candRows[HEADER_ROW] || [];
141
142
  const colMap = {};
@@ -174,16 +175,17 @@ for (let i = 0; i < headerRow.length; i++) {
174
175
  const hdr = String(headerRow[i]).trim().toLowerCase();
175
176
  const field = HEADER_ALIASES[hdr];
176
177
  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
178
+ // "Job Application" appears twice (cols A and B). Always take the latest
179
+ // occurrence. We then end up with the second one (index 1), which has the
180
+ // name.
179
181
  colMap[field] = i;
180
182
  }
181
183
  }
182
184
 
183
- // Fallback: if "name" wasn't mapped, use index 0 (new format) or 1 (old format)
185
+ // Fallback: with no "name" column, use index 0 (new format) or 1 (old format)
184
186
  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.
187
+ // In the new format there is only one "Job Application" column (index 0).
188
+ // The "always take latest" logic already handles this correctly.
187
189
 
188
190
  /** Get a cell value by field name, with fallback to empty string. */
189
191
  function col(row, field) {
@@ -193,8 +195,8 @@ function col(row, field) {
193
195
  }
194
196
 
195
197
  /**
196
- * Clean a candidate name by stripping annotations like (Prior Worker),
197
- * (Internal), etc. Returns { cleanName, internalExternal }.
198
+ * Clean a candidate name. Strip annotations like (Prior Worker), (Internal),
199
+ * etc. Returns { cleanName, internalExternal }.
198
200
  */
199
201
  function parseName(raw) {
200
202
  const name = String(raw).trim();
@@ -212,18 +214,18 @@ function parseName(raw) {
212
214
  return { cleanName: name, internalExternal: "" };
213
215
  }
214
216
 
215
- /** Detect source-based internal/external when name annotation is absent. */
217
+ /** Detect source-based internal/external when the name annotation is absent. */
216
218
  function inferInternalExternal(source, nameAnnotation) {
217
219
  if (nameAnnotation) return nameAnnotation;
218
220
  if (/internal/i.test(source)) return "Internal";
219
221
  return "External";
220
222
  }
221
223
 
222
- /** Format a date value (may be Date object or string). */
224
+ /** Format a date value (it may be a Date object or a string). */
223
225
  function fmtDate(val) {
224
226
  if (!val) return "";
225
227
  if (val instanceof Date) {
226
- // Use local date parts to avoid UTC offset shifting the day
228
+ // Use local date parts so a UTC offset does not shift the day
227
229
  const y = val.getFullYear();
228
230
  const m = String(val.getMonth() + 1).padStart(2, "0");
229
231
  const d = String(val.getDate()).padStart(2, "0");
@@ -253,7 +255,7 @@ for (let i = DATA_START; i < candRows.length; i++) {
253
255
  const rawName = String(col(row, "name") || "").trim();
254
256
  const stage = String(col(row, "stage") || "").trim();
255
257
 
256
- // Skip empty rows; stop at stage-summary rows (name present but no stage)
258
+ // Skip empty rows. Stop at stage-summary rows (name present but no stage)
257
259
  if (!rawName) continue;
258
260
  if (!stage) break;
259
261
 
@@ -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,10 @@ 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
+ Send chat messages to people with browser automation against a web-based chat
12
+ platform (Microsoft Teams, Slack, or similar). This skill resolves recipients by
13
+ name from the knowledge graph. The user can say "message Sarah about the
14
+ standup" without an exact display name.
15
15
 
16
16
  ## Trigger
17
17
 
@@ -19,37 +19,38 @@ Run when the user asks to:
19
19
 
20
20
  - Send a message on Teams / Slack / chat
21
21
  - Ping / chat / DM someone
22
- - Follow up with someone via chat
22
+ - Follow up with someone through chat
23
23
  - Send a message about a topic
24
24
 
25
25
  ## Prerequisites
26
26
 
27
- - Chat platform web app open and authenticated in the browser
27
+ - The web app for the chat platform, open and authenticated in the browser
28
28
  - Browser automation available (e.g. Chrome MCP, Playwright)
29
29
  - Knowledge base populated with people notes
30
30
 
31
31
  ## Critical: Always Look Up Context First
32
32
 
33
- **BEFORE messaging anyone, you MUST look up the person in the knowledge base.**
33
+ **BEFORE you message anyone, you MUST look up the person in the knowledge
34
+ base.**
34
35
 
35
36
  When the user mentions ANY person:
36
37
 
37
38
  1. **STOP** — Do not open the chat platform yet
38
39
  2. **SEARCH** — Look them up: `rg -l "{name}" Knowledge/People/`
39
40
  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
41
+ 4. **UNDERSTAND** — Know who they are and what you work on together
41
42
  5. **THEN PROCEED** — Only now compose the message and use browser automation
42
43
 
43
- This context is essential for:
44
+ You need this context to:
44
45
 
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
46
+ - Find the right person if the name is ambiguous
47
+ - Draft an appropriate message if the user gave a loose prompt
48
+ - Know the person's role and relationship for tone
48
49
 
49
- ## Resolving People
50
+ ## Resolve People
50
51
 
51
- The user will refer to people by first name, last name, or nickname. Resolve to
52
- a full name using the knowledge graph:
52
+ The user will refer to people by first name, last name, or nickname. Resolve the
53
+ reference to a full name with the knowledge graph:
53
54
 
54
55
  ```bash
55
56
  # Find person by partial name
@@ -59,16 +60,16 @@ rg -l -i "{name}" Knowledge/People/
59
60
  cat "Knowledge/People/{Candidate}.md"
60
61
  ```
61
62
 
62
- **If ambiguous** (multiple matches), ask the user which person they mean — list
63
+ **If ambiguous** (multiple matches), ask the user which person they mean. List
63
64
  the matches with roles/orgs to help them pick.
64
65
 
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.
66
+ **If no match**, tell the user this person is not in the knowledge base. Ask for
67
+ their full name as it appears in the chat platform.
67
68
 
68
- ## Composing the Message
69
+ ## Compose the Message
69
70
 
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.
71
+ **You MUST draft every message as a text file first.** This makes sure the user
72
+ can review and edit the exact message before you send it.
72
73
 
73
74
  ### Draft Workflow
74
75
 
@@ -92,32 +93,33 @@ Date: {YYYY-MM-DD}
92
93
  {message body}
93
94
  ```
94
95
 
95
- The message body (everything below the `---` separator) is what gets pasted into
96
+ The message body is everything below the `---` separator. Paste that body into
96
97
  the chat.
97
98
 
98
99
  **Message guidelines:**
99
100
 
100
- - Match the user's usual tone casual for peers, professional for leadership
101
- - Keep it concise — chat is informal, not email
101
+ - Match the user's usual tone. Use casual tone for peers and professional tone
102
+ for leadership
103
+ - Keep it concise. Chat is informal. It is not email
102
104
  - Reference specific context naturally (project names, recent decisions)
103
- - If the user provides exact wording, use it verbatim
105
+ - If the user gives exact words, use them verbatim
104
106
  - 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
107
+ - Draft one message based on context. Do not offer multiple options
106
108
  - **Keep messages on a single line with no formatting.** No line breaks, no
107
109
  markdown. Use inline separators (e.g. `•`, `—`) to keep structure. Multi-line
108
- formatting is unreliable via browser automation.
110
+ formatting is unreliable through browser automation.
109
111
 
110
112
  ## Browser Automation Flow
111
113
 
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.
114
+ After the user approves the draft, send it as a **single submission**. Paste the
115
+ entire message at once. Do not type it line by line.
114
116
 
115
117
  ### Step 1: Identify the Chat Platform
116
118
 
117
119
  Check which platform is available:
118
120
 
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
121
+ - Look for an open tab that matches the configured chat URL
122
+ - If no tab is open, ask the user which platform to use. Then navigate to it
121
123
 
122
124
  ### Step 2: Open a Chat with the Recipient
123
125
 
@@ -126,7 +128,7 @@ Check which platform is available:
126
128
  3. Wait for search results to populate (take a screenshot to verify)
127
129
  4. Click the correct person from the results
128
130
 
129
- If the person doesn't appear in search, inform the user they may not be in the
131
+ If the person does not appear in search, tell the user. They may not be in the
130
132
  same organization.
131
133
 
132
134
  ### Step 3: Send the Approved Message
@@ -150,21 +152,23 @@ on the person's knowledge note:
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
@@ -7,8 +7,8 @@ compatibility: Requires macOS with Apple Calendar configured and Full Disk Acces
7
7
  # Sync Apple Calendar
8
8
 
9
9
  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
10
+ `~/.cache/fit/outpost/apple_calendar/` as JSON files. This is an automated skill
11
+ in the data pipeline. It ingests raw calendar data that other skills (like
12
12
  `extract-entities` and `meeting-prep`) consume downstream.
13
13
 
14
14
  ## Trigger
@@ -38,8 +38,8 @@ their calendar.
38
38
  ## Implementation
39
39
 
40
40
  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:
41
+ process invocations (one per event for attendees). It also transforms all the
42
+ data in one pass:
43
43
 
44
44
  ```text
45
45
  node scripts/sync.mjs [--days N]
@@ -60,8 +60,9 @@ The script:
60
60
  ## Database Schema
61
61
 
62
62
  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`).
63
+ SQLite schema. It gives the table structures, the column names, and the
64
+ important caveats. For example, Identity uses `address` and has no `email`
65
+ column. Participant has no `display_name`.
65
66
 
66
67
  ## Output Format
67
68
 
@@ -98,10 +99,10 @@ Each `{event_id}.json` file:
98
99
  - Database locked → wait 2 seconds, retry once
99
100
  - Skip events with no summary (likely cancelled or placeholder)
100
101
 
101
- ## Querying Events
102
+ ## Query Events
102
103
 
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.**
104
+ After the sync, use the query script to filter events by date or time window.
105
+ **Agents should use this script. Do not write bespoke calendar parsers.**
105
106
 
106
107
  ```text
107
108
  node scripts/query.mjs [options]
@@ -109,13 +110,13 @@ node scripts/query.mjs [options]
109
110
 
110
111
  ### Time filters (combinable)
111
112
 
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) |
113
+ | Flag | Description |
114
+ | ------------------------------- | --------------------------------------------------------------- |
115
+ | `--today` | Events that start today (default if no filter given) |
116
+ | `--tomorrow` | Events that start tomorrow |
117
+ | `--upcoming 2h` | Events that start within the interval (e.g., `2h`, `30m`, `1d`) |
118
+ | `--date 2026-03-09` | Events on a specific date |
119
+ | `--range 2026-03-09 2026-03-11` | Events between two dates (inclusive) |
119
120
 
120
121
  ### Output options
121
122
 
@@ -144,6 +145,6 @@ node scripts/query.mjs --today --json | node -e "process.stdin.on('data',d=>cons
144
145
  ## Constraints
145
146
 
146
147
  - 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
148
+ - This sync is stateless. It always queries the current sliding window
149
+ - All-day events may have null end times. Use the start date as the end date
150
+ - 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"
@@ -7,8 +7,8 @@ compatibility: Requires macOS with Apple Mail configured and Full Disk Access gr
7
7
  # Sync Apple Mail
8
8
 
9
9
  Sync email threads from the macOS Mail app's local SQLite database into
10
- `~/.cache/fit/outpost/apple_mail/` as markdown files. This is an automated data
11
- pipeline skill it ingests raw email data that other skills (like
10
+ `~/.cache/fit/outpost/apple_mail/` as markdown files. This is an automated skill
11
+ in the data pipeline. It ingests raw email data that other skills (like
12
12
  `extract-entities`) consume downstream.
13
13
 
14
14
  ## Trigger
@@ -46,7 +46,7 @@ their email.
46
46
  ## Implementation
47
47
 
48
48
  Run the sync as a single Node.js script with embedded SQLite. This avoids N+1
49
- process invocations and handles all data transformation in one pass:
49
+ process invocations. It also transforms all the data in one pass:
50
50
 
51
51
  ```text
52
52
  node scripts/sync.mjs [--days N]
@@ -60,27 +60,29 @@ The script:
60
60
  2. Loads last sync timestamp (or defaults to `--days` days ago for first sync)
61
61
  3. Discovers the thread grouping column (`conversation_id` or `thread_id`)
62
62
  4. Loads last-seen ROWID (or defaults to 0 for first sync)
63
- 5. Finds threads with new messages since last sync (up to 500), using both
64
- timestamp and ROWID to catch late-arriving emails (emails downloaded after a
65
- delay may have `date_received` before the last sync timestamp, but their
66
- ROWID will be higher than the last-seen ROWID)
63
+ 5. Finds threads with new messages since last sync (up to 500). It uses both the
64
+ timestamp and the ROWID to catch emails that arrive late. An email downloaded
65
+ after a delay may have a `date_received` before the last sync timestamp. But
66
+ its ROWID is higher than the last-seen ROWID
67
67
  6. For each thread: fetches messages, batch-fetches recipients and attachment
68
- metadata, parses `.emlx` files for full email bodies (falling back to
68
+ metadata, parses `.emlx` files for full email bodies (with a fallback to
69
69
  database summaries), copies attachment files to the output directory
70
70
  7. Writes one markdown file per thread to `~/.cache/fit/outpost/apple_mail/`
71
71
  8. Updates sync state (timestamp and max ROWID)
72
72
  9. Reports summary (threads processed, files written)
73
73
 
74
74
  The script imports `scripts/parse-emlx.mjs` to extract plain text bodies from
75
- `.emlx` / `.partial.emlx` files (handles HTML-only emails by stripping tags).
75
+ `.emlx` / `.partial.emlx` files. The parser strips the tags from HTML-only
76
+ emails.
76
77
 
77
78
  ## Database Schema
78
79
 
79
80
  See [references/SCHEMA.md](references/SCHEMA.md) for the complete Apple Mail
80
- SQLite schema including table structures, column names, and important caveats
81
- (e.g., `date_received` is Unix timestamps not Core Data, `addresses.comment`
82
- holds display names, `recipients` columns are `message`/`address` not
83
- `message_id`/`address_id`).
81
+ SQLite schema. It gives the table structures, the column names, and the
82
+ important caveats. For example, `date_received` holds Unix timestamps. It does
83
+ not hold Core Data timestamps. `addresses.comment` holds display names. The
84
+ `recipients` columns are `message` and `address`. They are not `message_id` and
85
+ `address_id`.
84
86
 
85
87
  ## Output Format
86
88
 
@@ -139,7 +141,8 @@ Rules:
139
141
  - `.emlx` parse error → fall back to database summary field
140
142
  - HTML-only email → strip tags and use as plain text body (handled by
141
143
  parse-emlx.mjs)
142
- - `find` timeout → skip that message's body, use summary; attachment index empty
144
+ - `find` timeout → skip that message's body and use the summary. The attachment
145
+ index stays empty
143
146
  - Attachment file not found on disk → listed as `*(not available)*` in markdown
144
147
  - Attachment copy fails (permissions, disk full) → listed as `*(not available)*`
145
148
  - Filename collision across messages → prefixed with `{message_id}_`
@@ -25,8 +25,8 @@ Typical path: `~/Library/Mail/V10/MailData/Envelope Index`
25
25
  | `flagged` | INTEGER | 1 = flagged |
26
26
 
27
27
  **IMPORTANT:** `date_received` stores **Unix timestamps** (seconds since
28
- 1970-01-01 UTC), NOT Core Data timestamps (which use 2001-01-01 epoch). Do NOT
29
- apply Core Data conversion.
28
+ 1970-01-01 UTC). It does NOT store Core Data timestamps, which use the
29
+ 2001-01-01 epoch. Do NOT apply Core Data conversion.
30
30
 
31
31
  ## addresses (sender and recipient addresses)
32
32
 
@@ -36,8 +36,8 @@ apply Core Data conversion.
36
36
  | `address` | TEXT | Email address |
37
37
  | `comment` | TEXT | Display name (e.g., `"Chen, Sarah"`) |
38
38
 
39
- **IMPORTANT:** The display name is in `comment`, not a `name` or `display_name`
40
- column.
39
+ **IMPORTANT:** The display name is in `comment`. There is no `name` column and
40
+ no `display_name` column.
41
41
 
42
42
  ## subjects
43
43
 
@@ -46,8 +46,8 @@ column.
46
46
  | `ROWID` | INTEGER | Primary key |
47
47
  | `subject` | TEXT | Base subject text |
48
48
 
49
- Note: `subject_prefix` (Re:, Fwd:, etc.) is stored on the `messages` table
50
- directly, not here.
49
+ Note: the `messages` table stores `subject_prefix` (Re:, Fwd:, etc.) directly.
50
+ This table does not store it.
51
51
 
52
52
  ## recipients (To/Cc/Bcc per message)
53
53
 
@@ -59,8 +59,8 @@ directly, not here.
59
59
  | `type` | INTEGER | 0 = To, 1 = Cc, 2 = Bcc |
60
60
  | `position` | INTEGER | Order within the type group |
61
61
 
62
- **IMPORTANT:** Column names are `message` and `address` (not `message_id` or
63
- `address_id`).
62
+ **IMPORTANT:** The column names are `message` and `address`. They are not
63
+ `message_id` or `address_id`.
64
64
 
65
65
  ## summaries (Apple Intelligence email summaries)
66
66
 
@@ -99,8 +99,8 @@ Use case-insensitive `LIKE` patterns to match both:
99
99
  **Constraints:** `UNIQUE(message, attachment_id)` — each attachment within a
100
100
  message has a unique identifier.
101
101
 
102
- **IMPORTANT:** Column is `message` (not `message_id`), matching the convention
103
- used by the `recipients` table.
102
+ **IMPORTANT:** The column is `message`. It is not `message_id`. This matches the
103
+ convention of the `recipients` table.
104
104
 
105
105
  ### Filesystem mapping
106
106