@forwardimpact/outpost 3.12.1 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/package.json +3 -2
  2. package/src/kb-manager.js +58 -5
  3. package/src/kb-validator.js +762 -0
  4. package/src/outpost.js +73 -8
  5. package/templates/.claude/agents/chief-of-staff.md +14 -6
  6. package/templates/.claude/agents/concierge.md +9 -2
  7. package/templates/.claude/agents/head-hunter.md +9 -2
  8. package/templates/.claude/agents/librarian.md +11 -4
  9. package/templates/.claude/agents/postman.md +9 -2
  10. package/templates/.claude/agents/recruiter.md +9 -2
  11. package/templates/.claude/skills/anarlog-follow/SKILL.md +53 -29
  12. package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
  13. package/templates/.claude/skills/anarlog-process/SKILL.md +88 -45
  14. package/templates/.claude/skills/anarlog-process/references/extraction.md +19 -11
  15. package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
  16. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +404 -163
  17. package/templates/.claude/skills/candidate-report/SKILL.md +12 -9
  18. package/templates/.claude/skills/changelog/SKILL.md +106 -56
  19. package/templates/.claude/skills/deck-create/SKILL.md +60 -2
  20. package/templates/.claude/skills/deck-review/SKILL.md +4 -1
  21. package/templates/.claude/skills/deck-summarize/SKILL.md +6 -2
  22. package/templates/.claude/skills/deck-summarize/references/brief-template.md +1 -1
  23. package/templates/.claude/skills/doc-collab/SKILL.md +16 -11
  24. package/templates/.claude/skills/doc-create/SKILL.md +6 -3
  25. package/templates/.claude/skills/draft-emails/SKILL.md +31 -25
  26. package/templates/.claude/skills/draft-emails/references/template.md +1 -1
  27. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +14 -8
  28. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +9 -3
  29. package/templates/.claude/skills/extract-entities/SKILL.md +17 -16
  30. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +2 -1
  31. package/templates/.claude/skills/extract-entities/references/conditions.md +6 -5
  32. package/templates/.claude/skills/extract-entities/references/links.md +34 -10
  33. package/templates/.claude/skills/extract-entities/references/recruitment.md +15 -10
  34. package/templates/.claude/skills/extract-entities/references/resolution.md +1 -1
  35. package/templates/.claude/skills/extract-entities/references/sources.md +1 -1
  36. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +10 -4
  37. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
  38. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +8 -2
  39. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
  40. package/templates/.claude/skills/meeting-prep/SKILL.md +23 -18
  41. package/templates/.claude/skills/organize-files/SKILL.md +3 -0
  42. package/templates/.claude/skills/person-identify/SKILL.md +58 -10
  43. package/templates/.claude/skills/person-identify/scripts/identify.sh +110 -11
  44. package/templates/.claude/skills/person-lookup/SKILL.md +8 -3
  45. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +27 -1
  46. package/templates/.claude/skills/req-assess/SKILL.md +19 -11
  47. package/templates/.claude/skills/req-assess/references/interview-template.md +6 -1
  48. package/templates/.claude/skills/req-assess/references/panel-template.md +5 -1
  49. package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
  50. package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
  51. package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
  52. package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
  53. package/templates/.claude/skills/req-decide/SKILL.md +26 -18
  54. package/templates/.claude/skills/req-decide/references/template.md +2 -2
  55. package/templates/.claude/skills/req-forget/SKILL.md +39 -22
  56. package/templates/.claude/skills/req-forget/references/classify.md +12 -10
  57. package/templates/.claude/skills/req-forget/references/locations.md +27 -22
  58. package/templates/.claude/skills/req-forget/references/report-template.md +23 -15
  59. package/templates/.claude/skills/req-scan/SKILL.md +6 -3
  60. package/templates/.claude/skills/req-scan/references/template.md +11 -1
  61. package/templates/.claude/skills/req-screen/SKILL.md +78 -34
  62. package/templates/.claude/skills/req-screen/references/rubric.md +38 -42
  63. package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
  64. package/templates/.claude/skills/req-screen/references/template.md +26 -14
  65. package/templates/.claude/skills/req-track/SKILL.md +44 -44
  66. package/templates/.claude/skills/req-track/references/fields.md +18 -15
  67. package/templates/.claude/skills/req-track/references/overlays.md +42 -0
  68. package/templates/.claude/skills/req-track/references/signals.md +2 -2
  69. package/templates/.claude/skills/req-track/references/templates.md +28 -27
  70. package/templates/.claude/skills/req-workday/SKILL.md +49 -20
  71. package/templates/.claude/skills/req-workday/references/brief.md +81 -0
  72. package/templates/.claude/skills/req-workday/references/templates.md +6 -73
  73. package/templates/.claude/skills/req-workday/references/xlsx-format.md +19 -0
  74. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +96 -20
  75. package/templates/.claude/skills/send-chat/SKILL.md +11 -11
  76. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +3 -0
  77. package/templates/.claude/skills/sync-apple-mail/SKILL.md +3 -0
  78. package/templates/.claude/skills/sync-teams/SKILL.md +3 -0
  79. package/templates/.claude/skills/upstream-instructions/SKILL.md +8 -5
  80. package/templates/.claude/skills/upstream-instructions/references/examples.md +3 -3
  81. package/templates/CLAUDE.md +96 -87
  82. package/templates/MIGRATION.md +359 -0
  83. package/templates/registry.yaml +25 -0
  84. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
  85. package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -186
@@ -3,6 +3,12 @@
3
3
  ## Projects
4
4
 
5
5
  ```markdown
6
+ ---
7
+ type: project
8
+ created: {YYYY-MM-DD}
9
+ updated: {YYYY-MM-DD}
10
+ ---
11
+
6
12
  # {Project Name}
7
13
 
8
14
  ## Info
@@ -15,17 +21,17 @@
15
21
  {2-3 sentences}
16
22
 
17
23
  ## Priorities
18
- - [[Priorities/{Priority}]] — {how this project contributes}
24
+ - [[3-Team/Priorities/{Priority}]] — {how this project contributes}
19
25
 
20
26
  ## People
21
- - [[People/{Person}]] — {role}
27
+ - [[3-Team/People/{Person}]] — {role}
22
28
 
23
29
  ## Organizations
24
- - [[Organizations/{Org}]] — {relationship}
30
+ - [[3-Team/Organizations/{Org}]] — {relationship}
25
31
 
26
32
  ## Related
27
- - [[Priorities/{Priority}]] — {relationship}
28
- - [[Topics/{Topic}]] — {relationship}
33
+ - [[3-Team/Priorities/{Priority}]] — {relationship}
34
+ - [[3-Team/Topics/{Topic}]] — {relationship}
29
35
 
30
36
  ## Timeline
31
37
  **{YYYY-MM-DD}** ({type})
@@ -42,6 +48,12 @@
42
48
  ## Topics
43
49
 
44
50
  ```markdown
51
+ ---
52
+ type: topic
53
+ created: {YYYY-MM-DD}
54
+ updated: {YYYY-MM-DD}
55
+ ---
56
+
45
57
  # {Topic Name}
46
58
 
47
59
  ## About
@@ -53,8 +65,8 @@
53
65
  **Last mentioned:** {YYYY-MM-DD}
54
66
 
55
67
  ## Related
56
- - [[People/{Person}]] — {relationship}
57
- - [[Organizations/{Org}]] — {relationship}
68
+ - [[3-Team/People/{Person}]] — {relationship}
69
+ - [[3-Team/Organizations/{Org}]] — {relationship}
58
70
 
59
71
  ## Log
60
72
  **{YYYY-MM-DD}** ({type}: {title})
@@ -5,6 +5,9 @@ description: Prepare for meetings. Gather context from the knowledge base and ca
5
5
 
6
6
  # Meeting Prep
7
7
 
8
+ Write tier: none (the output is personal `Briefings/`)
9
+ Frontmatter: none
10
+
8
11
  Help the user prepare for meetings. Gather context from the knowledge base and
9
12
  calendar. Create personalized briefing documents with attendee history, open
10
13
  items, and suggested talking points.
@@ -22,14 +25,16 @@ meetings.
22
25
  ## Inputs
23
26
 
24
27
  - `~/.cache/fit/outpost/apple_calendar/*.json` — calendar events
25
- - `Knowledge/People/*.md` — attendee context
26
- - `Knowledge/Organizations/*.md` — company context
27
- - `Knowledge/Projects/*.md` — project context
28
- - `Knowledge/Priorities/*.md` — active priorities and strategic context to
28
+ - `3-Team/People/*.md` — attendee context
29
+ - `3-Team/Organizations/*.md` — company context
30
+ - `3-Team/Projects/*.md` — project context
31
+ - `3-Team/Priorities/*.md` — active priorities and strategic context to
29
32
  frame the brief
30
- - `Knowledge/Candidates/*/brief.md` — candidate context (for interview meetings)
31
- - `Knowledge/Roles/*.md` — role/requisition context (for interview meetings).
32
- Check the `**Status:**` field to distinguish active from historical reqs
33
+ - `2-Confidential/Candidates/*/brief.md` — candidate context (for interview
34
+ meetings)
35
+ - `2-Confidential/Roles/*.md` role/requisition context (for interview
36
+ meetings). Check the `**Status:**` field to distinguish active from historical
37
+ reqs
33
38
 
34
39
  ## Outputs
35
40
 
@@ -45,8 +50,8 @@ knowledge base.**
45
50
  When the user asks to prep for a meeting:
46
51
 
47
52
  1. **STOP** — Do not create a generic brief
48
- 2. **SEARCH** — Look up each attendee: `rg -l "Attendee Name" Knowledge/`
49
- 3. **READ** — Read their notes: `cat "Knowledge/People/Attendee Name.md"`
53
+ 2. **SEARCH** — Look up each attendee: `rg -l "Attendee Name" [0-9]-*/`
54
+ 3. **READ** — Read their notes: `cat "3-Team/People/Attendee Name.md"`
50
55
  4. **UNDERSTAND** — Extract role, organization, history, open items
51
56
  5. **THEN BRIEF** — Create the meeting brief with this context
52
57
 
@@ -93,16 +98,16 @@ Extract: summary, start/end time, attendees (names and emails), description.
93
98
  For each attendee:
94
99
 
95
100
  ```bash
96
- rg -l "attendee_name" Knowledge/People/
97
- rg -l "attendee_email" Knowledge/People/
98
- cat "Knowledge/People/Attendee Name.md"
99
- cat "Knowledge/Organizations/Their Company.md"
100
- rg -l "attendee_name" Knowledge/Projects/
101
+ rg -l "attendee_name" 3-Team/People/
102
+ rg -l "attendee_email" 3-Team/People/
103
+ cat "3-Team/People/Attendee Name.md"
104
+ cat "3-Team/Organizations/Their Company.md"
105
+ rg -l "attendee_name" 3-Team/Projects/
101
106
  ```
102
107
 
103
108
  Extract: role/title, company, key facts, previous interactions, open items.
104
109
 
105
- Also check `Knowledge/Priorities/` for context relevant to the meeting topic.
110
+ Also check `3-Team/Priorities/` for context relevant to the meeting topic.
106
111
  For example, if the meeting is about hiring, surface the relevant hiring
107
112
  Priority's status and progress.
108
113
 
@@ -143,11 +148,11 @@ Suggested Talking Points
143
148
 
144
149
  When you prepare for interview meetings (title contains "Interview",
145
150
  "Screening", "Decomposition", "Panel", or a candidate name from
146
- `Knowledge/Candidates/`):
151
+ `2-Confidential/Candidates/`):
147
152
 
148
- 1. **Read the candidate brief:** `Knowledge/Candidates/{Name}/brief.md`
153
+ 1. **Read the candidate brief:** `2-Confidential/Candidates/{Name}/brief.md`
149
154
  2. **Read the Role file:** Look up the `Req` field and read the corresponding
150
- `Knowledge/Roles/*.md` file. Check the `**Status:**` field for context.
155
+ `2-Confidential/Roles/*.md` file. Check the `**Status:**` field for context.
151
156
  3. **Include in the briefing:**
152
157
  - Candidate's current status, skills, and screening recommendation
153
158
  - Role context: hiring manager, domain lead, remaining positions
@@ -6,6 +6,9 @@ compatibility: Requires macOS filesystem access
6
6
 
7
7
  # Organize Files
8
8
 
9
+ Write tier: none (delegates graph writes to extract-entities)
10
+ Frontmatter: none
11
+
9
12
  Organize, tidy up, and find files in `~/Desktop/` and `~/Downloads/`. Always
10
13
  preview changes before you act. Never delete without explicit confirmation.
11
14
 
@@ -1,10 +1,13 @@
1
1
  ---
2
2
  name: person-identify
3
- description: Look up the current user's identity (real name, company, job title, department, email, employee ID, and manager) from the corporate directory through LDAP. The bind uses the existing Kerberos ticket. Use to establish who the knowledge base belongs to, when CLAUDE.md needs the user's identity, or when the user asks "who am I" / for their own directory record. To look up someone *other* than the current user, use the sibling `person-lookup` skill instead.
3
+ description: Look up the current user's identity (real name, company, job title, department, email, employee ID, manager, and direct reports) from the corporate directory through LDAP. The bind uses the existing Kerberos ticket. Captures the user's org edges — manager and direct reports — so the KB can resolve who "our team" is. Use to establish who the knowledge base belongs to, when CLAUDE.md needs the user's identity, or when the user asks "who am I" / for their own directory record. To look up someone *other* than the current user, use the sibling `person-lookup` skill instead.
4
4
  ---
5
5
 
6
6
  # Person Identify
7
7
 
8
+ Write tier: none (cache only)
9
+ Frontmatter: none
10
+
8
11
  Resolve the current user's identity from the corporate Active Directory over
9
12
  LDAP. This is the canonical way to establish **who the knowledge base belongs
10
13
  to**. It replaces any static identity file. Results reflect the live
@@ -12,9 +15,10 @@ directory, so the skill picks up a job change or a reorg automatically.
12
15
 
13
16
  ## Trigger
14
17
 
15
- - CLAUDE.md (or another skill) needs the current user's identity.
18
+ - CLAUDE.md (or another skill) needs the current user's identity, or needs to
19
+ resolve who "our team" is.
16
20
  - The user asks "who am I", for their own directory record, or for their
17
- employee ID, title, department, or manager.
21
+ employee ID, title, department, manager, or direct reports.
18
22
 
19
23
  ## Prerequisites
20
24
 
@@ -33,8 +37,8 @@ password. The bind uses SASL/GSSAPI against the existing ticket.
33
37
  bash .claude/skills/person-identify/scripts/identify.sh
34
38
  ```
35
39
 
36
- This prints the user's directory record, resolves the manager to a name, and
37
- writes the result to the identity cache (below).
40
+ This prints the user's directory record, resolves the manager and each direct
41
+ report to a name, and writes the result to the identity cache (below).
38
42
 
39
43
  ## Identity cache
40
44
 
@@ -42,14 +46,25 @@ The script writes `~/.cache/fit/outpost/state/identity.md`. That file is the
42
46
  **canonical identity source** for the rest of the knowledge base. It replaces
43
47
  the old static `USER.md`. It is auto-generated markdown with `Name`, `Email`,
44
48
  and `Domain` fields (plus title, department, company, employee ID, office, and
45
- manager):
49
+ the user's two org edges — `Manager` and `Direct reports`):
46
50
 
47
51
  ```markdown
48
52
  - **Name:** Jane Doe
49
53
  - **Email:** jane.doe@example.com
50
54
  - **Domain:** example.com
55
+ - **Manager:** Roe, Richard
56
+ - **Direct reports:**
57
+ - Chen, Sarah
58
+ - Okafor, Ada
51
59
  ```
52
60
 
61
+ The **Manager** and **Direct reports** edges let the KB resolve who "our team"
62
+ is — see the `## User Identity & Team` section of `CLAUDE.md` for the rule.
63
+ `Direct reports` is always written: a list when the user manages people, or
64
+ the explicit sentinel `- **Direct reports:** none` for an individual
65
+ contributor. Reports are name-sorted so re-runs of the synced cache diff
66
+ cleanly.
67
+
53
68
  Other skills (e.g. `extract-entities`, `anarlog-process`, `req-track`,
54
69
  `req-workday`, `candidate-report`, `sync-teams`) read this file for the user's
55
70
  name/email/domain. They use it for self-exclusion and author attribution. They
@@ -74,12 +89,29 @@ dc=$(dig +short SRV "_ldap._tcp.dc._msdcs.$dom" | awk 'NR==1{print $4}' | sed 's
74
89
 
75
90
  # 4. Look up the current user (GSSAPI = existing ticket, no password)
76
91
  ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "ldap://$dc" -b "$base" \
77
- "(sAMAccountName=$user)" displayName company title department employeeID mail manager
92
+ "(sAMAccountName=$user)" displayName company title department employeeID mail \
93
+ manager directReports
78
94
  ```
79
95
 
80
- The `manager` attribute is a DN that may live in another domain. So the script
81
- resolves it against the **Global Catalog** (port 3268). The Global Catalog is
82
- forest-wide.
96
+ Both org edges come back as DNs that may live in another domain. So the script
97
+ resolves them against the **Global Catalog** (port 3268, forest-wide) through
98
+ one shared `dn_name` helper. `manager` is single-valued. `directReports` is
99
+ the multi-valued back-link of the same edge, so the script pulls every value
100
+ (not just the first), resolves each to a name, and sorts the list. The
101
+ resolver decodes the base64 `displayName:: …` form (used for accented names)
102
+ and retries past the directory's occasional attribute-less response under
103
+ load.
104
+
105
+ **Inactive reports are filtered from the roster.** AD does not clear the
106
+ `manager` edge when an account is offboarded — it just moves the account to a
107
+ deprovisioned OU — so the user's `directReports` back-link keeps counting
108
+ ex-reports as phantom team members. Reports resolve through `dn_report`, which
109
+ drops a DN when either (1) it sits under the obsolete OU
110
+ (`INACTIVE_OU_PATTERN`, a config knob at the top of the script; `OU=Obsolete`
111
+ by default, empty to disable), or (2) the account is disabled in AD —
112
+ `userAccountControl` bit `0x2` (`ACCOUNTDISABLE`), fetched in the same GC
113
+ query as the name. The manager edge stays unfiltered: a disabled manager is a
114
+ signal worth surfacing, not hiding.
83
115
 
84
116
  ## Output
85
117
 
@@ -95,6 +127,8 @@ Key attributes returned (names per Active Directory schema):
95
127
  | `mail` | Email address |
96
128
  | `physicalDeliveryOfficeName`| Office / location |
97
129
  | `manager` | Manager (DN → resolved to name) |
130
+ | `directReports` | Direct reports (multi-valued DN back-link → each resolved to a name; offboarded/disabled accounts filtered out) |
131
+ | `userAccountControl` | Fetched per report to drop disabled accounts (bit `0x2`) |
98
132
 
99
133
  ## Notes
100
134
 
@@ -102,5 +136,19 @@ Key attributes returned (names per Active Directory schema):
102
136
  free-text input (email or name). It searches the Global Catalog forest-wide
103
137
  (`ldap://$dc:3268 -b ''`). It handles multiple matches. It does **not** touch
104
138
  the identity cache.
139
+ - `directReports` is the back-link stored on the user's own home-domain
140
+ record, so a report whose account lives in *another* forest domain may be
141
+ omitted. For a single-domain manager (the common case) it is complete. If
142
+ cross-domain reports matter, swap it for a forest-wide reverse search over
143
+ the GC — `(manager=<userDN>)`, where the user's DN is the `dn:` line of the
144
+ fetched record — feeding the same `dn_report` resolver.
145
+ - **Offboarded reports linger in AD.** The `directReports` back-link keeps
146
+ counting a report after they leave, because offboarding moves the account to
147
+ a deprovisioned OU without clearing its `manager` edge. `dn_report` filters
148
+ these out (obsolete-OU DN, or a disabled `userAccountControl`). Tune
149
+ `INACTIVE_OU_PATTERN` at the top of `scripts/identify.sh` to your
150
+ directory's convention — it mirrors `person-lookup`'s `VENDOR_OU_PATTERN`
151
+ knob. The real fix is upstream (clear the manager edge at offboarding); the
152
+ filter shields the KB until then.
105
153
  - Active Directory is not required. The same `ldapsearch -Y GSSAPI` shape works
106
154
  against any Kerberos-backed LDAP directory. Only the attribute names differ.
@@ -12,6 +12,15 @@ set -u
12
12
 
13
13
  CACHE="$HOME/.cache/fit/outpost/state/identity.md"
14
14
 
15
+ # Org-specific: substring of the DN's OU under which offboarded/deprovisioned
16
+ # accounts are parked. AD keeps the `manager` edge on such accounts when they
17
+ # are moved here at offboarding, so the user's `directReports` back-link still
18
+ # counts them — surfacing ex-reports as phantom team members. Reports whose DN
19
+ # matches this pattern are dropped. Set to your directory's convention (empty
20
+ # disables the OU filter; disabled-account filtering via userAccountControl
21
+ # still applies). `OU=Obsolete` is a common AD default.
22
+ INACTIVE_OU_PATTERN="OU=Obsolete"
23
+
15
24
  # 1. Take the user principal from the Kerberos ticket: USER@REALM.EXAMPLE.COM
16
25
  princ=$(klist 2>/dev/null | sed -n 's/.*[Pp]rincipal: *//p' | head -1)
17
26
  if [ -z "$princ" ]; then
@@ -34,15 +43,24 @@ if [ -z "$dc" ]; then
34
43
  fi
35
44
 
36
45
  # 4. Look up the current user's record (GSSAPI = use the existing ticket, no password)
37
- attrs="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName manager"
46
+ attrs="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName manager directReports"
38
47
  rec=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "ldap://$dc" -b "$base" "(sAMAccountName=$user)" $attrs 2>/dev/null)
39
48
  if [ -z "$rec" ]; then
40
49
  echo "No directory record found for '$user' under $base on $dc." >&2
41
50
  exit 1
42
51
  fi
43
52
 
44
- # Pull a single attribute value out of the LDIF record.
45
- field() { printf '%s\n' "$rec" | sed -n "s/^$1: //p" | head -1; }
53
+ # Pull a single attribute value out of the LDIF record. Decode the base64
54
+ # `attr:: <b64>` form that LDIF uses for any non-ASCII value (accented names,
55
+ # and DNs whose CN is accented — e.g. the manager attribute).
56
+ field() { # $1=attr
57
+ local attr="$1" line
58
+ line=$(printf '%s\n' "$rec" | grep -m1 -E "^$attr:: ?|^$attr: ")
59
+ case "$line" in
60
+ "$attr:: "*) printf '%s' "${line#"$attr":: }" | base64 -D 2>/dev/null ;;
61
+ "$attr: "*) printf '%s' "${line#"$attr": }" ;;
62
+ esac
63
+ }
46
64
 
47
65
  disp=$(field displayName); gn=$(field givenName); sn=$(field sn)
48
66
  company=$(field company); title=$(field title); dept=$(field department)
@@ -54,14 +72,85 @@ name="$gn $sn"; name=$(printf '%s' "$name" | sed 's/^ *//;s/ *$//')
54
72
  [ -z "$name" ] && name="$disp"
55
73
  domain=$(printf '%s' "$mail" | sed 's/.*@//' | tr '[:upper:]' '[:lower:]')
56
74
 
57
- # 5. Resolve the manager DN to a name, if present. The manager may live in
58
- # another domain. Query the Global Catalog (port 3268). It is forest-wide.
59
- mgr=$(field manager)
60
- mgr_name=""
61
- if [ -n "$mgr" ]; then
62
- mgr_name=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "ldap://$dc:3268" -b "$mgr" -s base \
63
- displayName 2>/dev/null | sed -n 's/^displayName: //p' | head -1)
64
- fi
75
+ # 5. Resolve the user's org edges the manager (up) and the direct reports
76
+ # (down) — to names. Both are DNs that may live in another domain, so
77
+ # resolve against the Global Catalog (port 3268). It is forest-wide.
78
+ gc="ldap://$dc:3268"
79
+
80
+ # One shared DN -> displayName resolver. Decodes the base64 `displayName:: <b64>`
81
+ # form (accented names are encoded that way) and retries past the directory's
82
+ # occasional attribute-less response under load, mirroring person-lookup.
83
+ dn_name() { # $1=DN -> displayName ("" if unresolvable)
84
+ local dn="$1" line tries=0
85
+ [ -z "$dn" ] && return 0
86
+ while [ "$tries" -lt 4 ]; do
87
+ line=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "$gc" -b "$dn" -s base \
88
+ displayName 2>/dev/null | grep -m1 -E '^displayName:: ?|^displayName: ')
89
+ case "$line" in
90
+ "displayName:: "*) printf '%s' "${line#displayName:: }" | base64 -D 2>/dev/null; return 0 ;;
91
+ "displayName: "*) printf '%s' "${line#displayName: }"; return 0 ;;
92
+ esac
93
+ tries=$((tries + 1)); sleep "$tries"
94
+ done
95
+ }
96
+
97
+ # Resolve a direct-report DN to a name, but ONLY if the account is still active.
98
+ # A report is inactive — and therefore dropped from the team roster — when either:
99
+ # 1. its DN sits under the obsolete/deprovisioned OU ($INACTIVE_OU_PATTERN), or
100
+ # 2. its account is disabled in AD (userAccountControl bit 0x2, ACCOUNTDISABLE).
101
+ # Offboarding leaves the `manager` edge dangling, so without this filter ex-reports
102
+ # linger in the roster forever. Fetches displayName + userAccountControl together,
103
+ # reusing dn_name's base64 decode and retry-under-load behaviour. Prints the name,
104
+ # or nothing to skip the report.
105
+ dn_report() { # $1=DN -> displayName if active, else ""
106
+ local dn="$1" out line uac tries=0
107
+ [ -z "$dn" ] && return 0
108
+ # (1) Obsolete-OU filter — case-insensitive substring match on the DN.
109
+ if [ -n "$INACTIVE_OU_PATTERN" ]; then
110
+ if printf '%s' "$dn" | grep -qiF "$INACTIVE_OU_PATTERN"; then return 0; fi
111
+ fi
112
+ while [ "$tries" -lt 4 ]; do
113
+ out=$(ldapsearch -Y GSSAPI -LLL -o ldif-wrap=no -H "$gc" -b "$dn" -s base \
114
+ displayName userAccountControl 2>/dev/null)
115
+ line=$(printf '%s\n' "$out" | grep -m1 -E '^displayName:: ?|^displayName: ')
116
+ if [ -n "$line" ]; then
117
+ # (2) Disabled-account filter. userAccountControl is a GC-replicated
118
+ # attribute; if absent (older DC / partial reply) we simply don't filter.
119
+ uac=$(printf '%s\n' "$out" | sed -n 's/^userAccountControl: *//p' | head -1)
120
+ case "$uac" in
121
+ ''|*[!0-9]*) : ;; # missing/non-numeric -> can't judge
122
+ *) [ $((uac & 2)) -ne 0 ] && return 0 ;; # ACCOUNTDISABLE set -> skip
123
+ esac
124
+ case "$line" in
125
+ "displayName:: "*) printf '%s' "${line#displayName:: }" | base64 -D 2>/dev/null; return 0 ;;
126
+ "displayName: "*) printf '%s' "${line#displayName: }"; return 0 ;;
127
+ esac
128
+ fi
129
+ tries=$((tries + 1)); sleep "$tries"
130
+ done
131
+ }
132
+
133
+ # Up-edge: the manager (single-valued DN on the user's own record).
134
+ mgr_name=$(dn_name "$(field manager)")
135
+
136
+ # Down-edges: direct reports — a multi-valued back-link on the user's OWN record.
137
+ # field() keeps only the first value, so pull EVERY value: emit one report DN per
138
+ # line (decoding the base64 `directReports:: <b64>` form), resolve each to a name
139
+ # via dn_report (which drops offboarded/disabled accounts), and sort for stable,
140
+ # diff-friendly cache output. Kept in a function because a `case` inside `$(...)`
141
+ # trips the bash 3.2 parser that macOS ships.
142
+ resolve_reports() {
143
+ printf '%s\n' "$rec" | while IFS= read -r line; do
144
+ case "$line" in
145
+ "directReports:: "*) printf '%s' "${line#directReports:: }" | base64 -D 2>/dev/null; echo ;;
146
+ "directReports: "*) printf '%s\n' "${line#directReports: }" ;;
147
+ esac
148
+ done | while IFS= read -r dn; do
149
+ [ -n "$dn" ] || continue
150
+ n=$(dn_report "$dn"); [ -n "$n" ] && printf '%s\n' "$n"
151
+ done | sort
152
+ }
153
+ reports=$(resolve_reports)
65
154
 
66
155
  # 6. Write the cache. It keeps the Name/Email/Domain shape other skills already
67
156
  # parse, plus extra fields. The script generates it. Never edit it by hand.
@@ -81,6 +170,16 @@ mkdir -p "$(dirname "$CACHE")"
81
170
  echo "- **Employee ID:** $empid"
82
171
  echo "- **Office:** $office"
83
172
  [ -n "$mgr_name" ] && echo "- **Manager:** $mgr_name"
173
+ # Always emit Direct reports so the "our team" rule in CLAUDE.md can branch
174
+ # explicitly: a list => the user manages people (the team is in this cache);
175
+ # `none` => an individual contributor (peers resolved on demand from the
176
+ # manager). An omitted line would be indistinguishable from "not yet resolved".
177
+ if [ -n "$reports" ]; then
178
+ echo "- **Direct reports:**"
179
+ printf '%s\n' "$reports" | while IFS= read -r r; do echo " - $r"; done
180
+ else
181
+ echo "- **Direct reports:** none"
182
+ fi
84
183
  } > "$CACHE"
85
184
 
86
185
  # 7. Show the result.
@@ -1,10 +1,13 @@
1
1
  ---
2
2
  name: person-lookup
3
- description: Look up ANY person in the corporate directory from free-text input (an email address, or a first / last / full name) and return their record: real name, title, department, company, email, employee ID, office, and manager. Searches the Global Catalog forest-wide through LDAP. The bind uses the existing Kerberos ticket. Use when the user asks "who is X", needs someone's title / department / manager / email, or wants to disambiguate a name. For the *current* user's own identity, use the sibling `person-identify` skill instead.
3
+ description: Look up ANY person in the corporate directory from free-text input (an email address, or a first / last / full name) and return their record: real name, title, department, company, email, employee ID, office, manager, and direct reports. Searches the Global Catalog forest-wide through LDAP. The bind uses the existing Kerberos ticket. Use when the user asks "who is X", needs someone's title / department / manager / reports / email, wants to disambiguate a name, or needs to resolve a team around a person (a person's peers = their manager's other reports). For the *current* user's own identity, use the sibling `person-identify` skill instead.
4
4
  ---
5
5
 
6
6
  # Person Lookup
7
7
 
8
+ Write tier: none (cache only)
9
+ Frontmatter: none
10
+
8
11
  Resolve **any** person in the corporate Active Directory from natural-language
9
12
  input. The input is an email, a login, or any part of a name. This skill is the
10
13
  sibling of `person-identify`, and it aims at *other* people. It searches the
@@ -56,8 +59,9 @@ The argument is free text: an email, a full name, or just a surname.
56
59
  nothing, fall back to a substring search on `mail` / `displayName` /
57
60
  `proxyAddresses`.
58
61
  4. **Resolve per match.** For a single hit, print the full record and resolve
59
- the `manager` DN to a name. For several hits, print a compact
60
- disambiguation list and suggest a narrower search by email.
62
+ both org edges — the `manager` DN and each `directReports` DN to names.
63
+ For several hits, print a compact disambiguation list and suggest a
64
+ narrower search by email (org edges resolve only for an exact, single hit).
61
65
 
62
66
  ## Output
63
67
 
@@ -72,6 +76,7 @@ The argument is free text: an email, a full name, or just a surname.
72
76
  | Employee ID | `employeeID` |
73
77
  | Phone / Office | `telephoneNumber` / `physicalDeliveryOfficeName` |
74
78
  | Manager | `manager` (DN → resolved to a name) |
79
+ | Direct reports | `directReports` (multi-valued DN back-link → each resolved to a name; single hit only, shown when non-empty) |
75
80
  | DN | distinguished name (region + OU, useful for disambiguation) |
76
81
 
77
82
  ## Notes
@@ -53,7 +53,7 @@ fi
53
53
 
54
54
  # Global Catalog: forest-wide, base "" spans every domain.
55
55
  GC="ldap://$dc:3268"
56
- ATTRS="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName objectClass manager"
56
+ ATTRS="displayName givenName sn company title department employeeID mail telephoneNumber physicalDeliveryOfficeName objectClass manager directReports"
57
57
  MAX_SHOW=12 # cap detailed output for very broad name matches
58
58
 
59
59
  # Pull one attribute out of an LDIF record (passed as $2). Decode base64 (::).
@@ -90,6 +90,23 @@ name_of_dn() { # $1=dn
90
90
  field displayName "$(fetch "$1")"
91
91
  }
92
92
 
93
+ # List a person's direct reports — the multi-valued `directReports` back-link
94
+ # on their record — as one resolved name per line, sorted. $1 = the person's
95
+ # fetched LDIF record. field() keeps only the first value, so pull EVERY value
96
+ # here, decoding the base64 `directReports:: <b64>` form. Kept in a function
97
+ # because a `case` inside $(...) trips the bash 3.2 parser that macOS ships.
98
+ reports_of() { # $1=record
99
+ printf '%s\n' "$1" | while IFS= read -r line; do
100
+ case "$line" in
101
+ "directReports:: "*) printf '%s' "${line#directReports:: }" | base64 -D 2>/dev/null; echo ;;
102
+ "directReports: "*) printf '%s\n' "${line#directReports: }" ;;
103
+ esac
104
+ done | while IFS= read -r dn; do
105
+ [ -n "$dn" ] || continue
106
+ n=$(name_of_dn "$dn"); [ -n "$n" ] && printf '%s\n' "$n"
107
+ done | sort
108
+ }
109
+
93
110
  # Classify an entry: internal employee, external contact, or vendor account.
94
111
  kind_of() { # $1=record $2=dn
95
112
  local oc; oc=$(printf '%s\n' "$1" | grep -i '^objectClass:' | tr 'A-Z' 'a-z')
@@ -136,6 +153,15 @@ if [ "${#DNS[@]}" -eq 1 ]; then
136
153
  echo "- **Phone:** $(field telephoneNumber "$rec")"
137
154
  echo "- **Office:** $(field physicalDeliveryOfficeName "$rec")"
138
155
  [ -n "$mgr" ] && echo "- **Manager:** $mgr"
156
+ # Direct reports — the down-edge, symmetric to person-identify. Lets a
157
+ # caller resolve a team from any person: an IC's peers are their manager's
158
+ # other reports (look up the manager, read this list). Printed only when
159
+ # non-empty.
160
+ reports=$(reports_of "$rec")
161
+ if [ -n "$reports" ]; then
162
+ echo "- **Direct reports:**"
163
+ printf '%s\n' "$reports" | while IFS= read -r r; do echo " - $r"; done
164
+ fi
139
165
  echo "- **DN:** ${DNS[0]}"
140
166
  exit 0
141
167
  fi
@@ -9,6 +9,9 @@ description: >
9
9
 
10
10
  # Assess Interview
11
11
 
12
+ Write tier: `2-Confidential`
13
+ Frontmatter: candidate (stamps `status`, `updated`)
14
+
12
15
  Analyze a candidate's interview transcripts. Update their skill and behaviour
13
16
  profile with **observed** (not claimed) evidence. Interview evidence is
14
17
  higher-fidelity than CV evidence. It confirms or contradicts the screening
@@ -22,7 +25,7 @@ This is **Stage 2** of the three-stage hiring pipeline:
22
25
 
23
26
  ## Trigger
24
27
 
25
- - A new `transcript-*.md` file appears in `Knowledge/Candidates/{Name}/`.
28
+ - A new `transcript-*.md` file appears in `2-Confidential/Candidates/{Name}/`.
26
29
  - The user asks to analyze an interview or debrief.
27
30
  - The user asks to prepare a panel brief.
28
31
  - The concierge agent processes an Anarlog interview recording.
@@ -30,22 +33,23 @@ This is **Stage 2** of the three-stage hiring pipeline:
30
33
  ## Prerequisites
31
34
 
32
35
  - `fit-pathway` CLI installed.
33
- - At least one transcript in `Knowledge/Candidates/{Name}/`.
36
+ - At least one transcript in `2-Confidential/Candidates/{Name}/`.
34
37
  - `screening.md` should exist. If it is missing, run `req-screen` first.
35
38
  Proceed regardless.
36
39
 
37
40
  ## Inputs
38
41
 
39
- - `Knowledge/Candidates/{Name}/transcript-{date}.md`.
40
- - `Knowledge/Candidates/{Name}/screening.md`.
41
- - `Knowledge/Candidates/{Name}/brief.md` — target role.
42
+ - `2-Confidential/Candidates/{Name}/transcript-{date}.md`.
43
+ - `2-Confidential/Candidates/{Name}/screening.md`.
44
+ - `2-Confidential/Candidates/{Name}/brief.md` — target role.
42
45
 
43
46
  ## Outputs
44
47
 
45
- - `Knowledge/Candidates/{Name}/interview-{date}.md`.
46
- - `Knowledge/Candidates/{Name}/panel.md` — only when more interviews are
48
+ - `2-Confidential/Candidates/{Name}/interview-{date}.md`.
49
+ - `2-Confidential/Candidates/{Name}/panel.md` — only when more interviews are
47
50
  planned.
48
- - Updated `Knowledge/Candidates/{Name}/brief.md`.
51
+ - Updated `2-Confidential/Candidates/{Name}/brief.md` — frontmatter `status`
52
+ and `updated` stamped.
49
53
 
50
54
  <do_confirm_checklist goal="Verify the assessment is grounded in transcript
51
55
  evidence">
@@ -55,6 +59,8 @@ evidence">
55
59
  - [ ] Assess the level with standard progression criteria. Ignore gut feel.
56
60
  - [ ] Attribute interviewer observations by name.
57
61
  - [ ] Distinguish confirmed strengths from new concerns.
62
+ - [ ] Assessment prose about the candidate stays in the candidate's own
63
+ folder. Wider notes record only that the interview happened.
58
64
  - [ ] Write the panel brief (if created) for non-technical readers. Tie the
59
65
  suggested questions to the remaining gaps.
60
66
  - [ ] Update the brief's Pipeline section, links, and Status.
@@ -105,7 +111,7 @@ Apply the [level signals](references/rubric.md#level-signals).
105
111
 
106
112
  ### 6. Write the interview assessment
107
113
 
108
- Save to `Knowledge/Candidates/{Name}/interview-{date}.md` with
114
+ Save to `2-Confidential/Candidates/{Name}/interview-{date}.md` with
109
115
  [references/interview-template.md](references/interview-template.md). Include
110
116
  only skills with new evidence. Do not repeat the full matrix.
111
117
 
@@ -118,20 +124,22 @@ candidates:
118
124
  bunx fit-pathway interview {discipline} {level} --track={track}
119
125
  ```
120
126
 
121
- Save `Knowledge/Candidates/{Name}/panel.md` with
127
+ Save `2-Confidential/Candidates/{Name}/panel.md` with
122
128
  [references/panel-template.md](references/panel-template.md). The audience is
123
129
  next-stage interviewers, often non-engineers. Explain without jargon. Tie the
124
130
  suggested questions to the remaining gaps.
125
131
 
126
132
  ### 8. Update the candidate brief
127
133
 
128
- Apply targeted Edit operations to `Knowledge/Candidates/{Name}/brief.md`:
134
+ Apply targeted Edit operations to `2-Confidential/Candidates/{Name}/brief.md`:
129
135
 
130
136
  - Append a Pipeline entry with date, type, and outcome.
131
137
  - Add `## Interview Notes` if missing, with key observations.
132
138
  - Append `- [Interview Assessment](./interview-{date}.md)`.
133
139
  - Append `- [Panel Brief](./panel.md)` when one was created.
134
140
  - Update `Status` to reflect the current pipeline stage.
141
+ - Stamp the frontmatter: set `updated` to the interview date and `status` to
142
+ the closest registry value (agents select from `registry.yaml`).
135
143
 
136
144
  Never rewrite the file. Never update the Gender field from interview
137
145
  observations.
@@ -1,9 +1,14 @@
1
1
  # Interview Assessment Template
2
2
 
3
3
  Reference template for `req-assess` Step 6. Save to
4
- `Knowledge/Candidates/{Name}/interview-{date}.md`. Include only skills the
4
+ `2-Confidential/Candidates/{Name}/interview-{date}.md`. Include only skills the
5
5
  interview produced new evidence for. Do not repeat the full matrix.
6
6
 
7
+ Interview-outcome prose about the candidate lives only in the candidate's own
8
+ folder. A wider note (an interviewer's People note, a project note) records
9
+ only that the interview happened. Interviewer names may appear here; write no
10
+ assessment of the interviewers.
11
+
7
12
  ```markdown
8
13
  # Interview Assessment — {Full Name}
9
14
 
@@ -1,10 +1,14 @@
1
1
  # Panel Brief Template
2
2
 
3
3
  Reference template for `req-assess` Step 7. Save to
4
- `Knowledge/Candidates/{Name}/panel.md`. The audience is **next-stage
4
+ `2-Confidential/Candidates/{Name}/panel.md`. The audience is **next-stage
5
5
  interviewers, often non-engineers**. Explain without jargon. Focus on
6
6
  behaviours and scope.
7
7
 
8
+ The brief goes to the panel as an export. The note keeps its tier. Assessment
9
+ prose about the candidate stays in this folder; never copy it into panel
10
+ members' People notes.
11
+
8
12
  ```markdown
9
13
  # Panel Brief — {Full Name}
10
14
  ## {Role Title} — {Level}