From c38467ad84dd0bec2cf871bcb93dc914f53b1120 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sun, 5 Jul 2026 20:41:40 +0300 Subject: [PATCH] feat(framework): support event: key in the-framework.yml (#267) Finishes the per-repo config surface: the-framework.yml can now set the build event kind (event: bug-fix) alongside preset and modes. Precedence is --kind flag > the-framework.yml event > preset defaultEvent > major-change, so file-set events flow through the same runFramework({buildEvent}) path #266 added. Threads event through parseFrameworkConfig, mergeRunConfig, and the config narration; the no-op note now fires when any build event is set without a preset. Help text updated. --- .changeset/framework-yml-event.md | 5 +++++ packages/framework/src/cli.test.ts | 6 +++++- packages/framework/src/cli.ts | 24 ++++++++++++++---------- packages/framework/src/config.test.ts | 17 +++++++++++------ packages/framework/src/config.ts | 10 +++++++--- 5 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 .changeset/framework-yml-event.md diff --git a/.changeset/framework-yml-event.md b/.changeset/framework-yml-event.md new file mode 100644 index 0000000..66c4c29 --- /dev/null +++ b/.changeset/framework-yml-event.md @@ -0,0 +1,5 @@ +--- +'@gemstack/framework': minor +--- + +the-framework.yml gains an `event:` key so a repo can pin its build event kind (e.g. `bug-fix`) alongside `preset`/`autopilot`/`technical`. Precedence: `--kind` flag > `the-framework.yml` event > preset default > `major-change`. diff --git a/packages/framework/src/cli.test.ts b/packages/framework/src/cli.test.ts index c5b9679..7368eca 100644 --- a/packages/framework/src/cli.test.ts +++ b/packages/framework/src/cli.test.ts @@ -100,6 +100,10 @@ test('mergeRunConfig: the-framework.yml supplies defaults, flags override (#258) }) // nothing set anywhere: no preset, no modes assert.deepEqual(mergeRunConfig(flags, {}), { autopilot: false, technical: false }) + // build event: the file's `event` supplies a default, --kind overrides it (#265) + assert.equal(mergeRunConfig(flags, { event: 'bug-fix' }).buildEvent, 'bug-fix') + assert.equal(mergeRunConfig({ ...flags, buildEvent: 'major-change' }, { event: 'bug-fix' }).buildEvent, 'major-change') + assert.equal(mergeRunConfig(flags, {}).buildEvent, undefined) }) test('resolveDomainPreset resolves a shipped preset by name (#254/#256)', async () => { @@ -136,7 +140,7 @@ test('runCli notes --kind given without a preset (#265)', async () => { const { io, err } = capture() const code = await runCli(['--fake', '--no-dashboard', '--kind', 'bug-fix'], io) assert.equal(code, 0) - assert.ok(err.some(l => /--kind bug-fix has no effect without a preset/.test(l))) + assert.ok(err.some(l => /build event "bug-fix" has no effect without a preset/.test(l))) }) test('chooseSessionLink defaults a live run to the claude.ai/code session list (#212)', () => { diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index ca9fce2..e197560 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -76,11 +76,12 @@ Options: + skills frame the build), e.g. software-development. --autopilot Activate the preset's Autopilot mode variants. --technical Activate the preset's Technical mode variants. - (--preset / --autopilot / --technical can also be set per - repo in the-framework.yml; these flags override it.) + (--preset / --autopilot / --technical / --kind can also be + set per repo in the-framework.yml; these flags override it.) --kind Build event kind the preset's review loop fires for, e.g. - bug-fix or major-change (default: the preset's own, else - major-change). Selects which review chain gates the run. + bug-fix or major-change (default: the-framework.yml's event, + else the preset's own, else major-change). Selects which + review chain gates the run. --compose-extensions Opt the built-in capability extensions in (auth, data, rbac, crud, shell) so the agent composes them instead of hand-rolling. Vike-only; installed framework-* extensions @@ -329,23 +330,26 @@ export function activeModes(opts: Pick): * (a flag can only *enable* a mode, so there is nothing to override the other way). */ export function mergeRunConfig( - opts: Pick, + opts: Pick, file: FrameworkFileConfig, -): { presetName?: string; autopilot: boolean; technical: boolean } { +): { presetName?: string; autopilot: boolean; technical: boolean; buildEvent?: string } { const presetName = opts.preset ?? file.preset + const buildEvent = opts.buildEvent ?? file.event return { ...(presetName ? { presetName } : {}), + ...(buildEvent ? { buildEvent } : {}), autopilot: opts.autopilot || file.autopilot === true, technical: opts.technical || file.technical === true, } } /** A short summary of what the-framework.yml contributed and is in effect, or `''` for nothing to report. */ -function describeConfigSource(opts: Pick, file: FrameworkFileConfig): string { +function describeConfigSource(opts: Pick, file: FrameworkFileConfig): string { const parts: string[] = [] if (file.preset && !opts.preset) parts.push(`preset=${file.preset}`) // a --preset flag would override it if (file.autopilot) parts.push('autopilot') if (file.technical) parts.push('technical') + if (file.event && !opts.buildEvent) parts.push(`event=${file.event}`) // a --kind flag would override it return parts.join(', ') } @@ -432,8 +436,8 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise { const link = chooseSessionLink(opts, fake) diff --git a/packages/framework/src/config.test.ts b/packages/framework/src/config.test.ts index 2b4f5af..d862728 100644 --- a/packages/framework/src/config.test.ts +++ b/packages/framework/src/config.test.ts @@ -5,12 +5,16 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { loadFrameworkConfig, parseFrameworkConfig } from './config.js' -test('parseFrameworkConfig reads preset + mode booleans', () => { - assert.deepEqual(parseFrameworkConfig('preset: software-development\nautopilot: true\ntechnical: false\n'), { - preset: 'software-development', - autopilot: true, - technical: false, - }) +test('parseFrameworkConfig reads preset + mode booleans + event', () => { + assert.deepEqual( + parseFrameworkConfig('preset: software-development\nautopilot: true\ntechnical: false\nevent: bug-fix\n'), + { + preset: 'software-development', + autopilot: true, + technical: false, + event: 'bug-fix', + }, + ) }) test('parseFrameworkConfig treats an empty document as {}', () => { @@ -21,6 +25,7 @@ test('parseFrameworkConfig treats an empty document as {}', () => { test('parseFrameworkConfig rejects a non-map document and mistyped fields', () => { assert.throws(() => parseFrameworkConfig('- a\n- b\n'), /must be a YAML map/) assert.throws(() => parseFrameworkConfig('preset: 3\n'), /"preset" must be a string/) + assert.throws(() => parseFrameworkConfig('event: 3\n'), /"event" must be a string/) assert.throws(() => parseFrameworkConfig('autopilot: yep\n'), /"autopilot" must be a boolean/) }) diff --git a/packages/framework/src/config.ts b/packages/framework/src/config.ts index f6de4a3..f9db4a1 100644 --- a/packages/framework/src/config.ts +++ b/packages/framework/src/config.ts @@ -14,6 +14,8 @@ export interface FrameworkFileConfig { autopilot?: boolean /** Activate the preset's Technical mode variants. */ technical?: boolean + /** Build event kind the preset's review loop fires for, e.g. `bug-fix` (#265). */ + event?: string } /** Config file names read from the workspace root, in precedence order. */ @@ -59,9 +61,11 @@ export function parseFrameworkConfig(raw: string, source = 'the-framework.yml'): } const obj = data as Record const config: FrameworkFileConfig = {} - if (obj['preset'] !== undefined) { - if (typeof obj['preset'] !== 'string') throw new Error(`${source}: "preset" must be a string`) - config.preset = obj['preset'] + for (const key of ['preset', 'event'] as const) { + if (obj[key] !== undefined) { + if (typeof obj[key] !== 'string') throw new Error(`${source}: "${key}" must be a string`) + config[key] = obj[key] as string + } } for (const key of ['autopilot', 'technical'] as const) { if (obj[key] !== undefined) {