mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-22 18:45:19 +02:00
Deprecate generated Today.md live note
This commit is contained in:
parent
fe5e67f810
commit
95c313de89
6 changed files with 111 additions and 109 deletions
|
|
@ -175,7 +175,7 @@ Internal trigger enum (`LiveNoteTriggerType`) is `'manual' | 'cron' | 'window' |
|
|||
|
||||
`buildMessage` always emits a `**Trigger:**` paragraph in the agent's run message — one paragraph per kind. `manual` and the two timed variants (`cron`, `window`) include any optional `context` as a `**Context:**` block. `event` includes the eventMatchCriteria + payload + Pass 2 decision directive (no `**Context:**`; the payload *is* the context).
|
||||
|
||||
This lets the user-authored objective branch on trigger kind when warranted (the canonical example is the Today.md emails section: cron/window scans `gmail_sync/` from scratch, event integrates the new thread). The skill teaches the pattern under "Per-trigger guidance (advanced)".
|
||||
This lets the user-authored objective branch on trigger kind when warranted (for example, an email digest can scan `gmail_sync/` from scratch on cron/window runs, while event runs integrate just the new thread). The skill teaches the pattern under "Per-trigger guidance (advanced)".
|
||||
|
||||
### Run flow (`runLiveNoteAgent`)
|
||||
|
||||
|
|
@ -254,17 +254,15 @@ The contract (defined in the run-agent system prompt — `packages/core/src/know
|
|||
|
||||
---
|
||||
|
||||
## Daily-Note Template & Migrations
|
||||
## Default Note Policy
|
||||
|
||||
`Today.md` is the canonical demo of what a live note can do. It ships with one objective covering an Overview / Calendar / Emails / What you missed / Priorities layout — driven by three windows and an event-match criterion for in-day signals.
|
||||
Rowboat no longer creates a default `Today.md` live dashboard for new users. Live notes are user-created notes with an explicit `live:` frontmatter block.
|
||||
|
||||
**Versioning** — `packages/core/src/knowledge/ensure_daily_note.ts` carries a `CANONICAL_DAILY_NOTE_VERSION` constant and a `templateVersion` scalar in the frontmatter. On app start, `ensureDailyNote()`:
|
||||
**Deprecated Today.md migration** — `packages/core/src/knowledge/deprecate_today_note.ts` runs once per workspace on app start:
|
||||
|
||||
- File missing → fresh write at canonical version.
|
||||
- File at-or-above canonical → no-op.
|
||||
- File below canonical → rename existing to `Today.md.bkp.<ISO-stamp>` (which doesn't end in `.md`, so the scheduler/event router skip it), then write the canonical template body-from-scratch (live notes regenerate their own body).
|
||||
|
||||
The bump from v1 (the old `track:` array model) to v2 (the live-note rewrite) is handled by this same path. Pre-v2 notes get backed up and replaced.
|
||||
- File missing → mark processed and do nothing.
|
||||
- File present → set `live.active: false` if a `live:` block exists, prepend a user-facing deprecation notice once, and preserve the note body.
|
||||
- Future launches → no-op via `config/today-note-deprecation.json`, so a user who re-enables the note is not paused again.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -393,7 +391,7 @@ Conventions:
|
|||
| Run orchestrator (`runLiveNoteAgent`, `buildMessage`) | `packages/core/src/knowledge/live-note/runner.ts` |
|
||||
| Live-note agent definition (`LIVE_NOTE_AGENT_INSTRUCTIONS`, `buildLiveNoteAgent`) | `packages/core/src/knowledge/live-note/agent.ts` |
|
||||
| Live-note bus (pub-sub for lifecycle events) | `packages/core/src/knowledge/live-note/bus.ts` |
|
||||
| Daily-note template + version migration | `packages/core/src/knowledge/ensure_daily_note.ts` |
|
||||
| Deprecated Today.md one-time migration | `packages/core/src/knowledge/deprecate_today_note.ts` |
|
||||
| Gmail event producer | `packages/core/src/knowledge/sync_gmail.ts` |
|
||||
| Calendar event producer + digest | `packages/core/src/knowledge/sync_calendar.ts` |
|
||||
| Copilot skill | `packages/core/src/application/assistant/skills/live-note/skill.ts` |
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ function isSamePath(a: string, b: string | undefined): boolean {
|
|||
* - Ticks every minute so callers using `formatRelativeTime` get a fresh label
|
||||
* without the underlying data changing.
|
||||
*
|
||||
* `notePath` may be either knowledge-relative (`Today.md`) or workspace-rooted
|
||||
* (`knowledge/Today.md`); the hook normalises internally.
|
||||
* `notePath` may be either knowledge-relative (`Digest.md`) or workspace-rooted
|
||||
* (`knowledge/Digest.md`); the hook normalises internally.
|
||||
*/
|
||||
export function useLiveNoteForPath(notePath: string | null | undefined): UseLiveNoteForPathResult {
|
||||
const knowledgeRelPath = stripKnowledgePrefix(notePath ?? null)
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ The agent always receives a \`**Trigger:**\` line in its run message telling it
|
|||
- \`Scheduled refresh — fired inside the configured window\` — forgiving once-per-day baseline refresh.
|
||||
- \`Event match — Pass 1 routing flagged this note\` — comes with the event payload and a Pass 2 decision directive.
|
||||
|
||||
**When to branch in the objective:** there's a meaningful difference between the work to do on a *baseline* refresh (cron/window — pull a full snapshot from local data) and a *reactive* update (event — integrate one new signal). The flagship case is the **Today.md emails section**: on a window run it scans \`gmail_sync/\` for everything worth attention; on an event run with an incoming email payload it integrates that one thread into the existing digest without re-listing previously-seen threads. Same objective, two branches.
|
||||
**When to branch in the objective:** there's a meaningful difference between the work to do on a *baseline* refresh (cron/window — pull a full snapshot from local data) and a *reactive* update (event — integrate one new signal). For example, an email digest can scan \`gmail_sync/\` for everything worth attention on a window run, then integrate one incoming thread on an event run without re-listing previously-seen threads. Same objective, two branches.
|
||||
|
||||
How to write it — use plain conditional language inside the objective:
|
||||
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ function ensureDefaultConfigs() {
|
|||
ensureDirs();
|
||||
ensureDefaultConfigs();
|
||||
|
||||
// Ensure default knowledge files exist
|
||||
import('../knowledge/ensure_daily_note.js').then(m => m.ensureDailyNote()).catch(err => {
|
||||
console.error('[DailyNote] Failed to ensure daily note:', err);
|
||||
// One-time deprecation for the old Today.md live dashboard. New installs no
|
||||
// longer get a generated Today.md; existing files are paused in place.
|
||||
import('../knowledge/deprecate_today_note.js').then(m => m.deprecateTodayNote()).catch(err => {
|
||||
console.error('[TodayNoteDeprecation] Failed to deprecate Today.md:', err);
|
||||
});
|
||||
|
||||
// Initialize version history repo (async, fire-and-forget on startup)
|
||||
|
|
|
|||
96
apps/x/packages/core/src/knowledge/deprecate_today_note.ts
Normal file
96
apps/x/packages/core/src/knowledge/deprecate_today_note.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
import { LiveNoteSchema } from '@x/shared/dist/live-note.js';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { splitFrontmatter, joinFrontmatter } from '../application/lib/parse-frontmatter.js';
|
||||
|
||||
const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge');
|
||||
const TODAY_NOTE_PATH = path.join(KNOWLEDGE_DIR, 'Today.md');
|
||||
const STATE_FILE = path.join(WorkDir, 'config', 'today-note-deprecation.json');
|
||||
const NOTICE_MARKER = '<!-- rowboat-today-md-deprecated -->';
|
||||
const DEPRECATION_NOTICE = `${NOTICE_MARKER}
|
||||
> Rowboat's Today.md live dashboard is paused for now while we work on a better experience. You can keep using this note as a regular markdown file. If you want Rowboat to keep updating it automatically, re-enable the live note settings; automatic updates may use credits.
|
||||
|
||||
`;
|
||||
|
||||
const StateSchema = z.object({
|
||||
processed_at: z.string().min(1).optional(),
|
||||
});
|
||||
type State = z.infer<typeof StateSchema>;
|
||||
|
||||
const TodayNoteFrontmatterSchema = z.object({
|
||||
live: LiveNoteSchema.optional(),
|
||||
}).passthrough();
|
||||
|
||||
async function pathExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadState(): Promise<State> {
|
||||
try {
|
||||
if (!await pathExists(STATE_FILE)) return {};
|
||||
const raw = await fs.readFile(STATE_FILE, 'utf-8');
|
||||
return StateSchema.parse(JSON.parse(raw));
|
||||
} catch (error) {
|
||||
console.warn('[TodayNoteDeprecation] Failed to load state:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function saveState(state: State): Promise<void> {
|
||||
const dir = path.dirname(STATE_FILE);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
async function markProcessed(): Promise<void> {
|
||||
await saveState({ processed_at: new Date().toISOString() });
|
||||
}
|
||||
|
||||
function disableLiveBlock(frontmatter: Record<string, unknown>): Record<string, unknown> {
|
||||
const parsed = TodayNoteFrontmatterSchema.safeParse(frontmatter);
|
||||
if (!parsed.success || !parsed.data.live) {
|
||||
return frontmatter;
|
||||
}
|
||||
|
||||
return {
|
||||
...frontmatter,
|
||||
live: {
|
||||
...parsed.data.live,
|
||||
active: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function prependNotice(body: string): string {
|
||||
if (body.includes(NOTICE_MARKER)) return body;
|
||||
return `${DEPRECATION_NOTICE}${body}`;
|
||||
}
|
||||
|
||||
export async function deprecateTodayNote(): Promise<void> {
|
||||
const state = await loadState();
|
||||
if (state.processed_at) return;
|
||||
|
||||
if (!await pathExists(TODAY_NOTE_PATH)) {
|
||||
await markProcessed();
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(TODAY_NOTE_PATH, 'utf-8');
|
||||
const { frontmatter, body } = splitFrontmatter(content);
|
||||
const nextFrontmatter = disableLiveBlock(frontmatter);
|
||||
const nextBody = prependNotice(body);
|
||||
|
||||
if (nextFrontmatter !== frontmatter || nextBody !== body) {
|
||||
await fs.writeFile(TODAY_NOTE_PATH, joinFrontmatter(nextFrontmatter, nextBody), 'utf-8');
|
||||
console.log('[TodayNoteDeprecation] Deprecated Today.md live dashboard');
|
||||
}
|
||||
|
||||
await markProcessed();
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { LiveNoteSchema } from '@x/shared/dist/live-note.js';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
import { splitFrontmatter } from '../application/lib/parse-frontmatter.js';
|
||||
import z from 'zod';
|
||||
|
||||
const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge');
|
||||
const DAILY_NOTE_PATH = path.join(KNOWLEDGE_DIR, 'Today.md');
|
||||
|
||||
// Bump this whenever the canonical Today.md template changes (objective,
|
||||
// triggers, default body, etc.). On app start, ensureDailyNote() compares the
|
||||
// on-disk `templateVersion` against this constant — if older or missing, the
|
||||
// existing file is renamed to Today.md.bkp.<ISO-stamp> and replaced with the
|
||||
// new template. v2 is the live-note rewrite (single objective, no `track:`).
|
||||
const CANONICAL_DAILY_NOTE_VERSION = 2;
|
||||
|
||||
const TODAY_LIVE_NOTE: z.infer<typeof LiveNoteSchema> = {
|
||||
objective:
|
||||
`Keep Today.md current as a living dashboard for the day. Maintain these H2 sections in this order:
|
||||
|
||||
1. **Overview** — 2-3 prose sentences greeting the user and reading the day (warm, confident tone — use today's calendar density from \`calendar_sync/\` and the existing Priorities section if populated). Below the prose, render exactly one \`image\` block fitting the mood (use weather + calendar density as cues). Source the image via web-search from a permissive host (Unsplash/Pexels/Pixabay/Wikimedia, direct .jpg/.png/.webp URLs only); fall back to NASA APOD (https://apod.nasa.gov/apod/astropix.html) if nothing suitable. Keep the image **wide / low-height**. Skip refreshing this section if its content is still suitable and less than 24h old.
|
||||
|
||||
2. **Calendar** — today's meetings as a single \`calendar\` block titled "Today's Meetings". Read \`calendar_sync/\` via \`workspace-readdir\` → \`workspace-readFile\` each \`.json\`. Filter to today; after 10am drop meetings that have already ended. Always emit the block (use \`events: []\` when empty). Set \`showJoinButton: true\` if any event has a \`conferenceLink\`.
|
||||
|
||||
3. **Emails** — a digest of email threads worth attention today, as a **single** fenced \`emails\` block (plural — never individual \`email\` blocks per thread). Body shape: \`{"title":"Today's Emails","emails":[...]}\`. Each entry: \`threadId\`, \`subject\`, \`from\`, \`date\`, \`summary\`, \`latest_email\`. For threads needing a reply, add \`draft_response\` written in the user's voice — direct, informal, no fluff. For FYI threads, omit \`draft_response\`. Skip marketing, auto-notifications, and closed threads. Without an event payload, scan \`gmail_sync/\` (skip \`sync_state.json\` and \`attachments/\`), prioritising threads where frontmatter \`action = "reply"\` or \`"respond"\`. With an event payload, integrate qualifying new threads into the existing digest (add a new entry for a new threadId; update the existing entry if shown). Don't re-list threads the user has already seen unless their state changed. If nothing qualifies: "No new emails."
|
||||
|
||||
4. **What you missed** — a short markdown summary of yesterday's meetings + emails that matter this morning. Pull decisions / action items from \`knowledge/Meetings/<source>/<yesterday>/\` (\`workspace-readdir\` recursive on \`knowledge/Meetings\`, filter folders matching yesterday's date, read each file). Skim \`gmail_sync/\` for unresolved threads. Skip recurring/routine events. If nothing notable: "Quiet day yesterday — nothing to flag."
|
||||
|
||||
5. **Priorities** — a ranked markdown list of actionable items the user should focus on today. Sources: yesterday's meeting action items (\`knowledge/Meetings/<source>/<yesterday>/\`), open follow-ups across \`knowledge/\` (\`workspace-grep\` for "- [ ]"), the **What you missed** section above. Don't list calendar events as tasks (Calendar section has them) and don't list trivial admin. Rank by importance; note time-sensitivity inline. With an event payload (gmail or calendar), only re-emit the full list if the event genuinely shifts priorities (urgent reply, deadline arrival, blocking reschedule). If nothing pressing: "No pressing tasks today — good day to make progress on bigger items."
|
||||
|
||||
Treat the note as a coherent artifact. Make small, incremental edits — one section at a time — rather than rewriting the whole body each run.`,
|
||||
active: true,
|
||||
triggers: {
|
||||
// Three windows give the user a fresh dashboard morning, midday, and
|
||||
// post-lunch even with no calendar/email events landing in between.
|
||||
windows: [
|
||||
{ startTime: '08:00', endTime: '12:00' },
|
||||
{ startTime: '12:00', endTime: '15:00' },
|
||||
{ startTime: '15:00', endTime: '18:00' },
|
||||
],
|
||||
// Event-driven updates handle in-day shifts (new email threads worth
|
||||
// attention, calendar reshuffles, urgent escalations).
|
||||
eventMatchCriteria:
|
||||
`Email or calendar events that may shift today's dashboard: new or updated email threads needing the user's attention, urgent reply requests, deadline-bearing items, escalations from people the user cares about, calendar additions/cancellations/reschedules affecting today, or anything that changes the user's near-term priorities. Skip marketing, newsletters, auto-notifications, and chatter on closed threads.`,
|
||||
},
|
||||
};
|
||||
|
||||
function buildDailyNoteContent(body: string = '# Today\n'): string {
|
||||
const fm = stringifyYaml(
|
||||
{ templateVersion: CANONICAL_DAILY_NOTE_VERSION, live: TODAY_LIVE_NOTE },
|
||||
{ lineWidth: 0, blockQuote: 'literal' },
|
||||
).trimEnd();
|
||||
return `---\n${fm}\n---\n${body}`;
|
||||
}
|
||||
|
||||
function readCurrentTemplateVersion(): number {
|
||||
if (!fs.existsSync(DAILY_NOTE_PATH)) return -1;
|
||||
const raw = fs.readFileSync(DAILY_NOTE_PATH, 'utf-8');
|
||||
const { frontmatter } = splitFrontmatter(raw);
|
||||
const v = frontmatter.templateVersion;
|
||||
return typeof v === 'number' ? v : 0;
|
||||
}
|
||||
|
||||
export function ensureDailyNote(): void {
|
||||
// Fresh install — no existing file.
|
||||
if (!fs.existsSync(DAILY_NOTE_PATH)) {
|
||||
fs.writeFileSync(DAILY_NOTE_PATH, buildDailyNoteContent(), 'utf-8');
|
||||
console.log(`[DailyNote] Created Today.md (v${CANONICAL_DAILY_NOTE_VERSION})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Up-to-date — nothing to do.
|
||||
const currentVersion = readCurrentTemplateVersion();
|
||||
if (currentVersion >= CANONICAL_DAILY_NOTE_VERSION) return;
|
||||
|
||||
// Migrate aggressively: rename existing → backup, write a fresh canonical
|
||||
// template (no body carried over). Today.md is a flagship demo whose
|
||||
// content is meant to be regenerated by the live-note agent anyway —
|
||||
// preserving the old body just leaves orphan sections behind on
|
||||
// restructure. The .bkp file is the recovery path; its name doesn't end
|
||||
// in `.md`, so the scheduler and event router naturally skip it. Pre-v2
|
||||
// notes (with the old `track:` array) are caught by this same path.
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = `${DAILY_NOTE_PATH}.bkp.${stamp}`;
|
||||
fs.renameSync(DAILY_NOTE_PATH, backupPath);
|
||||
fs.writeFileSync(DAILY_NOTE_PATH, buildDailyNoteContent(), 'utf-8');
|
||||
console.log(
|
||||
`[DailyNote] Migrated v${currentVersion} → v${CANONICAL_DAILY_NOTE_VERSION}; ` +
|
||||
`previous version saved to ${backupPath}`,
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue