@forwardimpact/outpost 3.0.3

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 (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +15 -0
  3. package/bin/fit-outpost.js +3 -0
  4. package/config/scheduler.json +34 -0
  5. package/package.json +63 -0
  6. package/src/agent-runner.js +189 -0
  7. package/src/index.js +5 -0
  8. package/src/kb-manager.js +228 -0
  9. package/src/outpost.js +417 -0
  10. package/src/scheduler.js +195 -0
  11. package/src/socket-server.js +336 -0
  12. package/src/state-manager.js +126 -0
  13. package/templates/.claude/agents/chief-of-staff.md +58 -0
  14. package/templates/.claude/agents/concierge.md +46 -0
  15. package/templates/.claude/agents/head-hunter.md +57 -0
  16. package/templates/.claude/agents/librarian.md +60 -0
  17. package/templates/.claude/agents/postman.md +47 -0
  18. package/templates/.claude/agents/recruiter.md +63 -0
  19. package/templates/.claude/settings.json +88 -0
  20. package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
  21. package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
  22. package/templates/.claude/skills/candidate-report/references/report.css +438 -0
  23. package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
  24. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
  25. package/templates/.claude/skills/deck-create/SKILL.md +75 -0
  26. package/templates/.claude/skills/deck-create/references/slide.css +35 -0
  27. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
  28. package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
  29. package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
  30. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
  31. package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
  32. package/templates/.claude/skills/doc-create/SKILL.md +105 -0
  33. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
  34. package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
  35. package/templates/.claude/skills/draft-emails/references/template.md +26 -0
  36. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
  37. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
  38. package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
  39. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
  40. package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
  41. package/templates/.claude/skills/extract-entities/references/content.md +104 -0
  42. package/templates/.claude/skills/extract-entities/references/links.md +48 -0
  43. package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
  44. package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
  45. package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
  46. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
  47. package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
  48. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
  49. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
  50. package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
  51. package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
  52. package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
  53. package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
  54. package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
  55. package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
  56. package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
  57. package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
  58. package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
  59. package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
  60. package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
  61. package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
  62. package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
  64. package/templates/.claude/skills/organize-files/SKILL.md +146 -0
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
  67. package/templates/.claude/skills/req-assess/SKILL.md +137 -0
  68. package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
  69. package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
  70. package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
  71. package/templates/.claude/skills/req-decide/SKILL.md +163 -0
  72. package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
  73. package/templates/.claude/skills/req-decide/references/template.md +118 -0
  74. package/templates/.claude/skills/req-forget/SKILL.md +154 -0
  75. package/templates/.claude/skills/req-forget/references/classify.md +23 -0
  76. package/templates/.claude/skills/req-forget/references/locations.md +59 -0
  77. package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
  78. package/templates/.claude/skills/req-scan/SKILL.md +145 -0
  79. package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
  80. package/templates/.claude/skills/req-scan/references/filters.md +83 -0
  81. package/templates/.claude/skills/req-scan/references/sources.md +76 -0
  82. package/templates/.claude/skills/req-scan/references/state.md +54 -0
  83. package/templates/.claude/skills/req-scan/references/template.md +36 -0
  84. package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
  85. package/templates/.claude/skills/req-screen/SKILL.md +151 -0
  86. package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
  87. package/templates/.claude/skills/req-screen/references/template.md +91 -0
  88. package/templates/.claude/skills/req-track/SKILL.md +189 -0
  89. package/templates/.claude/skills/req-track/references/fields.md +114 -0
  90. package/templates/.claude/skills/req-track/references/signals.md +42 -0
  91. package/templates/.claude/skills/req-track/references/statuses.md +32 -0
  92. package/templates/.claude/skills/req-track/references/templates.md +127 -0
  93. package/templates/.claude/skills/req-workday/SKILL.md +162 -0
  94. package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
  95. package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
  96. package/templates/.claude/skills/req-workday/references/templates.md +126 -0
  97. package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
  98. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
  99. package/templates/.claude/skills/send-chat/SKILL.md +170 -0
  100. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
  101. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
  102. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
  103. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
  104. package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
  105. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
  106. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
  107. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
  108. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
  109. package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
  110. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
  111. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
  112. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
  113. package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
  114. package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
  115. package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
  116. package/templates/.claude/skills/weekly-update/references/template.md +55 -0
  117. package/templates/CLAUDE.md +134 -0
  118. package/templates/USER.md +3 -0
  119. package/templates/apm.yml +5 -0
  120. package/templates/knowledge/Briefings/.gitkeep +0 -0
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sync Microsoft Teams chat messages to ~/.cache/fit/outpost/teams_chat/ as
4
+ * markdown files. Reads the Teams IndexedDB cache (LevelDB on disk) directly —
5
+ * no browser automation, no API tokens, no network access needed.
6
+ *
7
+ * Requires macOS with the Microsoft Teams desktop app installed.
8
+ * Requires snappyjs: npm install snappyjs
9
+ */
10
+
11
+ if (process.argv.includes("-h") || process.argv.includes("--help")) {
12
+ console.log(`sync-teams — sync Teams chat messages to markdown
13
+
14
+ Usage: node scripts/sync.mjs [--days N] [-h|--help]
15
+
16
+ Options:
17
+ --days N Only include messages from the last N days (default: 30)
18
+ -h, --help Show this help message and exit
19
+
20
+ Reads the Teams IndexedDB cache from disk. Requires macOS with Teams installed.`);
21
+ process.exit(0);
22
+ }
23
+
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { homedir } from "node:os";
27
+ import { readIndexedDb } from "./idb-reader.mjs";
28
+
29
+ const HOME = homedir();
30
+ const OUTDIR = join(HOME, ".cache/fit/outpost/teams_chat");
31
+ const STATE_DIR = join(HOME, ".cache/fit/outpost/state");
32
+ const SYNC_STATE_FILE = join(STATE_DIR, "teams_last_sync");
33
+ const INDEX_FILE = join(STATE_DIR, "teams_chat_index.tsv");
34
+
35
+ const TEAMS_IDB_DIR = join(
36
+ HOME,
37
+ "Library/Containers/com.microsoft.teams2/Data/Library/Application Support",
38
+ "Microsoft/MSTeams/EBWebView/WV2Profile_tfw/IndexedDB",
39
+ "https_teams.microsoft.com_0.indexeddb.leveldb",
40
+ );
41
+
42
+ function normalizeName(name) {
43
+ if (!name) return "";
44
+ const trimmed = name.trim();
45
+ if (trimmed.includes(",")) {
46
+ const [last, ...first] = trimmed.split(",").map((s) => s.trim());
47
+ const firstPart = first.join(" ").trim();
48
+ if (firstPart && last) return `${firstPart} ${last}`;
49
+ return last || firstPart;
50
+ }
51
+ return trimmed;
52
+ }
53
+
54
+ function toSlug(name) {
55
+ const slug = name
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9]+/g, "-")
58
+ .replace(/^-|-$/g, "");
59
+ return slug.slice(0, 80);
60
+ }
61
+
62
+ function htmlToText(html) {
63
+ if (!html) return "";
64
+ let text = html;
65
+ text = text.replace(/<(style|script)[^>]*>[\s\S]*?<\/\1>/gi, "");
66
+ text = text.replace(/<br\s*\/?>/gi, "\n");
67
+ text = text.replace(/<\/p>/gi, "\n");
68
+ text = text.replace(/<\/div>/gi, "\n");
69
+ text = text.replace(/<\/li>/gi, "\n");
70
+ text = text.replace(
71
+ /<span[^>]*itemtype="http:\/\/schema\.skype\.com\/Mention"[^>]*>([^<]*)<\/span>/gi,
72
+ "$1",
73
+ );
74
+ text = text.replace(/<[^>]+>/g, "");
75
+ text = text.replace(/&amp;/g, "&");
76
+ text = text.replace(/&lt;/g, "<");
77
+ text = text.replace(/&gt;/g, ">");
78
+ text = text.replace(/&quot;/g, '"');
79
+ text = text.replace(/&nbsp;/g, " ");
80
+ text = text.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
81
+ text = text.replace(/[ \t]+/g, " ");
82
+ text = text.replace(/\n{3,}/g, "\n\n");
83
+ return text.trim();
84
+ }
85
+
86
+ function formatTimestamp(ts) {
87
+ if (!ts) return "Unknown";
88
+ const ms = typeof ts === "string" ? parseInt(ts, 10) : ts;
89
+ if (isNaN(ms)) return "Unknown";
90
+ const dt = new Date(ms);
91
+ if (isNaN(dt.getTime())) return "Unknown";
92
+ return dt
93
+ .toISOString()
94
+ .replace("T", " ")
95
+ .replace(/\.\d+Z/, "");
96
+ }
97
+
98
+ function _formatDateOnly(ts) {
99
+ if (!ts) return "Unknown";
100
+ const ms = typeof ts === "string" ? parseInt(ts, 10) : ts;
101
+ const dt = new Date(ms);
102
+ if (isNaN(dt.getTime())) return "Unknown";
103
+ return dt.toISOString().slice(0, 10);
104
+ }
105
+
106
+ function isSystemMessage(msg) {
107
+ const type = msg.messageType ?? msg.messagetype ?? "";
108
+ if (type === "Event/Call" || type === "ThreadActivity/AddMember") return true;
109
+ if (type === "ThreadActivity/DeleteMember") return true;
110
+ if (type === "ThreadActivity/TopicUpdate") return true;
111
+
112
+ const content = msg.content ?? "";
113
+ if (content.includes("has made the chat a meeting")) return true;
114
+ if (content.includes("added") && content.includes("to the chat")) return true;
115
+
116
+ return false;
117
+ }
118
+
119
+ function is1to1Chat(conv) {
120
+ const id = conv.id ?? "";
121
+ if (id.startsWith("48:")) return false;
122
+ if (
123
+ conv.threadProperties?.isGroup === "True" ||
124
+ conv.threadProperties?.isGroup === "true"
125
+ )
126
+ return false;
127
+ return true;
128
+ }
129
+
130
+ function isGroupChat(conv) {
131
+ if (is1to1Chat(conv)) return false;
132
+ const id = conv.id ?? "";
133
+ if (id.includes("@thread.tacv2")) return true;
134
+ return false;
135
+ }
136
+
137
+ function looksLikeOrgId(name) {
138
+ return /^8:orgid:[0-9a-f-]+$/i.test(name.trim());
139
+ }
140
+
141
+ function getChatDisplayName(conv) {
142
+ const topic = conv.threadProperties?.topic ?? conv.properties?.topic ?? null;
143
+ if (topic) return topic;
144
+
145
+ const members = conv.members ?? [];
146
+ const otherMembers = members.filter((m) => {
147
+ const isSelf = m.isSelf || m.isSelf === "true" || m.isSelf === true;
148
+ return !isSelf;
149
+ });
150
+
151
+ if (otherMembers.length > 0) {
152
+ const names = otherMembers
153
+ .map((m) => {
154
+ const raw = m.friendlyName ?? m.displayName ?? "";
155
+ if (!raw || looksLikeOrgId(raw)) return null;
156
+ return normalizeName(raw);
157
+ })
158
+ .filter(Boolean);
159
+ if (names.length > 0) return names.join(", ");
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ function extractParticipantsFromMessages(chatMessages) {
166
+ const senders = new Set();
167
+ for (const msg of chatMessages) {
168
+ if (msg.sender) senders.add(msg.sender);
169
+ }
170
+ return [...senders];
171
+ }
172
+
173
+ function getChatDisplayNameFromMessages(chatMessages, userIdentity) {
174
+ const senders = new Set();
175
+ for (const msg of chatMessages) {
176
+ if (msg.sender && msg.sender !== userIdentity) {
177
+ senders.add(msg.sender);
178
+ }
179
+ }
180
+ if (senders.size === 1) return [...senders][0];
181
+ if (senders.size > 1) return [...senders].join(", ");
182
+ return null;
183
+ }
184
+
185
+ function messageFromRecord(msg, cutoffMs) {
186
+ if (isSystemMessage(msg)) return null;
187
+
188
+ const arrivalTime = msg.originalArrivalTime ?? msg.clientArrivalTime ?? null;
189
+ const ts =
190
+ typeof arrivalTime === "string" ? parseInt(arrivalTime, 10) : arrivalTime;
191
+
192
+ if (ts && cutoffMs && ts < cutoffMs) return null;
193
+
194
+ const content = htmlToText(msg.content ?? "");
195
+ if (!content) return null;
196
+
197
+ const senderName = normalizeName(
198
+ msg.imDisplayName ??
199
+ msg.fromDisplayNameInToken ??
200
+ msg.prioritizeImDisplayName ??
201
+ "",
202
+ );
203
+
204
+ return {
205
+ timestamp: ts,
206
+ sender: senderName,
207
+ content,
208
+ edited: !!msg.skypeeditedid,
209
+ };
210
+ }
211
+
212
+ function extractMessagesFromReplychains(replychains, conversationId, cutoffMs) {
213
+ const messages = [];
214
+ for (const rc of replychains) {
215
+ if (rc.conversationId !== conversationId) continue;
216
+ if (!rc.messageMap) continue;
217
+
218
+ for (const [, msg] of Object.entries(rc.messageMap)) {
219
+ const record = messageFromRecord(msg, cutoffMs);
220
+ if (record) messages.push(record);
221
+ }
222
+ }
223
+
224
+ messages.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
225
+ return messages;
226
+ }
227
+
228
+ function writeChatMarkdown(
229
+ slug,
230
+ displayName,
231
+ chatType,
232
+ participants,
233
+ messages,
234
+ ) {
235
+ const lines = [];
236
+
237
+ if (chatType === "group") {
238
+ lines.push(`# Chat: ${displayName} (group)`);
239
+ lines.push("");
240
+ lines.push("**Platform:** Microsoft Teams");
241
+ lines.push("**Type:** Group chat");
242
+ if (participants.length > 0) {
243
+ lines.push(`**Participants:** ${participants.join(", ")}`);
244
+ }
245
+ } else {
246
+ lines.push(`# Chat with ${displayName}`);
247
+ lines.push("");
248
+ lines.push("**Platform:** Microsoft Teams");
249
+ }
250
+
251
+ lines.push(`**Last Synced:** ${new Date().toISOString().slice(0, 10)}`);
252
+ lines.push("");
253
+
254
+ for (const msg of messages) {
255
+ lines.push("---");
256
+ lines.push("");
257
+ lines.push(`### ${msg.sender || "Unknown"}`);
258
+ lines.push(`**Date:** ${formatTimestamp(msg.timestamp)}`);
259
+ if (msg.edited) lines.push("**Edited**");
260
+ lines.push("");
261
+ lines.push(msg.content);
262
+ lines.push("");
263
+ }
264
+
265
+ writeFileSync(join(OUTDIR, `${slug}.md`), lines.join("\n"));
266
+ }
267
+
268
+ function loadUserIdentity() {
269
+ try {
270
+ const userMd = readFileSync(join(process.cwd(), "USER.md"), "utf-8");
271
+ const nameMatch = userMd.match(/\*\*Name:\*\*\s*(.+)/);
272
+ if (nameMatch) return nameMatch[1].trim();
273
+ } catch {
274
+ // USER.md not found or not readable
275
+ }
276
+ return "";
277
+ }
278
+
279
+ function processConversation(conv, replychains, cutoffMs, userIdentity) {
280
+ const convId = conv.id;
281
+ if (!convId) return null;
282
+
283
+ const chatType = is1to1Chat(conv)
284
+ ? "1to1"
285
+ : isGroupChat(conv)
286
+ ? "group"
287
+ : null;
288
+ if (!chatType) return null;
289
+
290
+ const chatMessages = extractMessagesFromReplychains(
291
+ replychains,
292
+ convId,
293
+ cutoffMs,
294
+ );
295
+ if (chatMessages.length === 0) return null;
296
+
297
+ let displayName = getChatDisplayName(conv);
298
+ if (!displayName) {
299
+ displayName = getChatDisplayNameFromMessages(chatMessages, userIdentity);
300
+ }
301
+ if (!displayName) return null;
302
+
303
+ const slug = toSlug(displayName);
304
+ if (!slug) return null;
305
+
306
+ const participants =
307
+ chatType === "group" ? extractParticipantsFromMessages(chatMessages) : [];
308
+
309
+ writeChatMarkdown(slug, displayName, chatType, participants, chatMessages);
310
+
311
+ const lastMsgTime = chatMessages[chatMessages.length - 1]?.timestamp;
312
+ const lastMsgIso = lastMsgTime
313
+ ? new Date(lastMsgTime).toISOString().slice(0, 19)
314
+ : "";
315
+
316
+ return `${slug}\t${displayName}\t${lastMsgIso}`;
317
+ }
318
+
319
+ function main() {
320
+ let daysBack = 30;
321
+ const daysIdx = process.argv.indexOf("--days");
322
+ if (daysIdx !== -1 && process.argv[daysIdx + 1]) {
323
+ daysBack = parseInt(process.argv[daysIdx + 1], 10);
324
+ }
325
+
326
+ if (!existsSync(TEAMS_IDB_DIR)) {
327
+ console.error(
328
+ "Error: Teams IndexedDB not found. Is the Teams desktop app installed?",
329
+ );
330
+ console.error(`Expected: ${TEAMS_IDB_DIR}`);
331
+ process.exit(1);
332
+ }
333
+
334
+ mkdirSync(OUTDIR, { recursive: true });
335
+ mkdirSync(STATE_DIR, { recursive: true });
336
+
337
+ const cutoffMs = Date.now() - daysBack * 86400000;
338
+
339
+ console.log(`Reading Teams IndexedDB from disk...`);
340
+ const startTime = Date.now();
341
+ const { conversations, messages: replychains } = readIndexedDb(TEAMS_IDB_DIR);
342
+ const readTime = Date.now() - startTime;
343
+ console.log(
344
+ `Read ${conversations.length} conversations, ${replychains.length} message chains in ${readTime}ms`,
345
+ );
346
+
347
+ const userIdentity = loadUserIdentity();
348
+
349
+ const indexLines = [];
350
+ let written = 0;
351
+
352
+ for (const conv of conversations) {
353
+ const indexLine = processConversation(
354
+ conv,
355
+ replychains,
356
+ cutoffMs,
357
+ userIdentity,
358
+ );
359
+ if (indexLine) {
360
+ indexLines.push(indexLine);
361
+ written++;
362
+ }
363
+ }
364
+
365
+ indexLines.sort((a, b) => {
366
+ const tsA = a.split("\t")[2] ?? "";
367
+ const tsB = b.split("\t")[2] ?? "";
368
+ return tsB.localeCompare(tsA);
369
+ });
370
+
371
+ writeFileSync(INDEX_FILE, indexLines.join("\n") + "\n");
372
+ writeFileSync(SYNC_STATE_FILE, new Date().toISOString());
373
+
374
+ console.log("\nTeams Sync Complete");
375
+ console.log(`Chats written: ${written}`);
376
+ console.log(`Messages within last ${daysBack} days`);
377
+ console.log(`Read time: ${readTime}ms`);
378
+ console.log(`Output: ${OUTDIR}`);
379
+ }
380
+
381
+ main();
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: upstream-skill
3
+ description: Track changes made to skills in this installation and produce a changelog that can be included upstream. Use when skills have been modified, added, or removed locally and those changes should be contributed back to the monorepo.
4
+ ---
5
+
6
+ # Upstream
7
+
8
+ Track changes to skills in this installation and produce a structured changelog
9
+ so improvements can be contributed back to the upstream monorepo.
10
+
11
+ ## Trigger
12
+
13
+ - The user asks to prepare local skill changes for upstream contribution.
14
+ - Skills in `.claude/skills/` have been modified, added, or removed.
15
+ - The user wants to document what changed locally before syncing upstream.
16
+
17
+ ## Prerequisites
18
+
19
+ - A working Outpost installation with `.claude/skills/`.
20
+ - Git available for change detection.
21
+
22
+ ## Inputs
23
+
24
+ - `.claude/skills/*/SKILL.md` — current skill files.
25
+ - Git history — change detection.
26
+
27
+ ## Outputs
28
+
29
+ - `.claude/skills/<skill>/CHANGELOG.md` — one per modified skill,
30
+ reverse-chronological.
31
+
32
+ <do_confirm_checklist goal="Verify changelogs are upstream-ready">
33
+
34
+ - [ ] Every changed skill has a `CHANGELOG.md` in its directory.
35
+ - [ ] Each entry has **What**, **Why**, and **Details** sections.
36
+ - [ ] Descriptions are specific enough for an upstream maintainer to act on (not
37
+ "updated SKILL.md" / "fixed stuff").
38
+ - [ ] New skills include a brief description of their purpose.
39
+ - [ ] Removed skills explain why they were removed.
40
+ - [ ] Dates come from git history, not guessed.
41
+ - [ ] No duplicate entries for the same change.
42
+
43
+ </do_confirm_checklist>
44
+
45
+ ## Procedure
46
+
47
+ ### 1. Identify changed skills
48
+
49
+ Use git to find skills added, modified, or deleted since the last changelog
50
+ entry (or since initial commit if no changelog exists):
51
+
52
+ ```bash
53
+ # Latest changelog date for a skill (if any)
54
+ head -20 .claude/skills/<skill>/CHANGELOG.md 2>/dev/null
55
+
56
+ # All changes touching skills
57
+ git log --oneline --name-status -- '.claude/skills/'
58
+
59
+ # Changes since last documented date for one skill
60
+ git log --after="<last-entry-date>" --name-status -- '.claude/skills/<skill>/'
61
+ ```
62
+
63
+ If no changelog exists for a skill, consider all commits that touched that
64
+ skill's directory.
65
+
66
+ ### 2. Classify each change
67
+
68
+ | Type | Description |
69
+ | ---------- | ---------------------------------------------------- |
70
+ | `added` | New skill that doesn't exist upstream |
71
+ | `modified` | Existing skill updated (workflow, checklists, tools) |
72
+ | `removed` | Skill deleted from the installation |
73
+ | `renamed` | Skill directory or file renamed |
74
+
75
+ For modifications, diff the current file against the prior version:
76
+
77
+ ```bash
78
+ git diff HEAD~N -- '.claude/skills/<skill>/SKILL.md'
79
+ git diff <commit> -- '.claude/skills/<skill>/SKILL.md'
80
+ ```
81
+
82
+ ### 3. Describe each change
83
+
84
+ Every entry must answer:
85
+
86
+ 1. **What changed?** — the specific section or behaviour modified.
87
+ 2. **Why?** — the problem encountered or improvement discovered in use.
88
+ 3. **How?** — a summary of the actual change (not a full diff).
89
+
90
+ Good: "Added a safety check to Step 3 — agents were skipping validation when the
91
+ source directory was empty, causing silent failures."
92
+
93
+ Bad: "Updated SKILL.md" / "Fixed stuff" / "Changed line 42".
94
+
95
+ ### 4. Write the changelog
96
+
97
+ For each changed skill, create or update `.claude/skills/<skill>/CHANGELOG.md`
98
+ using this format (newest first):
99
+
100
+ ```markdown
101
+ # <skill> Changelog
102
+
103
+ Changes to this skill that should be considered for upstream inclusion in
104
+ the Forward Impact monorepo.
105
+
106
+ ## <YYYY-MM-DD>
107
+
108
+ **What:** <one-line summary>
109
+
110
+ **Why:** <problem or improvement that motivated it>
111
+
112
+ **Details:**
113
+ <2–5 lines describing the specific changes>
114
+
115
+ ---
116
+ ```
117
+
118
+ For **new** skills, write a single `added` entry describing the skill's purpose.
119
+ For **removed** skills, the changelog is the last file remaining in the
120
+ directory and explains the removal.
121
+
122
+ Worked examples in [references/examples.md](references/examples.md).
123
+
124
+ ## Notes
125
+
126
+ - This skill **documents only** — it does not push or merge anything.
127
+ - Per-skill changelogs are consumed by the **downstream** skill in the upstream
128
+ monorepo.
129
+ - When in doubt about whether a change is upstream-worthy, include it; the
130
+ upstream maintainer decides what to incorporate.
@@ -0,0 +1,80 @@
1
+ # Changelog Examples
2
+
3
+ Reference output for `upstream-skill` Step 4. Each modified skill has its own
4
+ `CHANGELOG.md` with reverse-chronological entries.
5
+
6
+ ## Modified skill
7
+
8
+ `.claude/skills/req-track/CHANGELOG.md`:
9
+
10
+ ```markdown
11
+ # req-track Changelog
12
+
13
+ Changes to this skill that should be considered for upstream inclusion in
14
+ the Forward Impact monorepo.
15
+
16
+ ## 2026-03-01
17
+
18
+ **What:** Added gender field extraction for diversity tracking
19
+
20
+ **Why:** Recruitment pipeline lacked diversity metrics — pool composition
21
+ was invisible without structured gender data.
22
+
23
+ **Details:**
24
+ - Added Gender field to candidate brief template (Woman / Man / —)
25
+ - Added extraction rules: pronouns, gendered titles
26
+ - Added explicit note that the field has no bearing on hiring decisions
27
+ - Updated quality checklist to verify the gender field
28
+
29
+ ---
30
+ ```
31
+
32
+ ## New skill
33
+
34
+ `.claude/skills/hyprnote-process/CHANGELOG.md`:
35
+
36
+ ```markdown
37
+ # hyprnote-process Changelog
38
+
39
+ Changes to this skill that should be considered for upstream inclusion in
40
+ the Forward Impact monorepo.
41
+
42
+ ## 2026-03-01
43
+
44
+ **What:** New skill for processing Hyprnote meeting recordings
45
+
46
+ **Why:** Meeting notes were being lost — Hyprnote captures transcriptions
47
+ but they weren't being integrated into the knowledge base.
48
+
49
+ **Details:**
50
+ - Reads transcription files from `~/.cache/fit/outpost/hyprnote/`
51
+ - Extracts people, decisions, and action items
52
+ - Creates meeting notes in `knowledge/Meetings/`
53
+ - Links attendees to `knowledge/People/` entries
54
+
55
+ ---
56
+ ```
57
+
58
+ ## Tweak to existing skill
59
+
60
+ `.claude/skills/extract-entities/CHANGELOG.md`:
61
+
62
+ ```markdown
63
+ # extract-entities Changelog
64
+
65
+ Changes to this skill that should be considered for upstream inclusion in
66
+ the Forward Impact monorepo.
67
+
68
+ ## 2026-02-15
69
+
70
+ **What:** Increased batch size from 5 to 10 files per run
71
+
72
+ **Why:** Processing was too slow for large inboxes — 5 files per batch
73
+ meant dozens of runs to catch up after a week of email.
74
+
75
+ **Details:**
76
+ - Changed batch size constant from 5 to 10 in Step 1
77
+ - Added a note about context window limits for batches > 15
78
+
79
+ ---
80
+ ```