From dfb61da7364d04f11d2f3e6a0e6dc712924be8f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 09:35:53 +0000 Subject: [PATCH 1/2] fix(cli): retire the `agent` generator, and make the refusal name skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os g agent` scaffolded into `src/agents`, a surface ADR-0063 §2 withdrew. The kernel ships exactly two agents (`ask`, `build`) and the runtime catalog filters out every other agent record, so the generated file passed `os validate`, published without complaint, and then never appeared — no error at any step. That is the silent-strip failure mode, arriving through the scaffolder. Deleting the roster entry alone would have moved the silence one step earlier rather than ending it: `Unknown type: agent` plus the surviving roster tells the author their spelling is not on the list, and the natural next move is to hunt for the right spelling of something that no longer exists. So `agent` moves to a `RETIRED_GENERATORS` ledger whose entries owe both halves — the decision that withdrew the surface, and the surface to author instead. The refusal names ADR-0063, the two platform agents, the silent strip, and skills (`src/skills/.skill.ts`, `defineSkill`) as the live extension primitive, and it exits 1 so a CI script that still calls the command stops. Advertising sites swept, not fixed by line number: the generator entry and the `type` argument's roster string in `generate.ts`, `packages/cli/README.md`'s "Available generate types" line, and three sites in `content/docs/deployment/cli.mdx` (the `os g agent support` example, the `agent` / `src/agents/` row of the Available types table, and `os g agent sales-assistant` in Typical Workflow — the last of which the card did not name). `content/docs/getting-started/quick-start.mdx`'s project-layout map advertised `src/agents` as the AI area's authoring directory; it now names `src/skills`, which is what ADR-0063 says an app author writes. `scripts/adr-anchors/packages__cli__src__commands__generate.ts.json` pins ADR-0063 to the file, so the absence of an `agent` generator reads as a decision rather than an oversight to the next author. `os g skill` is deliberately NOT in this change — see the follow-up card. The message says the scaffolder does not exist yet rather than promising it. Part of #10359 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r --- content/docs/deployment/cli.mdx | 17 +- content/docs/getting-started/quick-start.mdx | 2 +- packages/cli/README.md | 8 +- packages/cli/src/commands/generate.ts | 93 ++++++++--- .../test/generate-agent-retired.e2e.test.ts | 150 ++++++++++++++++++ ...ages__cli__src__commands__generate.ts.json | 7 + 6 files changed, 246 insertions(+), 31 deletions(-) create mode 100644 packages/cli/test/generate-agent-retired.e2e.test.ts create mode 100644 scripts/adr-anchors/packages__cli__src__commands__generate.ts.json diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index b2db97142e..8e12d67573 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -1027,7 +1027,6 @@ os g object customer # Generate a Customer object os g view customer # Generate a Customer list view os g action approve # Generate an action os g flow customer # Generate an automation flow -os g agent support # Generate an AI agent os g dashboard sales # Generate a dashboard os g app crm # Generate an app definition @@ -1043,10 +1042,23 @@ os g object task --dry-run # Preview without writing | `view` | `src/views/` | List or form view definition | | `action` | `src/actions/` | Button or batch action | | `flow` | `src/flows/` | Automation flow | -| `agent` | `src/agents/` | AI agent | | `dashboard` | `src/dashboards/` | Analytics dashboard | | `app` | `src/apps/` | Application navigation | + +There is no `agent` type. Running `os g agent ` fails with a message +naming [ADR-0063](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0063-two-kernel-agents-skills-are-the-extension-primitive.md) +and pointing at skills, rather than the generic "unknown type" listing. + +Agents are **platform-internal**: the kernel ships exactly two (`ask` and +`build`), and the runtime catalog filters out every other agent record. A +scaffolded `src/agents/*.ts` therefore passed `os validate`, published without +complaint, and never appeared — silently. Skills (plus tools / MCP) are the +third-party extension primitive, authored as `src/skills/.skill.ts` with +`defineSkill`; see [AI Agents](/docs/ai/agents). There is no `os g skill` +scaffolder yet, so write that file by hand for now. + + **Options:** - `-d, --dir ` — Override target directory - `--dry-run` — Preview without writing files @@ -1583,7 +1595,6 @@ os g object opportunity # 3. Add business logic os g flow lead-qualification -os g agent sales-assistant # 4. Validate everything os validate diff --git a/content/docs/getting-started/quick-start.mdx b/content/docs/getting-started/quick-start.mdx index 16c09fbb9d..612137850b 100644 --- a/content/docs/getting-started/quick-start.mdx +++ b/content/docs/getting-started/quick-start.mdx @@ -37,7 +37,7 @@ a project: | **Automation** | Flows, workflows, triggers, approvals (`src/flows`) | Event handlers, approval chains, scheduled jobs | | **Interface** | Views, apps, dashboards, actions (`src/views`, `src/apps`) | The Console UI, navigation, responsive layout | | **Access** | Roles, permissions, sharing, row-level security | Middleware, RLS policies, field masking | -| **AI** | Agents, tools, RAG, MCP exposure (`src/agents`) | Chat, search indexes, an MCP server | +| **AI** | Skills, tools, RAG, MCP exposure (`src/skills`) | Chat, search indexes, an MCP server | The through-line: **you author intent once as metadata, and the runtime derives the database, the API, the UI, and the AI tool surface from it.** That's why an agent diff --git a/packages/cli/README.md b/packages/cli/README.md index 5c1a68f934..8b88656e12 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -59,7 +59,13 @@ os compile | `os generate ` | Generate metadata files (alias: `os g`) | | `os create [name]` | Create a new package/plugin/example from template | -Available generate types: `object`, `view`, `action`, `flow`, `agent`, `dashboard`, `app` +Available generate types: `object`, `view`, `action`, `flow`, `dashboard`, `app` + +`agent` is **retired** (ADR-0063 §2): agents are platform-internal, so a scaffolded +`src/agents/*.ts` validated, published and was then filtered out of the runtime +catalog without a word. `os g agent` now says so and points at skills — the +third-party extension primitive — which are authored as `src/skills/.skill.ts` +with `defineSkill`. There is no `os g skill` scaffolder yet. ### Cloud — publish & install diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index 661c5c82a8..573e891744 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -4,7 +4,7 @@ import { Args, Command, Flags } from '@oclif/core'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; -import { printHeader, printSuccess, printError, printInfo, printStep, createTimer } from '../utils/format.js'; +import { printHeader, printSuccess, printError, printInfo, printStep, createTimer, CLI_ALIAS } from '../utils/format.js'; // ─── Metadata Type Templates ──────────────────────────────────────── @@ -125,30 +125,6 @@ export default ${toCamelCase(name)}Flow; `, }, - agent: { - description: 'AI agent', - defaultDir: 'src/agents', - generate: (name: string) => `import * as AI from '@objectstack/spec/ai'; - -/** - * ${toTitleCase(name)} Agent - */ -const ${toCamelCase(name)}Agent: AI.Agent = { - name: '${toSnakeCase(name)}_agent', - label: '${toTitleCase(name)} Agent', - role: '${toTitleCase(name)} assistant', - instructions: 'You are a helpful ${toTitleCase(name).toLowerCase()} assistant.', - model: { - provider: 'openai', - model: 'gpt-4o', - }, - tools: [], -}; - -export default ${toCamelCase(name)}Agent; -`, - }, - dashboard: { description: 'Analytics dashboard', defaultDir: 'src/dashboards', @@ -189,6 +165,58 @@ export default ${toCamelCase(name)}App; }, }; +// ─── Retired Generators ───────────────────────────────────────────── + +/** + * Scaffolder types that were withdrawn, and what this command says when one + * of them is run. + * + * A retired type is NOT an unknown type, and deliberately does not fall + * through to the `Unknown type:` branch in {@link runMetadataGeneration}. + * That branch prints the surviving roster and nothing else, so an author + * arriving from a doc page, a tutorial or a CI script that still names the + * retired type would learn only that their spelling is not on the list — + * and the natural next move is to hunt for the right spelling of something + * that no longer exists. + * + * `agent` (ADR-0063 §2, which reversed ADR-0040 §3): the kernel ships exactly + * two agents, `ask` and `build`, bound by surface and never picked from a + * roster. Tenant / app-package agents were withdrawn, and the runtime catalog + * filters out every non-platform agent record. The file this generator wrote + * into `src/agents/` therefore passed `os validate`, published without + * complaint, and was then dropped on the floor: no error at any step, the + * agent simply never appeared. Retiring the command silently would have moved + * that silence one step earlier instead of ending it, which is why each entry + * owes both halves — the decision that withdrew the surface, and the surface + * to author instead. + */ +const RETIRED_GENERATORS: Record` was retired — …". */ + reason: string; + /** Body lines, printed in order; an empty string prints a blank line. */ + detail: string[]; +}> = { + agent: { + reason: 'agents are platform-internal (ADR-0063 §2).', + detail: [ + 'The kernel ships exactly two agents, `ask` and `build`, bound by surface.', + 'An agent you author still parses and still publishes — and the runtime', + 'catalog then filters it out, so it never appears and nothing tells you.', + 'This command scaffolded exactly that file, so it is retired, not repaired.', + '', + 'Author a SKILL instead. Skills (plus tools / MCP) are the third-party', + 'extension primitive ADR-0063 names — the live surface this one was not.', + '', + 'There is no `os g skill` scaffolder yet. Write the file by hand:', + '', + ' src/skills/.skill.ts', + " import { defineSkill } from '@objectstack/spec/ai';", + '', + 'Docs: https://objectstack.ai/docs/ai/agents', + ], + }, +}; + // ─── Helpers ──────────────────────────────────────────────────────── function toCamelCase(str: string): string { @@ -307,6 +335,19 @@ function generateTypesFromConfig(config: Record): string { async function runMetadataGeneration(type: string, name: string, flags: { dir?: string; dryRun?: boolean }): Promise { printHeader('Generate'); + // A withdrawn type answers for itself, ahead of the roster lookup — see + // RETIRED_GENERATORS for why "unknown type" is the wrong answer here. + const retired = RETIRED_GENERATORS[type]; + if (retired) { + printError(`\`${CLI_ALIAS} g ${type}\` was retired — ${retired.reason}`); + console.log(''); + for (const line of retired.detail) { + console.log(line ? chalk.dim(` ${line}`) : ''); + } + console.log(''); + process.exit(1); + } + const generator = GENERATORS[type]; if (!generator) { printError(`Unknown type: ${type}`); @@ -874,7 +915,7 @@ export default class Generate extends Command { static override aliases = ['g']; static override args = { - type: Args.string({ description: 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)', required: true }), + type: Args.string({ description: 'Metadata type to generate (object, view, action, flow, dashboard, app)', required: true }), name: Args.string({ description: 'Name for the metadata (use kebab-case)', required: false }), }; diff --git a/packages/cli/test/generate-agent-retired.e2e.test.ts b/packages/cli/test/generate-agent-retired.e2e.test.ts new file mode 100644 index 0000000000..0186ad7f37 --- /dev/null +++ b/packages/cli/test/generate-agent-retired.e2e.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * PIN (#10359) — `os g agent` is gone, and its refusal names the replacement. + * + * The generator scaffolded into `src/agents`, a surface ADR-0063 §2 withdrew: + * the kernel ships exactly two agents (`ask`, `build`) and the runtime catalog + * filters out every other agent record. So the file it wrote passed + * `os validate`, published without complaint, and then never appeared — no + * error at any step. That is the silent-strip failure mode, arriving through + * the scaffolder. + * + * Deleting the entry alone would have moved the silence one step earlier + * rather than ending it: `Unknown type: agent` followed by the surviving + * roster tells the author their spelling is not on the list, and the natural + * next move is to hunt for the right spelling of something that no longer + * exists. So the assertions below are about the CONTENT of the refusal, not + * only about its absence from the roster — a bare "unknown generator" passes + * every "the type is gone" assertion and fails this file. + * + * Assertions are on a REAL CHILD PROCESS and on stdout, for the two reasons + * `invocation-loudness.e2e.test.ts` documents at length: `process.exitCode` + * inside a vitest worker is not an exit status (a CI script judges this + * command by `$?`), and these commands print through `utils/format.ts`, whose + * `printError` writes to stdout. Spawned through `bin/run-dev.js` + tsx so the + * suite does not depend on `packages/cli/dist` having been built. + * + * ⛔ Not asserted, because it is out of scope and stays that way: that + * `os g skill` exists. It does not — the skill scaffolder is the split-out + * follow-up half, and the message says so rather than promising it. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { execFile } from 'node:child_process'; +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = resolve(fileURLToPath(import.meta.url), '..'); +const CLI = resolve(HERE, '../bin/run-dev.js'); +const TSX = resolve(HERE, '../../../node_modules/.bin/tsx'); + +/** oclif + tsx cold start, with every command module loaded; ~2-10 s when healthy. */ +const RUN_TIMEOUT_MS = 180_000; + +interface Run { + code: number; + stdout: string; + stderr: string; +} + +function runTsx(args: string[], cwd: string): Promise { + return new Promise((resolvePromise) => { + execFile( + TSX, + args, + { cwd, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, NO_COLOR: '1' } }, + (err, stdout, stderr) => { + resolvePromise({ + // `err.code` is the real exit status; null/undefined means the child + // was signalled — a different failure, never reported as 0. + code: err + ? typeof (err as { code?: unknown }).code === 'number' + ? (err as unknown as { code: number }).code + : 1 + : 0, + stdout: String(stdout), + stderr: String(stderr), + }); + }, + ); + }); +} + +let dir: string; +let retired: Run; +let unknown: Run; +let survivor: Run; + +beforeAll(async () => { + dir = mkdtempSync(join(tmpdir(), 'os-g-agent-retired-')); + + // Sequential on purpose: three cold tsx starts, each loading every command + // module, in a container several agents share. + retired = await runTsx([CLI, 'g', 'agent', 'support'], dir); + unknown = await runTsx([CLI, 'g', 'nonexistent-type', 'support'], dir); + survivor = await runTsx([CLI, 'g', 'object', 'customer', '--dry-run'], dir); +}, RUN_TIMEOUT_MS); + +afterAll(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe('[#10359] `os g agent` is retired', () => { + it('fails instead of scaffolding — a CI script that still calls it stops', () => { + expect(retired.code).toBe(1); + }); + + it('says the command was RETIRED, not that the type is unrecognised', () => { + expect(retired.stdout).toContain('was retired'); + // The generic branch would have swallowed the whole explanation. + expect(retired.stdout).not.toContain('Unknown type:'); + }); + + it('names the decision that withdrew the surface', () => { + expect(retired.stdout).toContain('ADR-0063'); + expect(retired.stdout).toContain('platform-internal'); + }); + + it('names the two platform agents and the silent strip, so the WHY is in the message', () => { + expect(retired.stdout).toContain('`ask`'); + expect(retired.stdout).toContain('`build`'); + expect(retired.stdout).toContain('never appears'); + }); + + it('points the author at SKILLS — the half a bare removal would drop', () => { + expect(retired.stdout).toContain('SKILL'); + expect(retired.stdout).toContain('src/skills/'); + expect(retired.stdout).toContain('defineSkill'); + }); + + it('writes nothing — no `src/agents/`, no barrel index', () => { + expect(existsSync(join(dir, 'src', 'agents'))).toBe(false); + expect(existsSync(join(dir, 'src'))).toBe(false); + }); +}); + +describe('[#10359] the roster no longer advertises `agent`', () => { + it('omits `agent` from the available types an unknown spelling prints', () => { + expect(unknown.code).toBe(1); + expect(unknown.stdout).toContain('Unknown type:'); + expect(unknown.stdout).toContain('Available types:'); + expect(unknown.stdout).not.toMatch(/^\s*agent\s/m); + }); + + it('still lists the six that survive', () => { + for (const type of ['object', 'view', 'action', 'flow', 'dashboard', 'app']) { + expect(unknown.stdout).toContain(type); + } + }); +}); + +describe('[#10359] the generators that were not retired still work', () => { + it('`os g object … --dry-run` still previews a typed object file', () => { + expect(survivor.code).toBe(0); + expect(survivor.stdout).toContain('Dry run'); + expect(survivor.stdout).toContain("import * as Data from '@objectstack/spec/data'"); + }); +}); diff --git a/scripts/adr-anchors/packages__cli__src__commands__generate.ts.json b/scripts/adr-anchors/packages__cli__src__commands__generate.ts.json new file mode 100644 index 0000000000..d340defecf --- /dev/null +++ b/scripts/adr-anchors/packages__cli__src__commands__generate.ts.json @@ -0,0 +1,7 @@ +{ + "file": "packages/cli/src/commands/generate.ts", + "adrs": [ + "ADR-0063" + ], + "invariant": "There is no `agent` scaffolder, and the absence is a decision, not an oversight. ADR-0063 §2 closed `*.agent.ts` to third parties — the kernel ships exactly two agents (`ask`, `build`) and the runtime catalog filters out every other agent record — so a scaffolded `src/agents/*.ts` validated, published, and then vanished with no error at any step. Do not re-add an `agent` entry to `GENERATORS`. `agent` lives in `RETIRED_GENERATORS` instead, and that branch owes both halves of the message: the decision that withdrew the surface, and skills (`src/skills/.skill.ts`, `defineSkill`) as the surface to author instead. Falling back to the generic `Unknown type:` roster drops the pointer and leaves the author as stranded as the silent strip did, one step earlier." +} From 567a1a036c00fe647f58a160e117318de6e23c9a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 09:38:20 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(changeset):=20the=20`agent`=20generato?= =?UTF-8?q?r=20is=20retired=20=E2=80=94=20breaking,=20and=20loud=20about?= =?UTF-8?q?=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declares the removal as breaking (shipped `minor` under the launch-window convention) and states the consequence a release-notes reader needs: a CI step that still calls `os g agent` starts exiting 1. The ADR-0087 disposition is `no-migration-prescription`, and it is a positive finding rather than a detector miss: a CLI command name is an invocation surface with no authorable key, no stored row and no schema tombstone, so no ledger reader has anything to carry — and unlike the metric retirement that category is usually weighed against, the ledger is not the only notification channel here, because the command itself now refuses at the point of use. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r --- .changeset/retire-agent-generator.md | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .changeset/retire-agent-generator.md diff --git a/.changeset/retire-agent-generator.md b/.changeset/retire-agent-generator.md new file mode 100644 index 0000000000..1000ac17d8 --- /dev/null +++ b/.changeset/retire-agent-generator.md @@ -0,0 +1,66 @@ +--- +"@objectstack/cli": minor +--- + +fix(cli): **BREAKING** — the `agent` generator is retired, and `os g agent` now says why and points at skills (ADR-0063 §2, #10359) + +**⛔ If a script, a Makefile or a CI step in your project runs `os g agent`, it +will now exit 1.** That is the intended outcome and the one way this change can +interrupt you: the command is gone, deliberately, and the failure is how you +find out. Everything it used to produce was already being discarded — read on. + +`minor`, not `major`: during the launch window this stack ships breaking changes +as `minor` (pre-1.0 semantics under lockstep versioning — see +`scripts/check-changeset-no-major.mjs`). + +**What the command actually did.** `os g agent ` scaffolded a typed +`AI.Agent` into `src/agents/`. Per ADR-0063 §2 (which reversed ADR-0040 §3) the +kernel ships exactly **two** agents — `ask` and `build` — bound by surface and +never picked from a roster, and the runtime catalog **filters out every +non-platform agent record**. So the scaffolded file parsed, passed +`os validate`, published without complaint, and then never appeared anywhere. +No error at any step. An author who followed the documented example got a file, +a green validate, a successful publish, and nothing to show for it. + +**Why the roster entry was not simply deleted.** A deleted type falls through to +`Unknown type: agent` plus a list of what is left, which tells the author their +spelling is not on the list and invites them to hunt for the right spelling of +something that no longer exists — the same silence, one step earlier. `agent` is +now a **retirement ledger entry** instead, and the refusal carries both halves: +the decision that withdrew the surface, and the surface to author in its place. +What you see: + +``` + ✗ `os g agent` was retired — agents are platform-internal (ADR-0063 §2). + + The kernel ships exactly two agents, `ask` and `build`, bound by surface. + An agent you author still parses and still publishes — and the runtime + catalog then filters it out, so it never appears and nothing tells you. + This command scaffolded exactly that file, so it is retired, not repaired. + + Author a SKILL instead. Skills (plus tools / MCP) are the third-party + extension primitive ADR-0063 names — the live surface this one was not. + + There is no `os g skill` scaffolder yet. Write the file by hand: + + src/skills/.skill.ts + import { defineSkill } from '@objectstack/spec/ai'; + + Docs: https://objectstack.ai/docs/ai/agents +``` + +**There is no command to rewrite the call into.** A skill is a different +artifact with a different schema, not a renamed agent, and `os g skill` does not +exist yet — the skill scaffolder is tracked separately, and this message says so +rather than promising it. Delete the call; author the skill file by hand. + +The generator roster is now `object`, `view`, `action`, `flow`, `dashboard`, +`app`. The docs that advertised the retired one — the `os g agent support` +example, the `agent` / `src/agents/` row of the Available types table, and +`os g agent sales-assistant` in the Typical Workflow block — are gone from +`content/docs/deployment/cli.mdx`, which carries the retirement note instead; +`packages/cli/README.md`'s type roster follows. The quick-start project-layout +map, which listed `src/agents` as the directory an app author writes AI metadata +into, now names `src/skills`. + +