diff --git a/.agents/skills/daily-plan/AGENT_INSTRUCTIONS.md b/.agents/skills/daily-plan/AGENT_INSTRUCTIONS.md index d94d5c023..2314f67da 100644 --- a/.agents/skills/daily-plan/AGENT_INSTRUCTIONS.md +++ b/.agents/skills/daily-plan/AGENT_INSTRUCTIONS.md @@ -19,9 +19,27 @@ context. Do not rely on hook-driven side effects for any write you make. ## Phase 1: Context Gathering -Gather ALL of the following, in parallel where possible. If any source fails or -an optional integration is not set up, skip it silently and note the skipped -source in your final report. Never error to the user. +Gather ALL of the following. If any source fails or an optional integration is +not set up, skip it silently and note the skipped source in your final report. +Never error to the user. + +**Issue the steps in three batches, each as one turn of parallel tool calls.** +The numbered steps below are a checklist, not an order of execution. Running +them one after another is what made this brief take minutes; nothing in batch +A depends on anything else in batch A. + +- **Batch A (independent reads, one turn):** 1.1 week progress, 1.2 calendar, + 1.4 commitments and feeds, 1.5 tasks, 1.6 work summary, 1.7 completed + reminders, 1.8 Dex Inbox, 1.9 email connection check, 1.10 chat connection + check, 1.12 ideas, and the profile read plus note listing for 1.13. +- **Batch B (needs batch A's results, one turn):** 1.3 meeting context for + every meeting, 1.9 and 1.10 queries for connected sources, 1.11 semantic + queries, and the `lookup_person` calls for 1.13. +- **Batch C (writes):** the 1.7 task status updates, the 1.13 person-page + updates, then Phase 3. + +Every step's own rules, gates, and post-conditions still apply exactly as +written; batching changes when a call is issued, never whether it is made. ### 1.1 Week Progress @@ -133,9 +151,12 @@ from them; triage is an interactive step in the main conversation. ### 1.9 Email Intelligence (if connected) Check `System/integrations/config.yaml`. Also treat a registered `apple-mail-mcp` -server as connected. Before querying any connected email source, run -`python3 core/utils/doctor.py --deep`; Apple Mail search is usable only when the -`mail.apple-search` check reports `OK` / `feature_status: ok`. +server as connected. Before querying Apple Mail, run +`python3 core/utils/doctor.py --deep --only mail.apple-search`; Apple Mail search +is usable only when the `mail.apple-search` check reports `OK` / +`feature_status: ok`. `--only` runs that one probe, not the whole live checkup. +Google Workspace needs no local probe: its own tool response carries the health +status. If the source is connected and healthy: diff --git a/.agents/skills/daily-plan/SKILL.md b/.agents/skills/daily-plan/SKILL.md index 8685f54f8..2f70255ba 100644 --- a/.agents/skills/daily-plan/SKILL.md +++ b/.agents/skills/daily-plan/SKILL.md @@ -28,8 +28,11 @@ Agent tool, using the self-contained prompt in this skill's 1. Read `.agents/skills/daily-plan/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{TARGET_DATE}}`, `{{TARGET_DATE_PLUS_1}}`, `{{DAY_NAME}}`, `{{MONTH}}`, `{{DD}}`). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical gathering, so it runs on the fast tier declared by this skill's + `model_routing.steps.data-gathering`; the judgement calls stay in this + conversation on the default model. 4. Verify it wrote the draft plan to `00-Inbox/Daily_Plans/YYYY-MM-DD.md`, then run the remaining interactive steps from its findings and present the plan. 5. **Close out every `` line in the draft.** The subagent @@ -92,13 +95,27 @@ Before executing this command, read `System/user-profile.yaml` → `communicatio --- -## Step 0: Process Unprocessed Meetings +## Step 0: Process Unprocessed Meetings (only when something is waiting) -Before gathering context, ensure recent meetings are in the vault by running `/process-meetings`. This pulls any unprocessed meetings from the meeting source (Otter.ai, Granola, etc.), creates meeting notes, updates person/company pages, and extracts tasks — so the daily plan has complete data from yesterday and any earlier gaps. +Before gathering context, ask the session-start sweep how many meeting records +are still waiting, without re-scanning the vault yourself. The count mode +prints one integer, bypasses the sweep's once-per-30-minutes notice throttle, +and writes nothing, so it is safe to call every morning: -- If no new meetings are found, continue silently -- If meetings are processed, note the count and use the extracted context in the plan -- Do NOT ask for a skill rating after this sub-step — save that for the end of the full plan +Ask the host for its count of waiting meeting records if it keeps one; if it does not, treat the count as unknown and run the meeting pass. + +Branch on the answer: + +- **`0`:** skip this step entirely and say nothing. Yesterday evening's review + already ran the catch-up pass, so on most mornings this is the answer, and + re-running a full seven-day `/process-meetings` here found nothing new while + costing minutes. +- **Above `0`:** run `/process-meetings`. This creates meeting notes, updates + person/company pages, and extracts tasks, so the plan has complete data. + Note the count and use the extracted context in the plan. Do NOT ask for a + skill rating after this sub-step — save that for the end of the full plan. +- **The command fails or prints nothing:** treat that as unknown, not as zero: + run `/process-meetings` as before. ## Step 0.5: Dex Inbox Check (Phone Captures) @@ -343,9 +360,12 @@ For each completed item: ### 5.8 Email Intelligence (if connected) Check `System/integrations/config.yaml` for `google-workspace.enabled: true`. Also treat a -registered `apple-mail-mcp` server as a connected source. Before querying a connected email -source, run `python3 core/utils/doctor.py --deep`; Apple Mail search is usable only when the -`mail.apple-search` check reports `OK` / `feature_status: ok`. +registered `apple-mail-mcp` server as a connected source. Before querying Apple Mail, run +`python3 core/utils/doctor.py --deep --only mail.apple-search`; Apple Mail search is usable +only when the `mail.apple-search` check reports `OK` / `feature_status: ok`. `--only` runs +that one probe instead of the whole live checkup (smoke journeys, search index, connected +tools), which is what a full `--deep` run does and what made this step slow every morning. +Google Workspace needs no local probe: its own tool response carries the health status. If connected and healthy: 1. Use an attention-inbox count for the headline, never a provider-wide unread total. diff --git a/.agents/skills/daily-review/AGENT_INSTRUCTIONS.md b/.agents/skills/daily-review/AGENT_INSTRUCTIONS.md index a3c20f4f5..1083bb2c4 100644 --- a/.agents/skills/daily-review/AGENT_INSTRUCTIONS.md +++ b/.agents/skills/daily-review/AGENT_INSTRUCTIONS.md @@ -11,6 +11,16 @@ any section gracefully if a tool fails; never error out. **Note:** PostToolUse hooks from the parent skill do not fire in this subagent context. Do not rely on hook-driven side effects for any write you make. +**Issue the steps in batches, each as one turn of parallel tool calls.** Step 0 +runs first because it changes the notes the rest reads. After it, everything in +Steps 1 through 3 and 5 through 6 is independent: the file discovery, the task +file read, `get_week_progress()` and `get_week_priorities()`, the meeting note +reads, `calendar_get_today()`, the Reminders calls, the email check, today's +plan file, and tomorrow's calendar all go out in one turn. Step 2.4's semantic +queries follow in a second turn once you know today's tasks and meetings. Then +write. Running the steps one after another is what made this brief slow; each +step's own rules and gates still apply exactly as written. + --- ## Step 0: Meeting Catch-Up (not same-day only) @@ -32,10 +42,14 @@ Why the window matters: a same-day filter loses meetings permanently on any day the review does not run. Process local candidates following the process-meetings instructions, including -provider-neutral discovery and updating person pages directly rather than -counting on a hook. Note the actual path of every meeting processed. If the -profile is missing or malformed, report that once and use the safe local -fallback. If nothing is unprocessed, skip silently. +its batching rules, provider-neutral discovery and updating person pages +directly rather than counting on a hook. Note the actual path of every meeting +processed. If the profile is missing or malformed, report that once and use the +safe local fallback. If nothing is unprocessed, skip silently. + +This is the only meeting pass of the evening. The main conversation no longer +runs `/process-meetings` itself, so a meeting you skip here is not caught later +today; the catch-up window above is what makes that safe. --- diff --git a/.agents/skills/daily-review/SKILL.md b/.agents/skills/daily-review/SKILL.md index ded1b5fcc..a73006606 100644 --- a/.agents/skills/daily-review/SKILL.md +++ b/.agents/skills/daily-review/SKILL.md @@ -38,8 +38,10 @@ Agent tool, using the self-contained prompt in this skill's 1. Read `.agents/skills/daily-review/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{TARGET_DATE}}`, `{{TOMORROW_DATE}}`, `{{TOMORROW_DATE_PLUS_1}}`, `{{DAY_NAME}}`, `{{MONTH}}`, `{{DD}}`, `{{YYYY}}`). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical gathering, so it runs on the fast tier; the judgement calls stay + in this conversation on the default model. 4. Verify it wrote the draft to `07-Archives/Reviews/Daily_Review_YYYY-MM-DD.md`, then run the interactive steps from its findings and complete the placeholder sections. @@ -93,13 +95,20 @@ find . -type f -name "*.md" -newermt "$TODAY 00:00:00" ! -newermt "$TODAY 23:59: --- -## Step 1.5: Process Today's Meetings +## Step 1.5: Today's Meetings Are Already Processed -Before gathering context, ensure today's meetings are in the vault by running `/process-meetings today`. This pulls any unprocessed meetings from the meeting source (Otter.ai, Granola, etc.), creates meeting notes, updates person/company pages, and extracts tasks — so the rest of the review has complete data. +Meeting catch-up happens once, inside the gathering helper (its Step 0 processes +every unprocessed meeting since the last one that was, following +`process-meetings/AGENT_INSTRUCTIONS.md`). Do NOT also run `/process-meetings` +from this conversation: that ran the whole meeting pass twice per evening, and +the second pass found nothing new. -- If no new meetings are found, continue silently -- If meetings are processed, note the count for the review summary -- Do NOT ask for a skill rating after this sub-step — save that for the end of the full review +- Read the helper's "Meetings processed in catch-up" line and carry the count + into the review summary +- If the helper reported meetings left unstamped or an older backlog, surface + those lines; do not re-run the pass to check them +- Only if the helper failed and the inline fallback is running does this + conversation process meetings itself, following the same brief --- @@ -551,7 +560,7 @@ Add one line at the end of the review output: | Integration | MCP Server | Tools Used | |-------------|------------|------------| -| Meetings | Meeting source MCP (via `/process-meetings today`) | Fetches and processes unprocessed meetings | +| Meetings | Local meeting notes (helper Step 0 catch-up, following `process-meetings/AGENT_INSTRUCTIONS.md`) | Processes unprocessed meetings once per review | | Work | work-mcp | `list_tasks`, `get_week_progress`, `get_commitments_due`, `analyze_calendar_capacity` | | Calendar | calendar-mcp | `calendar_get_today` | | Reminders | calendar-mcp | `reminders_list_completed`, `reminders_find_and_complete`, `reminders_clear_completed`, `reminders_list_items` | diff --git a/.agents/skills/process-meetings/AGENT_INSTRUCTIONS.md b/.agents/skills/process-meetings/AGENT_INSTRUCTIONS.md index 55a62c937..b7baa5cd5 100644 --- a/.agents/skills/process-meetings/AGENT_INSTRUCTIONS.md +++ b/.agents/skills/process-meetings/AGENT_INSTRUCTIONS.md @@ -17,6 +17,24 @@ hook does also run, because both skip a page that already lists the meeting. - `--people-only`: only update person/company pages (skip tasks) - `--no-todos`: create notes but do not extract tasks +**Work in batches, not one meeting at a time.** Every step below is written per +meeting for clarity, but the tool calls are independent across meetings, so +issue them together: + +1. **Read everything first, in one turn:** the profile, the state file, the + directory listings, and every candidate note. Do not read one note, process + it, then read the next. +2. **Look up everyone at once:** one turn of `lookup_person` calls covering + every participant across every meeting, plus the calendar fetches (one per + date) and `detect_soft_commitments` for every meeting. +3. **Then write:** person and company page updates, `create_task` calls, and + markers. Writes to the same file stay sequential; writes to different files + can share a turn. + +Sequential per-meeting processing is what made this brief take minutes for a +handful of meetings. The verification rules in Step 6 are unchanged: batching +changes when calls are issued, never whether their results are checked. + --- ## Step 1: Resolve the Local Meeting Sources @@ -331,9 +349,19 @@ Processing complete. |------|---------|---------|--------------| | ... | ... | ... | ... | +### Touched + +- Tasks created: [every `task-YYYYMMDD-XXX` ID returned by `create_task`, or none] +- Notes stamped: [every vault-relative note path you appended the marker to, or none] +- Pages created: [every vault-relative person or company page path you created, or none] + [Any warnings or issues encountered] ``` +The `Touched` block is what the conversation verifies before repeating your +counts, so it must list every item exactly, one targeted check each. A count +without a matching `Touched` entry is treated as unverified. + --- ## Important Notes diff --git a/.agents/skills/process-meetings/SKILL.md b/.agents/skills/process-meetings/SKILL.md index 966e10c90..1f36178c6 100644 --- a/.agents/skills/process-meetings/SKILL.md +++ b/.agents/skills/process-meetings/SKILL.md @@ -28,8 +28,11 @@ subagent via the Agent tool, using the self-contained prompt in this skill's 1. Read `.agents/skills/process-meetings/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{ARGS}}`, the arguments passed to this skill). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical processing with its own verification steps, so it runs on the + fast tier; anything that needs a judgement call comes back to this + conversation. 4. Display its summary report. The subagent inherits MCP connections, runs in its own context, and that context @@ -71,13 +74,21 @@ until someone resolves it. **The report is a claim, not evidence — check it before repeating it.** This subagent writes to the vault, and its summary states counts the user will act -on. Before displaying it, verify the claims cheaply against the vault: - -- Every task it says it created: confirm the ID appears in `03-Tasks/Tasks.md` - (`list_tasks`, or read the file). -- Every meeting it says it stamped: confirm the `tasks-extracted` marker is - actually in that note. -- Every person or company page it says it created: confirm the file exists. +on. Its report ends with a `Touched` block naming every task ID it created, +every note it stamped, and every page it created. Verify exactly those items, +and nothing wider: + +- Each listed task ID: confirm it appears in `03-Tasks/Tasks.md` with one + targeted search per ID (`grep`, or `list_tasks` filtered to that ID). Do not + read the whole task file. +- Each listed stamped note: confirm the `tasks-extracted` marker is actually in + that one file. +- Each listed created page: confirm the file exists. + +Do not re-read every meeting note or the full task list to look for claims the +report did not make; the full sweep was what made this step slow, and it never +found anything the `Touched` block did not name. If the report has no `Touched` +block, treat every count in it as unverified and say so. If a claim does not hold, say so plainly in the summary you present and treat that meeting as unprocessed. Never pass an unverified count to the user as fact, diff --git a/.claude/hooks/meeting-queue-check.cjs b/.claude/hooks/meeting-queue-check.cjs index 63b057362..bfd8ed157 100644 --- a/.claude/hooks/meeting-queue-check.cjs +++ b/.claude/hooks/meeting-queue-check.cjs @@ -277,11 +277,19 @@ function noticeLines(count) { ]; } +// `countOnly` answers "how many meeting records are waiting?" for a skill that +// is about to decide whether to run a meeting pass at all (daily-plan Step 0). +// It bypasses the 30-minute notice throttle and never writes the throttle +// marker: the throttle exists so concurrent sessions do not pile on the same +// notice, and a count is not a notice. Without this, a skill asking right after +// session start would always be told zero, because session start had just +// spent the throttle window. function checkMeetingQueue(options = {}) { try { const { vaultRoot = process.cwd(), now = Date.now(), + countOnly = false, } = options || {}; const nowMilliseconds = Number(now); if (typeof vaultRoot !== 'string' || !Number.isFinite(nowMilliseconds)) { @@ -293,7 +301,7 @@ function checkMeetingQueue(options = {}) { const markerPath = path.join(paths.systemDir, '.last-meeting-queue-notice'); const nowSeconds = Math.floor(nowMilliseconds / 1000); - if (isThrottled(markerPath, nowSeconds)) return emptyResult(); + if (!countOnly && isThrottled(markerPath, nowSeconds)) return emptyResult(); let meetingsDirectory; try { @@ -310,6 +318,7 @@ function checkMeetingQueue(options = {}) { existingGranolaIds, ) + countQueueFiles(paths.meetingsDir, existingGranolaIds); if (count === 0) return emptyResult(); + if (countOnly) return { count, lines: [] }; try { fs.mkdirSync(paths.systemDir, { recursive: true }); @@ -328,10 +337,18 @@ module.exports = { checkMeetingQueue }; if (require.main === module) { try { - const result = checkMeetingQueue({ vaultRoot: process.argv[2] || process.cwd() }); - if (result.count > 0) process.stdout.write(`${result.lines.join('\n')}\n`); + const args = process.argv.slice(2); + const countOnly = args.includes('--count'); + const vaultRoot = args.find((arg) => !arg.startsWith('--')) || process.cwd(); + const result = checkMeetingQueue({ vaultRoot, countOnly }); + if (countOnly) { + // One integer, always, so a skill can branch on it without parsing prose. + process.stdout.write(`${result.count}\n`); + } else if (result.count > 0) { + process.stdout.write(`${result.lines.join('\n')}\n`); + } } catch (error) { - // Silent by design. + // Silent by design. In --count mode the caller treats no output as unknown. } process.exitCode = 0; } diff --git a/.claude/hooks/tests/meeting-queue-check.test.cjs b/.claude/hooks/tests/meeting-queue-check.test.cjs index 3a1d2ef9e..c489f50f3 100644 --- a/.claude/hooks/tests/meeting-queue-check.test.cjs +++ b/.claude/hooks/tests/meeting-queue-check.test.cjs @@ -520,3 +520,61 @@ test('an unreadable meetings directory returns zero without throwing', (t) => { } assert.deepEqual(result, { count: 0, lines: [] }); }); + +test('countOnly reports the waiting count even inside the notice throttle window', (t) => { + const root = fixture(t); + writeDayMeeting(root, TODAY, 'manual-notes.md', meetingNote()); + const marker = path.join(root, 'System', '.last-meeting-queue-notice'); + fs.mkdirSync(path.dirname(marker), { recursive: true }); + fs.writeFileSync(marker, `${Math.floor(NOW / 1000) - 60}\n`); + + const throttled = checkMeetingQueue({ vaultRoot: root, now: NOW }); + const counted = checkMeetingQueue({ vaultRoot: root, now: NOW, countOnly: true }); + + assert.deepEqual(throttled, { count: 0, lines: [] }); + assert.deepEqual(counted, { count: 1, lines: [] }); +}); + +test('countOnly never writes the notice marker', (t) => { + const root = fixture(t); + writeDayMeeting(root, TODAY, 'manual-notes.md', meetingNote()); + const marker = path.join(root, 'System', '.last-meeting-queue-notice'); + + const counted = checkMeetingQueue({ vaultRoot: root, now: NOW, countOnly: true }); + + assert.equal(counted.count, 1); + assert.equal(fs.existsSync(marker), false, 'a count is not a notice, so it must not spend the throttle'); + + const notice = checkMeetingQueue({ vaultRoot: root, now: NOW }); + assert.equal(notice.count, 1, 'the notice still fires after a count-only read'); + assert.equal(fs.existsSync(marker), true); +}); + +test('countOnly returns zero, not a notice, when nothing is waiting', (t) => { + const root = fixture(t); + writeDayMeeting(root, TODAY, 'done.md', meetingNote({ tasksExtracted: true })); + + assert.deepEqual( + checkMeetingQueue({ vaultRoot: root, now: NOW, countOnly: true }), + { count: 0, lines: [] }, + ); +}); + +test('--count on the command line prints one integer and nothing else', (t) => { + const { execFileSync } = require('node:child_process'); + const root = fixture(t); + // The CLI uses the real clock, so these notes must be dated today for real. + const realToday = new Date().toISOString().slice(0, 10); + writeDayMeeting(root, realToday, 'a.md', meetingNote({ day: realToday })); + writeDayMeeting(root, realToday, 'b.md', meetingNote({ day: realToday })); + + const output = execFileSync(process.execPath, [HOOK_PATH, '--count', root], { + encoding: 'utf8', + }); + + assert.equal(output, '2\n'); + assert.equal( + fs.existsSync(path.join(root, 'System', '.last-meeting-queue-notice')), + false, + ); +}); diff --git a/.claude/skills/daily-plan/AGENT_INSTRUCTIONS.md b/.claude/skills/daily-plan/AGENT_INSTRUCTIONS.md index d94d5c023..2314f67da 100644 --- a/.claude/skills/daily-plan/AGENT_INSTRUCTIONS.md +++ b/.claude/skills/daily-plan/AGENT_INSTRUCTIONS.md @@ -19,9 +19,27 @@ context. Do not rely on hook-driven side effects for any write you make. ## Phase 1: Context Gathering -Gather ALL of the following, in parallel where possible. If any source fails or -an optional integration is not set up, skip it silently and note the skipped -source in your final report. Never error to the user. +Gather ALL of the following. If any source fails or an optional integration is +not set up, skip it silently and note the skipped source in your final report. +Never error to the user. + +**Issue the steps in three batches, each as one turn of parallel tool calls.** +The numbered steps below are a checklist, not an order of execution. Running +them one after another is what made this brief take minutes; nothing in batch +A depends on anything else in batch A. + +- **Batch A (independent reads, one turn):** 1.1 week progress, 1.2 calendar, + 1.4 commitments and feeds, 1.5 tasks, 1.6 work summary, 1.7 completed + reminders, 1.8 Dex Inbox, 1.9 email connection check, 1.10 chat connection + check, 1.12 ideas, and the profile read plus note listing for 1.13. +- **Batch B (needs batch A's results, one turn):** 1.3 meeting context for + every meeting, 1.9 and 1.10 queries for connected sources, 1.11 semantic + queries, and the `lookup_person` calls for 1.13. +- **Batch C (writes):** the 1.7 task status updates, the 1.13 person-page + updates, then Phase 3. + +Every step's own rules, gates, and post-conditions still apply exactly as +written; batching changes when a call is issued, never whether it is made. ### 1.1 Week Progress @@ -133,9 +151,12 @@ from them; triage is an interactive step in the main conversation. ### 1.9 Email Intelligence (if connected) Check `System/integrations/config.yaml`. Also treat a registered `apple-mail-mcp` -server as connected. Before querying any connected email source, run -`python3 core/utils/doctor.py --deep`; Apple Mail search is usable only when the -`mail.apple-search` check reports `OK` / `feature_status: ok`. +server as connected. Before querying Apple Mail, run +`python3 core/utils/doctor.py --deep --only mail.apple-search`; Apple Mail search +is usable only when the `mail.apple-search` check reports `OK` / +`feature_status: ok`. `--only` runs that one probe, not the whole live checkup. +Google Workspace needs no local probe: its own tool response carries the health +status. If the source is connected and healthy: diff --git a/.claude/skills/daily-plan/SKILL.md b/.claude/skills/daily-plan/SKILL.md index d0a7e67b9..8f0cbd2d5 100644 --- a/.claude/skills/daily-plan/SKILL.md +++ b/.claude/skills/daily-plan/SKILL.md @@ -35,8 +35,11 @@ Agent tool, using the self-contained prompt in this skill's 1. Read `.claude/skills/daily-plan/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{TARGET_DATE}}`, `{{TARGET_DATE_PLUS_1}}`, `{{DAY_NAME}}`, `{{MONTH}}`, `{{DD}}`). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical gathering, so it runs on the fast tier declared by this skill's + `model_routing.steps.data-gathering`; the judgement calls stay in this + conversation on the default model. 4. Verify it wrote the draft plan to `00-Inbox/Daily_Plans/YYYY-MM-DD.md`, then run the remaining interactive steps from its findings and present the plan. 5. **Close out every `` line in the draft.** The subagent @@ -99,13 +102,29 @@ Before executing this command, read `System/user-profile.yaml` → `communicatio --- -## Step 0: Process Unprocessed Meetings +## Step 0: Process Unprocessed Meetings (only when something is waiting) -Before gathering context, ensure recent meetings are in the vault by running `/process-meetings`. This pulls any unprocessed meetings from the meeting source (Otter.ai, Granola, etc.), creates meeting notes, updates person/company pages, and extracts tasks — so the daily plan has complete data from yesterday and any earlier gaps. +Before gathering context, ask the session-start sweep how many meeting records +are still waiting, without re-scanning the vault yourself. The count mode +prints one integer, bypasses the sweep's once-per-30-minutes notice throttle, +and writes nothing, so it is safe to call every morning: -- If no new meetings are found, continue silently -- If meetings are processed, note the count and use the extracted context in the plan -- Do NOT ask for a skill rating after this sub-step — save that for the end of the full plan +```bash +node .claude/hooks/meeting-queue-check.cjs --count +``` + +Branch on the answer: + +- **`0`:** skip this step entirely and say nothing. Yesterday evening's review + already ran the catch-up pass, so on most mornings this is the answer, and + re-running a full seven-day `/process-meetings` here found nothing new while + costing minutes. +- **Above `0`:** run `/process-meetings`. This creates meeting notes, updates + person/company pages, and extracts tasks, so the plan has complete data. + Note the count and use the extracted context in the plan. Do NOT ask for a + skill rating after this sub-step — save that for the end of the full plan. +- **The command fails or prints nothing:** treat that as unknown, not as zero: + run `/process-meetings` as before. ## Step 0.5: Dex Inbox Check (Phone Captures) @@ -350,9 +369,12 @@ For each completed item: ### 5.8 Email Intelligence (if connected) Check `System/integrations/config.yaml` for `google-workspace.enabled: true`. Also treat a -registered `apple-mail-mcp` server as a connected source. Before querying a connected email -source, run `python3 core/utils/doctor.py --deep`; Apple Mail search is usable only when the -`mail.apple-search` check reports `OK` / `feature_status: ok`. +registered `apple-mail-mcp` server as a connected source. Before querying Apple Mail, run +`python3 core/utils/doctor.py --deep --only mail.apple-search`; Apple Mail search is usable +only when the `mail.apple-search` check reports `OK` / `feature_status: ok`. `--only` runs +that one probe instead of the whole live checkup (smoke journeys, search index, connected +tools), which is what a full `--deep` run does and what made this step slow every morning. +Google Workspace needs no local probe: its own tool response carries the health status. If connected and healthy: 1. Use an attention-inbox count for the headline, never a provider-wide unread total. diff --git a/.claude/skills/daily-review/AGENT_INSTRUCTIONS.md b/.claude/skills/daily-review/AGENT_INSTRUCTIONS.md index 139cfd037..8b0ffa109 100644 --- a/.claude/skills/daily-review/AGENT_INSTRUCTIONS.md +++ b/.claude/skills/daily-review/AGENT_INSTRUCTIONS.md @@ -11,6 +11,16 @@ any section gracefully if a tool fails; never error out. **Note:** PostToolUse hooks from the parent skill do not fire in this subagent context. Do not rely on hook-driven side effects for any write you make. +**Issue the steps in batches, each as one turn of parallel tool calls.** Step 0 +runs first because it changes the notes the rest reads. After it, everything in +Steps 1 through 3 and 5 through 6 is independent: the file discovery, the task +file read, `get_week_progress()` and `get_week_priorities()`, the meeting note +reads, `calendar_get_today()`, the Reminders calls, the email check, today's +plan file, and tomorrow's calendar all go out in one turn. Step 2.4's semantic +queries follow in a second turn once you know today's tasks and meetings. Then +write. Running the steps one after another is what made this brief slow; each +step's own rules and gates still apply exactly as written. + --- ## Step 0: Meeting Catch-Up (not same-day only) @@ -32,10 +42,14 @@ Why the window matters: a same-day filter loses meetings permanently on any day the review does not run. Process local candidates following the process-meetings instructions, including -provider-neutral discovery and updating person pages directly rather than -counting on a hook. Note the actual path of every meeting processed. If the -profile is missing or malformed, report that once and use the safe local -fallback. If nothing is unprocessed, skip silently. +its batching rules, provider-neutral discovery and updating person pages +directly rather than counting on a hook. Note the actual path of every meeting +processed. If the profile is missing or malformed, report that once and use the +safe local fallback. If nothing is unprocessed, skip silently. + +This is the only meeting pass of the evening. The main conversation no longer +runs `/process-meetings` itself, so a meeting you skip here is not caught later +today; the catch-up window above is what makes that safe. --- diff --git a/.claude/skills/daily-review/SKILL.md b/.claude/skills/daily-review/SKILL.md index 0c0b51d38..c5259c9d1 100644 --- a/.claude/skills/daily-review/SKILL.md +++ b/.claude/skills/daily-review/SKILL.md @@ -36,8 +36,10 @@ Agent tool, using the self-contained prompt in this skill's 1. Read `.claude/skills/daily-review/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{TARGET_DATE}}`, `{{TOMORROW_DATE}}`, `{{TOMORROW_DATE_PLUS_1}}`, `{{DAY_NAME}}`, `{{MONTH}}`, `{{DD}}`, `{{YYYY}}`). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical gathering, so it runs on the fast tier; the judgement calls stay + in this conversation on the default model. 4. Verify it wrote the draft to `07-Archives/Reviews/Daily_Review_YYYY-MM-DD.md`, then run the interactive steps from its findings and complete the placeholder sections. @@ -91,13 +93,20 @@ find . -type f -name "*.md" -newermt "$TODAY 00:00:00" ! -newermt "$TODAY 23:59: --- -## Step 1.5: Process Today's Meetings +## Step 1.5: Today's Meetings Are Already Processed -Before gathering context, ensure today's meetings are in the vault by running `/process-meetings today`. This pulls any unprocessed meetings from the meeting source (Otter.ai, Granola, etc.), creates meeting notes, updates person/company pages, and extracts tasks — so the rest of the review has complete data. +Meeting catch-up happens once, inside the gathering helper (its Step 0 processes +every unprocessed meeting since the last one that was, following +`process-meetings/AGENT_INSTRUCTIONS.md`). Do NOT also run `/process-meetings` +from this conversation: that ran the whole meeting pass twice per evening, and +the second pass found nothing new. -- If no new meetings are found, continue silently -- If meetings are processed, note the count for the review summary -- Do NOT ask for a skill rating after this sub-step — save that for the end of the full review +- Read the helper's "Meetings processed in catch-up" line and carry the count + into the review summary +- If the helper reported meetings left unstamped or an older backlog, surface + those lines; do not re-run the pass to check them +- Only if the helper failed and the inline fallback is running does this + conversation process meetings itself, following the same brief --- @@ -549,7 +558,7 @@ Add one line at the end of the review output: | Integration | MCP Server | Tools Used | |-------------|------------|------------| -| Meetings | Meeting source MCP (via `/process-meetings today`) | Fetches and processes unprocessed meetings | +| Meetings | Local meeting notes (helper Step 0 catch-up, following `process-meetings/AGENT_INSTRUCTIONS.md`) | Processes unprocessed meetings once per review | | Work | work-mcp | `list_tasks`, `get_week_progress`, `get_commitments_due`, `analyze_calendar_capacity` | | Calendar | calendar-mcp | `calendar_get_today` | | Reminders | calendar-mcp | `reminders_list_completed`, `reminders_find_and_complete`, `reminders_clear_completed`, `reminders_list_items` | diff --git a/.claude/skills/process-meetings/AGENT_INSTRUCTIONS.md b/.claude/skills/process-meetings/AGENT_INSTRUCTIONS.md index 55a62c937..b7baa5cd5 100644 --- a/.claude/skills/process-meetings/AGENT_INSTRUCTIONS.md +++ b/.claude/skills/process-meetings/AGENT_INSTRUCTIONS.md @@ -17,6 +17,24 @@ hook does also run, because both skip a page that already lists the meeting. - `--people-only`: only update person/company pages (skip tasks) - `--no-todos`: create notes but do not extract tasks +**Work in batches, not one meeting at a time.** Every step below is written per +meeting for clarity, but the tool calls are independent across meetings, so +issue them together: + +1. **Read everything first, in one turn:** the profile, the state file, the + directory listings, and every candidate note. Do not read one note, process + it, then read the next. +2. **Look up everyone at once:** one turn of `lookup_person` calls covering + every participant across every meeting, plus the calendar fetches (one per + date) and `detect_soft_commitments` for every meeting. +3. **Then write:** person and company page updates, `create_task` calls, and + markers. Writes to the same file stay sequential; writes to different files + can share a turn. + +Sequential per-meeting processing is what made this brief take minutes for a +handful of meetings. The verification rules in Step 6 are unchanged: batching +changes when calls are issued, never whether their results are checked. + --- ## Step 1: Resolve the Local Meeting Sources @@ -331,9 +349,19 @@ Processing complete. |------|---------|---------|--------------| | ... | ... | ... | ... | +### Touched + +- Tasks created: [every `task-YYYYMMDD-XXX` ID returned by `create_task`, or none] +- Notes stamped: [every vault-relative note path you appended the marker to, or none] +- Pages created: [every vault-relative person or company page path you created, or none] + [Any warnings or issues encountered] ``` +The `Touched` block is what the conversation verifies before repeating your +counts, so it must list every item exactly, one targeted check each. A count +without a matching `Touched` entry is treated as unverified. + --- ## Important Notes diff --git a/.claude/skills/process-meetings/SKILL.md b/.claude/skills/process-meetings/SKILL.md index 9d85213a7..1798c3caa 100644 --- a/.claude/skills/process-meetings/SKILL.md +++ b/.claude/skills/process-meetings/SKILL.md @@ -31,8 +31,11 @@ subagent via the Agent tool, using the self-contained prompt in this skill's 1. Read `.claude/skills/process-meetings/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{ARGS}}`, the arguments passed to this skill). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical processing with its own verification steps, so it runs on the + fast tier; anything that needs a judgement call comes back to this + conversation. 4. Display its summary report. The subagent inherits MCP connections, runs in its own context, and that context @@ -74,13 +77,21 @@ until someone resolves it. **The report is a claim, not evidence — check it before repeating it.** This subagent writes to the vault, and its summary states counts the user will act -on. Before displaying it, verify the claims cheaply against the vault: - -- Every task it says it created: confirm the ID appears in `03-Tasks/Tasks.md` - (`list_tasks`, or read the file). -- Every meeting it says it stamped: confirm the `tasks-extracted` marker is - actually in that note. -- Every person or company page it says it created: confirm the file exists. +on. Its report ends with a `Touched` block naming every task ID it created, +every note it stamped, and every page it created. Verify exactly those items, +and nothing wider: + +- Each listed task ID: confirm it appears in `03-Tasks/Tasks.md` with one + targeted search per ID (`grep`, or `list_tasks` filtered to that ID). Do not + read the whole task file. +- Each listed stamped note: confirm the `tasks-extracted` marker is actually in + that one file. +- Each listed created page: confirm the file exists. + +Do not re-read every meeting note or the full task list to look for claims the +report did not make; the full sweep was what made this step slow, and it never +found anything the `Touched` block did not name. If the report has no `Touched` +block, treat every count in it as unverified and say so. If a claim does not hold, say so plainly in the summary you present and treat that meeting as unprocessed. Never pass an unverified count to the user as fact, diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b81abbc..fd00c65b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ All notable changes to Dex will be documented in this file. --- +## [1.97.19] — Your morning plan and evening review stop doing the same work twice (2026-09-22) + +Your morning plan and evening review had been getting slower for weeks. One user timed a day: about fifteen minutes of reading and processing before Dex asked a single question, with the evening the worst of it. When we traced where the time went, most of it was Dex doing the same work twice, or asking for far more than it needed. Thanks to Michelle for the timings that made this visible. + +**What this fixes for you:** + +* **Your meetings are processed once an evening, not twice.** The evening review used to run the whole meeting pass itself and then send its helper off to run it again. The second pass never found anything new. Now the helper's catch-up is the only pass, and it still covers every meeting since the last one that was processed, so nothing is lost on a day you skip the review. +* **Mornings skip the meeting pass when nothing is waiting.** The plan ran a full seven-day sweep of your meetings every morning, even though the previous evening had just done it. It now asks the same check Dex already runs at the start of every session how many meetings are waiting, and only runs the pass when the answer is above zero. +* **The reading runs on Dex's fast tier.** The helper that gathers your calendar, tasks, notes and mail is doing mechanical reading, so it now runs on the faster model. The parts that need judgement, and every question Dex asks you, stay on the full model in your conversation. +* **The reading happens all at once instead of one source at a time.** The helper's instructions listed a dozen sources in a row, and it read them in a row. They are now grouped so everything independent is fetched together. +* **Dex checks its own work by name, not by re-reading everything.** After processing meetings, Dex used to re-read your whole task list and every meeting note to confirm what its helper had done. The helper now names exactly what it created and stamped, and Dex checks those items only. +* **The morning mail check asks one question instead of running a full checkup.** Before reading Apple Mail, the plan ran Dex's entire live health checkup: smoke tests, the search index, every connected tool. It now runs the one Apple Mail check it needs, and Google Workspace users skip the probe altogether. `/dex-doctor` gained the same ability to run a single named check. + +Being honest about what is measured: the time saved has not yet been timed side by side. The next release adds the timing line that will make that routine. + Connecting Zoom actually works now. Before, turning Zoom on failed for everyone: setup told Dex to install a piece of software that was never published, so it could never finish. diff --git a/System/.installed-files.manifest b/System/.installed-files.manifest index a0c629825..cd95c6ec1 100644 --- a/System/.installed-files.manifest +++ b/System/.installed-files.manifest @@ -271,6 +271,7 @@ .agents/skills/week-review/AGENT_INSTRUCTIONS.md .agents/skills/week-review/SKILL.md .agents/skills/weekly-reflection/SKILL.md +.agents/skills/wispr-setup/SKILL.md .agents/skills/zoom-setup/SKILL.md .ci/test-durations.json .claude/flows/README.md @@ -287,6 +288,8 @@ .claude/hooks/claude-composition-refresh.sh .claude/hooks/company-context-injector.cjs .claude/hooks/connection-health-checker.cjs +.claude/hooks/correction-capture.py +.claude/hooks/correction-capture.sh .claude/hooks/daily-plan-quick-ref.cjs .claude/hooks/dex-core-orientation.sh .claude/hooks/dex-safety-guard.sh @@ -296,9 +299,11 @@ .claude/hooks/maintenance.cjs .claude/hooks/meeting-cache-builder.cjs .claude/hooks/meeting-queue-check.cjs +.claude/hooks/observation-recorder.py .claude/hooks/paths.cjs .claude/hooks/person-context-injector.cjs .claude/hooks/post-meeting-person-update.cjs +.claude/hooks/session-clock.sh .claude/hooks/session-end.sh .claude/hooks/session-start.sh .claude/hooks/skill-freshness.py @@ -309,6 +314,7 @@ .claude/hooks/tests/career-evidence-capture.test.cjs .claude/hooks/tests/connection-health-checker.test.cjs .claude/hooks/tests/context-injectors.test.cjs +.claude/hooks/tests/correction-capture.test.cjs .claude/hooks/tests/daily-plan-quick-ref.test.cjs .claude/hooks/tests/data-safety-instructions.test.cjs .claude/hooks/tests/first-run-directive.test.cjs @@ -320,6 +326,8 @@ .claude/hooks/tests/meeting-queue-check.test.cjs .claude/hooks/tests/post-meeting-person-update.test.cjs .claude/hooks/tests/safety-guard-settings.test.cjs +.claude/hooks/tests/session-clock.test.cjs +.claude/hooks/tests/session-end-record.test.cjs .claude/hooks/tests/session-staleness.test.cjs .claude/hooks/tests/things-adapter.test.cjs .claude/hooks/tests/todoist-adapter.test.cjs @@ -885,6 +893,7 @@ .claude/skills/week-review/AGENT_INSTRUCTIONS.md .claude/skills/week-review/SKILL.md .claude/skills/weekly-reflection/SKILL.md +.claude/skills/wispr-setup/SKILL.md .claude/skills/xray/SKILL.md .claude/skills/zoom-setup/SKILL.md .cursor/rules/search-routing.mdc @@ -1018,6 +1027,8 @@ System/Templates/README.md System/claude-code-state.json System/integrations/config.yaml System/integrations/pipedrive.yaml.example +System/knowledge-half-life.example.yaml +System/knowledge-half-life.yaml System/pillars.example.yaml System/pillars.yaml System/trusted-mcps.example.yaml @@ -1133,6 +1144,7 @@ core/integrations/connection-manager/lib/oauth-refresh.js core/integrations/connection-manager/lib/rate-limit.js core/integrations/connection-manager/lifted-conformance.test.cjs core/integrations/connection-manager/oauth-flow.cjs +core/integrations/connection-manager/oauth-remote-mcp.test.cjs core/integrations/connection-manager/pinned-providers.cjs core/integrations/connection-manager/presence-approve-preload.test.cjs core/integrations/connection-manager/presence.cjs @@ -1215,6 +1227,17 @@ core/mcp/tests/test_onboarding_server.py core/mcp/update_checker.py core/mcp/work_server.py core/meeting_capture_match.py +core/meeting_sources/__init__.py +core/meeting_sources/attribution.py +core/meeting_sources/calendar_lookup.py +core/meeting_sources/granola_adapter.py +core/meeting_sources/landing_zone.py +core/meeting_sources/record.py +core/meeting_sources/wispr_adapter.py +core/meeting_sources/wispr_auth.py +core/meeting_sources/wispr_client.py +core/meeting_sources/wispr_setup.py +core/meeting_sources/wispr_sync.py core/migrations/README.md core/migrations/__init__.py core/migrations/migrate_v1_to_v2.py @@ -1295,6 +1318,7 @@ core/tests/fixtures/entity_pages/render-company.expected.md core/tests/fixtures/entity_pages/render-company.input.json core/tests/fixtures/entity_pages/render-person.expected.md core/tests/fixtures/entity_pages/render-person.input.json +core/tests/fixtures/google_workspace_mcp_pair.py core/tests/fixtures/release-catalog-v1.96.2-historic-shape.json core/tests/fixtures/release-catalog-v1.96.2-installed-files.manifest core/tests/fixtures/release-catalog-v1.96.2-tag-split.json @@ -1364,7 +1388,9 @@ core/tests/test_architecture_inventory.py core/tests/test_automation_ownership_contract.py core/tests/test_backup_vault.py core/tests/test_branch_protection_script.py +core/tests/test_calendar_create_event_month_end.py core/tests/test_calendar_eventkit_script.py +core/tests/test_calendar_lookup.py core/tests/test_calendar_provider_routing_instruction_contract.py core/tests/test_calendar_server.py core/tests/test_calendar_skill_degradation_instruction_contract.py @@ -1428,9 +1454,12 @@ core/tests/test_feedback_client_script.py core/tests/test_feedback_sweep.py core/tests/test_feedback_trigger_routing.py core/tests/test_first_run_trigger.py +core/tests/test_fiscal_year_goal_anchors.py core/tests/test_fixture_vault.py core/tests/test_founder_content_gate.py +core/tests/test_freshness.py core/tests/test_fuzz_properties.py +core/tests/test_fuzzy_person_match_guard.py core/tests/test_golden_journeys.py core/tests/test_google_workspace_setup_instruction_contract.py core/tests/test_google_workspace_setup_truth.py @@ -1442,6 +1471,7 @@ core/tests/test_harness_plugin.py core/tests/test_harness_portability.py core/tests/test_harness_registry.py core/tests/test_harness_safety_gates.py +core/tests/test_harness_safety_parity.py core/tests/test_harness_session_context.py core/tests/test_health_compatibility.py core/tests/test_health_doctor_ref.py @@ -1468,6 +1498,7 @@ core/tests/test_launch_agents.py core/tests/test_learning_automation.py core/tests/test_learning_capture_command_name.py core/tests/test_learning_review_prompt.py +core/tests/test_learning_routing.py core/tests/test_lens_catalog_ci_trigger.py core/tests/test_lens_catalog_discovery.py core/tests/test_lens_catalog_enriched_discovery.py @@ -1483,11 +1514,14 @@ core/tests/test_manifest_lifecycle_paths.py core/tests/test_manual_note_match.py core/tests/test_mcp_registration_lifecycle.py core/tests/test_mcp_servers.py +core/tests/test_mcp_session_lifecycle.py core/tests/test_mcp_stdio_startup.py core/tests/test_mcp_tool_behavior.py +core/tests/test_meeting_attribution.py core/tests/test_meeting_capture_calendar_match.py core/tests/test_meeting_closeout_skill.py core/tests/test_meeting_prep_calendar_journey.py +core/tests/test_meeting_record_contract.py core/tests/test_meeting_source_instruction_contract.py core/tests/test_messy_vault_parsers.py core/tests/test_no_deprecated_models.py @@ -1510,6 +1544,7 @@ core/tests/test_process_meetings_capture_calendar_wiring.py core/tests/test_process_meetings_soft_commitment_wiring.py core/tests/test_product_agents_bootstrap.py core/tests/test_provision_parity.py +core/tests/test_provision_python_selection.py core/tests/test_provision_transaction.py core/tests/test_qmd_query_grep_fallback.py core/tests/test_reanchor_cli.py @@ -1555,6 +1590,7 @@ core/tests/test_soft_promise_detection.py core/tests/test_soft_promise_hook.py core/tests/test_split_probe_regression.py core/tests/test_sync_daemon.py +core/tests/test_task_id_not_reused_after_archive.py core/tests/test_task_id_rollover.py core/tests/test_task_sync.py core/tests/test_temperature.py @@ -1573,6 +1609,10 @@ core/tests/test_update_rollback_journey.py core/tests/test_wave3_mature_skill_amendments.py core/tests/test_wave3_role_skill_contracts.py core/tests/test_week_review_clear_completed_tasks_instruction_contract.py +core/tests/test_wispr_auth.py +core/tests/test_wispr_client.py +core/tests/test_wispr_pipeline.py +core/tests/test_wispr_setup.py core/tests/test_work_server_entity_links.py core/tests/test_work_server_legacy_task_completion.py core/tests/test_work_server_meeting_context.py @@ -1612,14 +1652,17 @@ core/utils/doctor.py core/utils/entity_pages.py core/utils/feature_status.py core/utils/feedback_sweep.py +core/utils/freshness.py core/utils/health_session.py core/utils/health_telemetry.py core/utils/history_hygiene.py core/utils/integration_credentials.py core/utils/launch_agents.py +core/utils/learning_routing.py core/utils/local_git.py core/utils/manifest.py core/utils/mcp_handshake.py +core/utils/mcp_session_lifecycle.py core/utils/nudge_calendar.py core/utils/page_generators.py core/utils/preflight.py @@ -1640,6 +1683,7 @@ core/utils/trust_registry.py core/utils/update_verifier.py core/utils/validators.py core/utils/working_week.py +core/vault_selection.py docs/Dex_System/Background_Processing_Guide.md docs/Dex_System/Calendar_Setup.md docs/Dex_System/Dex_Jobs_to_Be_Done.md @@ -1675,7 +1719,9 @@ docs/dexdiff-runtime-boundary.md docs/examples/dex-lens-catalog-enriched-preview.json docs/feedback-loop-contract.md docs/gate-omission-audit-2026-08-11.md +docs/harness-safety-vault-contract.md docs/health-telemetry.md +docs/meeting-sources.md docs/merge-gates.md docs/plans/2026-07-23-brain-vault-existing-user-readiness.md docs/plans/2026-07-23-pr141-reconciliation.md @@ -1759,6 +1805,7 @@ packages/dex-agent-plugin/runtime/core/gates/__init__.py packages/dex-agent-plugin/runtime/core/gates/safety.py packages/dex-agent-plugin/runtime/core/path_safety.py packages/dex-agent-plugin/runtime/core/paths.py +packages/dex-agent-plugin/runtime/core/vault_selection.py packages/dex-agent-plugin/server.py packages/dex-agent-plugin/skills/_available/capabilities/career/skills/career-coach/SKILL.md packages/dex-agent-plugin/skills/_available/capabilities/career/skills/career-setup/SKILL.md @@ -2031,6 +2078,7 @@ packages/dex-agent-plugin/skills/week-plan/SKILL.md packages/dex-agent-plugin/skills/week-review/AGENT_INSTRUCTIONS.md packages/dex-agent-plugin/skills/week-review/SKILL.md packages/dex-agent-plugin/skills/weekly-reflection/SKILL.md +packages/dex-agent-plugin/skills/wispr-setup/SKILL.md packages/dex-agent-plugin/skills/zoom-setup/SKILL.md packages/dex-claude-desktop/README.md packages/dex-claude-desktop/manifest.json diff --git a/System/.local-only-preservation-transition.json b/System/.local-only-preservation-transition.json index 1755159b2..302ce9264 100644 --- a/System/.local-only-preservation-transition.json +++ b/System/.local-only-preservation-transition.json @@ -2,5 +2,5 @@ "schema_version": 2, "baseline_version": 3, "phase": "untrack-v3", - "release_version": "1.97.18" + "release_version": "1.97.19" } diff --git a/System/.release-evidence-profile.json b/System/.release-evidence-profile.json index 990b0eee3..e4bba1559 100644 --- a/System/.release-evidence-profile.json +++ b/System/.release-evidence-profile.json @@ -1,5 +1,5 @@ { "profile": "legacy-v1", - "release_version": "1.97.18", + "release_version": "1.97.19", "schema_version": 1 } diff --git a/core/harnesses/portability.json b/core/harnesses/portability.json index 25d0c58e6..7cde8c931 100644 --- a/core/harnesses/portability.json +++ b/core/harnesses/portability.json @@ -245,10 +245,14 @@ { "pattern": "\\.claude/settings\\.json", "replacement": "the host's lifecycle settings" + }, + { + "pattern": "(?ms)^```bash\\nnode \\.claude/hooks/meeting-queue-check\\.cjs --count\\n```[ \\t]*$", + "replacement": "Ask the host for its count of waiting meeting records if it keeps one; if it does not, treat the count as unknown and run the meeting pass." } ], "classification": "portable", - "reason": "Claude settings mention is explanatory only and has a host-neutral wording" + "reason": "Claude settings mention is explanatory only and has a host-neutral wording; the waiting-meetings count command has a portable fallback" }, "daily-review": { "body_replacements": [ diff --git a/core/lens-catalog/registry.json b/core/lens-catalog/registry.json index edb4c65bd..a9c8a6c48 100644 --- a/core/lens-catalog/registry.json +++ b/core/lens-catalog/registry.json @@ -52,8 +52,8 @@ "source": { "kind": "active-skill", "path": ".claude/skills/daily-plan/SKILL.md", - "sha256": "1269beb481343b7d15437b1d454bd269e22a3295aef086fd2f640051513a73fd", - "byte_size": 33593 + "sha256": "cd015e214ecdd0b34b3c71caeadaf7ade8096c7a8c077bee7d58e85f271b0824", + "byte_size": 34631 }, "value": "Helps a person choose a small, realistic focus list before the day scatters across meetings, tasks and loose commitments.", "jobs_served": [ @@ -193,8 +193,8 @@ "source": { "kind": "active-skill", "path": ".claude/skills/process-meetings/SKILL.md", - "sha256": "85d29ad3157ce18d15a8f39df0ee8da80e26f39ab87b0ebc43bbc5ebb1008e7f", - "byte_size": 21878 + "sha256": "87459ac0371e53ca5b7fb3ccb9634f46f29f4cb4e6e0e585e5fed9227ac251b3", + "byte_size": 22510 }, "value": "Turns meeting material into people context and follow-up tasks, reducing the chance that decisions or promises disappear after the call.", "jobs_served": [ @@ -483,8 +483,8 @@ "source": { "kind": "active-skill", "path": ".claude/skills/daily-review/SKILL.md", - "sha256": "bdaa24a66374f77568bcab0ba35c1e78dd163bc97bae53bc49cf7290c0854c0b", - "byte_size": 19259 + "sha256": "d724c4193c8e67d73d8385fb37716b8e4c4d6805daedf66ba563a1e812398be0", + "byte_size": 19664 }, "value": "Closes the day the morning plan opened: what actually got done against what was intended, what a meeting left behind, and what tomorrow starts with — so the loop finishes instead of drifting.", "jobs_served": [ diff --git a/core/lifecycle/catalog/bridge-release.json b/core/lifecycle/catalog/bridge-release.json index e12ff2fea..6355ab597 100644 --- a/core/lifecycle/catalog/bridge-release.json +++ b/core/lifecycle/catalog/bridge-release.json @@ -1 +1 @@ -{"bridge_contract_version":1,"release_version":"1.97.18","transaction_journal":{"current_schema":2,"incompatible_action":"rollback-only","minimum_resumable_schema":1,"previous_schema":1}} +{"bridge_contract_version":1,"release_version":"1.97.19","transaction_journal":{"current_schema":2,"incompatible_action":"rollback-only","minimum_resumable_schema":1,"previous_schema":1}} diff --git a/core/tests/test_daily_plan_email_intelligence.py b/core/tests/test_daily_plan_email_intelligence.py index a8e2f3e61..97b0e1d11 100644 --- a/core/tests/test_daily_plan_email_intelligence.py +++ b/core/tests/test_daily_plan_email_intelligence.py @@ -23,3 +23,18 @@ def test_daily_plan_headline_unread_count_uses_the_attention_inbox() -> None: skill = (DAILY_PLAN / "SKILL.md").read_text(encoding="utf-8") assert '"Email: [X] unread in Primary/Inbox' in skill + + +def test_daily_plan_probes_only_apple_mail_health_not_the_whole_deep_checkup() -> None: + """The morning plan asks Doctor one question, not for every live probe. + + A full ``--deep`` run before the email step ran smoke journeys, the search + index, and every connected tool each morning. The plan needs one answer: + is Apple Mail search usable. Google Workspace carries its own status. + """ + for path in EMAIL_INSTRUCTION_PATHS: + text = path.read_text(encoding="utf-8") + contract = " ".join(text.split()) + assert "python3 core/utils/doctor.py --deep --only mail.apple-search" in contract, path + assert "python3 core/utils/doctor.py --deep`" not in contract, path + assert "Google Workspace needs no local probe" in contract, path diff --git a/core/tests/test_doctor.py b/core/tests/test_doctor.py index 352d556a8..8b204235a 100644 --- a/core/tests/test_doctor.py +++ b/core/tests/test_doctor.py @@ -1893,6 +1893,48 @@ def test_main_deep_flag_runs_the_deep_registry(monkeypatch, context, capsys): assert [check["id"] for check in report["checks"]] == QUICK_IDS + DEEP_IDS +def test_main_only_flag_narrows_the_registry_to_the_named_check(monkeypatch, context, capsys): + """``--deep --only mail.apple-search`` answers one question, not every probe. + + The morning plan needs to know whether Apple Mail search is usable. Running + the whole deep registry for that answer cost smoke journeys, the search + index, and every connected tool each morning. + """ + _stub_probes(monkeypatch) + published = [] + monkeypatch.setattr(doctor, "_publish_health_snapshot", lambda report, ctx: published.append(report)) + + assert doctor.main(["--deep", "--only", "mail.apple-search"], context=context) == 0 + report = json.loads(capsys.readouterr().out) + + assert report["mode"] == "deep" + # Registry order is kept: the quick list (ending in doctor.self) precedes the deep list. + assert [check["id"] for check in report["checks"]] == ["doctor.self", "mail.apple-search"] + assert report["instruments"]["attempted"] == 2 + assert published == [], "a narrowed run must not replace the whole-system health snapshot" + + +def test_main_only_flag_accepts_several_checks_in_registry_order(monkeypatch, context, capsys): + _stub_probes(monkeypatch) + + assert doctor.main(["--only", QUICK_IDS[2], "--only", QUICK_IDS[0]], context=context) == 0 + report = json.loads(capsys.readouterr().out) + + assert [check["id"] for check in report["checks"]] == [QUICK_IDS[0], QUICK_IDS[2], "doctor.self"] + + +def test_main_only_flag_rejects_an_unknown_or_out_of_registry_check(monkeypatch, context, capsys): + _stub_probes(monkeypatch) + + assert doctor.main(["--only", "mail.apple-search"], context=context) == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert "unknown check id(s) for the quick registry: mail.apple-search" in captured.err + + assert doctor.main(["--deep", "--only", "no.such-check"], context=context) == 1 + assert "no.such-check" in capsys.readouterr().err + + def test_cli_still_emits_json_when_yaml_is_not_importable(tmp_path): vault = tmp_path / "vault-without-yaml" (vault / "System").mkdir(parents=True) diff --git a/core/utils/doctor.py b/core/utils/doctor.py index dcd096e64..5a14996a5 100644 --- a/core/utils/doctor.py +++ b/core/utils/doctor.py @@ -23,7 +23,7 @@ from dataclasses import asdict, dataclass, replace from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Iterator, Mapping +from typing import Any, Iterator, Mapping, Sequence REPO_ROOT = Path(__file__).resolve().parents[2] if str(REPO_ROOT) not in sys.path: @@ -1103,10 +1103,32 @@ def collect( heal: bool = False, progress: bool = False, context: DoctorContext | None = None, + only: Sequence[str] | None = None, ) -> dict[str, Any]: - """Run the selected registry and return its JSON-serializable report.""" + """Run the selected registry and return its JSON-serializable report. + + ``only`` narrows the registry to the named check ids (``doctor.self`` is + always kept). A skill that needs one live answer, such as whether Apple + Mail search is usable before the morning plan reads mail, asks for that + check alone instead of paying for every live probe. An unknown id is an + error rather than a silently empty report. + """ context = context or DoctorContext.from_environment() definitions = [*QUICK_CHECKS, *DEEP_CHECKS] if deep else list(QUICK_CHECKS) + if only: + wanted = set(only) + known = {definition.id for definition in definitions} + unknown = sorted(wanted - known) + if unknown: + registry = "deep" if deep else "quick" + raise ValueError( + f"unknown check id(s) for the {registry} registry: {', '.join(unknown)}" + ) + definitions = [ + definition + for definition in definitions + if definition.id in wanted or definition.id == "doctor.self" + ] results: dict[str, ProbeResult] = {} failed: list[dict[str, str]] = [] @@ -6353,6 +6375,15 @@ def _probe_smoke_journeys(context: DoctorContext) -> ProbeResult: def main(argv: list[str] | None = None, *, context: DoctorContext | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--deep", action="store_true", help="Run live service probes.") + parser.add_argument( + "--only", + action="append", + metavar="CHECK_ID", + help=( + "Run only this check id (repeatable), e.g. --deep --only mail.apple-search. " + "A narrowed run never replaces the saved health snapshot." + ), + ) parser.add_argument("--heal", action="store_true", help="Apply safe Tier-1 repairs before checking.") parser.add_argument("--credential-scan", action="store_true", help="Run the bounded local credential scan.") parser.add_argument("--credential-migrate", action="store_true", help="Run safe local credential migration.") @@ -6383,13 +6414,16 @@ def main(argv: list[str] | None = None, *, context: DoctorContext | None = None) heal=args.heal, progress=True, context=context, + only=args.only, ) output = json.dumps(report, indent=2) except Exception as error: print(f"dex-doctor could not produce JSON: {_one_line(error)}", file=sys.stderr) return 1 - if args.deep: + # A narrowed run answers one question; it must not overwrite the whole-system + # snapshot that the health pulse and Doctor's summary read. + if args.deep and not args.only: _publish_health_snapshot(report, context) print(output) return 0 diff --git a/docs/UPDATE-RESCUE.md b/docs/UPDATE-RESCUE.md index 4c3db1428..a06ef2e26 100644 --- a/docs/UPDATE-RESCUE.md +++ b/docs/UPDATE-RESCUE.md @@ -146,23 +146,23 @@ identity. A newer source commit, a mutable branch, or a similarly named tag is not equivalent, and an older bridge must refuse rather than silently substituting a newer release. -Download the versioned bridge and its checksum from the public v1.97.18 release, +Download the versioned bridge and its checksum from the public v1.97.19 release, verify the bytes, then run that exact artifact. Run this block from the vault root. It keeps the downloaded files in a temporary folder outside the vault. ```bash BRIDGE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dex-update-bridge.XXXXXX")" curl -fL \ - "https://github.com/davekilleen/Dex/releases/download/v1.97.18/dex-update-bridge-v1.97.18.py" \ - -o "$BRIDGE_DIR/dex-update-bridge-v1.97.18.py" + "https://github.com/davekilleen/Dex/releases/download/v1.97.19/dex-update-bridge-v1.97.19.py" \ + -o "$BRIDGE_DIR/dex-update-bridge-v1.97.19.py" curl -fL \ - "https://github.com/davekilleen/Dex/releases/download/v1.97.18/dex-update-bridge-v1.97.18.py.sha256" \ - -o "$BRIDGE_DIR/dex-update-bridge-v1.97.18.py.sha256" + "https://github.com/davekilleen/Dex/releases/download/v1.97.19/dex-update-bridge-v1.97.19.py.sha256" \ + -o "$BRIDGE_DIR/dex-update-bridge-v1.97.19.py.sha256" ( cd "$BRIDGE_DIR" - shasum -a 256 -c "dex-update-bridge-v1.97.18.py.sha256" + shasum -a 256 -c "dex-update-bridge-v1.97.19.py.sha256" ) -python3 "$BRIDGE_DIR/dex-update-bridge-v1.97.18.py" --vault "$PWD" +python3 "$BRIDGE_DIR/dex-update-bridge-v1.97.19.py" --vault "$PWD" ``` It shows up to three independent previews and requires `APPLY` for each: the diff --git a/package-lock.json b/package-lock.json index 4e75bb164..69881797b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dex-pkm", - "version": "1.97.18", + "version": "1.97.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dex-pkm", - "version": "1.97.18", + "version": "1.97.19", "dependencies": { "@anthropic-ai/sdk": "^0.32.1", "@google/generative-ai": "^0.21.0", diff --git a/package.json b/package.json index 5886f641a..05a645507 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dex-pkm", - "version": "1.97.18", + "version": "1.97.19", "description": "Dex - Personal Knowledge Management System", "private": true, "scripts": { diff --git a/packages/dex-agent-plugin/metadata/harnesses/portability.json b/packages/dex-agent-plugin/metadata/harnesses/portability.json index 25d0c58e6..7cde8c931 100644 --- a/packages/dex-agent-plugin/metadata/harnesses/portability.json +++ b/packages/dex-agent-plugin/metadata/harnesses/portability.json @@ -245,10 +245,14 @@ { "pattern": "\\.claude/settings\\.json", "replacement": "the host's lifecycle settings" + }, + { + "pattern": "(?ms)^```bash\\nnode \\.claude/hooks/meeting-queue-check\\.cjs --count\\n```[ \\t]*$", + "replacement": "Ask the host for its count of waiting meeting records if it keeps one; if it does not, treat the count as unknown and run the meeting pass." } ], "classification": "portable", - "reason": "Claude settings mention is explanatory only and has a host-neutral wording" + "reason": "Claude settings mention is explanatory only and has a host-neutral wording; the waiting-meetings count command has a portable fallback" }, "daily-review": { "body_replacements": [ diff --git a/packages/dex-agent-plugin/skills/daily-plan/AGENT_INSTRUCTIONS.md b/packages/dex-agent-plugin/skills/daily-plan/AGENT_INSTRUCTIONS.md index d94d5c023..2314f67da 100644 --- a/packages/dex-agent-plugin/skills/daily-plan/AGENT_INSTRUCTIONS.md +++ b/packages/dex-agent-plugin/skills/daily-plan/AGENT_INSTRUCTIONS.md @@ -19,9 +19,27 @@ context. Do not rely on hook-driven side effects for any write you make. ## Phase 1: Context Gathering -Gather ALL of the following, in parallel where possible. If any source fails or -an optional integration is not set up, skip it silently and note the skipped -source in your final report. Never error to the user. +Gather ALL of the following. If any source fails or an optional integration is +not set up, skip it silently and note the skipped source in your final report. +Never error to the user. + +**Issue the steps in three batches, each as one turn of parallel tool calls.** +The numbered steps below are a checklist, not an order of execution. Running +them one after another is what made this brief take minutes; nothing in batch +A depends on anything else in batch A. + +- **Batch A (independent reads, one turn):** 1.1 week progress, 1.2 calendar, + 1.4 commitments and feeds, 1.5 tasks, 1.6 work summary, 1.7 completed + reminders, 1.8 Dex Inbox, 1.9 email connection check, 1.10 chat connection + check, 1.12 ideas, and the profile read plus note listing for 1.13. +- **Batch B (needs batch A's results, one turn):** 1.3 meeting context for + every meeting, 1.9 and 1.10 queries for connected sources, 1.11 semantic + queries, and the `lookup_person` calls for 1.13. +- **Batch C (writes):** the 1.7 task status updates, the 1.13 person-page + updates, then Phase 3. + +Every step's own rules, gates, and post-conditions still apply exactly as +written; batching changes when a call is issued, never whether it is made. ### 1.1 Week Progress @@ -133,9 +151,12 @@ from them; triage is an interactive step in the main conversation. ### 1.9 Email Intelligence (if connected) Check `System/integrations/config.yaml`. Also treat a registered `apple-mail-mcp` -server as connected. Before querying any connected email source, run -`python3 core/utils/doctor.py --deep`; Apple Mail search is usable only when the -`mail.apple-search` check reports `OK` / `feature_status: ok`. +server as connected. Before querying Apple Mail, run +`python3 core/utils/doctor.py --deep --only mail.apple-search`; Apple Mail search +is usable only when the `mail.apple-search` check reports `OK` / +`feature_status: ok`. `--only` runs that one probe, not the whole live checkup. +Google Workspace needs no local probe: its own tool response carries the health +status. If the source is connected and healthy: diff --git a/packages/dex-agent-plugin/skills/daily-plan/SKILL.md b/packages/dex-agent-plugin/skills/daily-plan/SKILL.md index 8685f54f8..2f70255ba 100644 --- a/packages/dex-agent-plugin/skills/daily-plan/SKILL.md +++ b/packages/dex-agent-plugin/skills/daily-plan/SKILL.md @@ -28,8 +28,11 @@ Agent tool, using the self-contained prompt in this skill's 1. Read `.agents/skills/daily-plan/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{TARGET_DATE}}`, `{{TARGET_DATE_PLUS_1}}`, `{{DAY_NAME}}`, `{{MONTH}}`, `{{DD}}`). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical gathering, so it runs on the fast tier declared by this skill's + `model_routing.steps.data-gathering`; the judgement calls stay in this + conversation on the default model. 4. Verify it wrote the draft plan to `00-Inbox/Daily_Plans/YYYY-MM-DD.md`, then run the remaining interactive steps from its findings and present the plan. 5. **Close out every `` line in the draft.** The subagent @@ -92,13 +95,27 @@ Before executing this command, read `System/user-profile.yaml` → `communicatio --- -## Step 0: Process Unprocessed Meetings +## Step 0: Process Unprocessed Meetings (only when something is waiting) -Before gathering context, ensure recent meetings are in the vault by running `/process-meetings`. This pulls any unprocessed meetings from the meeting source (Otter.ai, Granola, etc.), creates meeting notes, updates person/company pages, and extracts tasks — so the daily plan has complete data from yesterday and any earlier gaps. +Before gathering context, ask the session-start sweep how many meeting records +are still waiting, without re-scanning the vault yourself. The count mode +prints one integer, bypasses the sweep's once-per-30-minutes notice throttle, +and writes nothing, so it is safe to call every morning: -- If no new meetings are found, continue silently -- If meetings are processed, note the count and use the extracted context in the plan -- Do NOT ask for a skill rating after this sub-step — save that for the end of the full plan +Ask the host for its count of waiting meeting records if it keeps one; if it does not, treat the count as unknown and run the meeting pass. + +Branch on the answer: + +- **`0`:** skip this step entirely and say nothing. Yesterday evening's review + already ran the catch-up pass, so on most mornings this is the answer, and + re-running a full seven-day `/process-meetings` here found nothing new while + costing minutes. +- **Above `0`:** run `/process-meetings`. This creates meeting notes, updates + person/company pages, and extracts tasks, so the plan has complete data. + Note the count and use the extracted context in the plan. Do NOT ask for a + skill rating after this sub-step — save that for the end of the full plan. +- **The command fails or prints nothing:** treat that as unknown, not as zero: + run `/process-meetings` as before. ## Step 0.5: Dex Inbox Check (Phone Captures) @@ -343,9 +360,12 @@ For each completed item: ### 5.8 Email Intelligence (if connected) Check `System/integrations/config.yaml` for `google-workspace.enabled: true`. Also treat a -registered `apple-mail-mcp` server as a connected source. Before querying a connected email -source, run `python3 core/utils/doctor.py --deep`; Apple Mail search is usable only when the -`mail.apple-search` check reports `OK` / `feature_status: ok`. +registered `apple-mail-mcp` server as a connected source. Before querying Apple Mail, run +`python3 core/utils/doctor.py --deep --only mail.apple-search`; Apple Mail search is usable +only when the `mail.apple-search` check reports `OK` / `feature_status: ok`. `--only` runs +that one probe instead of the whole live checkup (smoke journeys, search index, connected +tools), which is what a full `--deep` run does and what made this step slow every morning. +Google Workspace needs no local probe: its own tool response carries the health status. If connected and healthy: 1. Use an attention-inbox count for the headline, never a provider-wide unread total. diff --git a/packages/dex-agent-plugin/skills/daily-review/AGENT_INSTRUCTIONS.md b/packages/dex-agent-plugin/skills/daily-review/AGENT_INSTRUCTIONS.md index a3c20f4f5..1083bb2c4 100644 --- a/packages/dex-agent-plugin/skills/daily-review/AGENT_INSTRUCTIONS.md +++ b/packages/dex-agent-plugin/skills/daily-review/AGENT_INSTRUCTIONS.md @@ -11,6 +11,16 @@ any section gracefully if a tool fails; never error out. **Note:** PostToolUse hooks from the parent skill do not fire in this subagent context. Do not rely on hook-driven side effects for any write you make. +**Issue the steps in batches, each as one turn of parallel tool calls.** Step 0 +runs first because it changes the notes the rest reads. After it, everything in +Steps 1 through 3 and 5 through 6 is independent: the file discovery, the task +file read, `get_week_progress()` and `get_week_priorities()`, the meeting note +reads, `calendar_get_today()`, the Reminders calls, the email check, today's +plan file, and tomorrow's calendar all go out in one turn. Step 2.4's semantic +queries follow in a second turn once you know today's tasks and meetings. Then +write. Running the steps one after another is what made this brief slow; each +step's own rules and gates still apply exactly as written. + --- ## Step 0: Meeting Catch-Up (not same-day only) @@ -32,10 +42,14 @@ Why the window matters: a same-day filter loses meetings permanently on any day the review does not run. Process local candidates following the process-meetings instructions, including -provider-neutral discovery and updating person pages directly rather than -counting on a hook. Note the actual path of every meeting processed. If the -profile is missing or malformed, report that once and use the safe local -fallback. If nothing is unprocessed, skip silently. +its batching rules, provider-neutral discovery and updating person pages +directly rather than counting on a hook. Note the actual path of every meeting +processed. If the profile is missing or malformed, report that once and use the +safe local fallback. If nothing is unprocessed, skip silently. + +This is the only meeting pass of the evening. The main conversation no longer +runs `/process-meetings` itself, so a meeting you skip here is not caught later +today; the catch-up window above is what makes that safe. --- diff --git a/packages/dex-agent-plugin/skills/daily-review/SKILL.md b/packages/dex-agent-plugin/skills/daily-review/SKILL.md index ded1b5fcc..a73006606 100644 --- a/packages/dex-agent-plugin/skills/daily-review/SKILL.md +++ b/packages/dex-agent-plugin/skills/daily-review/SKILL.md @@ -38,8 +38,10 @@ Agent tool, using the self-contained prompt in this skill's 1. Read `.agents/skills/daily-review/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{TARGET_DATE}}`, `{{TOMORROW_DATE}}`, `{{TOMORROW_DATE_PLUS_1}}`, `{{DAY_NAME}}`, `{{MONTH}}`, `{{DD}}`, `{{YYYY}}`). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical gathering, so it runs on the fast tier; the judgement calls stay + in this conversation on the default model. 4. Verify it wrote the draft to `07-Archives/Reviews/Daily_Review_YYYY-MM-DD.md`, then run the interactive steps from its findings and complete the placeholder sections. @@ -93,13 +95,20 @@ find . -type f -name "*.md" -newermt "$TODAY 00:00:00" ! -newermt "$TODAY 23:59: --- -## Step 1.5: Process Today's Meetings +## Step 1.5: Today's Meetings Are Already Processed -Before gathering context, ensure today's meetings are in the vault by running `/process-meetings today`. This pulls any unprocessed meetings from the meeting source (Otter.ai, Granola, etc.), creates meeting notes, updates person/company pages, and extracts tasks — so the rest of the review has complete data. +Meeting catch-up happens once, inside the gathering helper (its Step 0 processes +every unprocessed meeting since the last one that was, following +`process-meetings/AGENT_INSTRUCTIONS.md`). Do NOT also run `/process-meetings` +from this conversation: that ran the whole meeting pass twice per evening, and +the second pass found nothing new. -- If no new meetings are found, continue silently -- If meetings are processed, note the count for the review summary -- Do NOT ask for a skill rating after this sub-step — save that for the end of the full review +- Read the helper's "Meetings processed in catch-up" line and carry the count + into the review summary +- If the helper reported meetings left unstamped or an older backlog, surface + those lines; do not re-run the pass to check them +- Only if the helper failed and the inline fallback is running does this + conversation process meetings itself, following the same brief --- @@ -551,7 +560,7 @@ Add one line at the end of the review output: | Integration | MCP Server | Tools Used | |-------------|------------|------------| -| Meetings | Meeting source MCP (via `/process-meetings today`) | Fetches and processes unprocessed meetings | +| Meetings | Local meeting notes (helper Step 0 catch-up, following `process-meetings/AGENT_INSTRUCTIONS.md`) | Processes unprocessed meetings once per review | | Work | work-mcp | `list_tasks`, `get_week_progress`, `get_commitments_due`, `analyze_calendar_capacity` | | Calendar | calendar-mcp | `calendar_get_today` | | Reminders | calendar-mcp | `reminders_list_completed`, `reminders_find_and_complete`, `reminders_clear_completed`, `reminders_list_items` | diff --git a/packages/dex-agent-plugin/skills/process-meetings/AGENT_INSTRUCTIONS.md b/packages/dex-agent-plugin/skills/process-meetings/AGENT_INSTRUCTIONS.md index 55a62c937..b7baa5cd5 100644 --- a/packages/dex-agent-plugin/skills/process-meetings/AGENT_INSTRUCTIONS.md +++ b/packages/dex-agent-plugin/skills/process-meetings/AGENT_INSTRUCTIONS.md @@ -17,6 +17,24 @@ hook does also run, because both skip a page that already lists the meeting. - `--people-only`: only update person/company pages (skip tasks) - `--no-todos`: create notes but do not extract tasks +**Work in batches, not one meeting at a time.** Every step below is written per +meeting for clarity, but the tool calls are independent across meetings, so +issue them together: + +1. **Read everything first, in one turn:** the profile, the state file, the + directory listings, and every candidate note. Do not read one note, process + it, then read the next. +2. **Look up everyone at once:** one turn of `lookup_person` calls covering + every participant across every meeting, plus the calendar fetches (one per + date) and `detect_soft_commitments` for every meeting. +3. **Then write:** person and company page updates, `create_task` calls, and + markers. Writes to the same file stay sequential; writes to different files + can share a turn. + +Sequential per-meeting processing is what made this brief take minutes for a +handful of meetings. The verification rules in Step 6 are unchanged: batching +changes when calls are issued, never whether their results are checked. + --- ## Step 1: Resolve the Local Meeting Sources @@ -331,9 +349,19 @@ Processing complete. |------|---------|---------|--------------| | ... | ... | ... | ... | +### Touched + +- Tasks created: [every `task-YYYYMMDD-XXX` ID returned by `create_task`, or none] +- Notes stamped: [every vault-relative note path you appended the marker to, or none] +- Pages created: [every vault-relative person or company page path you created, or none] + [Any warnings or issues encountered] ``` +The `Touched` block is what the conversation verifies before repeating your +counts, so it must list every item exactly, one targeted check each. A count +without a matching `Touched` entry is treated as unverified. + --- ## Important Notes diff --git a/packages/dex-agent-plugin/skills/process-meetings/SKILL.md b/packages/dex-agent-plugin/skills/process-meetings/SKILL.md index 966e10c90..1f36178c6 100644 --- a/packages/dex-agent-plugin/skills/process-meetings/SKILL.md +++ b/packages/dex-agent-plugin/skills/process-meetings/SKILL.md @@ -28,8 +28,11 @@ subagent via the Agent tool, using the self-contained prompt in this skill's 1. Read `.agents/skills/process-meetings/AGENT_INSTRUCTIONS.md`. 2. Substitute its placeholders (`{{ARGS}}`, the arguments passed to this skill). -3. Call the Agent tool with `subagent_type: "general-purpose"`, that prompt, and - a short description. +3. Call the Agent tool with `subagent_type: "general-purpose"`, + `model: "sonnet"`, that prompt, and a short description. The brief is + mechanical processing with its own verification steps, so it runs on the + fast tier; anything that needs a judgement call comes back to this + conversation. 4. Display its summary report. The subagent inherits MCP connections, runs in its own context, and that context @@ -71,13 +74,21 @@ until someone resolves it. **The report is a claim, not evidence — check it before repeating it.** This subagent writes to the vault, and its summary states counts the user will act -on. Before displaying it, verify the claims cheaply against the vault: - -- Every task it says it created: confirm the ID appears in `03-Tasks/Tasks.md` - (`list_tasks`, or read the file). -- Every meeting it says it stamped: confirm the `tasks-extracted` marker is - actually in that note. -- Every person or company page it says it created: confirm the file exists. +on. Its report ends with a `Touched` block naming every task ID it created, +every note it stamped, and every page it created. Verify exactly those items, +and nothing wider: + +- Each listed task ID: confirm it appears in `03-Tasks/Tasks.md` with one + targeted search per ID (`grep`, or `list_tasks` filtered to that ID). Do not + read the whole task file. +- Each listed stamped note: confirm the `tasks-extracted` marker is actually in + that one file. +- Each listed created page: confirm the file exists. + +Do not re-read every meeting note or the full task list to look for claims the +report did not make; the full sweep was what made this step slow, and it never +found anything the `Touched` block did not name. If the report has no `Touched` +block, treat every count in it as unverified and say so. If a claim does not hold, say so plainly in the summary you present and treat that meeting as unprocessed. Never pass an unverified count to the user as fact, diff --git a/scripts/generate-harness-portability.py b/scripts/generate-harness-portability.py index 52f0685c8..82702fec8 100644 --- a/scripts/generate-harness-portability.py +++ b/scripts/generate-harness-portability.py @@ -119,7 +119,29 @@ def expected_manifest(repo_root: Path = ROOT) -> dict: {"pattern": r"AskUserQuestion", "replacement": "prompt_user"}, ], } - elif key in {"daily-plan", "daily-review", "meeting-prep", "week-review"}: + elif key == "daily-plan": + # daily-plan asks the session-start sweep for its waiting-meetings + # count before deciding whether to run a meeting pass. Hosts + # without that hook fall back to the pass itself, which is the + # pre-count behaviour and always safe. + entry = { + "classification": "portable", + "reason": ( + "Claude settings mention is explanatory only and has a host-neutral wording; " + "the waiting-meetings count command has a portable fallback" + ), + "body_replacements": [ + {"pattern": r"\.claude/settings\.json", "replacement": "the host's lifecycle settings"}, + { + "pattern": r"(?ms)^```bash\nnode \.claude/hooks/meeting-queue-check\.cjs --count\n```[ \t]*$", + "replacement": ( + "Ask the host for its count of waiting meeting records if it keeps one; " + "if it does not, treat the count as unknown and run the meeting pass." + ), + }, + ], + } + elif key in {"daily-review", "meeting-prep", "week-review"}: entry = { "classification": "portable", "reason": "Claude settings mention is explanatory only and has a host-neutral wording",