Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rename-todo-agents-underscore.md
Original file line number Diff line number Diff line change
@@ -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.
File renamed without changes.
6 changes: 3 additions & 3 deletions packages/framework/src/dashboard/docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
6 changes: 3 additions & 3 deletions packages/framework/src/dashboard/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { findFlatTodo } from '../tickets.js'
* `PLAN_<SESSION>.agent.md` (the plan for now) and `TODO_<SESSION>.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. */
Expand Down Expand Up @@ -45,7 +45,7 @@ async function surfacedFilenames(cwd: string): Promise<string[]> {
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)) {
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/framework/src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_<slug>.agent.md` / `TODO-AGENTS.md` one entry per turn until empty, gating
* `TODO_<slug>.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.
Expand Down
6 changes: 3 additions & 3 deletions packages/framework/src/system-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 3 additions & 3 deletions packages/framework/src/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,18 @@ 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
* browser, #520), and a test pins the literal to `TICKETING_FORMAT_FILE`.
*/
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}. */
Expand Down
16 changes: 11 additions & 5 deletions packages/framework/src/tickets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
})
Expand All @@ -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)
Expand All @@ -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 })
}
Expand Down
19 changes: 11 additions & 8 deletions packages/framework/src/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<DATE>_<SLUG>.md`).
*/
export const TICKETS_DIR = 'tickets'
Expand All @@ -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_<slug>.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`
Expand All @@ -56,13 +59,13 @@ async function isFile(cwd: string, rel: string): Promise<boolean> {
}

/**
* 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<string | undefined> {
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
Expand Down
14 changes: 7 additions & 7 deletions packages/framework/src/todo-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 })
}
Expand Down
4 changes: 2 additions & 2 deletions packages/framework/src/todo-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,7 +57,7 @@ export function parseTodoEntries(md: string): string[] {
/**
* The workspace's backlog files, best first: the most recently modified
* session-scoped `TODO_<slug>.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<string[]> {
Expand Down
Loading