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
2 changes: 1 addition & 1 deletion .changeset/ticketing-format-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@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.
Add the ticket-format spec (#684): `ticketing_format.md` describes the `tickets/<DATE>_<SLUG>.md` and `.spike.md` file shapes (including the optional `priority:` and `topics:` fields). Per #674 it ships inside the package and the run-start context fragment references it by its `node_modules` path, so the format versions with the package rather than being materialized into each repo.
3 changes: 2 additions & 1 deletion packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"node": ">=22.12.0"
},
"files": [
"dist"
"dist",
"prompts/ticketing_format.md"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
7 changes: 5 additions & 2 deletions packages/framework/prompts/ticketing_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ DATE: yyyy-mm-dd
SLUG: succinct kebab-case slug of the ticket title
Body:
```md
priority: low/medium/high/urgent [optional]
topics: [list-of-topics] [optional]

# Ticket title

## TLDR
Expand All @@ -16,7 +19,7 @@ Body:

...

[Optional: more info (any heading and format you want)]
[optional: more info (any heading and format you want)]
```

## tickets/<DATE>_<SLUG>.spike.md
Expand All @@ -35,7 +38,7 @@ Body:

...

[Optional: more info (any heading and format you want)]
[optional: more info (any heading and format you want)]
```

Typical spike content:
Expand Down
1 change: 0 additions & 1 deletion packages/framework/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,6 @@ export {
LEGACY_TODO_FILE,
TICKETS_DIR,
TICKETING_FORMAT_FILE,
materializeTicketingFormat,
DEFAULT_MAX_TODO_ITEMS,
type TodoBacklog,
type TodoLoopOptions,
Expand Down
6 changes: 2 additions & 4 deletions packages/framework/src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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 @@ -64,9 +63,8 @@ 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)
// The ticket-format spec is NOT materialized (#674): it ships inside the package and the
// #683 context fragment points at its node_modules path, so it versions with the package.

await git(['add', '-A'], cwd)
await git(['commit', '-m', '[The Framework] install The Framework'], cwd)
Expand Down
4 changes: 2 additions & 2 deletions packages/framework/src/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 packaged `ticketing_format.md` spec, #684/#674) 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
Expand All @@ -124,7 +124,7 @@ export const CONTEXT_DOCS: readonly ContextDoc[] = [
DECISIONS_DOC,
{ 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: 'tickets/**.md', comment: 'things to potentially work on; format: node_modules/@gemstack/framework/prompts/ticketing_format.md' },
{ path: 'TODO_AGENTS.md', comment: 'the AI task queue' },
]

Expand Down
27 changes: 10 additions & 17 deletions packages/framework/src/tickets.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { strict as assert } from 'node:assert'
import { test } from 'node:test'
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
Expand All @@ -11,7 +11,6 @@ import {
TICKETS_DIR,
TICKETING_FORMAT_FILE,
findFlatTodo,
materializeTicketingFormat,
} from './tickets.js'
import { TICKETING_FORMAT } from './prompts.generated.js'

Expand All @@ -23,21 +22,15 @@ test('the flat backlog lives at the root TODO_AGENTS.md, with the legacy locatio
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('the ticket-format spec ships in the package (not materialized), with priority/topics (#684/#674)', () => {
// Per Rom's #674 call it is not written into the repo; it ships inside the package and the
// context fragment reads it by its node_modules path, so the format versions with the package.
assert.equal(TICKETING_FORMAT_FILE, 'node_modules/@gemstack/framework/prompts/ticketing_format.md')
// The spec teaches both file shapes and the revised #684 optional priority/topics fields.
assert.ok(TICKETING_FORMAT.includes('tickets/<DATE>_<SLUG>.md'))
assert.ok(TICKETING_FORMAT.includes('tickets/<DATE>_<SLUG>.spike.md'))
assert.ok(TICKETING_FORMAT.includes('priority: low/medium/high/urgent'))
assert.ok(TICKETING_FORMAT.includes('topics:'))
})

test('findFlatTodo prefers TODO_AGENTS.md, then legacy tickets/TODO.md, then root TODO.md, else undefined (#682)', async () => {
Expand Down
25 changes: 6 additions & 19 deletions packages/framework/src/tickets.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
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 @@ -15,23 +12,13 @@ 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.
* `tickets/<DATE>_<SLUG>.md` (and `.spike.md`) file shape. Per Rom's #674 call it ships
* *inside the installed package* rather than being materialized into the repo, so a future
* breaking change to the format rides with the package version instead of going stale in a
* committed file. The package ships `prompts/ticketing_format.md` (see the `files` allowlist),
* so an agent reads it at this cwd-relative path; the #683 context fragment points here.
*/
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)
}
export const TICKETING_FORMAT_FILE = 'node_modules/@gemstack/framework/prompts/ticketing_format.md'

/**
* The flat, durable backlog/roadmap file — the confirmed-task queue (the "AI task queue"
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_HYPHEN_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 } from './tickets.js'

/**
* The backlog loop (#323): once the main work settles, consume the agent's own
Expand Down
Loading