@forwardimpact/basecamp 0.3.0 → 1.0.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.
@@ -16,7 +16,6 @@ import {
16
16
  writeFileSync,
17
17
  existsSync,
18
18
  mkdirSync,
19
- readdirSync,
20
19
  unlinkSync,
21
20
  chmodSync,
22
21
  } from "node:fs";
@@ -34,9 +33,24 @@ const STATE_PATH = join(BASECAMP_HOME, "state.json");
34
33
  const LOG_DIR = join(BASECAMP_HOME, "logs");
35
34
  const __dirname =
36
35
  import.meta.dirname || dirname(fileURLToPath(import.meta.url));
37
- const KB_TEMPLATE_DIR = join(__dirname, "template");
36
+ const SHARE_DIR = "/usr/local/share/fit-basecamp";
38
37
  const SOCKET_PATH = join(BASECAMP_HOME, "basecamp.sock");
39
38
 
39
+ // --- posix_spawn (macOS app bundle only) ------------------------------------
40
+
41
+ const USE_POSIX_SPAWN = !!process.env.BASECAMP_BUNDLE;
42
+ let posixSpawn;
43
+ if (USE_POSIX_SPAWN) {
44
+ try {
45
+ posixSpawn = await import("./posix-spawn.js");
46
+ } catch (err) {
47
+ console.error(
48
+ "Failed to load posix-spawn, falling back to child_process:",
49
+ err.message,
50
+ );
51
+ }
52
+ }
53
+
40
54
  let daemonStartedAt = null;
41
55
 
42
56
  // --- Helpers ----------------------------------------------------------------
@@ -79,23 +93,36 @@ function log(msg) {
79
93
  }
80
94
 
81
95
  function findClaude() {
82
- for (const c of [
83
- "claude",
96
+ const paths = [
84
97
  "/usr/local/bin/claude",
85
98
  join(HOME, ".claude", "bin", "claude"),
86
99
  join(HOME, ".local", "bin", "claude"),
87
- ]) {
88
- try {
89
- execSync(`which "${c}" 2>/dev/null || command -v "${c}" 2>/dev/null`, {
90
- encoding: "utf8",
91
- });
92
- return c;
93
- } catch {}
94
- if (existsSync(c)) return c;
95
- }
100
+ "/opt/homebrew/bin/claude",
101
+ ];
102
+ for (const p of paths) if (existsSync(p)) return p;
96
103
  return "claude";
97
104
  }
98
105
 
106
+ /**
107
+ * Detect if running from inside a macOS .app bundle.
108
+ * The binary is at Basecamp.app/Contents/MacOS/fit-basecamp.
109
+ * @returns {{ bundle: string, resources: string } | null}
110
+ */
111
+ function getBundlePath() {
112
+ try {
113
+ const exe = process.execPath || "";
114
+ const macosDir = dirname(exe);
115
+ const contentsDir = dirname(macosDir);
116
+ const resourcesDir = join(contentsDir, "Resources");
117
+ if (existsSync(join(resourcesDir, "config"))) {
118
+ return { bundle: dirname(contentsDir), resources: resourcesDir };
119
+ }
120
+ } catch {
121
+ /* not in bundle */
122
+ }
123
+ return null;
124
+ }
125
+
99
126
  function loadConfig() {
100
127
  return readJSON(CONFIG_PATH, { tasks: {} });
101
128
  }
@@ -172,7 +199,7 @@ function shouldRun(task, taskState, now) {
172
199
 
173
200
  // --- Task execution ---------------------------------------------------------
174
201
 
175
- function runTask(taskName, task, _config, state) {
202
+ async function runTask(taskName, task, _config, state) {
176
203
  if (!task.kb) {
177
204
  log(`Task ${taskName}: no "kb" specified, skipping.`);
178
205
  return;
@@ -201,6 +228,58 @@ function runTask(taskName, task, _config, state) {
201
228
  if (task.agent) spawnArgs.push("--agent", task.agent);
202
229
  spawnArgs.push("-p", prompt);
203
230
 
231
+ // Use posix_spawn when running inside the app bundle for TCC inheritance.
232
+ // Fall back to child_process.spawn for dev mode and other platforms.
233
+ if (posixSpawn) {
234
+ try {
235
+ const { pid, stdoutFd, stderrFd } = posixSpawn.spawn(
236
+ claude,
237
+ spawnArgs,
238
+ undefined,
239
+ kbPath,
240
+ );
241
+
242
+ // Read stdout and stderr concurrently to avoid pipe deadlocks,
243
+ // then wait for the child to exit.
244
+ const [stdout, stderr] = await Promise.all([
245
+ posixSpawn.readAll(stdoutFd),
246
+ posixSpawn.readAll(stderrFd),
247
+ ]);
248
+ const exitCode = await posixSpawn.waitForExit(pid);
249
+
250
+ if (exitCode === 0) {
251
+ log(`Task ${taskName} completed. Output: ${stdout.slice(0, 200)}...`);
252
+ Object.assign(ts, {
253
+ status: "finished",
254
+ startedAt: null,
255
+ lastRunAt: new Date().toISOString(),
256
+ lastError: null,
257
+ runCount: (ts.runCount || 0) + 1,
258
+ });
259
+ } else {
260
+ const errMsg = stderr || stdout || `Exit code ${exitCode}`;
261
+ log(`Task ${taskName} failed: ${errMsg.slice(0, 300)}`);
262
+ Object.assign(ts, {
263
+ status: "failed",
264
+ startedAt: null,
265
+ lastRunAt: new Date().toISOString(),
266
+ lastError: errMsg.slice(0, 500),
267
+ });
268
+ }
269
+ saveState(state);
270
+ } catch (err) {
271
+ log(`Task ${taskName} failed: ${err.message}`);
272
+ Object.assign(ts, {
273
+ status: "failed",
274
+ startedAt: null,
275
+ lastRunAt: new Date().toISOString(),
276
+ lastError: err.message.slice(0, 500),
277
+ });
278
+ saveState(state);
279
+ }
280
+ return;
281
+ }
282
+
204
283
  return new Promise((resolve) => {
205
284
  const child = spawn(claude, spawnArgs, {
206
285
  cwd: kbPath,
@@ -337,29 +416,6 @@ function handleStatusRequest(socket) {
337
416
  });
338
417
  }
339
418
 
340
- function handleRestartRequest(socket) {
341
- send(socket, { type: "ack", command: "restart" });
342
- setTimeout(() => process.exit(0), 100);
343
- }
344
-
345
- function handleRunRequest(socket, taskName) {
346
- if (!taskName) {
347
- send(socket, { type: "error", message: "Missing task name" });
348
- return;
349
- }
350
- const config = loadConfig();
351
- const task = config.tasks[taskName];
352
- if (!task) {
353
- send(socket, { type: "error", message: `Task not found: ${taskName}` });
354
- return;
355
- }
356
- send(socket, { type: "ack", command: "run", task: taskName });
357
- const state = loadState();
358
- runTask(taskName, task, config, state).catch((err) => {
359
- console.error(`[socket] runTask error for ${taskName}:`, err.message);
360
- });
361
- }
362
-
363
419
  function handleMessage(socket, line) {
364
420
  let request;
365
421
  try {
@@ -369,21 +425,32 @@ function handleMessage(socket, line) {
369
425
  return;
370
426
  }
371
427
 
372
- const handlers = {
373
- status: () => handleStatusRequest(socket),
374
- restart: () => handleRestartRequest(socket),
375
- run: () => handleRunRequest(socket, request.task),
376
- };
428
+ if (request.type === "status") return handleStatusRequest(socket);
377
429
 
378
- const handler = handlers[request.type];
379
- if (handler) {
380
- handler();
381
- } else {
382
- send(socket, {
383
- type: "error",
384
- message: `Unknown request type: ${request.type}`,
385
- });
430
+ if (request.type === "run") {
431
+ if (!request.task) {
432
+ send(socket, { type: "error", message: "Missing task name" });
433
+ return;
434
+ }
435
+ const config = loadConfig();
436
+ const task = config.tasks[request.task];
437
+ if (!task) {
438
+ send(socket, {
439
+ type: "error",
440
+ message: `Task not found: ${request.task}`,
441
+ });
442
+ return;
443
+ }
444
+ send(socket, { type: "ack", command: "run", task: request.task });
445
+ const state = loadState();
446
+ runTask(request.task, task, config, state).catch(() => {});
447
+ return;
386
448
  }
449
+
450
+ send(socket, {
451
+ type: "error",
452
+ message: `Unknown request type: ${request.type}`,
453
+ });
387
454
  }
388
455
 
389
456
  function startSocketServer() {
@@ -416,9 +483,6 @@ function startSocketServer() {
416
483
 
417
484
  const cleanup = () => {
418
485
  server.close();
419
- try {
420
- unlinkSync(SOCKET_PATH);
421
- } catch {}
422
486
  process.exit(0);
423
487
  };
424
488
  process.on("SIGTERM", cleanup);
@@ -434,7 +498,7 @@ function daemon() {
434
498
  log("Scheduler daemon started. Polling every 60 seconds.");
435
499
  log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
436
500
  startSocketServer();
437
- runDueTasks();
501
+ runDueTasks().catch((err) => log(`Error: ${err.message}`));
438
502
  setInterval(async () => {
439
503
  try {
440
504
  await runDueTasks();
@@ -446,17 +510,18 @@ function daemon() {
446
510
 
447
511
  // --- Init knowledge base ----------------------------------------------------
448
512
 
449
- function copyDirRecursive(src, dest) {
450
- for (const entry of readdirSync(src, { withFileTypes: true })) {
451
- const s = join(src, entry.name),
452
- d = join(dest, entry.name);
453
- if (entry.isDirectory()) {
454
- ensureDir(d);
455
- copyDirRecursive(s, d);
456
- } else if (!existsSync(d)) {
457
- writeFileSync(d, readFileSync(s));
458
- }
513
+ function findTemplateDir() {
514
+ const bundle = getBundlePath();
515
+ if (bundle) {
516
+ const tpl = join(bundle.resources, "template");
517
+ if (existsSync(tpl)) return tpl;
459
518
  }
519
+ for (const d of [
520
+ join(SHARE_DIR, "template"),
521
+ join(__dirname, "..", "template"),
522
+ ])
523
+ if (existsSync(d)) return d;
524
+ return null;
460
525
  }
461
526
 
462
527
  function initKB(targetPath) {
@@ -465,6 +530,11 @@ function initKB(targetPath) {
465
530
  console.error(`Knowledge base already exists at ${dest}`);
466
531
  process.exit(1);
467
532
  }
533
+ const tpl = findTemplateDir();
534
+ if (!tpl) {
535
+ console.error("Template not found. Reinstall fit-basecamp.");
536
+ process.exit(1);
537
+ }
468
538
 
469
539
  ensureDir(dest);
470
540
  for (const d of [
@@ -472,11 +542,10 @@ function initKB(targetPath) {
472
542
  "knowledge/Organizations",
473
543
  "knowledge/Projects",
474
544
  "knowledge/Topics",
475
- ".claude/skills",
476
545
  ])
477
546
  ensureDir(join(dest, d));
478
547
 
479
- if (existsSync(KB_TEMPLATE_DIR)) copyDirRecursive(KB_TEMPLATE_DIR, dest);
548
+ execSync(`cp -R "${tpl}/." "${dest}/"`);
480
549
 
481
550
  console.log(
482
551
  `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`,
@@ -492,26 +561,23 @@ function showStatus() {
492
561
 
493
562
  const tasks = Object.entries(config.tasks || {});
494
563
  if (tasks.length === 0) {
495
- console.log(
496
- `Tasks: (none configured)\n\nEdit ${CONFIG_PATH} to add tasks.`,
497
- );
564
+ console.log(`No tasks configured.\n\nEdit ${CONFIG_PATH} to add tasks.`);
498
565
  return;
499
566
  }
500
567
 
501
568
  console.log("Tasks:");
502
569
  for (const [name, task] of tasks) {
503
570
  const s = state.tasks[name] || {};
504
- const kbPath = task.kb ? expandPath(task.kb) : null;
505
- const kbStatus = kbPath ? (existsSync(kbPath) ? "" : " (not found)") : "";
506
- const lines = [
507
- ` ${task.enabled !== false ? "+" : "-"} ${name}`,
508
- ` KB: ${task.kb || "(none)"}${kbStatus} Schedule: ${JSON.stringify(task.schedule)}`,
509
- ` Status: ${s.status || "never-run"} Last run: ${s.lastRunAt ? new Date(s.lastRunAt).toLocaleString() : "never"} Runs: ${s.runCount || 0}`,
510
- ];
511
- if (task.agent) lines.push(` Agent: ${task.agent}`);
512
- if (task.skill) lines.push(` Skill: ${task.skill}`);
513
- if (s.lastError) lines.push(` Error: ${s.lastError.slice(0, 80)}`);
514
- console.log(lines.join("\n"));
571
+ const kbStatus =
572
+ task.kb && !existsSync(expandPath(task.kb)) ? " (not found)" : "";
573
+ console.log(
574
+ ` ${task.enabled !== false ? "+" : "-"} ${name}\n` +
575
+ ` KB: ${task.kb || "(none)"}${kbStatus} Schedule: ${JSON.stringify(task.schedule)}\n` +
576
+ ` Status: ${s.status || "never-run"} Last: ${s.lastRunAt ? new Date(s.lastRunAt).toLocaleString() : "never"} Runs: ${s.runCount || 0}` +
577
+ (task.agent ? `\n Agent: ${task.agent}` : "") +
578
+ (task.skill ? `\n Skill: ${task.skill}` : "") +
579
+ (s.lastError ? `\n Error: ${s.lastError.slice(0, 80)}` : ""),
580
+ );
515
581
  }
516
582
  }
517
583
 
@@ -544,7 +610,7 @@ function validate() {
544
610
  }
545
611
  const kbPath = expandPath(task.kb);
546
612
  if (!existsSync(kbPath)) {
547
- console.log(` [FAIL] ${name}: path does not exist: ${kbPath}`);
613
+ console.log(` [FAIL] ${name}: path not found: ${kbPath}`);
548
614
  errors++;
549
615
  continue;
550
616
  }
@@ -559,25 +625,17 @@ function validate() {
559
625
  ? join("agents", sub.endsWith(".md") ? sub : sub + ".md")
560
626
  : join("skills", sub, "SKILL.md");
561
627
  const found = findInLocalOrGlobal(kbPath, relPath);
562
- if (found) {
563
- console.log(` [OK] ${name}: ${kind} "${sub}" found at ${found}`);
564
- } else {
565
- console.log(
566
- ` [FAIL] ${name}: ${kind} "${sub}" not found in ${join(kbPath, ".claude", relPath)} or ${join(HOME, ".claude", relPath)}`,
567
- );
568
- errors++;
569
- }
628
+ console.log(
629
+ ` [${found ? "OK" : "FAIL"}] ${name}: ${kind} "${sub}"${found ? "" : " not found"}`,
630
+ );
631
+ if (!found) errors++;
570
632
  }
571
633
 
572
634
  if (!task.agent && !task.skill)
573
635
  console.log(` [OK] ${name}: no agent or skill to validate`);
574
636
  }
575
637
 
576
- console.log(
577
- errors > 0
578
- ? `\nValidation failed: ${errors} error(s) found.`
579
- : "\nAll tasks validated successfully.",
580
- );
638
+ console.log(errors > 0 ? `\n${errors} error(s).` : "\nAll OK.");
581
639
  if (errors > 0) process.exit(1);
582
640
  }
583
641
 
@@ -586,7 +644,7 @@ function validate() {
586
644
  function showHelp() {
587
645
  const bin = "fit-basecamp";
588
646
  console.log(`
589
- Basecamp Scheduler — Run scheduled tasks across multiple knowledge bases.
647
+ Basecamp — Run scheduled Claude tasks across knowledge bases.
590
648
 
591
649
  Usage:
592
650
  ${bin} Run due tasks once and exit
@@ -595,28 +653,10 @@ Usage:
595
653
  ${bin} --init <path> Initialize a new knowledge base
596
654
  ${bin} --validate Validate agents and skills exist
597
655
  ${bin} --status Show task status
598
- ${bin} --help Show this help
599
656
 
600
657
  Config: ~/.fit/basecamp/scheduler.json
601
658
  State: ~/.fit/basecamp/state.json
602
659
  Logs: ~/.fit/basecamp/logs/
603
-
604
- Config format:
605
- {
606
- "tasks": {
607
- "sync-mail": {
608
- "kb": "~/Documents/Personal",
609
- "schedule": { "type": "interval", "minutes": 5 },
610
- "prompt": "Sync Apple Mail.", "skill": "sync-apple-mail",
611
- "agent": null, "enabled": true
612
- }
613
- }
614
- }
615
-
616
- Schedule types:
617
- interval: { "type": "interval", "minutes": 5 }
618
- cron: { "type": "cron", "expression": "0 8 * * *" }
619
- once: { "type": "once", "runAt": "2025-02-12T10:00:00Z" }
620
660
  `);
621
661
  }
622
662
 
@@ -1,10 +1,47 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
3
  "permissions": {
4
+ "defaultMode": "acceptEdits",
4
5
  "allow": [
6
+ "Bash(python3 *)",
7
+ "Bash(sqlite3 *)",
8
+ "Bash(bash scripts/*)",
9
+ "Bash(sh scripts/*)",
10
+ "Bash(node *)",
11
+ "Bash(npm install *)",
12
+ "Bash(npx *)",
13
+ "Bash(rg *)",
14
+ "Bash(find *)",
15
+ "Bash(cat *)",
16
+ "Bash(head *)",
17
+ "Bash(tail *)",
18
+ "Bash(ls *)",
19
+ "Bash(ls)",
20
+ "Bash(grep *)",
21
+ "Bash(echo *)",
22
+ "Bash(date *)",
23
+ "Bash(mkdir *)",
24
+ "Bash(mv *)",
25
+ "Bash(cp *)",
26
+ "Bash(touch *)",
27
+ "Bash(sort *)",
28
+ "Bash(wc *)",
29
+ "Bash(cut *)",
30
+ "Bash(tr *)",
31
+ "Bash(uniq *)",
32
+ "Bash(awk *)",
33
+ "Bash(sed *)",
34
+ "Bash(basename *)",
35
+ "Bash(dirname *)",
36
+ "Bash(realpath *)",
37
+ "Bash(pwd)",
38
+ "Bash(which *)",
39
+ "Bash(textutil *)",
40
+ "Bash(pdftotext *)",
5
41
  "Read(~/Library/Mail/**)",
6
42
  "Read(~/Library/Group Containers/group.com.apple.calendar/**)",
7
43
  "Read(~/Library/Calendars/**)",
44
+ "Read(~/Library/Application Support/hyprnote/**)",
8
45
  "Read(~/Desktop/**)",
9
46
  "Read(~/Documents/**)",
10
47
  "Read(~/Downloads/**)",
@@ -15,6 +52,17 @@
15
52
  "Edit(~/.cache/fit/basecamp/**)"
16
53
  ],
17
54
  "deny": [
55
+ "Bash(curl *)",
56
+ "Bash(wget *)",
57
+ "Bash(open *)",
58
+ "Bash(osascript *)",
59
+ "Bash(sudo *)",
60
+ "Bash(rm -rf *)",
61
+ "Bash(chmod *)",
62
+ "Bash(chown *)",
63
+ "Bash(killall *)",
64
+ "Bash(launchctl *)",
65
+ "Bash(brew *)",
18
66
  "Edit(~/Library/**)",
19
67
  "Read(~/Pictures/**)",
20
68
  "Read(~/Music/**)",
@@ -31,6 +79,7 @@
31
79
  "~/Library/Mail",
32
80
  "~/Library/Group Containers/group.com.apple.calendar",
33
81
  "~/Library/Calendars",
82
+ "~/Library/Application Support/hyprnote",
34
83
  "~/Desktop",
35
84
  "~/Documents",
36
85
  "~/Downloads",
@@ -16,13 +16,15 @@ Run when the user asks to draft, reply to, or respond to an email.
16
16
  ## Prerequisites
17
17
 
18
18
  - Knowledge base populated (from `extract-entities` skill)
19
- - Synced email data in `~/.cache/fit/basecamp/apple_mail/`
19
+ - Synced email data in `~/.cache/fit/basecamp/apple_mail/` or
20
+ `~/.cache/fit/basecamp/gmail/`
20
21
 
21
22
  ## Inputs
22
23
 
23
24
  - `knowledge/People/*.md` — person context
24
25
  - `knowledge/Organizations/*.md` — organization context
25
- - `~/.cache/fit/basecamp/apple_mail/*.md` email threads
26
+ - `~/.cache/fit/basecamp/apple_mail/*.md` or `~/.cache/fit/basecamp/gmail/*.md`
27
+ — email threads
26
28
  - `~/.cache/fit/basecamp/apple_calendar/*.json` — calendar events (for
27
29
  scheduling)
28
30
  - `drafts/last_processed` — timestamp of last processing run
@@ -116,7 +118,7 @@ cat "knowledge/Organizations/Company Name.md"
116
118
  **Calendar (for scheduling emails):**
117
119
 
118
120
  ```bash
119
- ls ~/.cache/fit/basecamp/apple_calendar/ 2>/dev/null
121
+ ls ~/.cache/fit/basecamp/apple_calendar/ ~/.cache/fit/basecamp/google_calendar/ 2>/dev/null
120
122
  cat "$HOME/.cache/fit/basecamp/apple_calendar/event123.json"
121
123
  ```
122
124
 
@@ -184,6 +186,33 @@ date -u '+%Y-%m-%dT%H:%M:%SZ' > drafts/last_processed
184
186
  - {id}: {subject} — {reason}
185
187
  ```
186
188
 
189
+ ## Recruitment & Staffing Emails
190
+
191
+ **CRITICAL: Candidates must NEVER be copied on internal emails about them.**
192
+
193
+ When an email involves recruitment, staffing, or hiring:
194
+
195
+ 1. **Identify the candidate** — Determine who the candidate is from the email
196
+ thread and knowledge base (`knowledge/Candidates/`, `knowledge/People/`)
197
+ 2. **Strip the candidate from recipients** — The draft must ONLY be addressed to
198
+ internal stakeholders (hiring managers, recruiters, interview panel, etc.).
199
+ The candidate’s email address must NOT appear in To, CC, or BCC
200
+ 3. **Only recruiters email candidates directly** — If the email is a direct
201
+ reply TO a candidate (e.g., scheduling an interview, extending an offer),
202
+ flag it clearly so only the recruiter sends it. Add a note:
203
+ `⚠️ RECRUITER ONLY — This email goes directly to the candidate.`
204
+
205
+ **Examples of internal recruitment emails (candidate must NOT be copied):**
206
+
207
+ - Interview feedback or debrief
208
+ - Candidate evaluation or comparison
209
+ - Hiring decision discussions
210
+ - Compensation/offer discussions
211
+ - Reference check follow-ups between colleagues
212
+
213
+ **When in doubt:** If an email thread mentions a candidate by name and involves
214
+ multiple internal recipients, treat it as internal and exclude the candidate.
215
+
187
216
  ## Constraints
188
217
 
189
218
  - Never actually send emails — only create drafts
@@ -3,14 +3,15 @@
3
3
  #
4
4
  # Usage: bash scripts/scan-emails.sh
5
5
  #
6
- # Checks ~/.cache/fit/basecamp/apple_mail/ for email files
7
- # not yet listed in drafts/drafted or drafts/ignored.
6
+ # Checks ~/.cache/fit/basecamp/apple_mail/ for email files not yet
7
+ # listed in drafts/drafted or drafts/ignored.
8
8
  # Outputs tab-separated: email_id<TAB>subject
9
9
 
10
10
  set -euo pipefail
11
11
 
12
12
  MAIL_DIRS=(
13
13
  "$HOME/.cache/fit/basecamp/apple_mail"
14
+ "$HOME/.cache/fit/basecamp/gmail"
14
15
  )
15
16
 
16
17
  for dir in "${MAIL_DIRS[@]}"; do
@@ -264,7 +264,6 @@ person?"** test:
264
264
  - Transactional service providers (bank employees, support reps)
265
265
  - One-time administrative contacts
266
266
  - Large group meeting attendees you didn't interact with
267
- - Internal colleagues (@user.domain)
268
267
  - Assistants handling only logistics
269
268
 
270
269
  For people who don't get their own note, add to the Organization note's
@@ -10,6 +10,7 @@ Templates for creating new knowledge base notes.
10
10
  ## Info
11
11
  **Role:** {role or inferred role with qualifier}
12
12
  **Organization:** [[Organizations/{organization}]]
13
+ **Reports to:** [[People/{{Person}}]]
13
14
  **Email:** {email}
14
15
  **Aliases:** {comma-separated variants}
15
16
  **First met:** {YYYY-MM-DD}