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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task...>` | 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`) |
Expand Down
84 changes: 0 additions & 84 deletions context.md

This file was deleted.

18 changes: 16 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -90,7 +90,8 @@ program
"-t, --target <name...>",
`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 {
Expand All @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions src/core/vendor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
});
}
22 changes: 20 additions & 2 deletions tests/vendor.test.ts
Original file line number Diff line number Diff line change
@@ -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 = () => {};
Expand Down Expand Up @@ -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);
});
});
Loading