From 42bc8ee42cce93fbc22cea8656e08941b6082f0b Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Sun, 12 Jul 2026 14:07:16 +0200 Subject: [PATCH] =?UTF-8?q?docs(prd):=20backlog=20sharpened=20for=20cold?= =?UTF-8?q?=20sessions=20=E2=80=94=20execution=20footers,=20splits,=20D9?= =?UTF-8?q?=20parked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STATUS resume protocol: milestones ordered by due date, skip human-gated tops, named entry points (erp#38 / #51 / write-skill quartet); every issue now carries an Execution footer. - Backlog map: +erp#59 (T14 split from #48), +erp#60 (T11 loop split from #54), retitles, post-replay markers. - D9 meeting capture parked (nice-to-have; calls are iPhone-first). - prd_check.py preserved from the session scratchpad into scripts/ (the closure protocol references the pattern β€” now it's runnable). Co-Authored-By: Claude Fable 5 --- vibe/PRD/ai-back-office/STATUS.md | 12 +- vibe/PRD/ai-back-office/agent-architecture.md | 4 +- vibe/PRD/ai-back-office/scripts/prd_check.py | 230 ++++++++++++++++++ vibe/PRD/ai-back-office/task-inventory.md | 2 +- 4 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 vibe/PRD/ai-back-office/scripts/prd_check.py diff --git a/vibe/PRD/ai-back-office/STATUS.md b/vibe/PRD/ai-back-office/STATUS.md index 61a9fb9..db57f3f 100644 --- a/vibe/PRD/ai-back-office/STATUS.md +++ b/vibe/PRD/ai-back-office/STATUS.md @@ -2,8 +2,8 @@ # STATUS β€” implementation tracker -> **Status:** 🟒 Current β€” backlog decomposed into issues (2026-07-11) -> **Last Updated:** 2026-07-11 +> **Status:** 🟒 Current β€” backlog decomposed into issues (2026-07-11); sharpened with per-issue Execution footers + splits #59/#60 (2026-07-12) +> **Last Updated:** 2026-07-12 > **Up:** [AI back-office hub](README.md) > **Related:** [POC plan](poc-plan.md) Β· [Roadmap](roadmap.md) (dated plan; actuals and slips land here) @@ -14,19 +14,19 @@ | 0 β€” Foundations | read skills, sandbox + promote, backups, snapshots, bank reco, email ingest, Telegram gateway MVP | βœ… shipped pre-PRD (ledger below) | | 1 β€” Flagship pipeline | [POC-1](poc-plan.md#poc-1--supplier-invoice-end-to-end) + [POC-5](poc-plan.md#poc-5--model-routing-bench) | ⬜ decomposed β†’ [erp milestone P1](https://gitea.arcodange.lab/arcodange-org/erp/milestone/1) (erp#38–45, #47) | | 2 β€” Urgent compliance | [POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike) β€” **hard deadline 2026-09-01** | ⬜ decomposed β†’ [erp milestone P2](https://gitea.arcodange.lab/arcodange-org/erp/milestone/2) (erp#46) | -| 3 β€” Standing fleet | [POC-2](poc-plan.md#poc-2--pi-sentinel), queue, digest + approval cards | ⬜ decomposed β†’ [erp milestone P3](https://gitea.arcodange.lab/arcodange-org/erp/milestone/3) (erp#48–50) + [gateway#1](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/1)/[#2](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/2) | +| 3 β€” Standing fleet | [POC-2](poc-plan.md#poc-2--pi-sentinel), queue, digest + approval cards | ⬜ decomposed β†’ [erp milestone P3](https://gitea.arcodange.lab/arcodange-org/erp/milestone/3) (erp#48–50, #59) + [gateway#1](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/1)/[#2](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues/2) | | Ledger compliance (cross-cutting) | [Dolibarr verifications](compliance.md#dolibarr-verifications-sandbox-first) | ⬜ decomposed β†’ [erp milestone](https://gitea.arcodange.lab/arcodange-org/erp/milestone/4) (erp#51) | | 4 β€” Money loops | [POC-3](poc-plan.md#poc-3--reconciliation--payment-recording), dunning, cash report | ⬜ decomposed β†’ [erp milestone P4](https://gitea.arcodange.lab/arcodange-org/erp/milestone/5) (erp#52–53) | -| 5 β€” Fiscal autopilot | [POC-4](poc-plan.md#poc-4--tva-dry-run), compliance calendar | ⬜ decomposed β†’ [erp milestone P5](https://gitea.arcodange.lab/arcodange-org/erp/milestone/6) (erp#54–55) | +| 5 β€” Fiscal autopilot | [POC-4](poc-plan.md#poc-4--tva-dry-run), compliance calendar | ⬜ decomposed β†’ [erp milestone P5](https://gitea.arcodange.lab/arcodange-org/erp/milestone/6) (erp#54–55, #60) | | 6 β€” Emission era | e-invoice emission + e-reporting β€” **hard deadline 2027-09-01** | ⬜ not yet decomposed (starts 2027-05; requirements captured by erp#46 deliverable 4) | ## Backlog map -Every phase is decomposed into **self-contained issues** (context, deliverables, acceptance criteria, dependencies, PRD links). How a future session resumes: **pick the top unblocked issue of the earliest open milestone** β€” nothing else is needed; the issue body carries everything. Cross-cutting decisions get their ADRs via [factory#22](https://gitea.arcodange.lab/arcodange-org/factory/issues/22). +Every phase is decomposed into **self-contained issues** (context, deliverables, acceptance criteria, dependencies, PRD links); since 2026-07-12 each issue also carries an **Execution footer** β€” *Blocked by / Blocks / Human gates / Start (worktree + first command) / Done means (evidence comment β†’ PR with `Closes #N` β†’ doc sweep)*. How a future session resumes: **order open milestones by due date, pick the top issue whose "Blocked by" is clear β€” and skip issues whose only open step is a `[HUMAN]` gate** (surface those in the digest instead of stalling on them; e.g. erp#46 step 1 is operator-owned). Cold-start entry points as of 2026-07-12: **erp#38** (the fleet entry issue), **erp#51** (startable today), **erp#41/#42/#43/#44** (write-skill side, independent of the scaffold). The issue body carries everything; on any doubt the trust order is live system > code > STATUS > leaves. Cross-cutting decisions get their ADRs via [factory#22](https://gitea.arcodange.lab/arcodange-org/factory/issues/22). | Repo | Issues | | --- | --- | -| [erp](https://gitea.arcodange.lab/arcodange-org/erp/issues) | **P1:** #38 fleet scaffold+AGENTS.md (D8) Β· #39 golden set+injection fixtures Β· #40 invoice-extract atom Β· #41 provenance checker Β· #42 compliance linter Β· #43 GED attach op Β· #44 idempotency keys Β· #45 POC-5 routing bench (D5) Β· #47 POC-1 exit gate β€” **P2:** #46 POC-6 Qonto-as-PA (D4) β€” **P3:** #48 watchdogs T13/T14 Β· #49 T17 second-brain hooks (D7) Β· #50 POC-2 Pi sentinel (D6) β€” **Compliance:** #51 Dolibarr verifications (FEC/BlockedLog) β€” **P4:** #52 POC-3 reco+payments Β· #53 T05 client invoice (D3, ⚠️ July manual ~07-23) β€” **P5:** #54 fiscal profile+calendar+T11 Β· #55 POC-4 TVA dry-runs β€” **Ops:** #56 known-patterns fix Β· #57 bucket C + document gaps | +| [erp](https://gitea.arcodange.lab/arcodange-org/erp/issues) | **P1:** #38 fleet scaffold+AGENTS.md (D8, **entry issue**) Β· #39 golden set+injection fixtures Β· #40 invoice-extract atom Β· #41 provenance checker Β· #42 compliance linter Β· #43 GED attach op Β· #44 idempotency keys Β· #45 POC-5 routing bench (D5) Β· #47 POC-1 exit gate (umbrella) β€” **P2:** #46 POC-6 Qonto-as-PA (D4; step 1 = [HUMAN] Qonto UI) β€” **P3:** #48 T13 drift watchdog Β· #59 T14 backup freshness+drill Β· #49 T17 second-brain hooks (D7; meeting lane D9 parked) Β· #50 POC-2 Pi sentinel (D6) β€” **Compliance:** #51 Dolibarr verifications (FEC/BlockedLog, startable today) β€” **P4:** #52 POC-3 reco+payments Β· #53 T05 client invoice (D3, ⚠️ July manual ~07-23) β€” **P5:** #54 fiscal profile+calendar files Β· #60 T11 reminder loop Β· #55 POC-4 TVA dry-runs β€” **Ops (post-replay):** #56 known-patterns fix Β· #57 bucket C + document gaps | | [telegram-gateway](https://gitea.arcodange.lab/arcodange/telegram-gateway/issues) | #1 Postgres durable queue (D1) Β· #2 daily digest + approval cards | | [factory](https://gitea.arcodange.lab/arcodange-org/factory/issues) | #22 ADRs as decisions close (D1/D2/D4/D6/D7) | diff --git a/vibe/PRD/ai-back-office/agent-architecture.md b/vibe/PRD/ai-back-office/agent-architecture.md index 42af8c5..50eeb02 100644 --- a/vibe/PRD/ai-back-office/agent-architecture.md +++ b/vibe/PRD/ai-back-office/agent-architecture.md @@ -192,6 +192,6 @@ To be settled by POC evidence, each closing with a short ADR: | D6 | Pi inference serving | llama.cpp server vs. Ollama on arm64, resource limits, node pinning ([C5](challenges.md#c5--slm-capability-ceiling-on-pi-hardware)) | | D7 | Cluster↔vault access | git clone/pull of the SecondBrain remote (**leaning** β€” the Gitea remote exists, offline-friendly, reviewable) vs. tunneled Obsidian REST API (M4-only today) vs. keeping vault access M4-exclusive | | D8 | Fleet code home | erp repo `fleet/` next to the skills (**leaning** β€” the atoms are ERP-domain today; revisit into a dedicated repo when a second domain joins) vs. dedicated fleet repo vs. scattered per existing repo | -| D9 | Meeting capture tool (client-project notes lane) | **Leaning: Hyprnote free tier** (rebranded "Char" β€” local capture + transcription with **speaker diarization and Google Calendar sync both on the free plan**; manual trigger only, auto-record stays off by consent stance) vs. **Meetily** as OSS fallback (MIT, diarization in the community core, no calendar sync β€” `sb.py` can compensate by matching recording timestamps to the calendar ICS) vs. bare Whisper-class + Ornith | +| D9 | Meeting capture tool (client-project notes lane) β€” **parked 2026-07-12, nice-to-have** (first client calls happen on the iPhone: a phone call precedes the Meet, which desktop capture doesn't cover; revisit when desktop meetings become routine) | **Leaning: Hyprnote free tier** (rebranded "Char" β€” local capture + transcription with **speaker diarization and Google Calendar sync both on the free plan**; manual trigger only, auto-record stays off by consent stance) vs. **Meetily** as OSS fallback (MIT, diarization in the community core, no calendar sync β€” `sb.py` can compensate by matching recording timestamps to the calendar ICS) vs. bare Whisper-class + Ornith | -D4–D6 close with their mapped POCs ([POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike), [POC-5](poc-plan.md#poc-5--model-routing-bench), [POC-2](poc-plan.md#poc-2--pi-sentinel)); D1–D2 are settled while building phase 3's standing fleet (the queue and scheduler *are* its skeleton); D3 lands with phase 4's money loops; D7 closes when the first cluster-side atom needs vault context (phase 3 at the earliest); D8 rides POC-1 β€” the first atoms need their home on day one; D9 closes on diarization quality judged over a **real bilingual call** (EN client / FR partner) β€” erp#49 carries the steps. +D4–D6 close with their mapped POCs ([POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike), [POC-5](poc-plan.md#poc-5--model-routing-bench), [POC-2](poc-plan.md#poc-2--pi-sentinel)); D1–D2 are settled while building phase 3's standing fleet (the queue and scheduler *are* its skeleton); D3 lands with phase 4's money loops; D7 closes when the first cluster-side atom needs vault context (phase 3 at the earliest); D8 rides POC-1 β€” the first atoms need their home on day one; D9 is **parked** (nice-to-have; calls are iPhone-first today) β€” erp#49 carries the wake-up steps (gate: diarization quality on a real bilingual call). diff --git a/vibe/PRD/ai-back-office/scripts/prd_check.py b/vibe/PRD/ai-back-office/scripts/prd_check.py new file mode 100644 index 0000000..f904b51 --- /dev/null +++ b/vibe/PRD/ai-back-office/scripts/prd_check.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Mechanical link/anchor + convention checks for the ai-back-office PRD tree.""" +import re +import difflib +from pathlib import Path + +BASE = Path("/Users/gabrielradureau/Work/Arcodange/factory/.claude/worktrees/client-dossier-synergy/vibe/PRD/ai-back-office") +FILES = ["README.md", "task-inventory.md", "agent-architecture.md", "model-fleet.md", "compliance.md", "roadmap.md", "agent-catalog.md", + "challenges.md", "poc-plan.md", "qa-strategy.md", "STATUS.md"] + +LINK_RE = re.compile(r'\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)') +HEADING_RE = re.compile(r'^(#{1,6})\s+(.+?)\s*$') + + +def strip_md(text: str) -> str: + text = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', text) # links -> text + text = text.replace('`', '') + return text + + +def slugify(text: str) -> str: + """GitHub-style slug per the given spec: lowercase; keep alnum/space/hyphen; space->hyphen.""" + t = strip_md(text).strip().lower() + kept = ''.join(ch for ch in t if ch.isalnum() or ch in ' -') + return kept.replace(' ', '-') + + +def load(path: Path): + return path.read_text(encoding='utf-8').splitlines() + + +def headings_and_slugs(path: Path): + """Return ordered list of (level, text) headings outside code fences, plus slug set with GitHub dup handling.""" + slugs = {} + heads = [] + in_fence = False + for line in load(path): + if line.strip().startswith('```'): + in_fence = not in_fence + continue + if in_fence: + continue + m = HEADING_RE.match(line) + if m: + text = m.group(2) + heads.append((len(m.group(1)), text)) + base = slugify(text) + if base in slugs: + slugs[base] += 1 + slugs[f"{base}-{slugs[base]}"] = 0 + else: + slugs[base] = 0 + return heads, set(slugs.keys()) + + +def extract_links(path: Path): + """(lineno, text, target) for every markdown link.""" + out = [] + for i, line in enumerate(load(path), 1): + for m in LINK_RE.finditer(line): + out.append((i, m.group(1), m.group(2))) + return out + + +def main(): + slug_cache = {} + + def slugs_for(p: Path): + rp = p.resolve() + if rp not in slug_cache: + slug_cache[rp] = headings_and_slugs(rp)[1] if rp.exists() else set() + return slug_cache[rp] + + broken = [] + total_links = 0 + for fname in FILES: + fpath = BASE / fname + for lineno, text, target in extract_links(fpath): + total_links += 1 + t = target.strip('<>') + if t.startswith(('http://', 'https://', 'mailto:')): + continue + if '#' in t: + pathpart, anchor = t.split('#', 1) + else: + pathpart, anchor = t, None + if pathpart: + resolved = (fpath.parent / pathpart).resolve() + if not resolved.exists(): + broken.append(f"{fname}:{lineno} -> {target} [MISSING FILE {resolved}]") + continue + else: + resolved = fpath.resolve() + if anchor is not None: + if resolved.suffix != '.md': + broken.append(f"{fname}:{lineno} -> {target} [ANCHOR ON NON-MD]") + continue + sl = slugs_for(resolved) + if anchor not in sl: + Π±Π»ΠΈΠ· = difflib.get_close_matches(anchor, sl, n=2) + broken.append(f"{fname}:{lineno} -> {target} [UNRESOLVED ANCHOR; close: {Π±Π»ΠΈΠ·}]") + + print("=== JOB 1: LINK & ANCHOR CHECK ===") + print(f"total links scanned: {total_links}") + if broken: + for b in broken: + print("BROKEN:", b) + else: + print("all relative file links + anchors resolve: NONE BROKEN") + + # ---------------- Job 2 mechanical parts ---------------- + print("\n=== JOB 2a: BREADCRUMBS ===") + crumb_re = re.compile(r'^(\[[^\]]+\]\([^)]+\) > )+\*\*[^*]+\*\*$') + for fname in FILES: + first = load(BASE / fname)[0] + ok = bool(crumb_re.match(first)) + print(f"{fname}:1 breadcrumb {'OK' if ok else 'VIOLATION: ' + first!r}") + + print("\n=== JOB 2b: HEADER BLOCKQUOTE (Status + Last Updated) ===") + for fname in FILES: + lines = load(BASE / fname) + bq = [l for l in lines[:12] if l.startswith('>')] + joined = '\n'.join(bq) + has_status = '**Status:**' in joined + mdate = re.search(r'\*\*Last Updated:\*\*\s*(\S+)', joined) + date = mdate.group(1) if mdate else None + ok = has_status and date == '2026-07-11' + print(f"{fname}: Status={'Y' if has_status else 'N'} LastUpdated={date} -> {'OK' if ok else 'VIOLATION'}") + + print("\n=== JOB 2c: TOMBSTONE SCAN (eyeball hits) ===") + tomb = re.compile(r'(?i)previously|formerly|renamed from|was renamed|no longer|used to be|correction \(|changelog|superseded|deprecated|instead of the old|updated? on \d{4}') + hits = 0 + for fname in FILES: + for i, line in enumerate(load(BASE / fname), 1): + if tomb.search(line): + hits += 1 + print(f"{fname}:{i}: {line.strip()[:140]}") + if not hits: + print("no tombstone-pattern hits") + + print("\n=== JOB 2d: BIDIRECTIONAL LINKS ===") + hub_text = (BASE / "README.md").read_text() + leaves = [f for f in FILES if f != "README.md"] + for leaf in leaves: + in_hub = f']({leaf})' in hub_text or f']({leaf}#' in hub_text + leaf_text = (BASE / leaf).read_text() + back = '](README.md)' in leaf_text or '](README.md#' in leaf_text + print(f"{leaf}: hub->leaf {'OK' if in_hub else 'MISSING'} | leaf->hub {'OK' if back else 'MISSING'}") + + print("\n=== JOB 2e: MERMAID CONVENTION ===") + for fname in FILES: + lines = load(BASE / fname) + i = 0 + while i < len(lines): + if lines[i].strip().startswith('```mermaid'): + start = i + first_inner = lines[i + 1].strip() if i + 1 < len(lines) else '' + has_init = first_inner.startswith('%%{init') + j = i + 1 + while j < len(lines) and not lines[j].strip().startswith('```'): + j += 1 + k = j + 1 + while k < len(lines) and lines[k].strip() == '': + k += 1 + followed = k < len(lines) and re.match(r'^1[.)]\s', lines[k].strip()) + print(f"{fname}:{start+1} mermaid: init={'OK' if has_init else 'MISSING'} numbered-list-after={'OK' if followed else 'MISSING'}") + i = j + i += 1 + + print("\n=== JOB 2f: ID CONSISTENCY SCAN ===") + # definitions from headings + def defined_ids(fname, pat): + heads, _ = headings_and_slugs(BASE / fname) + out = set() + for _, h in heads: + m = re.match(pat, h) + if m: + out.add(m.group(1)) + return out + + tasks_def = defined_ids("task-inventory.md", r'^(T\d{2})\b') + chal_def = defined_ids("challenges.md", r'^(C\d{1,2})\b') + poc_def = defined_ids("poc-plan.md", r'^(POC-\d)\b') + + refs = {'T': {}, 'C': {}, 'POC': {}, 'D': {}, 'A': {}, 'phase': {}} + for fname in FILES: + text = (BASE / fname).read_text() + for m in re.finditer(r'\bT\d{2}\b', text): + refs['T'].setdefault(m.group(0), set()).add(fname) + for m in re.finditer(r'\bC\d{1,2}\b', text): + refs['C'].setdefault(m.group(0), set()).add(fname) + for m in re.finditer(r'\bPOC-\d\b', text): + refs['POC'].setdefault(m.group(0), set()).add(fname) + for m in re.finditer(r'\bD\d\b', text): + refs['D'].setdefault(m.group(0), set()).add(fname) + for m in re.finditer(r'\bA\d\b', text): + refs['A'].setdefault(m.group(0), set()).add(fname) + for m in re.finditer(r'(?i)\bphase\s+(\d)\b', text): + refs['phase'].setdefault(m.group(1), set()).add(fname) + + print(f"tasks defined: {sorted(tasks_def)}") + print(f"task refs outside defined set: {sorted(set(refs['T']) - tasks_def)}") + print(f"challenges defined: {sorted(chal_def, key=lambda x: int(x[1:]))}") + print(f"challenge refs outside defined set: {sorted(set(refs['C']) - chal_def)}") + print(f"POCs defined: {sorted(poc_def)}") + print(f"POC refs outside defined set: {sorted(set(refs['POC']) - poc_def)}") + print(f"D refs: {sorted(refs['D'])} (defined D1-D6 in agent-architecture open-decisions table)") + print(f"A-token refs: {sorted(refs['A'])} <- note A4 is a CA3 box, not an autonomy level") + print(f"phase numbers referenced: {sorted(refs['phase'])}") + + print("\n=== JOB 2g: DATE SCAN (regulatory) ===") + date_pat = re.compile(r'20\d{2}-\d{2}(?:-\d{2})?') + for fname in FILES: + for i, line in enumerate(load(BASE / fname), 1): + for m in date_pat.finditer(line): + d = m.group(0) + if d.startswith(('2026-09', '2027-09', '2027-01', '2026-12', '2027-05')): + print(f"{fname}:{i}: {d} | {line.strip()[:100]}") + + print("\n=== BONUS: parent PRD hub backlink ===") + parent = BASE.parent / "README.md" + if parent.exists(): + ptext = parent.read_text() + print(f"vibe/PRD/README.md links ai-back-office: {'YES' if 'ai-back-office' in ptext else 'NO β€” tree not registered in parent hub'}") + else: + print("parent PRD hub missing") + + +if __name__ == '__main__': + main() diff --git a/vibe/PRD/ai-back-office/task-inventory.md b/vibe/PRD/ai-back-office/task-inventory.md index a96fa2d..4e64ac9 100644 --- a/vibe/PRD/ai-back-office/task-inventory.md +++ b/vibe/PRD/ai-back-office/task-inventory.md @@ -252,4 +252,4 @@ Explicitly out of the current inventory; each becomes a task fiche when its trig - **Payroll & DSN** β€” dormant until the first salary is paid (see hub non-goals). - **Prospection/CRM admin** β€” the `prospection` repo exists (its own PRD, daily pipeline, Mode A/B scoring); its admin loops (follow-ups, pipeline hygiene) and the conversion bridge (a Mode-B prospect becoming a Dolibarr thirdparty + proposal through the gated write path) reuse this fleet's patterns later. Audited 2026-07-11: Dolibarr's propal/contrat/projet modules are enabled but empty β€” proposals and contracts live outside the ERP today. - **Contract lifecycle** β€” record contracts in the ERP (the KM retainer terms exist only as WIP JSON today: $5,500/mo = $2,500 net-30 + $3,000 deferred +3 months β€” the deferred schedule is the source of truth T06 dunning needs), renewal reminders, obligation extraction (the KM 4 % new-business clause), signed originals in the GED. Needs write-skill ops for contacts/contracts/products (same extension pattern as the GED-attach issue). -- **Client delivery agents β€” the third family.** Agents working *on* client projects (the KM architecture notes in the vault are the embryo) share the [client dossiers](agent-architecture.md#knowledge-layer--the-second-brain) with this fleet: they deposit decisions, meeting notes (via a **local capture lane** β€” [D9](agent-architecture.md#open-decisions): Hyprnote free tier on the M4 β€” local transcription + speaker diarization + Google Calendar sync, manual trigger; Meetily as OSS fallback; markdown export β†’ `sb.py` ingest matched to the calendar event β†’ Ornith summary, per the vault doctrine β€” no cloud notetaker) and new-business sightings; they retrieve contract scope, contacts and billing state. Reuses T17 rails and the vault routing doctrine; REX at project close feeds `03-Resources` β†’ prospection Mode B evidence β†’ CMS case studies. +- **Client delivery agents β€” the third family.** Agents working *on* client projects (the KM architecture notes in the vault are the embryo) share the [client dossiers](agent-architecture.md#knowledge-layer--the-second-brain) with this fleet: they deposit decisions, meeting notes (via a **local capture lane** β€” [D9](agent-architecture.md#open-decisions), **parked as nice-to-have 2026-07-12** (calls are iPhone-first today): Hyprnote free tier on the M4 β€” local transcription + speaker diarization + Google Calendar sync, manual trigger; Meetily as OSS fallback; markdown export β†’ `sb.py` ingest matched to the calendar event β†’ Ornith summary, per the vault doctrine β€” no cloud notetaker) and new-business sightings; they retrieve contract scope, contacts and billing state. Reuses T17 rails and the vault routing doctrine; REX at project close feeds `03-Resources` β†’ prospection Mode B evidence β†’ CMS case studies. -- 2.54.0