@sbains2/lifeos 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/context.js ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `lifeos context` — the get_user_context consumer that makes the priority
3
+ * enum real at convening time (see docs/SCHEMA.md, "How priority is consumed
4
+ * at runtime").
5
+ *
6
+ * Reads lifeos.config.json and produces a compact "convening context" block:
7
+ * foci / quadrants / council ranked hot → warm → lukewarm → cold, plus the
8
+ * user profile line and reference paths (writing_style, brain graph). Council
9
+ * and agent runtimes paste or pipe this at convening start so the council
10
+ * weights hot items first.
11
+ *
12
+ * Pure planning + rendering only — no writes, no network, no env reads
13
+ * (trust defaults per docs/TRUST.md). Side effects live in
14
+ * src/commands/context.js.
15
+ */
16
+
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { join, isAbsolute } from 'node:path';
19
+ import { loadPersonaTemplate } from './templates-loader.js';
20
+
21
+ export const PURPOSE = 'Paste or pipe this into a convening so the council weights hot items first.';
22
+
23
+ // Rank order matches the schema enum. Unknown priorities sink to the bottom
24
+ // rather than throwing — validate.js/doctor own schema enforcement, not us.
25
+ const PRIORITY_RANK = { hot: 0, warm: 1, lukewarm: 2, cold: 3 };
26
+
27
+ function byPriority(a, b) {
28
+ return (PRIORITY_RANK[a.priority] ?? 99) - (PRIORITY_RANK[b.priority] ?? 99);
29
+ }
30
+
31
+ /**
32
+ * Build the structured convening context for a scaffold.
33
+ * Returns { ok: true, ...data } or { ok: false, error, fix } — never throws.
34
+ * options.quadrant scopes output to one quadrant (its council subset + foci).
35
+ */
36
+ export function planContext(targetDir, options = {}) {
37
+ const { quadrant = null } = options;
38
+
39
+ const configPath = join(targetDir, 'lifeos.config.json');
40
+ if (!existsSync(configPath)) {
41
+ return {
42
+ ok: false,
43
+ error: `lifeos.config.json not found at ${configPath}`,
44
+ fix: `Run \`npx @sbains2/lifeos ${targetDir}\` to scaffold a fresh config, or \`npx @sbains2/lifeos doctor ${targetDir}\` to diagnose.`,
45
+ };
46
+ }
47
+ let config;
48
+ try {
49
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
50
+ } catch (err) {
51
+ return {
52
+ ok: false,
53
+ error: `lifeos.config.json parse failed: ${err.message}`,
54
+ fix: `Open ${configPath} in an editor and fix the JSON syntax, then re-check with \`npx @sbains2/lifeos doctor ${targetDir}\`.`,
55
+ };
56
+ }
57
+
58
+ // Persona-template voices are the fallback role summaries for council
59
+ // members whose config entry lacks a `voice`. Lazy + fault-tolerant:
60
+ // custom/unbundled personas have no template file, and that's fine.
61
+ let personaVoices = null;
62
+ const summaryFor = (member) => {
63
+ if (member.voice) return member.voice;
64
+ if (personaVoices === null) {
65
+ try {
66
+ const template = loadPersonaTemplate(config.user?.persona);
67
+ personaVoices = new Map((template.council ?? []).map((c) => [c.id, c.voice]));
68
+ } catch {
69
+ personaVoices = new Map();
70
+ }
71
+ }
72
+ return personaVoices.get(member.id) ?? member.id;
73
+ };
74
+
75
+ const allQuadrants = config.quadrants ?? [];
76
+ let scopedQuadrant = null;
77
+ if (quadrant) {
78
+ scopedQuadrant = allQuadrants.find((q) => q.id === quadrant) ?? null;
79
+ if (!scopedQuadrant) {
80
+ return {
81
+ ok: false,
82
+ error: `quadrant '${quadrant}' not found in lifeos.config.json. Available: ${
83
+ allQuadrants.map((q) => q.id).join(', ') || '(none)'
84
+ }`,
85
+ fix: 'Pass one of the ids above to --quadrant, or omit the flag for the full context.',
86
+ };
87
+ }
88
+ }
89
+
90
+ const quadrants = (scopedQuadrant ? [scopedQuadrant] : allQuadrants.filter((q) => q.active))
91
+ .slice()
92
+ .sort(byPriority)
93
+ .map((q) => ({
94
+ id: q.id,
95
+ name: q.name,
96
+ description: q.description ?? null,
97
+ path: q.path,
98
+ active: q.active,
99
+ priority: q.priority,
100
+ }));
101
+
102
+ const councilPool = scopedQuadrant
103
+ ? (config.council ?? []).filter((m) => (scopedQuadrant.council_member_ids ?? []).includes(m.id))
104
+ : (config.council ?? []);
105
+ const council = councilPool
106
+ .slice()
107
+ .sort(byPriority)
108
+ .map((m) => ({ id: m.id, name: m.name, priority: m.priority, summary: summaryFor(m) }));
109
+
110
+ // Foci are global — a quadrant scope narrows the council, not the user's
111
+ // direction (see docs/SCHEMA.md: foci set the *direction*).
112
+ const foci = (config.user?.context?.foci ?? [])
113
+ .slice()
114
+ .sort(byPriority)
115
+ .map((f) => ({ id: f.id, horizon: f.horizon, description: f.description, priority: f.priority }));
116
+
117
+ const brainRel = config.brain?.graphify?.output ?? './.lifeos/brain';
118
+ const brainAbs = isAbsolute(brainRel) ? brainRel : join(targetDir, brainRel);
119
+
120
+ return {
121
+ ok: true,
122
+ purpose: PURPOSE,
123
+ scope: { quadrant: scopedQuadrant?.id ?? null },
124
+ user: {
125
+ name: config.user?.name ?? null,
126
+ persona: config.user?.persona ?? null,
127
+ role: config.user?.context?.role ?? null,
128
+ affiliations: config.user?.context?.affiliations ?? [],
129
+ },
130
+ foci,
131
+ quadrants,
132
+ council,
133
+ writingStylePath: config.writing_style_path ?? null,
134
+ brainPath: existsSync(brainAbs) ? brainRel : null,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Render a plan (from planContext) as the markdown convening block.
140
+ * Plain markdown, no ANSI — it's meant to be piped into a convening.
141
+ */
142
+ export function renderContextMarkdown(plan) {
143
+ const lines = [];
144
+ lines.push('# LifeOS convening context');
145
+ lines.push('');
146
+ lines.push(`> ${plan.purpose}`);
147
+ lines.push('');
148
+ lines.push(`**User:** ${formatUserLine(plan.user)}`);
149
+ if (plan.scope.quadrant) {
150
+ lines.push(`**Scope:** quadrant \`${plan.scope.quadrant}\` only`);
151
+ }
152
+
153
+ lines.push('');
154
+ lines.push('## Foci (hot → cold)');
155
+ if (plan.foci.length === 0) {
156
+ lines.push('_none configured_');
157
+ }
158
+ plan.foci.forEach((f, i) => {
159
+ lines.push(`${i + 1}. [${f.priority} · ${f.horizon}] ${f.description} (\`${f.id}\`)`);
160
+ });
161
+
162
+ lines.push('');
163
+ lines.push(plan.scope.quadrant ? '## Quadrant' : '## Active quadrants (by priority)');
164
+ if (plan.quadrants.length === 0) {
165
+ lines.push('_none active_');
166
+ }
167
+ plan.quadrants.forEach((q, i) => {
168
+ lines.push(`${i + 1}. [${q.priority}] ${q.name} (\`${q.id}\`)${q.description ? ` — ${q.description}` : ''}`);
169
+ });
170
+
171
+ lines.push('');
172
+ lines.push('## Council (by priority)');
173
+ if (plan.council.length === 0) {
174
+ lines.push('_none configured_');
175
+ }
176
+ plan.council.forEach((m, i) => {
177
+ lines.push(`${i + 1}. [${m.priority}] ${m.name} (\`${m.id}\`) — ${m.summary}`);
178
+ });
179
+ if (plan.council.some((m) => m.priority === 'cold')) {
180
+ lines.push('');
181
+ lines.push('_cold council members are excluded from convenings by default (functionally disabled)._');
182
+ }
183
+
184
+ const refs = [];
185
+ if (plan.writingStylePath) refs.push(`- writing_style: ${plan.writingStylePath}`);
186
+ if (plan.brainPath) refs.push(`- brain graph: ${plan.brainPath}`);
187
+ if (refs.length > 0) {
188
+ lines.push('');
189
+ lines.push('## Reference paths');
190
+ lines.push(...refs);
191
+ }
192
+
193
+ lines.push('');
194
+ return lines.join('\n');
195
+ }
196
+
197
+ function formatUserLine(user) {
198
+ const parts = [user.name || '(unnamed)'];
199
+ if (user.persona) parts.push(`persona: ${user.persona}`);
200
+ if (user.role) parts.push(user.role);
201
+ if (user.affiliations.length > 0) parts.push(user.affiliations.join(', '));
202
+ return parts.join(' · ');
203
+ }
package/src/doctor.js CHANGED
@@ -201,7 +201,7 @@ export function runDoctor(targetDir) {
201
201
  .join('; '),
202
202
  fix: `Set the missing env vars in your shell rc (~/.zshrc or ~/.bashrc), e.g.:\n ${missingEnv
203
203
  .map((m) => `export ${m.env_var}=...`)
204
- .join('\n ')}\n Then reload your shell + restart Claude.`,
204
+ .join('\n ')}\n Then reload your shell, and reopen the project in your IDE (or restart Claude Desktop if you use it).`,
205
205
  });
206
206
  }
207
207
 
package/src/index.js CHANGED
@@ -26,7 +26,7 @@ export async function run(targetDir, options = {}) {
26
26
  const { minimal = false } = options;
27
27
 
28
28
  console.log('');
29
- console.log(c.bold('lifeos') + c.dim(' v0.2.2 — scaffold a personal LifeOS'));
29
+ console.log(c.bold('lifeos') + c.dim(' v0.2.3 — scaffold a personal LifeOS'));
30
30
  console.log(c.dim(`Target directory: ${targetDir}`));
31
31
  if (minimal) {
32
32
  console.log(c.dim('Mode: --minimal (speed-optimized; using template defaults for quadrants, council, MCPs)'));
@@ -248,10 +248,10 @@ export async function run(targetDir, options = {}) {
248
248
  if (groups.needs_env.length > 0) {
249
249
  console.log(` ${c.arrow} Set the env vars listed above (in your shell rc — ~/.zshrc or ~/.bashrc), then reload your shell`);
250
250
  }
251
- if (desktopMergeResult || groups.ready.length > 0 || groups.needs_oauth.length > 0) {
252
- console.log(` ${c.arrow} Restart Claude Desktop (Cmd+Q, then reopen) so the new MCPs load`);
251
+ console.log(` ${c.arrow} Open ${targetDir} in your IDE (Claude Code / Cursor) it reads the generated .mcp.json and try convening the council on a real artifact`);
252
+ if (desktopMergeResult) {
253
+ console.log(` ${c.arrow} Using the Claude Desktop app instead? Restart it (Cmd+Q, then reopen) so the merged MCPs load`);
253
254
  }
254
- console.log(` ${c.arrow} Open ${targetDir} in Claude Code and try convening the council on a real artifact`);
255
255
  console.log(` ${c.arrow} Edit lifeos.config.json to tune council priorities, add quadrants, or change foci`);
256
256
  if (groups.manual.length > 0 || groups.risk.length > 0) {
257
257
  console.log(` ${c.arrow} For manual / risky MCPs: see .lifeos/INSTALL_MCPS.md for the full hint per server`);
@@ -0,0 +1,82 @@
1
+ # Job-search pack — v0.2
2
+
3
+ This pack is a generalized version of a job-search loop that has been dogfooded end-to-end by a real early-career candidate. The personal data has been stripped out; the *structure* — which is what actually made it precise — is preserved as fill-in templates. You supply your own facts.
4
+
5
+ The loop exists to kill four failure modes observed in agent-run job searches:
6
+
7
+ 1. **Score incoherence** — an agent printing a score above its own stated maximum.
8
+ 2. **Fake network matches** — e.g. a professor counted as an "alum" because the school's name appeared somewhere on their profile.
9
+ 3. **Prestige-only score collapse** — every contact landing on the same number because the model scored fame instead of usefulness-to-you.
10
+ 4. **Generic personalization** — outreach that would read the same with anyone else's name swapped in.
11
+
12
+ Every template below carries a structural fix for each of these. Don't remove the fixes when you fill in your data — they are the pack.
13
+
14
+ ## The loop
15
+
16
+ ```
17
+ SCOPE.md ──────────────── who you are; single source of truth; every agent reads it FIRST
18
+
19
+ ├─► APPLY track gate roles hard, THEN score fit /5 ──► ranked apply queue (YOU apply)
20
+ │ │
21
+ │ │ fit ≥ threshold
22
+ │ ▼
23
+ ├─► bridge high-scoring APPLY companies feed the referral queue (bridge.md)
24
+ │ │
25
+ │ ▼
26
+ └─► RELATIONSHIP track gate contacts G1–G4, THEN score bridge value /100 ──► outreach DRAFTS (YOU send)
27
+
28
+ │ runs on a schedule (lifeos schedule); digest lands in .lifeos/schedule/out/
29
+
30
+ you review the digest each morning
31
+ ```
32
+
33
+ Two tracks, two rubrics, two maxima — never crossed:
34
+
35
+ | Track | Question it answers | Score | Gate first |
36
+ |---|---|---|---|
37
+ | **APPLY** | Which postings should I apply to *this week*? | fit **/5** (+ a separate velocity /15 for urgency) | Level lane, hard disqualifiers |
38
+ | **RELATIONSHIP** | Which people can give me a *warm referral*? | bridge value **/100** | G1–G4 (real network member, current employee, referral-capable, relevant team) |
39
+
40
+ The bridge is the one arrow between them: when an APPLY-track evaluation scores at or above your threshold, that company is appended to the relationship-track queue, and the next scheduled scout hunts for warm contacts there. See [bridge.md](bridge.md).
41
+
42
+ ## Trust defaults (non-negotiable, same as the rest of LifeOS)
43
+
44
+ - **Drafts only. You send.** The scout compiles, ranks, and *drafts* outreach. It never sends a message or connection request, and nothing auto-applies to any job. The digest exists so a human reviews before anything leaves the machine.
45
+ - **Local-first.** The scope file, queues, evaluations, and digests are all local files in your scaffold. Nothing is uploaded.
46
+ - **Env vars by name only.** MCP auth follows the schema rule — token names in config, values in your shell.
47
+
48
+ Full commitments: [docs/TRUST.md](../../../../docs/TRUST.md). If anything in this pack violates them, file an issue — that's a bug, not a feature.
49
+
50
+ ## What's in the pack
51
+
52
+ ```
53
+ packs/job-search/
54
+ README.md # this file
55
+ SCOPE.template.md # the candidate-scope contract — copy, fill in ~20 minutes
56
+ council_playbook.md # the two-track council spec (gates, rubrics, personalization rules)
57
+ scout_prompt.template.md # the scheduled scout prompt — fill placeholders, then schedule it
58
+ bridge.md # the APPLY → RELATIONSHIP bridge pattern + a copy-pasteable watcher sketch
59
+ ```
60
+
61
+ ## Quickstart (5 steps)
62
+
63
+ 1. **Copy the templates** into your Job Search quadrant:
64
+ ```bash
65
+ cp SCOPE.template.md <your-lifeos>/JobSearch/SCOPE.md
66
+ cp council_playbook.md <your-lifeos>/JobSearch/council_playbook.md
67
+ cp scout_prompt.template.md <your-lifeos>/JobSearch/scout_prompt.md
68
+ ```
69
+ 2. **Fill `SCOPE.md`** (~20 minutes). Every `{{PLACEHOLDER}}` has an inline guidance comment. Be honest in the proof bank and ruthless in the never-say list — the whole loop inherits its precision from this one file.
70
+ 3. **Wire the MCPs** the scout needs from the registry ([templates/mcps/registry.json](../../mcps/registry.json)): `linkedin` (read its `risk_advisory` first), `fetch` for web lookups, and optionally `gmail` for digest delivery — which ships draft-only (`send_permission: false`) and stays that way unless you explicitly change it.
71
+ 4. **Schedule the scout.** Register `scout_prompt.md` as a recurring task with `lifeos schedule` (or any cron-style scheduled task your agent runtime provides, pointed at the same prompt). Daily is the dogfooded cadence. Digests are written to `.lifeos/schedule/out/`.
72
+ 5. **Review the digest each morning.** Apply to the queue yourself; send the drafts you like, rewrite the ones you don't. Run the bridge watcher (see [bridge.md](bridge.md)) so high-fit APPLY companies flow into the next scout run.
73
+
74
+ ## Why gate-then-score
75
+
76
+ A keyword match is *necessary, not sufficient*. Scoring before gating is how agents end up polishing blurbs for roles that should have been cut — so both tracks gate first (hard disqualifiers with the exact firing string named), and only survivors get a score. Every score is a re-derivable sum with the arithmetic shown, clamped to its stated max. A numerator above its denominator is a bug, not an opinion: recompute.
77
+
78
+ ## What's NOT here yet
79
+
80
+ - A declarative bridge config (`bridges[]` in `lifeos.config.json`) — the watcher in bridge.md is the manual version; see that file for the forward spec.
81
+ - An ATS/portal scanner. The APPLY track assumes you find postings via the council playbook's search step (LinkedIn MCP + web fetch); a dedicated scanner integration is a future pack revision.
82
+ - Automated application submission. Deliberately absent — that's a trust default, not a missing feature.
@@ -0,0 +1,156 @@
1
+ # CANDIDATE SCOPE — {{CANDIDATE_NAME}} (canonical targeting spec)
2
+
3
+ > **This file is the single source of truth for *who you are* and *what counts as a tailored opportunity*.**
4
+ > Every job-search agent — the scout, the council, any workflow — **MUST read this file first**
5
+ > before searching, scoring, or drafting anything. If an agent cannot load it, it must STOP,
6
+ > not proceed from memory.
7
+ >
8
+ > **Posture: ruthless precision.** A keyword match is *necessary, not sufficient*. Read the
9
+ > responsibilities, not the title. Name the exact string that fires every gate. Default to
10
+ > **skeptical**: when a fit signal is ambiguous, downgrade and flag — do not pad the score.
11
+
12
+ <!-- Fill time budget: ~20 minutes. Every {{PLACEHOLDER}} has a guidance comment next to it.
13
+ Sample values throughout use a fictional candidate ("Alex Rivera", an MSc Business
14
+ Analytics candidate) — replace all of them. Delete these comments as you fill. -->
15
+
16
+ ---
17
+
18
+ ## How agents must use this file (Step 0 contract)
19
+
20
+ 1. **Read this file in full** before any search, scoring, or message drafting.
21
+ 2. Apply the **two-track split** — every output belongs to exactly one track:
22
+ - **APPLY track** = *jobs to apply to now*. Optimize for ruthless fit + right level + target geo + freshness.
23
+ - **RELATIONSHIP track** = *contacts for warm referrals*. Optimize for **bridge value to the candidate**, not the contact's prestige.
24
+ 3. Never invent a fact, metric, or claim beyond the **§D Proof Bank**. Honor every **§E Claim Boundary**.
25
+ 4. Every score must be a **re-derivable sum** with each line item shown, **clamped to its stated max** (§F). A printed numerator that exceeds its denominator is a hard bug — recompute.
26
+ 5. Distinguish **HARD KILL** (auto-reject, no scoring) from **SOFT FLAG** (downgrade, still scored). Always name the firing string and label it KILL vs FLAG. A borderline good-fit role is never *silently* auto-rejected.
27
+
28
+ ---
29
+
30
+ ## §A — Identity & non-negotiables
31
+
32
+ <!-- One line each. These are gates, not preferences — an agent treats every item here as binding. -->
33
+
34
+ - **Candidate:** {{CANDIDATE_NAME}}. Based in {{BASE_LOCATION}}. <!-- e.g. "Alex Rivera. Based in Chicago, IL / CST." -->
35
+ - **Work authorization:** {{WORK_AUTH}} <!-- e.g. "US work-authorized; no visa sponsorship needed." State it exactly as a recruiter would need to know it. -->
36
+ - **Networks valid for referrals:** <!-- List every school (or program/community) where you hold real member status. The relationship track gates on verifiable membership — a degree shown in the Education section, not a mention anywhere on a profile. -->
37
+ - **{{SCHOOL_1}}** — {{DEGREE_1}}, {{GRAD_YEAR_1}}. <!-- e.g. "Lakeside University — MSc Business Analytics, 2026." -->
38
+ - **{{SCHOOL_2}}** — {{DEGREE_2}}, {{GRAD_YEAR_2}}. <!-- Optional. Delete if you have one network. Add more if you have them. -->
39
+ - **Level:** {{LEVEL_BAND}}. <!-- e.g. "early-career / new-grad / 0–3 yrs / Associate / Analyst I–II." Then state what you are NOT: "NOT senior IC, staff, principal, manager, director, VP, or head-of." The NOT list is what the gates fire on. -->
40
+ - **Comp:** target {{COMP_TARGET}}, **floor {{COMP_FLOOR}} base**. <!-- e.g. "target $95–115K, floor $85K USD base." The floor is a structural wall only against a posted band whose MAXIMUM is clearly below it — a straddling band passes. -->
41
+ - **Geo:** {{METRO_1}}, {{METRO_2}}, {{METRO_3}}, Remote-{{COUNTRY}}. <!-- Gate on WORK ARRANGEMENT, not the HQ field — a non-target HQ that is remote/hybrid-eligible is in scope. Hard kill only: wrong country, wrong work auth, or mandatory on-site 4–5 days/week in a non-target metro with no remote/relocation. -->
42
+
43
+ ---
44
+
45
+ ## §A2 — Level lane: cold-apply vs warm-referral vs kill (the decisive APPLY filter)
46
+
47
+ **The biggest predictor of an early-career application converting is the req's *level*, not the content match.** Read the **Minimum Qualifications / years-of-experience line FIRST**, before scoring anything, and route the posting into a lane:
48
+
49
+ - **COLD-APPLY lane (score & apply normally):** the posting is explicitly {{EARLY_CAREER_TAGS}} <!-- e.g. "new-grad / university grad / early-career program / Associate / Analyst I–II" -->, **OR** its *required* minimum is **≤ {{COLD_APPLY_MAX_YOE}} years** (or unstated).
50
+ - **WARM-REFERRAL lane (route to the RELATIONSHIP track — do NOT cold-apply):** the req requires **{{REFERRAL_LANE_YOE}}+ years** and carries no early-career framing. Content fit may still be high, but a cold app is ~noise against experienced-hire competition; the realistic path in is a warm referral. This is *especially* true at high-applicant-volume brands.
51
+ - **KILL:** required **≥ {{KILL_YOE}} years**, or a Senior/Staff/Principal/Manager+ title (§C).
52
+
53
+ **Variant hunt (do this BEFORE discarding or referral-routing a desirable role):** if a company you fit on substance is running an *experienced* req, search that same company for an early-career variant of the role and target *that* instead. Large companies often run dedicated early-career pipelines that are genuinely reachable; the experienced req for the same work is not.
54
+
55
+ **This is track-routing, NOT a fit-score gate.** No company-tier or company-size gate exists in the fit score, and none may be added — in either direction. An in-archetype role at a 12-person startup and one at a famous lab are both cold-apply targets; an experienced role at either defaults to the referral lane. (This is the structural fix for prestige-only scoring.)
56
+
57
+ ---
58
+
59
+ ## §B — Target role archetypes (3–6; read responsibilities, not just the title)
60
+
61
+ <!-- 3–6 rows. Tier one of them SIGNATURE — the rare intersection that is distinctly yours; agents
62
+ treat it as a tie-breaker UP. "Substance signals" = strings an agent can actually find in a JD.
63
+ Sample row (delete): | 1 | Product Data Scientist | PRIMARY | experimentation / A-B testing,
64
+ product metrics, behavioral segmentation; Python + SQL; deliverable = a decision or metric | -->
65
+
66
+ | # | Archetype | Tier | True-positive substance signals |
67
+ |---|-----------|------|---------------------------------|
68
+ | 1 | {{ARCHETYPE_1}} | PRIMARY | {{ARCHETYPE_1_SIGNALS}} |
69
+ | 2 | {{ARCHETYPE_2}} | PRIMARY | {{ARCHETYPE_2_SIGNALS}} |
70
+ | 3 | {{ARCHETYPE_3}} | PRIMARY | {{ARCHETYPE_3_SIGNALS}} |
71
+ | 4 | {{ARCHETYPE_4}} | **SIGNATURE** | {{ARCHETYPE_4_SIGNALS}} |
72
+ | 5 | {{ARCHETYPE_5}} | ADJACENT | {{ARCHETYPE_5_SIGNALS}} |
73
+
74
+ **Off-list titles that PASS on substance (read harder — never auto-reject):** {{ADJACENT_TITLES}} <!-- e.g. "Decision Scientist, Insights Analyst, Quantitative UX Researcher" — titles that don't match your list but often ARE your work. -->
75
+
76
+ ---
77
+
78
+ ## §C — Hard job disqualifiers (APPLY track)
79
+
80
+ Run these **after** a title/keyword match — they **override** it. Each entry names the JD-detectable string. Label each firing as **KILL** (auto-reject) or **FLAG** (downgrade, keep scoring). *The years number is load-bearing; the title adjective is secondary.*
81
+
82
+ <!-- Keep the categories; edit the specifics. Each line should name strings an agent can grep a JD for. -->
83
+
84
+ 1. **Seniority token in title — KILL:** {{SENIORITY_KILL_TOKENS}}. <!-- e.g. "Senior / Sr. / Staff / Principal / Manager / Director / Head of / VP / Chief". Add a caveat for any borderline title you'd still consider (FLAG, not KILL). -->
85
+ 2. **People-management — KILL:** "manage a team of N", "direct reports", "hire and grow the team".
86
+ 3. **Hard experience floor — KILL at ≥ {{KILL_YOE}} yrs *required*.** Distinguish **required/minimum** from **preferred/bonus**: a big number in a *preferred* bullet does not fire; route the middle band per §A2. **Never reject for an unstated minimum.**
87
+ 4. **Wrong specialty — KILL:** {{WRONG_SPECIALTY_KILLS}}. <!-- The roles that pattern-match your keywords but are a different job. e.g. for a product-analytics candidate: "foundation-model research, low-level ML systems, production distributed-systems/backend/platform SWE". -->
88
+ 5. **Credential wall — KILL only when hard-required:** {{CREDENTIAL_KILLS}}. <!-- e.g. "PhD hard-required; regulated credentials (actuarial exams, clinical biostatistics, licensure)". A standalone "advanced degree preferred" is a FLAG, not a KILL, if your degree satisfies it. -->
89
+ 6. **Security clearance — KILL** if the posting requires one you don't hold.
90
+ 7. **Geo — KILL:** wrong country / wrong work auth / mandatory full on-site in a non-target metro with no remote or relocation. Ambiguous remote policy = **FLAG**.
91
+ 8. **Comp — KILL** only when the posted band **maximum** is clearly below {{COMP_FLOOR}} base, full-time. A straddling band passes. A borderline band on a high-fit role is a surfaced **FLAG**, not a silent kill.
92
+ 9. **Wrong function wearing your keywords — KILL:** {{WRONG_FUNCTION_TRAPS}}. <!-- e.g. "pre-sales 'data scientist', reporting-clerk 'analyst', functional-consultant 'business analyst'". Name the tells. -->
93
+ 10. **Employment type — KILL (default):** {{EMPLOYMENT_TYPE_KILLS}}. <!-- e.g. "contract / 1099 / staffing / commission-only / unpaid; internships if you're graduating into full-time". -->
94
+
95
+ **No company-tier or company-size gate exists, and none may be added — in either direction.**
96
+
97
+ ---
98
+
99
+ ## §D — Proof Bank (cite verbatim; never invent)
100
+
101
+ **Exact figures — never round or invent:** {{EXACT_FIGURES_LIST}}. *If a number is not on this list, the agent does not have it.*
102
+ <!-- List every number an agent may ever cite about you, e.g.: "2.1M users · 5-segment taxonomy · 0.81 out-of-sample AUC · $2.3M renewals isolated · 40% triage-time reduction". This one line kills invented metrics. -->
103
+
104
+ <!-- 3–7 rows. Each claim must be checkable against real evidence (a deck, a repo, a report).
105
+ The Boundary column is as load-bearing as the claim — it is what stops an agent overselling you.
106
+ Sample row (delete): | P1 — {{INTERNSHIP_1}} | Built the first behavioral segmentation of a
107
+ 2.1M-user base; 5-segment taxonomy; top segment converts 2–4× on paid plans | Product DS,
108
+ Analytics outreach | Applied ML on product data — intern scope, NOT senior ownership | -->
109
+
110
+ | ID | Claim (with metrics, verbatim) | Where it may be used | Boundary |
111
+ |----|--------------------------------|----------------------|----------|
112
+ | **P1 — {{PROOF_POINT_1_TITLE}}** | {{PROOF_POINT_1}} | {{PROOF_POINT_1_USE}} | {{PROOF_POINT_1_BOUNDARY}} |
113
+ | **P2 — {{PROOF_POINT_2_TITLE}}** | {{PROOF_POINT_2}} | {{PROOF_POINT_2_USE}} | {{PROOF_POINT_2_BOUNDARY}} |
114
+ | **P3 — {{PROOF_POINT_3_TITLE}}** | {{PROOF_POINT_3}} | {{PROOF_POINT_3_USE}} | {{PROOF_POINT_3_BOUNDARY}} |
115
+ | **P4 — {{PROOF_POINT_4_TITLE}}** | {{PROOF_POINT_4}} | {{PROOF_POINT_4_USE}} | {{PROOF_POINT_4_BOUNDARY}} |
116
+
117
+ **Per-archetype "lead-with":** <!-- For each §B archetype, name the ONE proof point outreach and applications lead with. If no lead-with proof point maps cleanly to a JD, the role is NOT tailored. -->
118
+
119
+ | Archetype | LEAD with |
120
+ |-----------|-----------|
121
+ | {{ARCHETYPE_1}} | {{ARCHETYPE_1_LEAD}} |
122
+ | {{ARCHETYPE_2}} | {{ARCHETYPE_2_LEAD}} |
123
+ | {{ARCHETYPE_3}} | {{ARCHETYPE_3_LEAD}} |
124
+ | {{ARCHETYPE_4}} | {{ARCHETYPE_4_LEAD}} |
125
+ | {{ARCHETYPE_5}} | {{ARCHETYPE_5_LEAD}} |
126
+
127
+ ---
128
+
129
+ ## §E — Claim boundaries / never-say list
130
+
131
+ <!-- The inverse of the proof bank: framings that are technically adjacent to your experience but
132
+ false or oversold. Agents may NEVER use these. Common shapes: (a) a brand-name employer implying
133
+ a different specialty than the work you did there; (b) in-progress work described as finished;
134
+ (c) "isolated/identified $X" inflated to "drove/generated $X"; (d) your level inflated. -->
135
+
136
+ **Never** sell the candidate as / imply:
137
+
138
+ - {{NEVER_SAY_1}} <!-- e.g. "a backend/platform/infra engineer — the work was product analytics" -->
139
+ - {{NEVER_SAY_2}} <!-- e.g. "the manuscript is published — it is IN PREP" -->
140
+ - {{NEVER_SAY_3}} <!-- e.g. "causal-credit verbs on the $2.3M ('drove/generated/saved') — it was ISOLATED for targeting" -->
141
+ - {{NEVER_SAY_4}} <!-- e.g. "senior / 5+ years / people-management experience" -->
142
+ - {{NEVER_SAY_5}} <!-- e.g. "project metrics framed as business outcomes" -->
143
+
144
+ ---
145
+
146
+ ## §F — Scoring maxima per track
147
+
148
+ **Denominators never cross tracks.** A job is scored on the APPLY rubric; a person on the RELATIONSHIP rubric. Full protocols live in [council_playbook.md](council_playbook.md) — this section only fixes the maxima so no agent can drift them.
149
+
150
+ | Track | Rubric | Max | Rule |
151
+ |---|---|---|---|
152
+ | APPLY | fit score | **/5** | gate first (§A2 lane + §C), then score by mapping the JD to the §D lead-with; arithmetic shown |
153
+ | APPLY | velocity (freshness/urgency) | **/15** | a separate line — never folded into the /5 fit score |
154
+ | RELATIONSHIP | bridge value | **/100** | gates G1–G4 first; one tier per factor; multiplier applied then clamped: `FINAL = min(100, round(BASE × M))` |
155
+
156
+ Any printed score above its max is a hard bug. Recompute — never publish it.
@@ -0,0 +1,89 @@
1
+ # The bridge — high-scoring APPLY hits feed the RELATIONSHIP queue
2
+
3
+ The two tracks stay deliberately decoupled — a job is scored /5, a person /100, and neither rubric leaks into the other. The **bridge** is the one sanctioned arrow between them:
4
+
5
+ ```
6
+ APPLY track bridge RELATIONSHIP track
7
+ evaluations (one row per role) ──► fit ≥ threshold? ──► append company to referral_queue.txt
8
+
9
+ next scheduled scout run reads the queue
10
+ ```
11
+
12
+ **Why this works:** the postings where you score highest on *substance* are exactly the companies where a warm referral pays off most — whether you cold-applied (a referral multiplies a live application) or the role was routed to the warm-referral lane (a referral is the only realistic path in). So instead of maintaining a target-company list by hand, the APPLY track's own output feeds the RELATIONSHIP track automatically.
13
+
14
+ ## The concrete pattern
15
+
16
+ 1. Your APPLY-track evaluations land as tab-separated rows, one file per scan batch, in `.lifeos/job-search/evaluations/`:
17
+
18
+ ```
19
+ num<TAB>date<TAB>company<TAB>role<TAB>status<TAB>score/5<TAB>notes
20
+ ```
21
+
22
+ 2. A small watcher reads each new file, promotes every company whose fit score is **≥ threshold** (4.25/5 dogfooded — high enough that only genuinely tailored roles trigger a scout run) into `.lifeos/job-search/referral_queue.txt`, then archives the file to `processed/` so no row is ever double-queued.
23
+
24
+ 3. The next scheduled scout run ([scout_prompt.template.md](scout_prompt.template.md), Step 1) picks the queue up and hunts for warm contacts at those companies.
25
+
26
+ The queue is a plain newline-delimited text file on purpose: you can inspect it, hand-edit it, or append a company yourself — the scout can't tell the difference.
27
+
28
+ ## Watcher sketch (copy-pasteable)
29
+
30
+ Pure local file ops — no network, no dependencies beyond node builtins, in keeping with the trust defaults. Save as `bridge-watcher.mjs` and run it after each APPLY scan (or on the same schedule as the scout, just before it):
31
+
32
+ ```js
33
+ #!/usr/bin/env node
34
+ // bridge-watcher.mjs — promote high-fit APPLY evaluations into the relationship queue.
35
+ import { readFileSync, appendFileSync, readdirSync, renameSync, mkdirSync, existsSync } from 'node:fs';
36
+ import { join } from 'node:path';
37
+
38
+ const EVAL_DIR = '.lifeos/job-search/evaluations'; // one .tsv per APPLY scan batch
39
+ const PROCESSED_DIR = join(EVAL_DIR, 'processed');
40
+ const QUEUE_FILE = '.lifeos/job-search/referral_queue.txt';
41
+ const THRESHOLD = 4.25; // fit /5 that earns a referral hunt
42
+
43
+ mkdirSync(PROCESSED_DIR, { recursive: true });
44
+ // De-dupe against companies already queued (the scout clears the queue after each run).
45
+ const queued = new Set(
46
+ existsSync(QUEUE_FILE) ? readFileSync(QUEUE_FILE, 'utf8').split('\n').filter(Boolean) : [],
47
+ );
48
+
49
+ for (const name of readdirSync(EVAL_DIR).filter((f) => f.endsWith('.tsv'))) {
50
+ const file = join(EVAL_DIR, name);
51
+ for (const row of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
52
+ // Row: num date company role status score/5 notes
53
+ const cols = row.split('\t');
54
+ const company = (cols[2] ?? '').trim();
55
+ const score = Number.parseFloat((cols[5] ?? '').replace('/5', ''));
56
+ if (company && Number.isFinite(score) && score >= THRESHOLD && !queued.has(company)) {
57
+ appendFileSync(QUEUE_FILE, `${company}\n`);
58
+ queued.add(company);
59
+ console.log(`queued ${company} (fit ${score}/5)`);
60
+ }
61
+ }
62
+ renameSync(file, join(PROCESSED_DIR, name)); // archive so a row is never double-queued
63
+ }
64
+ ```
65
+
66
+ Run: `node bridge-watcher.mjs` from your scaffold root.
67
+
68
+ ## Rules the bridge must honor
69
+
70
+ - **Never promote on raw score alone across a fired gate.** The watcher trusts that upstream evaluations were already gated (§C of your scope). If a row somehow carries a high score on a role that should have been killed, fix the evaluator — don't lower the threshold.
71
+ - **The queue holds companies, not people and not roles.** Which *contacts* to pursue at each company is entirely the scout's job, gated G1–G4.
72
+ - **Clearing the queue is the scout's last step, not its first** — only after the digest exists, so a failed run never silently drops companies.
73
+
74
+ ## Where this is going
75
+
76
+ In a future schema version this whole file becomes declarative config instead of a hand-run script — something like:
77
+
78
+ ```jsonc
79
+ // forward spec — NOT implemented; do not add to lifeos.config.json yet
80
+ "bridges": [
81
+ {
82
+ "from": { "quadrant": "job_search", "artifact": "evaluations", "field": "fit", "gte": 4.25 },
83
+ "to": { "quadrant": "job_search", "queue": "referral_queue" },
84
+ "carry": ["company"]
85
+ }
86
+ ]
87
+ ```
88
+
89
+ with a runtime executor watching the `from` artifact. Until then, the ~30-line watcher above is the whole mechanism — inspectable, local, and yours.