Skip to content

Commit ddb92ac

Browse files
Add @razroo/iso and release iso-harness 0.3.0
1 parent 78183d5 commit ddb92ac

23 files changed

Lines changed: 726 additions & 51 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ jobs:
7878
- name: Pipeline integration (agentmd → isolint → iso-harness)
7979
run: node examples/pipeline/build.mjs
8080

81+
- name: iso wrapper smoke build
82+
run: node packages/iso/bin/iso.mjs build packages/iso-harness/examples/minimal --skip-isolint --dry-run --out /tmp/iso-wrapper-smoke --target claude
83+
8184
- name: iso-harness smoke build
8285
working-directory: packages/iso-harness
8386
run: npm run smoke

.github/workflows/iso-release.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: iso Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
id-token: write
10+
11+
defaults:
12+
run:
13+
working-directory: packages/iso
14+
15+
jobs:
16+
publish:
17+
name: Publish @razroo/iso to npm
18+
if: startsWith(github.ref_name, 'iso-v')
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v5
22+
23+
- uses: actions/setup-node@v5
24+
with:
25+
node-version: "22"
26+
registry-url: "https://registry.npmjs.org"
27+
cache: npm
28+
29+
- name: Verify tag matches package.json version
30+
run: |
31+
TAG="${GITHUB_REF_NAME#iso-v}"
32+
PKG="$(node -p "require('./package.json').version")"
33+
if [ "$TAG" != "$PKG" ]; then
34+
echo "::error::Tag $GITHUB_REF_NAME does not match packages/iso/package.json version $PKG"
35+
exit 1
36+
fi
37+
38+
- name: Install (workspace root)
39+
run: npm ci
40+
working-directory: .
41+
42+
- run: npm run build --workspace @razroo/agentmd
43+
working-directory: .
44+
45+
- run: npm run build --workspace @razroo/isolint
46+
working-directory: .
47+
48+
- run: npm test
49+
50+
- name: Publish
51+
run: npm publish --provenance
52+
env:
53+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Today, writing agent instructions is fragmented on two axes:
2323
unstructured rationale all drop silently at 7B. You don't find out
2424
until the agent misbehaves in production.
2525

26-
The three packages in this repo compose into a pipeline that fixes both:
26+
Three core packages in this repo compose into a pipeline that fixes both,
27+
and a fourth wrapper package exposes that whole chain behind one CLI:
2728

2829
```
2930
authored source structural dialect portable prose fan-out to harnesses
@@ -58,6 +59,11 @@ The three packages in this repo compose into a pipeline that fixes both:
5859
MCP servers into `CLAUDE.md`, `AGENTS.md`, `.cursor/rules/*.mdc`,
5960
`.opencode/agents/*.md`, etc., so all four harnesses stay in lockstep.
6061

62+
- **[`packages/iso`](./packages/iso)**`@razroo/iso`
63+
The wrapper CLI for the whole flow: if `agent.md` is your authored source,
64+
`iso build` runs `agentmd lint`, `agentmd render`, `isolint lint`, then
65+
`iso-harness build` in one command.
66+
6167
Each package is independently published on npm and works on its own.
6268
They're in one repo because they're designed to compose.
6369

@@ -70,7 +76,8 @@ iso/
7076
└── packages/
7177
├── agentmd/ # structure + adherence
7278
├── isolint/ # portable prose
73-
└── iso-harness/ # one source, every harness
79+
├── iso-harness/ # one source, every harness
80+
└── iso/ # one command for the whole pipeline
7481
```
7582

7683
## Build & test
@@ -85,6 +92,7 @@ npm run test:pipeline # end-to-end demo (agentmd → isolint → iso-harne
8592
# Target a single package
8693
npm run build --workspace @razroo/isolint
8794
npm run test --workspace @razroo/agentmd
95+
npm run test --workspace @razroo/iso
8896
```
8997

9098
## Releasing
@@ -117,5 +125,6 @@ build, and `npm publish --provenance`.
117125
[`examples/pipeline/`](./examples/pipeline) is an executable demonstration
118126
of the composed pipeline: one authored `agent.md` is structurally linted,
119127
rendered, prose-linted, and fanned out into the 11 files each coding-agent
120-
harness expects. Run `npm run test:pipeline` to exercise all three packages
121-
against a single source.
128+
harness expects. Run `npm run test:pipeline` to exercise the core pipeline,
129+
or use `@razroo/iso` in your own project when you want the same chain behind
130+
one CLI.

package-lock.json

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/iso-harness/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# @razroo/iso-harness
22

3+
## 0.3.0
4+
5+
### Minor Changes
6+
7+
- Add `iso-harness build --dry-run` and `--watch`, and include dry-run
8+
summaries with per-file byte sizes.
9+
310
## 0.2.0
411

512
### Minor Changes

packages/iso-harness/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,18 @@ Or once installed as a CLI:
3333
iso-harness build # reads ./iso, writes to ./
3434
iso-harness build --target claude,cursor # only two targets
3535
iso-harness build --source path/to/iso --out path/to/project
36+
iso-harness build --dry-run # print planned writes, no disk changes
37+
iso-harness build --watch # rebuild on every change under iso/
3638
```
3739

40+
## Build modes
41+
42+
- `--dry-run` validates and renders the full output plan, but prints what
43+
would be written instead of touching disk.
44+
- `--watch` keeps a filesystem watcher on the source directory and reruns the
45+
build after changes. Combine it with `--target` when you only care about one
46+
harness while iterating.
47+
3848
## Source format
3949

4050
```

packages/iso-harness/bin/iso-harness.mjs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22
import { build, validate } from '../src/build.mjs';
33
import { formatDiagnostic } from '../src/validate.mjs';
4-
import { readFileSync } from 'node:fs';
4+
import { readFileSync, watch as fsWatch } from 'node:fs';
55
import { dirname, resolve } from 'node:path';
66
import { fileURLToPath } from 'node:url';
77

@@ -34,6 +34,7 @@ const USAGE = `iso-harness — one source directory, every agent harness
3434
Usage:
3535
iso-harness --version
3636
iso-harness build [--source <dir>] [--out <dir>] [--target claude,cursor,codex,opencode]
37+
[--dry-run] [--watch]
3738
iso-harness validate [--source <dir>] [--format text|json]
3839
3940
Commands:
@@ -47,6 +48,8 @@ Flags:
4748
--source <dir> Path to iso source directory (default: iso)
4849
--out <dir> Output root directory (default: .)
4950
--target <list> Comma-separated targets (default: all four)
51+
--dry-run Print what would be written, with byte sizes. No disk writes.
52+
--watch Rebuild on changes to the source directory. Ctrl-C to exit.
5053
--format <fmt> validate-only: text (default) | json
5154
`;
5255

@@ -70,21 +73,58 @@ if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
7073
process.exit(cmd ? 0 : 0);
7174
}
7275

76+
async function runBuildOnce({ source, out, targets, dryRun }) {
77+
try {
78+
const summary = await build({ source, out, targets, dryRun });
79+
for (const line of summary) console.log(line);
80+
return 0;
81+
} catch (err) {
82+
console.error(err.message);
83+
return 1;
84+
}
85+
}
86+
87+
function watchBuild({ source, out, targets, dryRun }) {
88+
const sourceAbs = resolve(source);
89+
let scheduled = null;
90+
let building = false;
91+
const trigger = () => {
92+
if (scheduled) clearTimeout(scheduled);
93+
scheduled = setTimeout(async () => {
94+
scheduled = null;
95+
if (building) return;
96+
building = true;
97+
console.log(`\n--- change detected — rebuilding ---`);
98+
await runBuildOnce({ source, out, targets, dryRun });
99+
building = false;
100+
}, 150);
101+
};
102+
try {
103+
fsWatch(sourceAbs, { recursive: true }, trigger);
104+
} catch (err) {
105+
console.error(`watch failed for ${sourceAbs}: ${err.message}`);
106+
process.exit(1);
107+
}
108+
console.log(`watching ${sourceAbs} — press ^C to exit`);
109+
}
110+
73111
if (cmd === 'build') {
74112
const source = flag('source', 'iso');
75113
const out = flag('out', '.');
76114
const targets = list('target') ?? ALL_TARGETS;
115+
const dryRun = boolFlag('dry-run');
116+
const watchMode = boolFlag('watch');
77117
const unknown = targets.filter(t => !ALL_TARGETS.includes(t));
78118
if (unknown.length) {
79119
console.error(`Unknown target(s): ${unknown.join(', ')}. Valid: ${ALL_TARGETS.join(', ')}`);
80120
process.exit(2);
81121
}
82-
try {
83-
const summary = await build({ source, out, targets });
84-
for (const line of summary) console.log(line);
85-
} catch (err) {
86-
console.error(err.message);
87-
process.exit(1);
122+
const initial = await runBuildOnce({ source, out, targets, dryRun });
123+
if (watchMode) {
124+
watchBuild({ source, out, targets, dryRun });
125+
// Keep the process alive; ^C exits.
126+
} else {
127+
process.exit(initial);
88128
}
89129
} else if (cmd === 'validate') {
90130
const source = flag('source', 'iso');

packages/iso-harness/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@razroo/iso-harness",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "One config for every coding agent — Cursor, Claude Code, Codex, OpenCode.",
55
"type": "module",
66
"bin": {

packages/iso-harness/src/build.mjs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,41 @@ const EMITTERS = {
1313
opencode: emitOpenCode,
1414
};
1515

16-
export async function build({ source, out, targets }) {
16+
function formatBytes(n) {
17+
if (n < 1024) return `${n} B`;
18+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
19+
return `${(n / 1024 / 1024).toFixed(2)} MB`;
20+
}
21+
22+
export async function build({ source, out, targets, dryRun = false }) {
1723
const src = await loadSource(source);
1824
const diagnostics = validateSource(src);
1925
const errors = diagnostics.filter((d) => d.severity === 'error');
2026
if (errors.length) throw new ValidationError(diagnostics);
2127

2228
const outAbs = path.resolve(out);
23-
const summary = [`iso-harness: loaded ${src.agents.length} agent(s), ${src.commands.length} command(s), ${Object.keys(src.mcp.servers).length} MCP server(s) from ${src.sourceDir}`];
29+
const prefix = dryRun ? 'iso-harness (dry-run)' : 'iso-harness';
30+
const summary = [`${prefix}: loaded ${src.agents.length} agent(s), ${src.commands.length} command(s), ${Object.keys(src.mcp.servers).length} MCP server(s) from ${src.sourceDir}`];
2431
const warnings = diagnostics.filter((d) => d.severity === 'warning');
2532
for (const w of warnings) summary.push(` warning: ${formatDiagnostic(w)}`);
2633

34+
const opts = { dryRun };
35+
let totalBytes = 0;
36+
let totalFiles = 0;
2737
for (const target of targets) {
2838
const emit = EMITTERS[target];
29-
const written = await emit(src, outAbs);
30-
summary.push(` [${target}] wrote ${written.length} file(s)`);
31-
for (const f of written) summary.push(` - ${path.relative(outAbs, f)}`);
39+
const written = await emit(src, outAbs, opts);
40+
const verb = dryRun ? 'would write' : 'wrote';
41+
summary.push(` [${target}] ${verb} ${written.length} file(s)`);
42+
for (const f of written) {
43+
totalBytes += f.bytes;
44+
totalFiles += 1;
45+
const size = dryRun ? ` (${formatBytes(f.bytes)})` : '';
46+
summary.push(` - ${path.relative(outAbs, f.path)}${size}`);
47+
}
48+
}
49+
if (dryRun) {
50+
summary.push(`\n${totalFiles} file(s), ${formatBytes(totalBytes)} — no files written`);
3251
}
3352
return summary;
3453
}
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { promises as fs } from 'node:fs';
22
import path from 'node:path';
33

4-
export async function writeFile(filePath, content) {
4+
// `dryRun` short-circuits the actual filesystem write so callers can still
5+
// accumulate the "what would be emitted" list without touching disk. The
6+
// return value reports bytes for dry-run summary display.
7+
export async function writeFile(filePath, content, { dryRun = false } = {}) {
8+
const bytes = Buffer.byteLength(content);
9+
if (dryRun) return { bytes, wrote: false };
510
await fs.mkdir(path.dirname(filePath), { recursive: true });
611
await fs.writeFile(filePath, content);
12+
return { bytes, wrote: true };
713
}
814

9-
export async function writeJson(filePath, obj) {
10-
await writeFile(filePath, JSON.stringify(obj, null, 2) + '\n');
15+
export async function writeJson(filePath, obj, opts) {
16+
return writeFile(filePath, JSON.stringify(obj, null, 2) + '\n', opts);
1117
}

0 commit comments

Comments
 (0)