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 },