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/bundled-builder-code-starter-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Bundle a `builder-code-starter` scaffold template as a layer over the Chat template: `agent-native template materialize --template builder-code-starter` copies Chat, removes the Chat shell, applies the starter's unified-diff patches to the Chat files it changes, adds its own files, and merges its Drizzle scripts and dependencies into Chat's `package.json`. A bundled template opts into layering with a `template-layer.json` naming its base; a patch that no longer applies fails the materialize instead of dropping the change. `skills update scaffold` assembles the starter's patched skills the same way for apps marked `scaffold.template: "builder-code-starter"`.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
build: ${{ steps.scope.outputs.build }}
trusted_acceptance: ${{ steps.scope.outputs.trusted_acceptance }}
scaffold: ${{ steps.scope.outputs.scaffold }}
builder_code_starter_scaffold: ${{ steps.scope.outputs.builder_code_starter_scaffold }}
ssr_boot: ${{ steps.scope.outputs.ssr_boot }}
ssr_boot_apps: ${{ steps.scope.outputs.ssr_boot_apps }}
guards: ${{ steps.scope.outputs.guards }}
Expand Down Expand Up @@ -1078,6 +1079,17 @@ jobs:
src/cli/create-e2e.spec.ts
-t "pnpm install, pnpm typecheck, and pnpm action hello all succeed"

- name: Builder Code starter — materialize + pnpm install + typecheck
if: needs.change-scope.outputs.builder_code_starter_scaffold == 'true'
env:
AGENT_NATIVE_CREATE_USE_LOCAL_CORE: "1"
run: |
out="$(mktemp -d)/app"
node "$GITHUB_WORKSPACE/packages/core/dist/cli/index.js" template materialize --template builder-code-starter --name app --out "$out"
cd "$out"
pnpm install
pnpm typecheck

- name: Scaffold workspace (chat + calendar + dispatch) and verify pnpm install + build
env:
AGENT_NATIVE_CREATE_USE_LOCAL_CORE: "1"
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/push-starter-template.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: Push chat template to starter
name: Push Builder Code starter template to starter

on:
push:
branches: [main]
paths:
- "templates/chat/**"
- "packages/core/src/templates/builder-code-starter/**"
- ".github/workflows/push-starter-template.yml"
repository_dispatch:
types: [agent-native-packages-published]
Expand Down Expand Up @@ -69,11 +70,14 @@ jobs:
- if: steps.gate.outputs.publishable == 'true'
run: pnpm install --frozen-lockfile

- name: Materialize post-processed templates/chat
# builder-code-starter is a layer over templates/chat: materialize copies Chat,
# then applies the starter's deletions, unified-diff patches, new files,
# and package.json fields.
- name: Materialize the bundled builder-code-starter template
if: steps.gate.outputs.publishable == 'true'
run: >-
pnpm exec tsx packages/core/src/cli/index.ts template materialize
--template chat --out ./dist/template
--template builder-code-starter --name app --out ./dist/template

- name: Pin toolkit to the published core's compatible range
if: steps.gate.outputs.publishable == 'true'
Expand Down Expand Up @@ -149,5 +153,5 @@ jobs:
echo "template branch already up to date."
exit 0
fi
git commit -m "chore: sync template from templates/chat@${SRC_SHA}"
git commit -m "chore: sync template from builder-code-starter@${SRC_SHA}"
git push origin template
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ instructions, and application state.
improvement, behavior-affecting fix)? Record it from that app with
`agent-native changelog add "<one sentence>" --type <added|improved|fixed>`.
Skip refactors, tooling, and tests. See the `changelog` skill.
- The Builder Code starter (`packages/core/src/templates/builder-code-starter`) is patches
over `templates/chat`. After editing Chat, run `pnpm guard:template-layers`.
If a patch fails, run `pnpm template-layer rebase builder-code-starter --out
.tmp/fs`, resolve conflicts keeping the starter's intent, then
`pnpm template-layer diff builder-code-starter --from .tmp/fs`. Never delete a
failing patch.

## Extensions

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@
"e2e:signup": "playwright test --config e2e/signup/playwright.config.ts",
"typecheck:e2e": "tsc --noEmit -p e2e/beta/tsconfig.json",
"typecheck:e2e:signup": "tsc --noEmit -p e2e/signup/tsconfig.json",
"guard:beta-e2e-suite": "tsx scripts/guard-beta-e2e-suite.ts"
"guard:beta-e2e-suite": "tsx scripts/guard-beta-e2e-suite.ts",
"template-layer": "tsx scripts/template-layer.ts",
"guard:template-layers": "tsx scripts/template-layer.ts check"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a verification script that agents can use to verify that changes to Chat app don't break any of the diffs built on top of them

},
"devDependencies": {
"@changesets/cli": "2.31.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"src/templates/default",
"src/templates/headless",
"src/templates/chat",
"src/templates/builder-code-starter",
"src/templates/workspace-core",
"src/templates/workspace-root",
"src/templates/factory"
Expand Down Expand Up @@ -668,6 +669,7 @@
"ajv": "^8.20.0",
"better-auth": "1.7.6",
"cron-parser": "^5.5.0",
"diff": "^9.0.0",
"diff-match-patch": "^1.0.5",
"dotenv": "^17.2.1",
"drizzle-orm": "^0.45.2",
Expand Down
34 changes: 31 additions & 3 deletions packages/core/src/cli/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "../shared/workspace-app-id.js";
import type { CreateStartKind } from "./create-tui.js";
import { setupAgentSymlinks } from "./setup-agents.js";
import { applyTemplateLayer, readTemplateLayer } from "./template-layer.js";
import {
coreTemplates,
getTemplate,
Expand Down Expand Up @@ -1447,7 +1448,7 @@ async function scaffoldAppTemplate(
const sourceTemplate = templateSourceName(resolved);
const localTemplate = findLocalTemplate(sourceTemplate);
if (localTemplate) {
copyDir(localTemplate, targetDir);
copyTemplateTree(localTemplate, targetDir);
Comment thread
samijaber marked this conversation as resolved.
removeWorkspaceOnlyTemplateWiring(targetDir);
return {
templateSource: localTemplateSourceKind(localTemplate),
Expand Down Expand Up @@ -1481,6 +1482,30 @@ function removeWorkspaceOnlyTemplateWiring(appDir: string): void {
if (changed) fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
}

/** Copies a bundled template's source tree, resolving template layers. */
function copyTemplateTree(templateDir: string, dest: string): void {
const layer = readTemplateLayer(templateDir);
if (!layer) {
copyDir(templateDir, dest);
return;
}
const base = findLocalTemplate(layer.base);
if (!base) {
throw new Error(
`No local copy of "${layer.base}", the base of ${templateDir}.`,
);
}
copyTemplateTree(base, dest);
applyTemplateLayer(templateDir, layer, dest);
}

// A bundled layer (template-layer.json) installs its base template's
// dependencies, so it needs the same workspace overrides as that base.
function firstPartyBaseTemplate(templateName: string): string {
const local = findLocalTemplate(templateName);
Comment thread
samijaber marked this conversation as resolved.
return (local && readTemplateLayer(local)?.base) || templateName;
}

function localTemplateSourceKind(
localTemplate: string,
): "bundled" | "local-checkout" {
Expand Down Expand Up @@ -2260,7 +2285,7 @@ function postProcessStandalone(
nf3: '"0.3.17"',
};
}
if (templateName && getTemplate(templateName)) {
if (templateName && getTemplate(firstPartyBaseTemplate(templateName))) {
sections.overrides = {
...sections.overrides,
...TIPTAP_WORKSPACE_OVERRIDES,
Expand Down Expand Up @@ -2552,6 +2577,7 @@ export {
ensureScaffoldEmailBrandingConfig as _ensureScaffoldEmailBrandingConfig,
fixWebManifestName as _fixWebManifestName,
copyDir as _copyDir,
copyTemplateTree as _copyTemplateTree,
localTemplateSourceKind as _localTemplateSourceKind,
REPO as _REPO,
TEMPLATES_DIR as _TEMPLATES_DIR,
Expand Down Expand Up @@ -3956,7 +3982,9 @@ function scaffoldGuidanceForTemplate(
if (!templateName || templateName.startsWith("github:")) return undefined;
const normalized = normalizeTemplateName(templateName);
if (normalized === "headless") return "headless";
return getTemplate(normalized) ? "default" : undefined;
return getTemplate(firstPartyBaseTemplate(normalized))
? "default"
: undefined;
}

function fixWebManifestName(
Expand Down
87 changes: 86 additions & 1 deletion packages/core/src/cli/skills.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
runSkills,
VISUAL_RECAP_SKILL_MD,
} from "./skills.js";
import { CHAT_STARTER_SKILLS } from "./workspace-skill-policy.js";
import {
CHAT_STARTER_SKILLS,
BUILDER_CODE_STARTER_SKILLS,
} from "./workspace-skill-policy.js";
import { WORKSPACE_SKILLS } from "./workspace-skill-policy.js";

const tmpRoots: string[] = [];
Expand Down Expand Up @@ -3331,6 +3334,13 @@ describe("agent-native skills", () => {
refreshedSkill: "review-prs",
omittedSkill: "build-an-app",
},
{
template: "builder-code-starter",
frameworkSkills: "default",
sourceTemplates: ["builder-code-starter", "chat"],
refreshedSkill: "authentication",
omittedSkill: "turn-into-app",
},
])(
"refreshes only installed skills in the $template scaffold",
async ({
Expand Down Expand Up @@ -3438,6 +3448,81 @@ describe("agent-native skills", () => {
},
);

it("prefers builder-code-starter skill overrides and inherits the rest from Chat", async () => {
const root = tmpDir();
const targetSkills = path.join(root, ".agents", "skills");
const bundled = path.join(workspaceRoot(), "packages/core/src/templates");
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify(
{
name: "app",
dependencies: { "@agent-native/core": "latest" },
"agent-native": {
scaffold: {
template: "builder-code-starter",
frameworkSkills: "default",
},
},
},
null,
2,
),
);
for (const skill of [
"storing-data",
"security",
"multi-app-workspace",
"turn-into-app",
"self-modifying-code",
]) {
fs.mkdirSync(path.join(targetSkills, skill), { recursive: true });
fs.writeFileSync(
path.join(targetSkills, skill, "SKILL.md"),
`old ${skill}\n`,
);
}
vi.spyOn(process.stdout, "write").mockImplementation(() => true);

await runSkills(["update", "scaffold", "--scope", "project", "--json"], {
baseDir: root,
runCommand: async () => 0,
});

const read = (dir: string, skill: string) =>
fs.readFileSync(path.join(dir, skill, "SKILL.md"), "utf8");
const builderCodeSkills = path.join(
bundled,
"builder-code-starter/.agents/skills",
);
const chatSkills = path.join(bundled, "chat/.agents/skills");
// The layer stores storing-data as a patch over Chat's copy.
expect(read(targetSkills, "storing-data")).toContain(
"managed Drizzle scaffold is the only app migration path",
);
expect(read(chatSkills, "storing-data")).not.toContain(
"managed Drizzle scaffold is the only app migration path",
);
expect(read(targetSkills, "security")).toBe(read(chatSkills, "security"));
expect(read(targetSkills, "multi-app-workspace")).toBe(
read(builderCodeSkills, "multi-app-workspace"),
);
// Skills outside the policy stay app-owned and untouched.
expect(read(targetSkills, "turn-into-app")).toBe("old turn-into-app\n");
expect(read(targetSkills, "self-modifying-code")).toBe(
"old self-modifying-code\n",
);
expect(fs.existsSync(path.join(targetSkills, "performance"))).toBe(false);
for (const excluded of [
"turn-into-app",
"turn-into-skill",
"workspace-conventions",
"self-modifying-code",
]) {
expect(BUILDER_CODE_STARTER_SKILLS).not.toContain(excluded);
}
});

it("refreshes existing Chat scaffold skills without adding omitted skills", async () => {
const root = tmpDir();
fs.mkdirSync(path.join(root, "app", "routes"), { recursive: true });
Expand Down
36 changes: 32 additions & 4 deletions packages/core/src/cli/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
WIREFRAME_REFERENCE_MD,
} from "./skills-content/index.js";
import { createCliTelemetry, type CliTelemetry } from "./telemetry.js";
import { applyTemplateLayer, readTemplateLayer } from "./template-layer.js";
import { allTemplateNames } from "./templates-meta.js";
import {
CLIPS_TEMPLATE_SHARED_SKILLS,
Expand All @@ -73,6 +74,7 @@ import {
DISPATCH_TEMPLATE_SHARED_SKILLS,
DOMAIN_TEMPLATE_SHARED_SKILLS,
FACTORY_TEMPLATE_SHARED_SKILLS,
BUILDER_CODE_STARTER_SKILLS,
HEADLESS_TEMPLATE_SHARED_SKILLS,
WORKSPACE_SKILLS,
} from "./workspace-skill-policy.js";
Expand Down Expand Up @@ -787,7 +789,12 @@ interface SkillInstallState {
interface ScaffoldGuidanceState {
kind: "workspace-core" | "standalone";
displayName: string;
templateName: "workspace-core" | "headless" | "default" | "chat";
templateName:
| "workspace-core"
| "headless"
| "default"
| "chat"
| "builder-code-starter";
path: string;
sourcePath: string;
additionalSourcePaths?: string[];
Expand Down Expand Up @@ -1776,15 +1783,29 @@ function corePackageRootDir(): string {
return path.resolve(here, "../..");
}

const layeredScaffoldSkillsDirs = new Map<string, string>();

function bundledScaffoldSkillsDir(templateName: string): string {
return path.join(
const templateDir = path.join(
corePackageRootDir(),
"src",
"templates",
templateName,
".agents",
"skills",
);
const layer = readTemplateLayer(templateDir);
if (!layer) return path.join(templateDir, ".agents", "skills");
// A layer stores its skills as patches over the base's, so the copies
// `skills update` installs have to be assembled first.
const cached = layeredScaffoldSkillsDirs.get(templateName);
if (cached) return cached;
const root = fs.mkdtempSync(path.join(os.tmpdir(), "an-layer-skills-"));
const skillsDir = path.join(root, ".agents", "skills");
fs.cpSync(bundledScaffoldSkillsDir(layer.base), skillsDir, {
recursive: true,
});
applyTemplateLayer(templateDir, layer, root, { only: ".agents/skills" });
layeredScaffoldSkillsDirs.set(templateName, skillsDir);
return skillsDir;
}

function readJsonRecord(file: string): Record<string, unknown> | undefined {
Expand Down Expand Up @@ -1881,6 +1902,13 @@ function markedScaffoldGuidanceTemplate(
skills: CHAT_STARTER_SKILLS,
};
}
if (templateName === "builder-code-starter") {
return {
templateName,
sourceTemplate: "builder-code-starter",
skills: BUILDER_CODE_STARTER_SKILLS,
};
}
if (templateName === "dispatch") {
return {
templateName,
Expand Down
Loading
Loading