diff --git a/README.md b/README.md index 532c753..34d5513 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ agenticscope pack "refactor handler" -p src/api/handler.ts -p src/db/schema.sql | :--- | :--- | | `agenticscope init [dir]` | Scaffold a manifest + `.scope/` tree | | `agenticscope lint [dir]` | Validate the manifest; flag missing paths, dupe ids, dead fragments, and fragments too big for the budget | -| `agenticscope build [dir]` | Compile `.scope/` into vendor files (`-t, --target claude\|gemini\|agents\|cursor` to pick a subset) | +| `agenticscope build [dir]` | Compile `.scope/` into vendor files (`-t, --target claude\|gemini\|agents\|cursor` to pick a subset; `--check` to verify they're up to date without writing — exits non-zero if stale, ideal for CI) | | `agenticscope pack ` | Resolve a task into a budgeted context block (`-d` dir, `-p` paths, `-b, --budget` override, `--exact` tokenizer, `--raw`) | | `agenticscope schema [dir]` | Generate `schema/manifest.schema.json` for TOML editor autocomplete (`-o` out path) | | `agenticscope mcp-config` | Print ready-to-paste MCP config (`--workspace`, `--host claude\|cursor\|generic`) | diff --git a/context.md b/context.md deleted file mode 100644 index 2136e5a..0000000 --- a/context.md +++ /dev/null @@ -1,84 +0,0 @@ -# agenticscope — project context - -Working notes: what exists, what's left. Last updated 2026-06-24. - -## What this is -A directory-as-context standard + a read-only MCP server that gives AI agents -live, structured, token-budgeted awareness of a multi-project workspace. -Solves: monolithic context files burning tokens, mixed rule/knowledge priority, -vendor-folder sprawl, and zero workspace-level awareness. - -- **Stack:** TypeScript (ESM, NodeNext), Node >= 22. -- **Repo:** https://github.com/jessn-dev/agentic-scope -- **npm:** `agenticscope` — `0.1.0`, `0.2.0`, `0.2.1` published (provenance, OIDC). -- **Two bins:** `agenticscope` (CLI), `agenticscope-mcp` (MCP server). - -## Release pipeline (working) -- Push to `main` → `.github/workflows/release.yml` runs semantic-release - (Conventional Commits → version/CHANGELOG/tag/GitHub release), then a - **top-level** `npm publish` via **npm OIDC Trusted Publishing** (no token). -- Node 24 in CI (npm 11.x, needed for OIDC). Key gotchas, all solved: - Trusted Publisher must match `jessn-dev` / `agentic-scope` / `release.yml` / - blank env; setup-node's `_authToken` placeholder is stripped from the npmrc; - publish must be top-level (semantic-release's exec env shadows OIDC). -- `ci.yml` runs typecheck + test + build on PRs. - -## Done - -### Core (`src/core/`) -- `types.ts` — zod schemas (Fragment, Manifest, precedence). -- `manifest.ts` — load/parse/validate `agenticscope.toml`. -- `tokens.ts` — `chars/4` estimator; `{ exact: true }` uses `gpt-tokenizer` (lazy). -- `fragments.ts` — `matchTriggers`, `pack` (budget override + exact opt), `renderPack`, - `oversizedFragments` (lint helper). -- `vendor.ts` — per-vendor `compile`/`build`, `resolveTargets` (target selection). -- `schema.ts` — JSON Schema generation from zod (`zod-to-json-schema`). - -### CLI (`src/cli.ts`) -- `init`, `lint` (+ oversized-fragment warning), `build` (`--target`), - `pack` (`--budget`, `--exact`, `-p`, `--raw`), `schema`, `mcp-config`. -- `--version` reads from package.json (no drift). - -### MCP server (`src/mcp/`) -- `server.ts` — `createServer()` factory; stdio transport (default) **and** - `--http [port]` Streamable HTTP transport for remote/hosted use. -- 6 tools, all `guard()`-wrapped → clean `isError`. -- `scope.ts` — path-scope guard: `project` args restricted to the workspace root. -- `pack_context` accepts a `budget` override. - -### Tests + CI -- Vitest, **34 tests passing** (manifest, match, pack, budget, vendor, tokens, - schema, scope, grep). `npm test`. - -### Docs / meta -- README: full spec + usage, badges (npm/CI/license/node), new commands/flags, - HTTP transport, security note. CHANGELOG.md (semantic-release-managed). -- `schema/manifest.schema.json` generated and committed. - -## State -- **`develop` carries the 1.0.0 work** (all of the former backlog below). -- Merging `develop` → `main` is intended to cut a **major (1.0.0)** release. - semantic-release needs a `BREAKING CHANGE:` footer (or `feat!:`) on at least - one commit to bump to 1.0.0 — the vendor-output format change is the break. - -## Backlog — DONE in this 1.0.0 cycle -- [x] #6 `pack --budget` override + lint warning for oversized fragments. -- [x] #7 `schema/manifest.schema.json` generated from zod (+ `schema` command). -- [x] #8 per-vendor formatting + `build --target`. -- [x] #9 `agenticscope mcp-config`. -- [x] #10 optional exact tokenizer (`gpt-tokenizer`) behind `--exact`. -- [x] #12 MCP HTTP transport (`--http`). -- [x] #13 MCP path-scope guard. -- [x] README badges (npm/CI/license/node). - -## Future ideas (not started) -- Watch mode for `build` (recompile on `.scope/` change). -- Stateful HTTP MCP sessions (current HTTP mode is stateless per-request). -- Per-fragment include/exclude globs beyond a single `path`. - -## Known issues / notes -- Dev-only audit vulns in `vite-node` (via vitest). Prod deps clean; shipped - package is `dist/` only. Clearing needs a breaking vitest bump — deferred. -- Token counts default to estimates (`chars/4`); use `--exact` / `exact: true` - for precise counts. Budgets are approximate by design otherwise. -- `context.md` is untracked working notes (not committed to any branch). diff --git a/src/cli.ts b/src/cli.ts index ed4cb2c..1b7dd15 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { loadManifest, MANIFEST_FILENAME, isProject } from "./core/manifest.js"; import { pack, renderPack, oversizedFragments } from "./core/fragments.js"; -import { build, VENDOR_TARGETS, resolveTargets } from "./core/vendor.js"; +import { build, checkBuild, VENDOR_TARGETS, resolveTargets } from "./core/vendor.js"; import { writeSchema, SCHEMA_PATH } from "./core/schema.js"; // Single source of truth for the version: package.json (kept in sync by semantic-release). @@ -90,7 +90,8 @@ program "-t, --target ", `Only build these vendors (${VENDOR_TARGETS.map((t) => t.name).join(", ")})`, ) - .action((dir: string, opts: { target?: string[] }) => { + .option("--check", "Verify vendor files are up to date with .scope/ (no writes); exit 1 if stale/missing") + .action((dir: string, opts: { target?: string[]; check?: boolean }) => { const loaded = loadManifest(resolve(dir)); let targets; try { @@ -99,6 +100,19 @@ program console.error(`✗ ${err instanceof Error ? err.message : err}`); process.exit(1); } + if (opts.check) { + const results = checkBuild(loaded, targets); + for (const r of results) { + console.log(` ${r.status === "ok" ? "✓" : "✗"} ${r.file} (${r.status})`); + } + const drift = results.filter((r) => r.status !== "ok"); + if (drift.length > 0) { + console.error(`✗ ${drift.length} vendor file(s) out of date — run \`agenticscope build\` and commit.`); + process.exit(1); + } + console.log("✓ Vendor files are up to date."); + return; + } const { written } = build(loaded, targets); console.log(`✓ Built ${written.length} vendor file(s):`); for (const f of written) { diff --git a/src/core/vendor.ts b/src/core/vendor.ts index f59514d..9c793f2 100644 --- a/src/core/vendor.ts +++ b/src/core/vendor.ts @@ -112,3 +112,26 @@ export function build(loaded: LoadedManifest, targets = VENDOR_TARGETS): BuildRe } return { written }; } + +export type VendorStatus = "ok" | "stale" | "missing"; +export interface VendorCheck { + file: string; + label: string; + status: VendorStatus; +} + +/** + * Check whether the on-disk vendor files match what `.scope/` would compile to, + * without writing anything. Lets CI fail when generated files have drifted from + * their source (`status: "stale"`) or were never built (`status: "missing"`). + */ +export function checkBuild(loaded: LoadedManifest, targets = VENDOR_TARGETS): VendorCheck[] { + return targets.map((t) => { + const path = join(loaded.root, t.file); + const expected = compile(loaded, t); + let status: VendorStatus; + if (!existsSync(path)) status = "missing"; + else status = readFileSync(path, "utf8") === expected ? "ok" : "stale"; + return { file: t.file, label: t.label, status }; + }); +} diff --git a/tests/vendor.test.ts b/tests/vendor.test.ts index 92b5c26..2f9205a 100644 --- a/tests/vendor.test.ts +++ b/tests/vendor.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, afterEach } from "vitest"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { compile, build, resolveTargets, VENDOR_TARGETS } from "../src/core/vendor.js"; +import { compile, build, checkBuild, resolveTargets, VENDOR_TARGETS } from "../src/core/vendor.js"; import { makeProject } from "./fixture.js"; let cleanup = () => {}; @@ -71,4 +71,22 @@ describe("vendor build", () => { it("resolveTargets rejects an unknown vendor", () => { expect(() => resolveTargets(["nope"])).toThrow(/Unknown vendor target/); }); + + it("checkBuild reports missing, then ok after build, then stale after edit", () => { + const fx = makeProject(MANIFEST, FILES); + cleanup = fx.cleanup; + + // Nothing built yet → all missing. + expect(checkBuild(fx.loaded).every((c) => c.status === "missing")).toBe(true); + + // After build → all ok. + build(fx.loaded); + expect(checkBuild(fx.loaded).every((c) => c.status === "ok")).toBe(true); + + // Tamper with one generated file → that one is stale, the rest ok. + writeFileSync(join(fx.root, "CLAUDE.md"), "hand-edited, now drifted", "utf8"); + const after = checkBuild(fx.loaded); + expect(after.find((c) => c.file === "CLAUDE.md")!.status).toBe("stale"); + expect(after.filter((c) => c.status === "ok").length).toBe(VENDOR_TARGETS.length - 1); + }); });