Skip to content

Commit dacc13a

Browse files
committed
Release iso-harness 0.8.0, iso-trace 0.5.0, and iso-orchestrator 0.2.0
1 parent cd397a4 commit dacc13a

31 files changed

Lines changed: 811 additions & 55 deletions

package-lock.json

Lines changed: 5 additions & 5 deletions
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# @razroo/iso-harness
22

3+
## 0.8.0
4+
5+
### Minor Changes
6+
7+
- Add optional per-target root instruction addenda to `iso-harness` so
8+
shared AGENTS text can stay harness-neutral while OpenCode-only guidance
9+
is emitted separately.
10+
11+
Add `inspectSession()` / `inspectSessions()` to `iso-trace` for
12+
harness-agnostic worker/session summaries, and surface OpenCode session
13+
titles in normalized session metadata.
14+
15+
Add heartbeat and renewable lease primitives to `iso-orchestrator` so
16+
consumers can track worker liveness and ownership without introducing
17+
harness-specific dispatch APIs.
18+
319
## 0.7.0
420

521
### Minor Changes

packages/iso-harness/README.md

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,27 @@ file layout each harness actually reads.
99
```
1010
iso/ → CLAUDE.md (Claude Code)
1111
├── instructions.md .claude/agents/*.md
12-
├── mcp.json .claude/commands/*.md
12+
├── instructions.agents.md* .claude/commands/*.md
13+
├── instructions.claude.md* .mcp.json
14+
├── instructions.cursor.md* → AGENTS.md (Codex + OpenCode + Pi)
15+
├── instructions.opencode.md* .codex/config.toml
16+
├── mcp.json .opencode/agents/*.md
17+
│ .opencode/skills/*.md
18+
│ .opencode/instructions.md* (OpenCode-only addendum)
19+
│ .opencode/opencode-model-fallback.json (optional; from `opencodeModelFallback` in iso/config.json)
20+
│ opencode.json
21+
│ → .cursor/rules/*.mdc (Cursor)
22+
│ .cursor/mcp.json
23+
│ → .pi/skills/*/SKILL.md (Pi)
24+
│ .pi/prompts/*.md
1325
├── agents/ .mcp.json
14-
│ └── researcher.md → AGENTS.md (Codex + OpenCode + Pi)
15-
└── commands/ .codex/config.toml
16-
└── review.md .opencode/agents/*.md
17-
.opencode/skills/*.md
18-
.opencode/opencode-model-fallback.json (optional; from `opencodeModelFallback` in iso/config.json)
19-
opencode.json
20-
→ .cursor/rules/*.mdc (Cursor)
21-
.cursor/mcp.json
22-
→ .pi/skills/*/SKILL.md (Pi)
23-
.pi/prompts/*.md
26+
│ └── researcher.md
27+
└── commands/
28+
└── review.md
2429
```
2530

31+
Files marked with `*` are optional.
32+
2633
## Quickstart
2734

2835
```bash
@@ -53,6 +60,10 @@ iso-harness build --watch # rebuild on every change under iso/
5360
```
5461
iso/
5562
├── instructions.md # root prompt → CLAUDE.md / AGENTS.md / .cursor/rules/main.mdc
63+
├── instructions.agents.md # optional addendum for shared AGENTS.md targets
64+
├── instructions.claude.md # optional Claude-only root addendum
65+
├── instructions.cursor.md # optional Cursor-only root addendum
66+
├── instructions.opencode.md # optional OpenCode-only addendum, loaded via opencode.json.instructions
5667
├── config.json # optional — targets.* merges + opencodeModelFallback file emit
5768
├── mcp.json # shared MCP server definitions
5869
├── agents/ # subagents
@@ -151,6 +162,19 @@ explicit hatches keep harness-specific features possible:
151162
plugin (`retryable_error_patterns`, global `fallback_models`, etc.).
152163
OpenCode-only; other harnesses ignore it.
153164

165+
### Root instruction addenda
166+
167+
`instructions.md` is still the shared base prompt. Optional sibling files let
168+
you add harness-specific root guidance without forking the whole source:
169+
170+
- `instructions.agents.md` appends only to the shared `AGENTS.md` output used by Codex, OpenCode, and Pi.
171+
- `instructions.claude.md` appends only to `CLAUDE.md`.
172+
- `instructions.cursor.md` appends only to `.cursor/rules/main.mdc`.
173+
- `instructions.opencode.md` is emitted to `.opencode/instructions.md` and automatically added to `opencode.json.instructions`.
174+
175+
This is especially useful when OpenCode needs extra orchestration guidance
176+
that should not leak into the shared `AGENTS.md` file that Codex and Pi also read.
177+
154178
```json
155179
// iso/config.json
156180
{

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.7.0",
3+
"version": "0.8.0",
44
"description": "One config for every coding agent — Cursor, Claude Code, Codex, OpenCode, Pi.",
55
"type": "module",
66
"bin": {

packages/iso-harness/src/source.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { promises as fs } from 'node:fs';
22
import path from 'node:path';
33
import { parse as parseFrontmatter } from './frontmatter.mjs';
44

5+
const INSTRUCTION_VARIANTS = [
6+
'agents',
7+
'claude',
8+
'cursor',
9+
'opencode',
10+
];
11+
512
async function readIfExists(p) {
613
try {
714
return await fs.readFile(p, 'utf8');
@@ -52,6 +59,11 @@ export async function loadSource(sourceDir) {
5259
}
5360

5461
const instructions = await readIfExists(path.join(abs, 'instructions.md'));
62+
const instructionVariants = {};
63+
for (const name of INSTRUCTION_VARIANTS) {
64+
const value = await readIfExists(path.join(abs, `instructions.${name}.md`));
65+
if (value != null) instructionVariants[name] = value;
66+
}
5567
const mcpRaw = await readIfExists(path.join(abs, 'mcp.json'));
5668
const configRaw = await readIfExists(path.join(abs, 'config.json'));
5769

@@ -83,12 +95,48 @@ export async function loadSource(sourceDir) {
8395
sourceDir: abs,
8496
config,
8597
instructions: instructions ?? '',
98+
instructionVariants,
8699
mcp,
87100
agents,
88101
commands,
89102
};
90103
}
91104

105+
function joinInstructionParts(parts) {
106+
const present = parts.filter((part) => typeof part === 'string' && part.trim());
107+
if (present.length === 0) return '';
108+
let out = present[0];
109+
for (const part of present.slice(1)) {
110+
out = `${out.replace(/\s+$/, '')}\n\n${part.replace(/^\s+/, '')}`;
111+
}
112+
return out;
113+
}
114+
115+
export function instructionsForTarget(source, target) {
116+
switch (target) {
117+
case 'claude':
118+
return joinInstructionParts([source.instructions, source.instructionVariants?.claude]);
119+
case 'cursor':
120+
return joinInstructionParts([source.instructions, source.instructionVariants?.cursor]);
121+
case 'codex':
122+
case 'pi':
123+
return joinInstructionParts([source.instructions, source.instructionVariants?.agents]);
124+
case 'opencode':
125+
return joinInstructionParts([source.instructions, source.instructionVariants?.agents]);
126+
default:
127+
return source.instructions ?? '';
128+
}
129+
}
130+
131+
export function supplementalInstructionsForTarget(source, target) {
132+
switch (target) {
133+
case 'opencode':
134+
return source.instructionVariants?.opencode ?? '';
135+
default:
136+
return '';
137+
}
138+
}
139+
92140
export function targetOverride(item, target) {
93141
const t = item.targets?.[target];
94142
if (t === 'skip' || t === false) return { skip: true };

packages/iso-harness/src/targets/claude.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
22
import path from 'node:path';
33
import { stringify as toFrontmatter } from '../frontmatter.mjs';
44
import { writeFile, writeJson } from '../fs-utils.mjs';
5-
import { targetOverride } from '../source.mjs';
5+
import { instructionsForTarget, targetOverride } from '../source.mjs';
66

77
function claudeTools(tools) {
88
if (!tools) return undefined;
@@ -46,9 +46,10 @@ export async function emitClaude(src, outDir, opts = {}) {
4646
};
4747
const roleMap = await loadResolvedRoleMap(outDir);
4848

49-
if (src.instructions) {
49+
const instructions = instructionsForTarget(src, 'claude');
50+
if (instructions) {
5051
const p = path.join(outDir, 'CLAUDE.md');
51-
await push(p, src.instructions.endsWith('\n') ? src.instructions : src.instructions + '\n');
52+
await push(p, instructions.endsWith('\n') ? instructions : instructions + '\n');
5253
}
5354

5455
for (const agent of src.agents) {

packages/iso-harness/src/targets/codex.mjs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'node:path';
22
import { promises as fs } from 'node:fs';
33
import { writeFile } from '../fs-utils.mjs';
4+
import { instructionsForTarget } from '../source.mjs';
45

56
function tomlString(v) {
67
return `"${String(v).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
@@ -64,9 +65,10 @@ export async function emitCodex(src, outDir, opts = {}) {
6465
written.push({ path: p, bytes });
6566
};
6667

67-
if (src.instructions) {
68+
const instructions = instructionsForTarget(src, 'codex');
69+
if (instructions) {
6870
const p = path.join(outDir, 'AGENTS.md');
69-
await push(p, src.instructions.endsWith('\n') ? src.instructions : src.instructions + '\n');
71+
await push(p, instructions.endsWith('\n') ? instructions : instructions + '\n');
7072
}
7173

7274
if (Object.keys(src.mcp.servers).length > 0) {

packages/iso-harness/src/targets/cursor.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'node:path';
22
import { stringify as toFrontmatter } from '../frontmatter.mjs';
33
import { writeFile, writeJson } from '../fs-utils.mjs';
4-
import { targetOverride } from '../source.mjs';
4+
import { instructionsForTarget, targetOverride } from '../source.mjs';
55

66
export async function emitCursor(src, outDir, opts = {}) {
77
const written = [];
@@ -10,13 +10,14 @@ export async function emitCursor(src, outDir, opts = {}) {
1010
written.push({ path: p, bytes });
1111
};
1212

13-
if (src.instructions) {
13+
const instructions = instructionsForTarget(src, 'cursor');
14+
if (instructions) {
1415
const data = {
1516
description: 'Project instructions',
1617
alwaysApply: true,
1718
};
1819
const p = path.join(outDir, '.cursor', 'rules', 'main.mdc');
19-
await push(p, toFrontmatter({ data, body: src.instructions }));
20+
await push(p, toFrontmatter({ data, body: instructions }));
2021
}
2122

2223
// Cursor has no native subagents or slash commands; emit agent prompts as

packages/iso-harness/src/targets/opencode.mjs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import path from 'node:path';
22
import { promises as fs } from 'node:fs';
33
import { stringify as toFrontmatter } from '../frontmatter.mjs';
44
import { writeFile, writeJson } from '../fs-utils.mjs';
5-
import { targetOverride } from '../source.mjs';
5+
import { instructionsForTarget, supplementalInstructionsForTarget, targetOverride } from '../source.mjs';
6+
7+
function mergeInstructionRefs(existing, extra) {
8+
const base = Array.isArray(existing) ? existing : existing ? [existing] : [];
9+
if (!extra) return base;
10+
return [...new Set([...base, extra])];
11+
}
612

713
async function readJsonIfExists(p) {
814
try {
@@ -34,9 +40,10 @@ export async function emitOpenCode(src, outDir, opts = {}) {
3440
written.push({ path: p, bytes });
3541
};
3642

37-
if (src.instructions) {
43+
const instructions = instructionsForTarget(src, 'opencode');
44+
if (instructions) {
3845
const p = path.join(outDir, 'AGENTS.md');
39-
await push(p, src.instructions.endsWith('\n') ? src.instructions : src.instructions + '\n');
46+
await push(p, instructions.endsWith('\n') ? instructions : instructions + '\n');
4047
}
4148

4249
// Load iso-route's opencode.json once so we can (a) fall back to its
@@ -46,6 +53,15 @@ export async function emitOpenCode(src, outDir, opts = {}) {
4653
// below with the later merge-write on opencode.json.
4754
const opencodeJsonPath = path.join(outDir, 'opencode.json');
4855
const existingConfig = opts.dryRun ? {} : await readJsonIfExists(opencodeJsonPath);
56+
const opencodeSupplement = supplementalInstructionsForTarget(src, 'opencode');
57+
const opencodeSupplementPath = path.join(outDir, '.opencode', 'instructions.md');
58+
59+
if (opencodeSupplement) {
60+
await push(
61+
opencodeSupplementPath,
62+
opencodeSupplement.endsWith('\n') ? opencodeSupplement : opencodeSupplement + '\n',
63+
);
64+
}
4965

5066
for (const agent of src.agents) {
5167
const { skip, override } = targetOverride(agent, 'opencode');
@@ -97,7 +113,8 @@ export async function emitOpenCode(src, outDir, opts = {}) {
97113
const opencodeExtras = src.config?.targets?.opencode ?? {};
98114
const hasMcp = Object.keys(src.mcp.servers).length > 0;
99115
const hasExtras = Object.keys(opencodeExtras).length > 0;
100-
if (hasMcp || hasExtras) {
116+
const needsInstructionRef = Boolean(opencodeSupplement);
117+
if (hasMcp || hasExtras || needsInstructionRef) {
101118
// Reuse the `existingConfig` loaded at the top — re-reading could race
102119
// with intermediate per-agent file writes on slower filesystems and is
103120
// wasted I/O. `@razroo/iso-route` writes model routing fields to
@@ -122,6 +139,9 @@ export async function emitOpenCode(src, outDir, opts = {}) {
122139
for (const [k, v] of Object.entries(opencodeExtras)) {
123140
output[k] = v;
124141
}
142+
if (needsInstructionRef) {
143+
output.instructions = mergeInstructionRefs(output.instructions, '.opencode/instructions.md');
144+
}
125145
await push(opencodeJsonPath, output, writeJson);
126146
}
127147

packages/iso-harness/src/targets/pi.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'node:path';
22
import { promises as fs } from 'node:fs';
33
import { stringify as toFrontmatter } from '../frontmatter.mjs';
44
import { writeFile, writeJson } from '../fs-utils.mjs';
5-
import { targetOverride } from '../source.mjs';
5+
import { instructionsForTarget, targetOverride } from '../source.mjs';
66

77
async function readJsonIfExists(p) {
88
try {
@@ -50,9 +50,10 @@ export async function emitPi(src, outDir, opts = {}) {
5050
written.push({ path: p, bytes });
5151
};
5252

53-
if (src.instructions) {
53+
const instructions = instructionsForTarget(src, 'pi');
54+
if (instructions) {
5455
const p = path.join(outDir, 'AGENTS.md');
55-
await push(p, src.instructions.endsWith('\n') ? src.instructions : src.instructions + '\n');
56+
await push(p, instructions.endsWith('\n') ? instructions : instructions + '\n');
5657
}
5758

5859
for (const agent of src.agents) {

0 commit comments

Comments
 (0)