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/ticketing-format-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gemstack/framework': minor
---

Add the ticket-format spec (#684): a static `.the-framework/ticketing-format.md` describing the `tickets/<DATE>_<SLUG>.md` and `.spike.md` file shapes, materialized on install beside the quality presets so an agent can open it by path.
51 changes: 51 additions & 0 deletions packages/framework/prompts/ticketing_format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Ticket format

## tickets/<DATE>_<SLUG>.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/<DATE>_<SLUG>.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)
2 changes: 2 additions & 0 deletions packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/framework/src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 27 additions & 2 deletions packages/framework/src/tickets.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
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')
assert.equal(FLAT_TODO_FILE, 'tickets/TODO.md')
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/<DATE>_<SLUG>.md'))
assert.ok(written.includes('tickets/<DATE>_<SLUG>.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 {
Expand Down
23 changes: 23 additions & 0 deletions packages/framework/src/tickets.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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/<DATE>_<SLUG>.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 `<cwd>/.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<void> {
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
Expand Down
2 changes: 1 addition & 1 deletion 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_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
Expand Down
Loading