From 22832699320ee773584cb9ca1a309bfe95bbe8b9 Mon Sep 17 00:00:00 2001 From: Time Attakc <89218912+time-attack@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:42:56 -0700 Subject: [PATCH] =?UTF-8?q?fix(context):=20read=20documented=20'##=20P1=20?= =?UTF-8?q?=E2=80=94=20Today'=20plain=20tasks=20in=20live=20context=20(#21?= =?UTF-8?q?86)=20(#3124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveTodayTasks only matched a bare '## Today' heading and bold-prefixed '- [ ] **task**' lines, while the daily-task-manager skill's documented Output Format writes '## P1 — Today' with plain '- [ ] task' lines — so documented writes surfaced zero tasks in live context. Reader now accepts both heading forms and both line forms, two-step: the legacy bold prefix extracts just the task name (dropping trailing metadata), falling back to the plain full-line form. Salvaged from PR #2188 (reader-side half). The skill-doc rewrites in that PR are dropped: master #2938 kept ops/ synced and made put_page write-through durable, so the 'gbrain get/put ops/tasks' docs are correct as-is. The PR's single-regex line matcher is replaced with the two-step match because its alternation captured '**name** — metadata' verbatim for bold lines. Takeover of #2188. Fixes #2186. Co-authored-by: Garry Tan Co-authored-by: caioribeiroclw-pixel Co-authored-by: Claude Fable 5 --- src/core/context-engine.ts | 19 +++++++++++++++---- test/context-engine.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/core/context-engine.ts b/src/core/context-engine.ts index e38114b98..cded472ba 100644 --- a/src/core/context-engine.ts +++ b/src/core/context-engine.ts @@ -453,7 +453,14 @@ function resolveActivity( * every `assemble()` call. 1 MB is generous for a human-edited task list. */ const MAX_TASKS_MD_BYTES = 1_000_000; -/** Extract open tasks from ops/tasks.md "## Today" section. */ +/** Extract open tasks from ops/tasks.md Today section. + * + * The daily-task-manager skill's documented Output Format uses priority + * headings (`## P1 — Today`) with plain `- [ ] task` lines; older fixtures + * used a bare `## Today` heading with bold task names. Accept both so the + * live-context reader matches the documented writer contract instead of + * silently surfacing no tasks (#2186). + */ function resolveTodayTasks(workspaceDir: string): string[] { try { const path = join(workspaceDir, 'ops', 'tasks.md'); @@ -461,14 +468,18 @@ function resolveTodayTasks(workspaceDir: string): string[] { // statSync throws if the file doesn't exist; that lands in the outer catch. if (statSync(path).size > MAX_TASKS_MD_BYTES) return []; const raw = readFileSync(path, 'utf8'); - const todayMatch = raw.match(/## Today[\s\S]*?(?=\n## |$)/); + const todayMatch = raw.match(/^##\s+(?:P\d\s*[—–-]\s*)?Today\b[\s\S]*?(?=\n##\s|$(?![\s\S]))/m); if (!todayMatch) return []; const lines = todayMatch[0].split('\n'); const open: string[] = []; for (const line of lines) { - // Match unchecked task lines: - [ ] **task name** ... - const m = line.match(/^\s*-\s*\[ \]\s*\*\*(.+?)\*\*/); + // Match unchecked task lines. Legacy bold form first (extracts just + // the task name, dropping trailing metadata), then the documented + // plain form (whole line body is the task). + const m = + line.match(/^\s*-\s*\[ \]\s*\*\*(.+?)\*\*/) ?? + line.match(/^\s*-\s*\[ \]\s*(.+?)\s*$/); if (m) open.push(sanitizeForPrompt(m[1].trim())); } return open.slice(0, 5); // cap at 5 to keep prompt lean diff --git a/test/context-engine.test.ts b/test/context-engine.test.ts index 6c00d2885..79b430665 100644 --- a/test/context-engine.test.ts +++ b/test/context-engine.test.ts @@ -322,6 +322,28 @@ describe('gbrain-context engine', () => { expect(result.systemPromptAddition).not.toContain('Something later'); }); + it('injects documented "## P1 — Today" plain tasks from ops/tasks.md (#2186)', async () => { + tmpDir = makeWorkspace({ + heartbeat: { garryAwake: true }, + tasks: `# Tasks\n\n## P0 — Urgent\n- [ ] **Escalate outage**\n\n## P1 — Today\n- [ ] Call Alice about launch plan\n- [ ] **Review Bob contract** — due Friday\n- [x] Completed item\n\n## P2 — This Week\n- [ ] Should not surface`, + }); + const engine = createGBrainContextEngine({ workspaceDir: tmpDir }); + + const result = await engine.assemble({ + sessionId: 'test-session', + messages: [], + }); + + expect(result.systemPromptAddition).toContain('Open tasks'); + expect(result.systemPromptAddition).toContain('Call Alice about launch plan'); + // Bold form still extracts just the task name, not trailing metadata. + expect(result.systemPromptAddition).toContain('Review Bob contract'); + expect(result.systemPromptAddition).not.toContain('due Friday'); + expect(result.systemPromptAddition).not.toContain('Escalate outage'); + expect(result.systemPromptAddition).not.toContain('Completed item'); + expect(result.systemPromptAddition).not.toContain('Should not surface'); + }); + it('no activity section when calendar is empty and no tasks', async () => { tmpDir = makeWorkspace({ heartbeat: { garryAwake: true },