From 0312147365db14adae3319d6802b3f73b45efbf0 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 18 Jul 2026 17:56:10 +0300 Subject: [PATCH] feat(framework): rename the backlog file to TODO_AGENTS.md (#674) Per Rom's #674 call, use the underscore spelling TODO_AGENTS.md (more standard, 'convention over proprietary') instead of the hyphenated TODO-AGENTS.md that #682 shipped. New backlogs are created at TODO_AGENTS.md; the brief hyphen spelling is kept as a read fallback (LEGACY_HYPHEN_TODO_FILE) so no repo loses a backlog, alongside the tickets/TODO.md and root TODO.md fallbacks. Migrates this repo's own backlog file, and expands the GOAL.md context gloss to match the revised #683. Refs #674, #683 --- .changeset/rename-todo-agents-underscore.md | 5 +++++ TODO-AGENTS.md => TODO_AGENTS.md | 0 packages/framework/src/dashboard/docs.test.ts | 6 +++--- packages/framework/src/dashboard/docs.ts | 6 +++--- packages/framework/src/index.ts | 1 + packages/framework/src/run.test.ts | 4 ++-- packages/framework/src/run.ts | 2 +- packages/framework/src/system-prompt.test.ts | 6 +++--- packages/framework/src/system-prompt.ts | 6 +++--- packages/framework/src/tickets.test.ts | 16 +++++++++++----- packages/framework/src/tickets.ts | 19 +++++++++++-------- packages/framework/src/todo-loop.test.ts | 14 +++++++------- packages/framework/src/todo-loop.ts | 4 ++-- 13 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 .changeset/rename-todo-agents-underscore.md rename TODO-AGENTS.md => TODO_AGENTS.md (100%) diff --git a/.changeset/rename-todo-agents-underscore.md b/.changeset/rename-todo-agents-underscore.md new file mode 100644 index 00000000..47e90e49 --- /dev/null +++ b/.changeset/rename-todo-agents-underscore.md @@ -0,0 +1,5 @@ +--- +'@gemstack/framework': minor +--- + +Rename the flat backlog file from `TODO-AGENTS.md` to `TODO_AGENTS.md` (#674): underscore is the more standard convention (per Rom's "convention over proprietary" call). New backlogs are created at `TODO_AGENTS.md`; the brief hyphen spelling is still read as a fallback so no repo loses a backlog, alongside the existing `tickets/TODO.md` and root `TODO.md` fallbacks. Also expands the `GOAL.md` context gloss to match the revised #683. diff --git a/TODO-AGENTS.md b/TODO_AGENTS.md similarity index 100% rename from TODO-AGENTS.md rename to TODO_AGENTS.md diff --git a/packages/framework/src/dashboard/docs.test.ts b/packages/framework/src/dashboard/docs.test.ts index bb398e57..05a3fda9 100644 --- a/packages/framework/src/dashboard/docs.test.ts +++ b/packages/framework/src/dashboard/docs.test.ts @@ -35,13 +35,13 @@ test('readDocs surfaces session-scoped PLAN_/TODO_ .agent.md files (#323/#326)', } }) -test('readDocs surfaces the flat backlog from the root TODO-AGENTS.md, after PLAN (#682)', async () => { +test('readDocs surfaces the flat backlog from the root TODO_AGENTS.md, after PLAN (#682)', async () => { const cwd = await mkdtemp(join(tmpdir(), 'framework-docs-')) try { - await writeFile(join(cwd, 'TODO-AGENTS.md'), '- [ ] roadmap\n') + await writeFile(join(cwd, 'TODO_AGENTS.md'), '- [ ] roadmap\n') await writeFile(join(cwd, 'PLAN.md'), '# Plan\n') const docs = await readDocs(cwd) - assert.deepEqual(docs.map(d => d.name), ['PLAN.md', 'TODO-AGENTS.md']) + assert.deepEqual(docs.map(d => d.name), ['PLAN.md', 'TODO_AGENTS.md']) assert.equal(docs[1]!.content, '- [ ] roadmap\n') } finally { await rm(cwd, { recursive: true, force: true }) diff --git a/packages/framework/src/dashboard/docs.ts b/packages/framework/src/dashboard/docs.ts index 678953cb..604fa5db 100644 --- a/packages/framework/src/dashboard/docs.ts +++ b/packages/framework/src/dashboard/docs.ts @@ -10,13 +10,13 @@ import { findFlatTodo } from '../tickets.js' * `PLAN_.agent.md` (the plan for now) and `TODO_.agent.md` (the * backlog), where SESSION is a git-branch slug. The flat fallbacks are `PLAN.md` * (root) and the flat backlog (`backlog: true` defers to {@link findFlatTodo}, which - * resolves the #682 root `TODO-AGENTS.md` or a legacy `tickets/TODO.md` / `TODO.md`). + * resolves the #682 root `TODO_AGENTS.md` or a legacy `tickets/TODO.md` / `TODO.md`). * Scoped and flat-root names are matched against a flat readdir of the root, never * taken from user input, so there is no path traversal to guard against. */ export const DOC_CATEGORIES = [ { flat: 'PLAN.md', scoped: /^PLAN_[a-z0-9-]+\.agent\.md$/ }, - { flat: 'TODO-AGENTS.md', backlog: true, scoped: /^TODO_[a-z0-9-]+\.agent\.md$/ }, + { flat: 'TODO_AGENTS.md', backlog: true, scoped: /^TODO_[a-z0-9-]+\.agent\.md$/ }, ] as const /** One surfaced document: its filename and current contents. */ @@ -45,7 +45,7 @@ async function surfacedFilenames(cwd: string): Promise { const names: string[] = [] for (const cat of DOC_CATEGORIES) { if ('backlog' in cat) { - // The flat backlog is `TODO-AGENTS.md` at the root (#682), with legacy fallbacks. + // The flat backlog is `TODO_AGENTS.md` at the root (#682), with legacy fallbacks. const flat = await findFlatTodo(cwd) if (flat) names.push(flat) } else if (present.has(cat.flat)) { diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index b7d71ac7..ffef1388 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -271,6 +271,7 @@ export { parseTodoEntries, TODO_FILE_PATTERN, FLAT_TODO_FILE, + LEGACY_HYPHEN_TODO_FILE, LEGACY_TICKETS_TODO_FILE, LEGACY_TODO_FILE, TICKETS_DIR, diff --git a/packages/framework/src/run.test.ts b/packages/framework/src/run.test.ts index 1c4fa328..5e5fa1c5 100644 --- a/packages/framework/src/run.test.ts +++ b/packages/framework/src/run.test.ts @@ -749,8 +749,8 @@ test('runFramework leaves a resume note on the backlog when it pauses (#529)', a }), ) // The backlog is what a later run drains, so the note needs no machinery of its own. - // With no existing backlog, it is created at the #682 location, the root TODO-AGENTS.md. - const todo = await readFile(join(cwd, 'TODO-AGENTS.md'), 'utf8') + // With no existing backlog, it is created at the #682 location, the root TODO_AGENTS.md. + const todo = await readFile(join(cwd, 'TODO_AGENTS.md'), 'utf8') assert.match(todo, /^- \[ \] Resume .+$/m) assert.ok(events.some(e => e.kind === 'log' && e.message.includes('to pick up when the limit resets'))) } finally { diff --git a/packages/framework/src/run.ts b/packages/framework/src/run.ts index ca4ff450..75c4fb7b 100644 --- a/packages/framework/src/run.ts +++ b/packages/framework/src/run.ts @@ -198,7 +198,7 @@ export interface RunFrameworkOptions { consumptionGate?: () => ConsumptionWindow | null /** * Run the backlog loop (#323) after the build settles: consume the agent's own - * `TODO_.agent.md` / `TODO-AGENTS.md` one entry per turn until empty, gating + * `TODO_.agent.md` / `TODO_AGENTS.md` one entry per turn until empty, gating * before each entry when {@link requestChoice} is wired. Default: on for real * drivers, off for the fake one (its scripted demo writes no backlog and must * stay deterministic). Set explicitly to force either way. diff --git a/packages/framework/src/system-prompt.test.ts b/packages/framework/src/system-prompt.test.ts index eeb00de0..3f99afd7 100644 --- a/packages/framework/src/system-prompt.test.ts +++ b/packages/framework/src/system-prompt.test.ts @@ -21,12 +21,12 @@ const KNOWLEDGE_CONTEXT = `Context:\n${KNOWLEDGE_LINES}` test('CONTEXT_DOCS is the #683 fragment: business knowledge plus the roadmap/queue pointers', () => { const paths = CONTEXT_DOCS.map(d => d.path) - assert.deepEqual(paths, ['DECISIONS.md', 'GOAL.md', 'KNOWLEDGE-BASE.md', 'tickets/**.md', 'TODO-AGENTS.md']) + assert.deepEqual(paths, ['DECISIONS.md', 'GOAL.md', 'KNOWLEDGE-BASE.md', 'tickets/**.md', 'TODO_AGENTS.md']) // The business-knowledge docs are a subset the agent also updates at merge. for (const doc of BUSINESS_KNOWLEDGE_DOCS) assert.ok(paths.includes(doc.path), `missing ${doc.path}`) - // GOAL / tickets / TODO-AGENTS are read-only context, so they are not in the merge-update set. + // GOAL / tickets / TODO_AGENTS are read-only context, so they are not in the merge-update set. const businessPaths = BUSINESS_KNOWLEDGE_DOCS.map(d => d.path) - for (const p of ['GOAL.md', 'tickets/**.md', 'TODO-AGENTS.md']) assert.ok(!businessPaths.includes(p)) + for (const p of ['GOAL.md', 'tickets/**.md', 'TODO_AGENTS.md']) assert.ok(!businessPaths.includes(p)) // The ticket-format pointer is inlined here (this module stays node-free), so pin it to the // canonical constant in tickets.ts (#684) — they must never drift. const tickets = CONTEXT_DOCS.find(d => d.path === 'tickets/**.md') diff --git a/packages/framework/src/system-prompt.ts b/packages/framework/src/system-prompt.ts index 0dea653d..286eea71 100644 --- a/packages/framework/src/system-prompt.ts +++ b/packages/framework/src/system-prompt.ts @@ -114,7 +114,7 @@ export const BUSINESS_KNOWLEDGE_DOCS: readonly ContextDoc[] = [DECISIONS_DOC, KN * {@link systemPromptBlock} renders as the `Context:` bullets. A superset of * {@link BUSINESS_KNOWLEDGE_DOCS}: it adds `GOAL.md` and the two roadmap/queue pointers the * agent reads but does *not* fold knowledge back into — `tickets/**.md` (the potential work, - * whose file shape is `.the-framework/ticketing-format.md`, #684) and the `TODO-AGENTS.md` + * whose file shape is `.the-framework/ticketing-format.md`, #684) and the `TODO_AGENTS.md` * task queue. Repo-root paths, because that is the agent's cwd. README is left out: a repo's * own `README.md` already covers the overview. The ticket-format path is inlined rather than * imported from `tickets.ts`: this module must stay free of `node:fs` (it renders in the @@ -122,10 +122,10 @@ export const BUSINESS_KNOWLEDGE_DOCS: readonly ContextDoc[] = [DECISIONS_DOC, KN */ export const CONTEXT_DOCS: readonly ContextDoc[] = [ DECISIONS_DOC, - { path: 'GOAL.md', comment: 'the goal of the project' }, + { path: 'GOAL.md', comment: 'the goal of the project (long-term direction, scope, non-scope, ...)' }, KNOWLEDGE_BASE_DOC, { path: 'tickets/**.md', comment: 'things to potentially work on; format: .the-framework/ticketing-format.md' }, - { path: 'TODO-AGENTS.md', comment: 'the AI task queue' }, + { path: 'TODO_AGENTS.md', comment: 'the AI task queue' }, ] /** The two halves of the rendered {@link SYSTEM_PROMPT_TEMPLATE}. */ diff --git a/packages/framework/src/tickets.test.ts b/packages/framework/src/tickets.test.ts index c3c8837f..2177c68e 100644 --- a/packages/framework/src/tickets.test.ts +++ b/packages/framework/src/tickets.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { FLAT_TODO_FILE, + LEGACY_HYPHEN_TODO_FILE, LEGACY_TICKETS_TODO_FILE, LEGACY_TODO_FILE, TICKETS_DIR, @@ -14,9 +15,10 @@ import { } from './tickets.js' import { TICKETING_FORMAT } from './prompts.generated.js' -test('the flat backlog lives at the root TODO-AGENTS.md, with the legacy locations named (#682)', () => { +test('the flat backlog lives at the root TODO_AGENTS.md, with the legacy locations named (#674/#682)', () => { assert.equal(TICKETS_DIR, 'tickets') - assert.equal(FLAT_TODO_FILE, 'TODO-AGENTS.md') + assert.equal(FLAT_TODO_FILE, 'TODO_AGENTS.md') + assert.equal(LEGACY_HYPHEN_TODO_FILE, 'TODO-AGENTS.md') assert.equal(LEGACY_TICKETS_TODO_FILE, 'tickets/TODO.md') assert.equal(LEGACY_TODO_FILE, 'TODO.md') }) @@ -38,7 +40,7 @@ test('the ticket-format spec materializes under .the-framework, not tickets/ (#6 } }) -test('findFlatTodo prefers TODO-AGENTS.md, then legacy tickets/TODO.md, then root TODO.md, else undefined (#682)', async () => { +test('findFlatTodo prefers TODO_AGENTS.md, then legacy tickets/TODO.md, then root TODO.md, else undefined (#682)', async () => { const cwd = await mkdtemp(join(tmpdir(), 'framework-tickets-')) try { assert.equal(await findFlatTodo(cwd), undefined) @@ -52,9 +54,13 @@ test('findFlatTodo prefers TODO-AGENTS.md, then legacy tickets/TODO.md, then roo await writeFile(join(cwd, LEGACY_TICKETS_TODO_FILE), '- [ ] newer\n') assert.equal(await findFlatTodo(cwd), 'tickets/TODO.md') - // The #682 root TODO-AGENTS.md wins over both legacy locations. - await writeFile(join(cwd, FLAT_TODO_FILE), '- [ ] current\n') + // The brief #682 hyphen spelling wins over the older locations. + await writeFile(join(cwd, LEGACY_HYPHEN_TODO_FILE), '- [ ] hyphen\n') assert.equal(await findFlatTodo(cwd), 'TODO-AGENTS.md') + + // The #674 root TODO_AGENTS.md (underscore) wins over every legacy location. + await writeFile(join(cwd, FLAT_TODO_FILE), '- [ ] current\n') + assert.equal(await findFlatTodo(cwd), 'TODO_AGENTS.md') } finally { await rm(cwd, { recursive: true, force: true }) } diff --git a/packages/framework/src/tickets.ts b/packages/framework/src/tickets.ts index 3d344395..710b1744 100644 --- a/packages/framework/src/tickets.ts +++ b/packages/framework/src/tickets.ts @@ -8,7 +8,7 @@ import { nodeStoreFs, type StoreFs } from './store/index.js' * The root `tickets/` directory (#629): a plain repo convention where The Framework * keeps its human-facing roadmap files, rather than hiding them in a proprietary * `.the-framework/` dir. It sits beside conventions like a root `DECISIONS.md`. Since - * #682 moved the backlog out to a root `TODO-AGENTS.md`, this directory holds only + * #682 moved the backlog out to a root `TODO_AGENTS.md`, this directory holds only * ticket files (`_.md`). */ export const TICKETS_DIR = 'tickets' @@ -35,12 +35,15 @@ export async function materializeTicketingFormat(cwd: string, fs: StoreFs = node /** * The flat, durable backlog/roadmap file — the confirmed-task queue (the "AI task queue" - * the #683 context fragment names). Lives at the repo root as `TODO-AGENTS.md` (#682): + * the #683 context fragment names). Lives at the repo root as `TODO_AGENTS.md` (#682): * moved out of `tickets/` so that directory holds only tickets. This is the file a run * drains and the dashboard surfaces; the session-scoped `TODO_.agent.md` files the * system prompt writes are separate and also live at the root. */ -export const FLAT_TODO_FILE = 'TODO-AGENTS.md' +export const FLAT_TODO_FILE = 'TODO_AGENTS.md' + +/** The brief hyphen spelling from #682, read as a fallback after #674 settled on the underscore. */ +export const LEGACY_HYPHEN_TODO_FILE = 'TODO-AGENTS.md' /** The #629 backlog location (under `tickets/`), read as a fallback after #682 moved it to the root. */ export const LEGACY_TICKETS_TODO_FILE = `${TICKETS_DIR}/TODO.md` @@ -56,13 +59,13 @@ async function isFile(cwd: string, rel: string): Promise { } /** - * The workspace's flat backlog file, newest convention first: the #682 root - * `TODO-AGENTS.md`, else the #629 `tickets/TODO.md`, else the pre-#629 root `TODO.md`. - * Returns the workspace-relative path, or `undefined` when none exists. New backlogs are - * created at {@link FLAT_TODO_FILE}. + * The workspace's flat backlog file, newest convention first: the root `TODO_AGENTS.md`, + * else the brief hyphen spelling, else the #629 `tickets/TODO.md`, else the pre-#629 root + * `TODO.md`. Returns the workspace-relative path, or `undefined` when none exists. New + * backlogs are created at {@link FLAT_TODO_FILE}. */ export async function findFlatTodo(cwd: string): Promise { - for (const rel of [FLAT_TODO_FILE, LEGACY_TICKETS_TODO_FILE, LEGACY_TODO_FILE]) { + for (const rel of [FLAT_TODO_FILE, LEGACY_HYPHEN_TODO_FILE, LEGACY_TICKETS_TODO_FILE, LEGACY_TODO_FILE]) { if (await isFile(cwd, rel)) return rel } return undefined diff --git a/packages/framework/src/todo-loop.test.ts b/packages/framework/src/todo-loop.test.ts index dd48258c..7976c9a3 100644 --- a/packages/framework/src/todo-loop.test.ts +++ b/packages/framework/src/todo-loop.test.ts @@ -61,11 +61,11 @@ test('findTodoBacklog prefers the newest session-scoped file, falls back to flat } }) -test('findTodoBacklog reads the flat backlog from the root TODO-AGENTS.md (#682)', async () => { +test('findTodoBacklog reads the flat backlog from the root TODO_AGENTS.md (#682)', async () => { const cwd = await tmpWorkspace() try { - await writeFile(join(cwd, 'TODO-AGENTS.md'), '- [ ] roadmap entry\n') - assert.deepEqual(await findTodoBacklog(cwd), { name: 'TODO-AGENTS.md', entries: ['roadmap entry'] }) + await writeFile(join(cwd, 'TODO_AGENTS.md'), '- [ ] roadmap entry\n') + assert.deepEqual(await findTodoBacklog(cwd), { name: 'TODO_AGENTS.md', entries: ['roadmap entry'] }) // A session-scoped backlog still wins over the flat one, wherever the flat one lives. await writeFile(join(cwd, 'TODO_feat-x.agent.md'), '- [ ] scoped entry\n') @@ -86,16 +86,16 @@ test('findTodoBacklog still reads a legacy tickets/TODO.md backlog (#682 fallbac } }) -test('appendTodoEntry creates the root TODO-AGENTS.md when the workspace has no backlog (#682)', async () => { +test('appendTodoEntry creates the root TODO_AGENTS.md when the workspace has no backlog (#682)', async () => { const cwd = await tmpWorkspace() try { const file = await appendTodoEntry(cwd, 'Resume the paused run') - assert.equal(file, 'TODO-AGENTS.md') - assert.equal(await readFile(join(cwd, 'TODO-AGENTS.md'), 'utf8'), '- [ ] Resume the paused run\n') + assert.equal(file, 'TODO_AGENTS.md') + assert.equal(await readFile(join(cwd, 'TODO_AGENTS.md'), 'utf8'), '- [ ] Resume the paused run\n') // A second entry appends to the same file, not a new one. await appendTodoEntry(cwd, 'And another') - assert.equal(await readFile(join(cwd, 'TODO-AGENTS.md'), 'utf8'), '- [ ] Resume the paused run\n- [ ] And another\n') + assert.equal(await readFile(join(cwd, 'TODO_AGENTS.md'), 'utf8'), '- [ ] Resume the paused run\n- [ ] And another\n') } finally { await rm(cwd, { recursive: true, force: true }) } diff --git a/packages/framework/src/todo-loop.ts b/packages/framework/src/todo-loop.ts index a38a6724..12dfb746 100644 --- a/packages/framework/src/todo-loop.ts +++ b/packages/framework/src/todo-loop.ts @@ -6,7 +6,7 @@ import { requestChoices, runAwaitRounds } from './run.js' import { FLAT_TODO_FILE, findFlatTodo } from './tickets.js' import { createTurnSignalEmitter } from './turn-gate.js' -export { FLAT_TODO_FILE, LEGACY_TICKETS_TODO_FILE, LEGACY_TODO_FILE, TICKETS_DIR, TICKETING_FORMAT_FILE, materializeTicketingFormat } from './tickets.js' +export { FLAT_TODO_FILE, LEGACY_HYPHEN_TODO_FILE, LEGACY_TICKETS_TODO_FILE, LEGACY_TODO_FILE, TICKETS_DIR, TICKETING_FORMAT_FILE, materializeTicketingFormat } from './tickets.js' /** * The backlog loop (#323): once the main work settles, consume the agent's own @@ -57,7 +57,7 @@ export function parseTodoEntries(md: string): string[] { /** * The workspace's backlog files, best first: the most recently modified * session-scoped `TODO_.agent.md` (#323/#326), then the flat backlog - * (`TODO-AGENTS.md`, or a legacy `tickets/TODO.md` / root `TODO.md`) — the same + * (`TODO_AGENTS.md`, or a legacy `tickets/TODO.md` / root `TODO.md`) — the same * convention as the doc sidebar. */ async function backlogCandidates(cwd: string): Promise {