@forwardimpact/basecamp 2.0.0 → 2.2.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 (39) hide show
  1. package/config/scheduler.json +5 -0
  2. package/package.json +1 -1
  3. package/src/basecamp.js +288 -57
  4. package/template/.claude/agents/chief-of-staff.md +6 -2
  5. package/template/.claude/agents/concierge.md +2 -3
  6. package/template/.claude/agents/librarian.md +4 -6
  7. package/template/.claude/agents/recruiter.md +222 -0
  8. package/template/.claude/settings.json +0 -4
  9. package/template/.claude/skills/analyze-cv/SKILL.md +267 -0
  10. package/template/.claude/skills/create-presentations/SKILL.md +2 -2
  11. package/template/.claude/skills/create-presentations/references/slide.css +1 -1
  12. package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.mjs +47 -0
  13. package/template/.claude/skills/draft-emails/SKILL.md +85 -123
  14. package/template/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
  15. package/template/.claude/skills/draft-emails/scripts/send-email.mjs +118 -0
  16. package/template/.claude/skills/extract-entities/SKILL.md +2 -2
  17. package/template/.claude/skills/extract-entities/scripts/state.mjs +130 -0
  18. package/template/.claude/skills/manage-tasks/SKILL.md +242 -0
  19. package/template/.claude/skills/organize-files/SKILL.md +3 -3
  20. package/template/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
  21. package/template/.claude/skills/organize-files/scripts/summarize.mjs +84 -0
  22. package/template/.claude/skills/process-hyprnote/SKILL.md +2 -2
  23. package/template/.claude/skills/send-chat/SKILL.md +170 -0
  24. package/template/.claude/skills/sync-apple-calendar/SKILL.md +5 -5
  25. package/template/.claude/skills/sync-apple-calendar/scripts/sync.mjs +325 -0
  26. package/template/.claude/skills/sync-apple-mail/SKILL.md +6 -6
  27. package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +374 -0
  28. package/template/.claude/skills/sync-apple-mail/scripts/sync.mjs +629 -0
  29. package/template/.claude/skills/track-candidates/SKILL.md +375 -0
  30. package/template/.claude/skills/weekly-update/SKILL.md +250 -0
  31. package/template/CLAUDE.md +63 -40
  32. package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.js +0 -32
  33. package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +0 -34
  34. package/template/.claude/skills/extract-entities/scripts/state.py +0 -100
  35. package/template/.claude/skills/organize-files/scripts/organize-by-type.sh +0 -42
  36. package/template/.claude/skills/organize-files/scripts/summarize.sh +0 -21
  37. package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +0 -242
  38. package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.py +0 -104
  39. package/template/.claude/skills/sync-apple-mail/scripts/sync.py +0 -455
@@ -19,6 +19,11 @@
19
19
  "kb": "~/Documents/Personal",
20
20
  "schedule": { "type": "cron", "expression": "0 7,18 * * *" },
21
21
  "enabled": true
22
+ },
23
+ "recruiter": {
24
+ "kb": "~/Documents/Personal",
25
+ "schedule": { "type": "interval", "minutes": 30 },
26
+ "enabled": true
22
27
  }
23
28
  }
24
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/basecamp",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Claude Code-native personal knowledge system with autonomous agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/basecamp.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // node basecamp.js --daemon Run continuously (poll every 60s)
8
8
  // node basecamp.js --wake <agent> Wake a specific agent immediately
9
9
  // node basecamp.js --init <path> Initialize a new knowledge base
10
+ // node basecamp.js --update [path] Update KB with latest CLAUDE.md, agents and skills
11
+ // node basecamp.js --stop Gracefully stop daemon and children
10
12
  // node basecamp.js --validate Validate agent definitions exist
11
13
  // node basecamp.js --status Show agent status
12
14
  // node basecamp.js --help Show this help
@@ -20,8 +22,9 @@ import {
20
22
  chmodSync,
21
23
  readdirSync,
22
24
  statSync,
25
+ cpSync,
26
+ copyFileSync,
23
27
  } from "node:fs";
24
- import { execSync } from "node:child_process";
25
28
  import { join, dirname, resolve } from "node:path";
26
29
  import { homedir } from "node:os";
27
30
  import { fileURLToPath } from "node:url";
@@ -48,10 +51,13 @@ let daemonStartedAt = null;
48
51
  // Matches the 30-minute child_process timeout plus a buffer.
49
52
  const MAX_AGENT_RUNTIME_MS = 35 * 60_000;
50
53
 
54
+ /** Active child PIDs spawned by posix_spawn (for graceful shutdown). */
55
+ const activeChildren = new Set();
56
+
51
57
  // --- Helpers ----------------------------------------------------------------
52
58
 
53
59
  function ensureDir(dir) {
54
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
60
+ mkdirSync(dir, { recursive: true });
55
61
  }
56
62
 
57
63
  function readJSON(path, fallback) {
@@ -162,13 +168,8 @@ function cronMatches(expr, d) {
162
168
  // --- Scheduling logic -------------------------------------------------------
163
169
 
164
170
  function floorToMinute(d) {
165
- return new Date(
166
- d.getFullYear(),
167
- d.getMonth(),
168
- d.getDate(),
169
- d.getHours(),
170
- d.getMinutes(),
171
- ).getTime();
171
+ const t = d.getTime();
172
+ return t - (t % 60_000);
172
173
  }
173
174
 
174
175
  function shouldWake(agent, agentState, now) {
@@ -197,7 +198,16 @@ function shouldWake(agent, agentState, now) {
197
198
 
198
199
  // --- Agent execution --------------------------------------------------------
199
200
 
200
- async function wakeAgent(agentName, agent, _config, state) {
201
+ function failAgent(agentState, error) {
202
+ Object.assign(agentState, {
203
+ status: "failed",
204
+ startedAt: null,
205
+ lastWokeAt: new Date().toISOString(),
206
+ lastError: String(error).slice(0, 500),
207
+ });
208
+ }
209
+
210
+ async function wakeAgent(agentName, agent, state) {
201
211
  if (!agent.kb) {
202
212
  log(`Agent ${agentName}: no "kb" specified, skipping.`);
203
213
  return;
@@ -226,6 +236,7 @@ async function wakeAgent(agentName, agent, _config, state) {
226
236
  undefined,
227
237
  kbPath,
228
238
  );
239
+ activeChildren.add(pid);
229
240
 
230
241
  // Read stdout and stderr concurrently to avoid pipe deadlocks,
231
242
  // then wait for the child to exit.
@@ -234,6 +245,7 @@ async function wakeAgent(agentName, agent, _config, state) {
234
245
  posixSpawn.readAll(stderrFd),
235
246
  ]);
236
247
  const exitCode = await posixSpawn.waitForExit(pid);
248
+ activeChildren.delete(pid);
237
249
 
238
250
  if (exitCode === 0) {
239
251
  log(`Agent ${agentName} completed. Output: ${stdout.slice(0, 200)}...`);
@@ -241,24 +253,13 @@ async function wakeAgent(agentName, agent, _config, state) {
241
253
  } else {
242
254
  const errMsg = stderr || stdout || `Exit code ${exitCode}`;
243
255
  log(`Agent ${agentName} failed: ${errMsg.slice(0, 300)}`);
244
- Object.assign(as, {
245
- status: "failed",
246
- startedAt: null,
247
- lastWokeAt: new Date().toISOString(),
248
- lastError: errMsg.slice(0, 500),
249
- });
256
+ failAgent(as, errMsg);
250
257
  }
251
- saveState(state);
252
258
  } catch (err) {
253
259
  log(`Agent ${agentName} failed: ${err.message}`);
254
- Object.assign(as, {
255
- status: "failed",
256
- startedAt: null,
257
- lastWokeAt: new Date().toISOString(),
258
- lastError: err.message.slice(0, 500),
259
- });
260
- saveState(state);
260
+ failAgent(as, err.message);
261
261
  }
262
+ saveState(state);
262
263
  }
263
264
 
264
265
  /**
@@ -336,7 +337,7 @@ async function wakeDueAgents() {
336
337
  let wokeAny = false;
337
338
  for (const [name, agent] of Object.entries(config.agents)) {
338
339
  if (shouldWake(agent, state.agents[name] || {}, now)) {
339
- await wakeAgent(name, agent, config, state);
340
+ await wakeAgent(name, agent, state);
340
341
  wokeAny = true;
341
342
  }
342
343
  }
@@ -392,25 +393,31 @@ function computeNextWakeAt(agent, agentState, now) {
392
393
  * @returns {string|null}
393
394
  */
394
395
  function resolveBriefingFile(agentName, agentConfig) {
395
- // 1. Scan state directory for agent-specific files
396
+ // 1. Scan state directory for agent-specific files (latest by mtime)
396
397
  const stateDir = join(CACHE_DIR, "state");
397
398
  if (existsSync(stateDir)) {
398
399
  const prefix = agentName.replace(/-/g, "_") + "_";
399
400
  const matches = readdirSync(stateDir).filter(
400
401
  (f) => f.startsWith(prefix) && f.endsWith(".md"),
401
402
  );
402
- if (matches.length === 1) return join(stateDir, matches[0]);
403
- if (matches.length > 1) {
404
- return matches
405
- .map((f) => join(stateDir, f))
406
- .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
403
+ if (matches.length > 0) {
404
+ let latest = join(stateDir, matches[0]);
405
+ let latestMtime = statSync(latest).mtimeMs;
406
+ for (let i = 1; i < matches.length; i++) {
407
+ const p = join(stateDir, matches[i]);
408
+ const mt = statSync(p).mtimeMs;
409
+ if (mt > latestMtime) {
410
+ latest = p;
411
+ latestMtime = mt;
412
+ }
413
+ }
414
+ return latest;
407
415
  }
408
416
  }
409
417
 
410
- // 2. Fall back to KB briefings directory
418
+ // 2. Fall back to KB briefings directory (latest by name)
411
419
  if (agentConfig.kb) {
412
- const kbPath = expandPath(agentConfig.kb);
413
- const dir = join(kbPath, "knowledge", "Briefings");
420
+ const dir = join(expandPath(agentConfig.kb), "knowledge", "Briefings");
414
421
  if (existsSync(dir)) {
415
422
  const files = readdirSync(dir)
416
423
  .filter((f) => f.endsWith(".md"))
@@ -475,6 +482,14 @@ function handleMessage(socket, line) {
475
482
 
476
483
  if (request.type === "status") return handleStatusRequest(socket);
477
484
 
485
+ if (request.type === "shutdown") {
486
+ log("Shutdown requested via socket.");
487
+ send(socket, { type: "ack", command: "shutdown" });
488
+ socket.end();
489
+ killActiveChildren();
490
+ process.exit(0);
491
+ }
492
+
478
493
  if (request.type === "wake") {
479
494
  if (!request.agent) {
480
495
  send(socket, { type: "error", message: "Missing agent name" });
@@ -491,7 +506,7 @@ function handleMessage(socket, line) {
491
506
  }
492
507
  send(socket, { type: "ack", command: "wake", agent: request.agent });
493
508
  const state = loadState();
494
- wakeAgent(request.agent, agent, config, state).catch(() => {});
509
+ wakeAgent(request.agent, agent, state).catch(() => {});
495
510
  return;
496
511
  }
497
512
 
@@ -530,7 +545,11 @@ function startSocketServer() {
530
545
  });
531
546
 
532
547
  const cleanup = () => {
548
+ killActiveChildren();
533
549
  server.close();
550
+ try {
551
+ unlinkSync(SOCKET_PATH);
552
+ } catch {}
534
553
  process.exit(0);
535
554
  };
536
555
  process.on("SIGTERM", cleanup);
@@ -539,6 +558,71 @@ function startSocketServer() {
539
558
  return server;
540
559
  }
541
560
 
561
+ // --- Graceful shutdown -------------------------------------------------------
562
+
563
+ /**
564
+ * Send SIGTERM to all tracked child processes (running claude sessions).
565
+ * Called on daemon shutdown to prevent orphaned processes.
566
+ */
567
+ function killActiveChildren() {
568
+ for (const pid of activeChildren) {
569
+ try {
570
+ process.kill(pid, "SIGTERM");
571
+ log(`Sent SIGTERM to child PID ${pid}`);
572
+ } catch {
573
+ // Already exited
574
+ }
575
+ }
576
+ activeChildren.clear();
577
+ }
578
+
579
+ /**
580
+ * Connect to the daemon socket and request graceful shutdown.
581
+ * Waits up to 5 seconds for the daemon to exit.
582
+ * @returns {Promise<boolean>} true if shutdown succeeded
583
+ */
584
+ async function requestShutdown() {
585
+ if (!existsSync(SOCKET_PATH)) {
586
+ console.log("Daemon not running (no socket).");
587
+ return false;
588
+ }
589
+
590
+ const { createConnection } = await import("node:net");
591
+ return new Promise((resolve) => {
592
+ const timeout = setTimeout(() => {
593
+ console.log("Shutdown timed out.");
594
+ socket.destroy();
595
+ resolve(false);
596
+ }, 5000);
597
+
598
+ const socket = createConnection(SOCKET_PATH, () => {
599
+ socket.write(JSON.stringify({ type: "shutdown" }) + "\n");
600
+ });
601
+
602
+ let buffer = "";
603
+ socket.on("data", (data) => {
604
+ buffer += data.toString();
605
+ if (buffer.includes("\n")) {
606
+ clearTimeout(timeout);
607
+ console.log("Daemon stopped.");
608
+ socket.destroy();
609
+ resolve(true);
610
+ }
611
+ });
612
+
613
+ socket.on("error", () => {
614
+ clearTimeout(timeout);
615
+ console.log("Daemon not running (connection refused).");
616
+ resolve(false);
617
+ });
618
+
619
+ socket.on("close", () => {
620
+ clearTimeout(timeout);
621
+ resolve(true);
622
+ });
623
+ });
624
+ }
625
+
542
626
  // --- Daemon -----------------------------------------------------------------
543
627
 
544
628
  function daemon() {
@@ -563,7 +647,11 @@ function daemon() {
563
647
 
564
648
  // --- Init knowledge base ----------------------------------------------------
565
649
 
566
- function findTemplateDir() {
650
+ /**
651
+ * Resolve the template directory or exit with an error.
652
+ * @returns {string}
653
+ */
654
+ function requireTemplateDir() {
567
655
  const bundle = getBundlePath();
568
656
  if (bundle) {
569
657
  const tpl = join(bundle.resources, "template");
@@ -574,7 +662,91 @@ function findTemplateDir() {
574
662
  join(__dirname, "..", "template"),
575
663
  ])
576
664
  if (existsSync(d)) return d;
577
- return null;
665
+ console.error("Template not found. Reinstall fit-basecamp.");
666
+ process.exit(1);
667
+ }
668
+
669
+ /**
670
+ * Copy bundled files (CLAUDE.md, skills, agents) from template to a KB.
671
+ * Shared by --init and --update.
672
+ * @param {string} tpl Path to the template directory
673
+ * @param {string} dest Path to the target knowledge base
674
+ */
675
+ function copyBundledFiles(tpl, dest) {
676
+ // CLAUDE.md
677
+ copyFileSync(join(tpl, "CLAUDE.md"), join(dest, "CLAUDE.md"));
678
+ console.log(` Updated CLAUDE.md`);
679
+
680
+ // Settings — merge template permissions into existing settings
681
+ mergeSettings(tpl, dest);
682
+
683
+ // Skills and agents
684
+ for (const sub of ["skills", "agents"]) {
685
+ const src = join(tpl, ".claude", sub);
686
+ if (!existsSync(src)) continue;
687
+ cpSync(src, join(dest, ".claude", sub), { recursive: true });
688
+ const entries = readdirSync(src, { withFileTypes: true }).filter((d) =>
689
+ sub === "skills" ? d.isDirectory() : d.name.endsWith(".md"),
690
+ );
691
+ const names = entries.map((d) =>
692
+ sub === "agents" ? d.name.replace(".md", "") : d.name,
693
+ );
694
+ console.log(` Updated ${names.length} ${sub}: ${names.join(", ")}`);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Merge template settings.json into the destination's settings.json.
700
+ * Adds any missing entries from allow, deny, and additionalDirectories
701
+ * without removing user customizations.
702
+ * @param {string} tpl Template directory
703
+ * @param {string} dest Knowledge base directory
704
+ */
705
+ function mergeSettings(tpl, dest) {
706
+ const src = join(tpl, ".claude", "settings.json");
707
+ if (!existsSync(src)) return;
708
+
709
+ const destPath = join(dest, ".claude", "settings.json");
710
+
711
+ // No existing settings — copy template directly
712
+ if (!existsSync(destPath)) {
713
+ ensureDir(join(dest, ".claude"));
714
+ copyFileSync(src, destPath);
715
+ console.log(` Created settings.json`);
716
+ return;
717
+ }
718
+
719
+ const template = readJSON(src, {});
720
+ const existing = readJSON(destPath, {});
721
+ const tp = template.permissions || {};
722
+ const ep = (existing.permissions ||= {});
723
+ let added = 0;
724
+
725
+ // Merge array fields
726
+ for (const key of ["allow", "deny", "additionalDirectories"]) {
727
+ if (!tp[key]?.length) continue;
728
+ const set = new Set((ep[key] ||= []));
729
+ for (const entry of tp[key]) {
730
+ if (!set.has(entry)) {
731
+ ep[key].push(entry);
732
+ set.add(entry);
733
+ added++;
734
+ }
735
+ }
736
+ }
737
+
738
+ // Merge scalar fields
739
+ if (tp.defaultMode && !ep.defaultMode) {
740
+ ep.defaultMode = tp.defaultMode;
741
+ added++;
742
+ }
743
+
744
+ if (added > 0) {
745
+ writeJSON(destPath, existing);
746
+ console.log(` Updated settings.json (${added} new entries)`);
747
+ } else {
748
+ console.log(` Settings up to date`);
749
+ }
578
750
  }
579
751
 
580
752
  function initKB(targetPath) {
@@ -583,11 +755,7 @@ function initKB(targetPath) {
583
755
  console.error(`Knowledge base already exists at ${dest}`);
584
756
  process.exit(1);
585
757
  }
586
- const tpl = findTemplateDir();
587
- if (!tpl) {
588
- console.error("Template not found. Reinstall fit-basecamp.");
589
- process.exit(1);
590
- }
758
+ const tpl = requireTemplateDir();
591
759
 
592
760
  ensureDir(dest);
593
761
  for (const d of [
@@ -599,13 +767,70 @@ function initKB(targetPath) {
599
767
  ])
600
768
  ensureDir(join(dest, d));
601
769
 
602
- execSync(`cp -R "${tpl}/." "${dest}/"`);
770
+ // User-specific files (not overwritten by --update)
771
+ copyFileSync(join(tpl, "USER.md"), join(dest, "USER.md"));
772
+
773
+ // Bundled files (shared with --update)
774
+ copyBundledFiles(tpl, dest);
603
775
 
604
776
  console.log(
605
777
  `Knowledge base initialized at ${dest}\n\nNext steps:\n 1. Edit ${dest}/USER.md with your name, email, and domain\n 2. cd ${dest} && claude`,
606
778
  );
607
779
  }
608
780
 
781
+ // --- Update knowledge base --------------------------------------------------
782
+
783
+ /**
784
+ * Update an existing knowledge base with the latest bundled files.
785
+ * User data (USER.md, knowledge/) is untouched.
786
+ * Settings.json is merged — new template entries are added without
787
+ * removing user customizations.
788
+ * @param {string} targetPath
789
+ */
790
+ function updateKB(targetPath) {
791
+ const dest = expandPath(targetPath);
792
+ if (!existsSync(join(dest, "CLAUDE.md"))) {
793
+ console.error(`No knowledge base found at ${dest}`);
794
+ process.exit(1);
795
+ }
796
+ const tpl = requireTemplateDir();
797
+ copyBundledFiles(tpl, dest);
798
+ console.log(`\nKnowledge base updated: ${dest}`);
799
+ }
800
+
801
+ /**
802
+ * Run --update for an explicit path or every unique KB in the scheduler config.
803
+ */
804
+ function runUpdate() {
805
+ if (args[1]) {
806
+ updateKB(args[1]);
807
+ return;
808
+ }
809
+
810
+ // Discover unique KB paths from config
811
+ const config = loadConfig();
812
+ const kbPaths = [
813
+ ...new Set(
814
+ Object.values(config.agents)
815
+ .filter((a) => a.kb)
816
+ .map((a) => expandPath(a.kb)),
817
+ ),
818
+ ];
819
+
820
+ if (kbPaths.length === 0) {
821
+ console.error(
822
+ "No knowledge bases configured and no path given.\n" +
823
+ "Usage: fit-basecamp --update [path]",
824
+ );
825
+ process.exit(1);
826
+ }
827
+
828
+ for (const kb of kbPaths) {
829
+ console.log(`\nUpdating ${kb}...`);
830
+ updateKB(kb);
831
+ }
832
+ }
833
+
609
834
  // --- Status -----------------------------------------------------------------
610
835
 
611
836
  function showStatus() {
@@ -693,6 +918,8 @@ Usage:
693
918
  ${bin} --daemon Run continuously (poll every 60s)
694
919
  ${bin} --wake <agent> Wake a specific agent immediately
695
920
  ${bin} --init <path> Initialize a new knowledge base
921
+ ${bin} --update [path] Update KB with latest CLAUDE.md, agents and skills
922
+ ${bin} --stop Gracefully stop daemon and all running agents
696
923
  ${bin} --validate Validate agent definitions exist
697
924
  ${bin} --status Show agent status
698
925
 
@@ -708,34 +935,38 @@ const args = process.argv.slice(2);
708
935
  const command = args[0];
709
936
  ensureDir(BASECAMP_HOME);
710
937
 
938
+ function requireArg(usage) {
939
+ if (!args[1]) {
940
+ console.error(usage);
941
+ process.exit(1);
942
+ }
943
+ return args[1];
944
+ }
945
+
711
946
  const commands = {
712
947
  "--help": showHelp,
713
948
  "-h": showHelp,
714
949
  "--daemon": daemon,
715
950
  "--validate": validate,
716
- "--status": showStatus,
717
- "--init": () => {
718
- if (!args[1]) {
719
- console.error("Usage: node basecamp.js --init <path>");
720
- process.exit(1);
721
- }
722
- initKB(args[1]);
951
+ "--stop": async () => {
952
+ const stopped = await requestShutdown();
953
+ if (!stopped) process.exit(1);
723
954
  },
955
+ "--status": showStatus,
956
+ "--init": () => initKB(requireArg("Usage: fit-basecamp --init <path>")),
957
+ "--update": runUpdate,
724
958
  "--wake": async () => {
725
- if (!args[1]) {
726
- console.error("Usage: node basecamp.js --wake <agent-name>");
727
- process.exit(1);
728
- }
959
+ const name = requireArg("Usage: fit-basecamp --wake <agent-name>");
729
960
  const config = loadConfig(),
730
961
  state = loadState(),
731
- agent = config.agents[args[1]];
962
+ agent = config.agents[name];
732
963
  if (!agent) {
733
964
  console.error(
734
- `Agent "${args[1]}" not found. Available: ${Object.keys(config.agents).join(", ") || "(none)"}`,
965
+ `Agent "${name}" not found. Available: ${Object.keys(config.agents).join(", ") || "(none)"}`,
735
966
  );
736
967
  process.exit(1);
737
968
  }
738
- await wakeAgent(args[1], agent, config, state);
969
+ await wakeAgent(name, agent, state);
739
970
  },
740
971
  };
741
972
 
@@ -6,6 +6,8 @@ description: >
6
6
  key moments (morning, evening) by the Basecamp scheduler.
7
7
  model: sonnet
8
8
  permissionMode: bypassPermissions
9
+ skills:
10
+ - weekly-update
9
11
  ---
10
12
 
11
13
  You are the chief of staff — the user's executive assistant. You create daily
@@ -18,10 +20,12 @@ Read the state files from other agents:
18
20
 
19
21
  1. **Postman:** `~/.cache/fit/basecamp/state/postman_triage.md`
20
22
  - Urgent emails, items needing reply, threads awaiting response
21
- 2. **Concierge:** `~/.cache/fit/basecamp/state/concierge_outlook.md`
23
+ 2. **Concierge:** `~/.cache/fit/basecamp/state/concierge_triage.md`
22
24
  - Today's meetings, prep status, unprocessed transcripts
23
- 3. **Librarian:** `~/.cache/fit/basecamp/state/librarian_digest.md`
25
+ 3. **Librarian:** `~/.cache/fit/basecamp/state/librarian_triage.md`
24
26
  - Pending processing, graph size
27
+ 4. **Recruiter:** `~/.cache/fit/basecamp/state/recruiter_triage.md`
28
+ - Candidate pipeline, new assessments, interview scheduling
25
29
 
26
30
  Also read directly:
27
31
 
@@ -36,11 +36,10 @@ Assess the current state:
36
36
  - Check each session's `_memo.md` against
37
37
  `~/.cache/fit/basecamp/state/graph_processed`
38
38
 
39
- Write the calendar outlook to
40
- `~/.cache/fit/basecamp/state/concierge_outlook.md`:
39
+ Write triage results to `~/.cache/fit/basecamp/state/concierge_triage.md`:
41
40
 
42
41
  ```
43
- # Calendar Outlook — {YYYY-MM-DD HH:MM}
42
+ # Calendar Triage — {YYYY-MM-DD HH:MM}
44
43
 
45
44
  ## Next Meeting
46
45
  **{title}** at {time} with {attendees}
@@ -9,6 +9,7 @@ permissionMode: bypassPermissions
9
9
  skills:
10
10
  - extract-entities
11
11
  - organize-files
12
+ - manage-tasks
12
13
  ---
13
14
 
14
15
  You are the librarian — the user's knowledge curator. Each time you are woken,
@@ -20,20 +21,17 @@ Assess what needs processing:
20
21
 
21
22
  1. Check for unprocessed synced files (mail and calendar data):
22
23
 
23
- python3 scripts/state.py check
24
-
25
- (Run from the extract-entities skill directory:
26
- `.claude/skills/extract-entities/`)
24
+ node .claude/skills/extract-entities/scripts/state.mjs check
27
25
 
28
26
  2. Count existing knowledge graph entities:
29
27
 
30
28
  ls knowledge/People/ knowledge/Organizations/ knowledge/Projects/
31
29
  knowledge/Topics/ 2>/dev/null | wc -l
32
30
 
33
- Write your digest to `~/.cache/fit/basecamp/state/librarian_digest.md`:
31
+ Write triage results to `~/.cache/fit/basecamp/state/librarian_triage.md`:
34
32
 
35
33
  ```
36
- # Knowledge Digest — {YYYY-MM-DD HH:MM}
34
+ # Knowledge Triage — {YYYY-MM-DD HH:MM}
37
35
 
38
36
  ## Pending Processing
39
37
  - {count} unprocessed synced files