diff --git a/.changeset/open-loop-software-development-preset.md b/.changeset/open-loop-software-development-preset.md new file mode 100644 index 0000000..cbbf1d9 --- /dev/null +++ b/.changeset/open-loop-software-development-preset.md @@ -0,0 +1,12 @@ +--- +'@gemstack/ai-autopilot': minor +--- + +Ship the "Software Development" domain preset (#243). + +The first built-in Open Loop preset, authored as a directory of `.md` files: +two loops (major-change -> code-review + test-coverage + security-review; bug-fix +-> root-cause + regression-test), five stack-agnostic prompt bodies, and one skill +pointer. Non-web and user-picked (no dependency detection). Load it with +`softwareDevelopmentPreset()`; `builtinPresetsDir()` points at the shipped +`presets/` directory. Proves the bundle unit end to end. diff --git a/packages/ai-autopilot/package.json b/packages/ai-autopilot/package.json index cbf516f..de11adc 100644 --- a/packages/ai-autopilot/package.json +++ b/packages/ai-autopilot/package.json @@ -29,7 +29,8 @@ }, "files": [ "dist", - "prompts" + "prompts", + "presets" ], "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/ai-autopilot/presets/software-development/loops/bug-fix.md b/packages/ai-autopilot/presets/software-development/loops/bug-fix.md new file mode 100644 index 0000000..f6aab6a --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/loops/bug-fix.md @@ -0,0 +1,10 @@ +--- +name: bug-fix-loop +description: What fires after a bug fix. +metadata: + on: bug-fix + run: [root-cause, regression-test] +--- + +When the agent fixes a bug, confirm the root cause is understood and addressed +(not just the symptom), then lock it in with a regression test. diff --git a/packages/ai-autopilot/presets/software-development/loops/major-change.md b/packages/ai-autopilot/presets/software-development/loops/major-change.md new file mode 100644 index 0000000..a55afcb --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/loops/major-change.md @@ -0,0 +1,10 @@ +--- +name: major-change-loop +description: What fires after a substantial code change. +metadata: + on: major-change + run: [code-review, test-coverage, security-review] +--- + +When the agent lands a substantial change, review it, check it is covered by +tests, then look for security regressions — in that order. diff --git a/packages/ai-autopilot/presets/software-development/preset.md b/packages/ai-autopilot/presets/software-development/preset.md new file mode 100644 index 0000000..d38d131 --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/preset.md @@ -0,0 +1,10 @@ +--- +name: software-development +description: A stack-agnostic engineering loop — review, tests, and security on every substantial change, root-cause and regression coverage on every fix. +metadata: + title: Software Development +--- + +The general software engineering domain. Not tied to a web framework: it runs the +same review / test / security discipline over any codebase. Pick it when you want +solid engineering hygiene without a stack-specific preset. diff --git a/packages/ai-autopilot/presets/software-development/prompts/code-review.md b/packages/ai-autopilot/presets/software-development/prompts/code-review.md new file mode 100644 index 0000000..52238e7 --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/prompts/code-review.md @@ -0,0 +1,21 @@ +--- +name: code-review +description: Review a change for correctness, clarity, and design. +appliesTo: ["**/*"] +metadata: + title: Code review + loopId: code-review + passes: 1 + event: major-change +--- + +You are reviewing a substantial change. Read the diff and the code around it, then +report the issues that actually matter — correctness first, then clarity and design. + +Focus on: +- **Correctness** — logic errors, unhandled cases, off-by-one, race conditions, wrong assumptions. +- **Clarity** — names, control flow, and comments a maintainer will thank or curse you for. +- **Design** — does the change fit the surrounding code, or does it fork a second way to do one thing? + +For each finding give one line on what is wrong and one line on the fix. Skip nits +the linter already catches. If the change is sound, say so plainly and stop. diff --git a/packages/ai-autopilot/presets/software-development/prompts/regression-test.md b/packages/ai-autopilot/presets/software-development/prompts/regression-test.md new file mode 100644 index 0000000..6e46838 --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/prompts/regression-test.md @@ -0,0 +1,21 @@ +--- +name: regression-test +description: Ensure a fix is locked in by a test that fails without it. +appliesTo: ["**/*"] +metadata: + title: Regression test + loopId: regression-test + passes: 1 + event: bug-fix +--- + +You are making sure a bug fix is locked in by a regression test — one that fails on +the old code and passes on the new. + +Check: +- Is there a test that reproduces the original bug and would fail without this fix? +- Does it assert the corrected behavior, not just the absence of the crash? +- Is it placed and named so a future reader knows which bug it guards? + +If the regression test is missing or weak, describe the exact case it should cover. +If it is already present and meaningful, confirm it and stop. diff --git a/packages/ai-autopilot/presets/software-development/prompts/root-cause.md b/packages/ai-autopilot/presets/software-development/prompts/root-cause.md new file mode 100644 index 0000000..f22638a --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/prompts/root-cause.md @@ -0,0 +1,22 @@ +--- +name: root-cause +description: Confirm a fix addresses the root cause, not the symptom. +appliesTo: ["**/*"] +metadata: + title: Root cause + loopId: root-cause + passes: 1 + event: bug-fix +--- + +You are checking that a bug fix addresses the **root cause**, not just the symptom +that was reported. + +Work backwards from the fix: +- What was the actual defect, and why did it produce the observed symptom? +- Does the fix remove the defect, or does it mask it (a guard, a retry, a catch that swallows)? +- Could the same root cause surface elsewhere in the codebase through a different path? + +State the root cause in one sentence. If the fix only treats the symptom, say what +the real fix is. If it correctly removes the cause, confirm it and note any sibling +sites worth the same treatment. diff --git a/packages/ai-autopilot/presets/software-development/prompts/security-review.md b/packages/ai-autopilot/presets/software-development/prompts/security-review.md new file mode 100644 index 0000000..68ba3eb --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/prompts/security-review.md @@ -0,0 +1,22 @@ +--- +name: security-review +description: Look for security regressions introduced by the change. +appliesTo: ["**/*"] +metadata: + title: Security review + loopId: security-review + passes: 1 + event: major-change +--- + +You are checking a change for security regressions. Scope the review to what the +change touches; do not audit the whole codebase. + +Look for: +- Untrusted input reaching a sink (injection, path traversal, deserialization). +- Authn/authz gaps — a route, action, or resource that skips a check the neighbors make. +- Secrets, tokens, or PII in code, logs, or error messages. +- Unsafe defaults and dependencies added with known advisories. + +Report each concrete risk with the file, why it is exploitable, and the fix. If the +change introduces no new exposure, say so and stop. diff --git a/packages/ai-autopilot/presets/software-development/prompts/test-coverage.md b/packages/ai-autopilot/presets/software-development/prompts/test-coverage.md new file mode 100644 index 0000000..225c1de --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/prompts/test-coverage.md @@ -0,0 +1,21 @@ +--- +name: test-coverage +description: Check the change is covered by meaningful tests. +appliesTo: ["**/*"] +metadata: + title: Test coverage + loopId: test-coverage + passes: 1 + event: major-change +--- + +You are checking that a substantial change is covered by tests that would actually +catch a regression — not coverage for its own sake. + +Ask: +- Does every new branch and edge case have a test that fails if the behavior breaks? +- Are the tests asserting behavior, or just that the code ran without throwing? +- Is anything important only exercised by a happy-path test? + +Name the specific untested paths and, for each, the one test worth adding. If the +change is a pure refactor with existing coverage, say so and stop. diff --git a/packages/ai-autopilot/presets/software-development/skills/eng-practices.md b/packages/ai-autopilot/presets/software-development/skills/eng-practices.md new file mode 100644 index 0000000..52e7b9a --- /dev/null +++ b/packages/ai-autopilot/presets/software-development/skills/eng-practices.md @@ -0,0 +1,11 @@ +--- +name: eng-practices +description: Google's engineering practices — code review and change-authoring guidelines. +metadata: + title: Engineering Practices + url: https://google.github.io/eng-practices/ +--- + +A stack-agnostic reference for what a good change and a good review look like: +the reviewer standard, what to look for, and how to write a change that is easy to +review. Consult it when framing review and authoring work. diff --git a/packages/ai-autopilot/src/index.ts b/packages/ai-autopilot/src/index.ts index eafde8d..c44948f 100644 --- a/packages/ai-autopilot/src/index.ts +++ b/packages/ai-autopilot/src/index.ts @@ -103,6 +103,7 @@ * - {@link defineDomainPreset} / {@link loadDomainPreset} — author, or load from a directory * - {@link composeDomainPresets} — merge presets into one (later wins on prompt/skill id) * - {@link selectPreset} — pick the user's chosen domain by name + * - {@link softwareDevelopmentPreset} — the shipped, stack-agnostic built-in */ export { Supervisor } from './supervisor.js' export { agentPlanner, type AgentPlannerOptions } from './planner.js' @@ -336,6 +337,8 @@ export { loadDomainPreset, loadLoopsFrom, loadSkillsFrom, + builtinPresetsDir, + softwareDevelopmentPreset, type DomainPreset, type DomainPresetSpec, type DomainPresetMeta, diff --git a/packages/ai-autopilot/src/preset/index.ts b/packages/ai-autopilot/src/preset/index.ts index e07aa1f..e597810 100644 --- a/packages/ai-autopilot/src/preset/index.ts +++ b/packages/ai-autopilot/src/preset/index.ts @@ -9,5 +9,11 @@ */ export { defineDomainPreset, DomainPresetError } from './define.js' export { composeDomainPresets, selectPreset } from './compose.js' -export { loadDomainPreset, loadLoopsFrom, loadSkillsFrom } from './load.js' +export { + loadDomainPreset, + loadLoopsFrom, + loadSkillsFrom, + builtinPresetsDir, + softwareDevelopmentPreset, +} from './load.js' export type { DomainPreset, DomainPresetSpec, DomainPresetMeta } from './types.js' diff --git a/packages/ai-autopilot/src/preset/load.ts b/packages/ai-autopilot/src/preset/load.ts index 3e06ae7..a59076b 100644 --- a/packages/ai-autopilot/src/preset/load.ts +++ b/packages/ai-autopilot/src/preset/load.ts @@ -1,4 +1,5 @@ import { readFile, readdir } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' import { join } from 'node:path' import { parseSkillManifest } from '@gemstack/ai-skills' import { defineLoop } from '../loop/define.js' @@ -51,6 +52,17 @@ export async function loadDomainPreset(dir: string): Promise { }) } +/** Absolute path to the package's shipped `presets/` directory. */ +export function builtinPresetsDir(): string { + // From dist/preset/load.js (and dist-test/…), the package root is two up. + return fileURLToPath(new URL('../../presets/', import.meta.url)) +} + +/** The shipped, stack-agnostic "Software Development" domain preset (#243). */ +export function softwareDevelopmentPreset(): Promise { + return loadDomainPreset(join(builtinPresetsDir(), 'software-development')) +} + /** Load every `*.md` loop file in a directory (a missing directory yields `[]`). */ export async function loadLoopsFrom(dir: string): Promise { const files = await mdFiles(dir) diff --git a/packages/ai-autopilot/src/preset/software-development.test.ts b/packages/ai-autopilot/src/preset/software-development.test.ts new file mode 100644 index 0000000..11b8b6c --- /dev/null +++ b/packages/ai-autopilot/src/preset/software-development.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { softwareDevelopmentPreset } from './load.js' + +describe('softwareDevelopmentPreset (shipped built-in)', () => { + it('loads the {loops, prompts, skills} bundle from the shipped directory', async () => { + const preset = await softwareDevelopmentPreset() + assert.equal(preset.name, 'software-development') + assert.equal(preset.title, 'Software Development') + assert.ok(preset.description.length > 0) + + assert.equal(preset.loops.length, 2) + assert.ok(preset.prompts.length >= 5) + assert.equal(preset.skills.length, 1) + assert.equal(preset.skills[0]!.url, 'https://google.github.io/eng-practices/') + }) + + it('every id a loop dispatches resolves to a shipped prompt body', async () => { + const preset = await softwareDevelopmentPreset() + const ids = new Set(preset.prompts.map(p => p.id)) + for (const loop of preset.loops) { + for (const id of loop.run) { + assert.ok(ids.has(id), `loop prompt "${id}" has a shipped body`) + assert.ok(preset.prompts.find(p => p.id === id)!.instructions.length > 0, `"${id}" body is non-empty`) + } + } + }) + + it('targets non-web events (major-change, bug-fix)', async () => { + const preset = await softwareDevelopmentPreset() + const kinds = preset.loops.flatMap(l => [...l.on]).sort() + assert.deepEqual(kinds, ['bug-fix', 'major-change']) + }) +})