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/framework-yml-event.md
Original file line number Diff line number Diff line change
@@ -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`.
6 changes: 5 additions & 1 deletion packages/framework/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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)', () => {
Expand Down
24 changes: 14 additions & 10 deletions packages/framework/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> 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
Expand Down Expand Up @@ -329,23 +330,26 @@ export function activeModes(opts: Pick<CliOptions, 'autopilot' | 'technical'>):
* (a flag can only *enable* a mode, so there is nothing to override the other way).
*/
export function mergeRunConfig(
opts: Pick<CliOptions, 'preset' | 'autopilot' | 'technical'>,
opts: Pick<CliOptions, 'preset' | 'autopilot' | 'technical' | 'buildEvent'>,
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<CliOptions, 'preset'>, file: FrameworkFileConfig): string {
function describeConfigSource(opts: Pick<CliOptions, 'preset' | 'buildEvent'>, 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(', ')
}

Expand Down Expand Up @@ -432,8 +436,8 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise<num
if (modes.length && !domainPreset) {
io.err(`note: ${modes.join(' + ')} mode(s) have no effect without a preset.`)
}
if (opts.buildEvent && !domainPreset) {
io.err(`note: --kind ${opts.buildEvent} has no effect without a preset.`)
if (merged.buildEvent && !domainPreset) {
io.err(`note: build event "${merged.buildEvent}" has no effect without a preset.`)
}

// Fail early and clearly if a live run's prerequisites are missing.
Expand Down Expand Up @@ -554,7 +558,7 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise<num
...(discovered ? { extensions: discovered } : {}),
...(opts.composeExtensions ? { composeExtensions: true } : {}),
...(domainPreset ? { preset: domainPreset, ...(modes.length ? { modes } : {}) } : {}),
...(opts.buildEvent ? { buildEvent: opts.buildEvent } : {}),
...(merged.buildEvent ? { buildEvent: merged.buildEvent } : {}),
...(memory.length ? { memory } : {}),
...((): { sessionLink?: string } => {
const link = chooseSessionLink(opts, fake)
Expand Down
17 changes: 11 additions & 6 deletions packages/framework/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}', () => {
Expand All @@ -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/)
})

Expand Down
10 changes: 7 additions & 3 deletions packages/framework/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -59,9 +61,11 @@ export function parseFrameworkConfig(raw: string, source = 'the-framework.yml'):
}
const obj = data as Record<string, unknown>
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) {
Expand Down
Loading