From 6122c1966c5eab8e2e9eccfa4f69e8ba6c237c15 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 18 Jul 2026 13:45:49 +0300 Subject: [PATCH] feat(framework): ticket-format spec materialized for agents (#684) Add prompts/ticketing_format.md describing the tickets/_.md and .spike.md file shapes, exposed as TICKETING_FORMAT_FILE and materialized into .the-framework/ticketing-format.md on install beside the quality presets, so an agent can open it by path. The #683 context fragment points tickets/**.md here. Closes #684 --- .changeset/ticketing-format-spec.md | 5 ++ .../framework/prompts/ticketing_format.md | 51 +++++++++++++++++++ packages/framework/src/index.ts | 2 + packages/framework/src/install.ts | 4 ++ packages/framework/src/tickets.test.ts | 29 ++++++++++- packages/framework/src/tickets.ts | 23 +++++++++ packages/framework/src/todo-loop.ts | 2 +- 7 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 .changeset/ticketing-format-spec.md create mode 100644 packages/framework/prompts/ticketing_format.md diff --git a/.changeset/ticketing-format-spec.md b/.changeset/ticketing-format-spec.md new file mode 100644 index 00000000..beab3a25 --- /dev/null +++ b/.changeset/ticketing-format-spec.md @@ -0,0 +1,5 @@ +--- +'@gemstack/framework': minor +--- + +Add the ticket-format spec (#684): a static `.the-framework/ticketing-format.md` describing the `tickets/_.md` and `.spike.md` file shapes, materialized on install beside the quality presets so an agent can open it by path. diff --git a/packages/framework/prompts/ticketing_format.md b/packages/framework/prompts/ticketing_format.md new file mode 100644 index 00000000..c4e034c7 --- /dev/null +++ b/packages/framework/prompts/ticketing_format.md @@ -0,0 +1,51 @@ +# Ticket format + +## tickets/_.md + +DATE: yyyy-mm-dd +SLUG: succinct kebab-case slug of the ticket title +Body: +```md +# Ticket title + +## TLDR + +... + +## Why it matters + +... + +[Optional: more info (any heading and format you want)] +``` + +## tickets/_.spike.md + +For an existing ticket (e.g. `tickets/2042-01-01_some-ticket.md`), a spike can be created (`tickets/2042-01-01_some-ticket.spike.md`). + +Body: +```md +# [Spike] Ticket title + +## TLDR + +... + +## Analysis + +... + +[Optional: more info (any heading and format you want)] +``` + +Typical spike content: +- High-level overview, for example: + - What it takes to implement it + - List all the ways to implement it + - Potential "laziness" shortcuts + - Full-fledged implementation VS minimal implementation + - Open questions + - ... +- Estimated effort (for each ways to implement it): + - Human intervention effort: trivial/low/medium/high/very-high + - Token consumption: time estimate (minutes, hours, or days) diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index 8db88d99..6211e807 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -273,6 +273,8 @@ export { FLAT_TODO_FILE, LEGACY_TODO_FILE, TICKETS_DIR, + TICKETING_FORMAT_FILE, + materializeTicketingFormat, DEFAULT_MAX_TODO_ITEMS, type TodoBacklog, type TodoLoopOptions, diff --git a/packages/framework/src/install.ts b/packages/framework/src/install.ts index 77f7b966..32213465 100644 --- a/packages/framework/src/install.ts +++ b/packages/framework/src/install.ts @@ -3,6 +3,7 @@ import { nodeGitRunner, type GitRunner } from './project.js' import { logsPath, LOGS_HEADER, THE_FRAMEWORK_DIR, gitignorePath, LOGS_GITIGNORE } from './logs.js' import { nodeStoreFs, type StoreFs } from './store/index.js' import { materializePresets } from './presets.js' +import { materializeTicketingFormat } from './tickets.js' /** * Install/activate a repo for The Framework (#391): create the @@ -63,6 +64,9 @@ export async function installProject(cwd: string, deps: InstallDeps = {}): Promi // of git (only LOGS.md is committed), so they are regenerated on install and track the // installed framework version rather than going stale in the repo's history. await materializePresets(cwd, fs) + // The ticket-format spec rides along the same way (#684): gitignored, refreshed on + // install, so the #683 context pointer to it resolves to a real file. + await materializeTicketingFormat(cwd, fs) await git(['add', '-A'], cwd) await git(['commit', '-m', '[The Framework] install The Framework'], cwd) diff --git a/packages/framework/src/tickets.test.ts b/packages/framework/src/tickets.test.ts index cea66b51..885349ec 100644 --- a/packages/framework/src/tickets.test.ts +++ b/packages/framework/src/tickets.test.ts @@ -1,9 +1,17 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' -import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' +import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { FLAT_TODO_FILE, LEGACY_TODO_FILE, TICKETS_DIR, findFlatTodo } from './tickets.js' +import { + FLAT_TODO_FILE, + LEGACY_TODO_FILE, + TICKETS_DIR, + TICKETING_FORMAT_FILE, + findFlatTodo, + materializeTicketingFormat, +} from './tickets.js' +import { TICKETING_FORMAT } from './prompts.generated.js' test('the flat backlog lives at tickets/TODO.md, with the legacy root file named', () => { assert.equal(TICKETS_DIR, 'tickets') @@ -11,6 +19,23 @@ test('the flat backlog lives at tickets/TODO.md, with the legacy root file named assert.equal(LEGACY_TODO_FILE, 'TODO.md') }) +test('the ticket-format spec materializes under .the-framework, not tickets/ (#684)', async () => { + // It is framework-authored, so it lives beside the presets and never masquerades as a ticket. + assert.equal(TICKETING_FORMAT_FILE, '.the-framework/ticketing-format.md') + + const cwd = await mkdtemp(join(tmpdir(), 'framework-ticket-format-')) + try { + await materializeTicketingFormat(cwd) + const written = await readFile(join(cwd, TICKETING_FORMAT_FILE), 'utf8') + assert.equal(written, TICKETING_FORMAT) + // The spec teaches both the ticket and spike file shapes. + assert.ok(written.includes('tickets/_.md')) + assert.ok(written.includes('tickets/_.spike.md')) + } finally { + await rm(cwd, { recursive: true, force: true }) + } +}) + test('findFlatTodo prefers tickets/TODO.md, falls back to legacy root TODO.md, else undefined (#629)', async () => { const cwd = await mkdtemp(join(tmpdir(), 'framework-tickets-')) try { diff --git a/packages/framework/src/tickets.ts b/packages/framework/src/tickets.ts index 28e6cd7e..83fc9724 100644 --- a/packages/framework/src/tickets.ts +++ b/packages/framework/src/tickets.ts @@ -1,5 +1,8 @@ import { stat } from 'node:fs/promises' import { join } from 'node:path' +import { TICKETING_FORMAT } from './prompts.generated.js' +import { THE_FRAMEWORK_DIR } from './logs.js' +import { nodeStoreFs, type StoreFs } from './store/index.js' /** * The root `tickets/` directory (#629): a plain repo convention where The Framework @@ -9,6 +12,26 @@ import { join } from 'node:path' */ export const TICKETS_DIR = 'tickets' +/** + * The ticket-format spec (#684): the static reference an agent opens to learn the + * `tickets/_.md` (and `.spike.md`) file shape. Materialized under + * `.the-framework/` beside the presets — not under `tickets/` — because it is + * framework-authored and rides with the installed version, so it must not look like + * a ticket. The #683 context fragment points `tickets/**.md` at this path. + */ +export const TICKETING_FORMAT_FILE = `${THE_FRAMEWORK_DIR}/ticketing-format.md` + +/** + * Write the ticket-format spec to `/.the-framework/ticketing-format.md` so the + * #683 context pointer resolves to a real file (#684). Mirrors {@link materializePresets}: + * gitignored, overwritten on install, tracks the installed framework version rather than + * going stale in the repo. Creates `.the-framework/` if it is missing. + */ +export async function materializeTicketingFormat(cwd: string, fs: StoreFs = nodeStoreFs()): Promise { + await fs.mkdir(join(cwd, THE_FRAMEWORK_DIR)) + await fs.write(join(cwd, TICKETING_FORMAT_FILE), TICKETING_FORMAT) +} + /** * The flat, durable backlog/roadmap file — the confirmed-task queue. Moved under * `tickets/` in #629 (it used to sit at the repo root). This is the one a run drains diff --git a/packages/framework/src/todo-loop.ts b/packages/framework/src/todo-loop.ts index 18a5f769..54745faa 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_TODO_FILE, TICKETS_DIR } from './tickets.js' +export { FLAT_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