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
12 changes: 12 additions & 0 deletions .changeset/scaffold-agents-md-wired-dirs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'create-objectstack': patch
---

fix(create-objectstack): the scaffolded AGENTS.md lists only the `src/` directories the project's `objectstack.config.ts` loads

Clause-②: no
Metadata-protocol: none

The "Project Structure" block of the `AGENTS.md` a new project gets named `src/reports/`, `src/agents/`, `src/i18n/` and `src/handlers/`, which the scaffolded `objectstack.config.ts` never imports, so metadata a coding agent wrote there was never loaded and `npm run validate` still passed. It left out `src/skills/` and `src/picklists/`, which the config does load. The block now lists exactly the eight wired directories (objects, views, actions, flows, dashboards, apps, skills, picklists) and says that a new `src/` directory is never loaded or validated until its barrel is imported in `objectstack.config.ts`. What the config loads is unchanged.

An existing project keeps the `AGENTS.md` it was created with; to adopt the fix, edit its Project Structure block to match the directories its own `objectstack.config.ts` imports.
69 changes: 69 additions & 0 deletions packages/create-objectstack/src/agents-project-structure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
//
// The scaffolded AGENTS.md is the first thing a coding agent reads in a new
// project, and its "Project Structure" block tells it where each kind of
// metadata goes. A directory that `objectstack.config.ts` does not import is
// never loaded, and `objectstack validate` neither counts nor checks what it
// holds — so a block that names such a directory sends the agent's work
// somewhere it silently does nothing, and validate still passes. This pins the
// block's `src/` directories to the barrels each bundled template's config
// imports, both ways: no directory the config never loads, none it loads left
// out.

import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { TEMPLATES } from './template-registry.js';

const templatesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'templates');
const AGENTS_MD = fs.readFileSync(path.join(templatesDir, 'AGENTS.md'), 'utf8');

/** The `src/` directories the "Project Structure" tree names, in order. */
function projectStructureDirs(markdown: string): string[] {
const section = markdown.split(/^## Project Structure$/m)[1];
if (section === undefined) throw new Error('AGENTS.md has no "## Project Structure" section');
const tree = /```\n([\s\S]*?)\n```/.exec(section)?.[1];
if (tree === undefined) throw new Error('the Project Structure section has no fenced tree');
const lines = tree.split('\n');
const srcAt = lines.findIndex((l) => /[├└]── src\/\s*$/.test(l));
if (srcAt === -1) throw new Error('the Project Structure tree has no src/ entry');
const dirs: string[] = [];
for (const line of lines.slice(srcAt + 1)) {
const m = /^│\s+[├└]── ([a-z0-9_-]+)\/\s/.exec(line);
if (!m) break;
dirs.push(m[1]);
}
return dirs;
}

/** The `./src/<dir>` barrels a template's config imports, in order. */
function configBarrels(configSource: string): string[] {
return [...configSource.matchAll(/^import \* as \w+ from '\.\/src\/([a-z0-9_-]+)(?:\/index(?:\.js|\.ts)?)?';$/gm)].map((m) => m[1]);
}

const bundled = Object.entries(TEMPLATES).flatMap(([name, t]) => {
const config = path.join(templatesDir, t.source.dir, 'objectstack.config.ts');
return fs.existsSync(config) ? [{ name, config }] : [];
});

describe('the scaffolded AGENTS.md Project Structure names exactly the directories the config loads', () => {
it('reads at least one bundled template with a config (the pin is not vacuous)', () => {
expect(bundled.length).toBeGreaterThan(0);
expect(projectStructureDirs(AGENTS_MD).length).toBeGreaterThan(0);
});

for (const { name, config } of bundled) {
it(`template "${name}": the block's src/ directories are its config's barrel imports`, () => {
const barrels = configBarrels(fs.readFileSync(config, 'utf8'));
expect(barrels.length, `no ./src barrel import read from ${config}`).toBeGreaterThan(0);
expect([...projectStructureDirs(AGENTS_MD)].sort()).toEqual([...barrels].sort());
});
}

it('says a directory must be wired in objectstack.config.ts before it is loaded or validated', () => {
const section = AGENTS_MD.split(/^## Project Structure$/m)[1].split(/^## /m)[0];
expect(section).toContain('objectstack.config.ts');
expect(section).toMatch(/never loaded/);
});
});
15 changes: 9 additions & 6 deletions packages/create-objectstack/src/templates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,19 @@ Run `npm run build` when you need the compiled `dist/objectstack.json` artifact.
├── src/
│ ├── objects/ # Business object definitions (snake_case names)
│ ├── views/ # UI view definitions (list, form, kanban, calendar)
│ ├── apps/ # App navigation & page structure
│ ├── flows/ # Automation flows & workflows
│ ├── actions/ # Custom actions (buttons, bulk ops)
│ ├── flows/ # Automation flows & workflows
│ ├── dashboards/ # BI dashboards
│ ├── reports/ # Analytics reports
│ ├── agents/ # AI agent definitions
│ ├── i18n/ # Translation bundles
│ └── handlers/ # Runtime hook handlers
│ ├── apps/ # App navigation & page structure
│ ├── skills/ # AI skills
│ └── picklists/ # Shared option sets that select fields reuse
```

These are exactly the directories `objectstack.config.ts` wires. A new directory
under `src/` is never loaded, and `npm run validate` neither counts nor checks
what it holds, until its `index.ts` barrel is imported in `objectstack.config.ts`
and handed to its stack key there.

## AI Skills

This project uses the ObjectStack skills catalog from the `@objectstack/skills`
Expand Down
Loading