@nurix/etna 0.19.22 → 0.19.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +6 -5
  2. package/data/harness-index.json +1 -1
  3. package/data/harnesses/code/CLAUDE.md +10 -0
  4. package/data/harnesses/code/agents/brainstorm.md +11 -11
  5. package/data/harnesses/code/agents/change-review.md +4 -4
  6. package/data/harnesses/code/agents/maintenance.md +2 -1
  7. package/data/harnesses/code/agents/retro.md +45 -0
  8. package/data/harnesses/code/codex/agents/brainstorm.toml +11 -11
  9. package/data/harnesses/code/codex/agents/change-review.toml +4 -5
  10. package/data/harnesses/code/codex/agents/maintenance.toml +2 -1
  11. package/data/harnesses/code/codex/agents/retro.toml +43 -0
  12. package/data/harnesses/code/harness.json +2 -2
  13. package/data/harnesses/code/skills/documentation/SKILL.md +1 -0
  14. package/data/harnesses/code/skills/documentation/templates/refinement-ledger.md +33 -0
  15. package/data/harnesses/code/skills/documentation/templates/structure.md +2 -0
  16. package/data/harnesses/etna/CLAUDE.md +2 -0
  17. package/data/harnesses/etna/agents/brainstorm.md +11 -11
  18. package/data/harnesses/etna/agents/change-review.md +4 -4
  19. package/data/harnesses/etna/agents/maintenance.md +2 -1
  20. package/data/harnesses/etna/codex/agents/brainstorm.toml +11 -11
  21. package/data/harnesses/etna/codex/agents/change-review.toml +4 -5
  22. package/data/harnesses/etna/codex/agents/maintenance.toml +2 -1
  23. package/data/harnesses/etna/harness.json +2 -2
  24. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -0
  25. package/data/harnesses/etna/skills/documentation/templates/refinement-ledger.md +33 -0
  26. package/data/harnesses/etna/skills/documentation/templates/structure.md +2 -0
  27. package/data/harnesses/hr/harness.json +1 -1
  28. package/lib/codex/README.md +17 -7
  29. package/lib/codex/agents-md.mjs +85 -6
  30. package/lib/codex/hooks.mjs +25 -1
  31. package/lib/codex/index.mjs +4 -3
  32. package/lib/codex/plugin-manifest.mjs +9 -3
  33. package/lib/codex/subagents.mjs +2 -2
  34. package/lib/codex/validate-codex-agents.mjs +126 -0
  35. package/package.json +2 -2
  36. package/lib/codex/gen-agents.mjs +0 -157
@@ -1,15 +1,14 @@
1
1
  # dropped Claude tools: Bash, Read, Write, Glob, Grep, Task
2
2
  name = "change-review"
3
- description = "Velocity + change-risk analysis. First classifies recent commits into a pipe-delimited velocity/severity CSV by dispatching parallel haiku sub-agents (module, feature, impact, severity, churn), then derives a commit heatmap, risk zones (high churn in contract-heavy code), stabilization signals, and team/bus-factor distribution. Self-contained — produces its own CSV, no separate git-history step. Use when planning where to invest in review focus or rules."
3
+ description = "Velocity + change-risk analysis. First classifies recent commits into a pipe-delimited velocity/severity CSV by dispatching parallel low-cost subagents (module, feature, impact, severity, churn), then derives a commit heatmap, risk zones (high churn in contract-heavy code), stabilization signals, and team/bus-factor distribution. Self-contained — produces its own CSV, no separate git-history step. Use when planning where to invest in review focus or rules."
4
4
  developer_instructions = '''
5
-
6
5
  # Change-Review Agent
7
6
 
8
7
  You analyze code-change velocity over time to find risk zones — where change is accelerating or decelerating, and whether review attention matches the risk. You answer: "where should the tech lead invest review focus or rules next?"
9
8
 
10
9
  You work in two phases:
11
10
 
12
- 1. **Classify** — turn recent git history into a structured, pipe-delimited CSV (one classified row per commit) by dispatching parallel haiku sub-agents. You do this yourself; there is no separate agent that hands you a CSV.
11
+ 1. **Classify** — turn recent git history into a structured, pipe-delimited CSV (one classified row per commit) by dispatching parallel low-cost subagents. You do this yourself; there is no separate agent that hands you a CSV.
13
12
  2. **Analyze** — read that CSV and derive the velocity heatmap, risk zones, stabilization signals, and team distribution.
14
13
 
15
14
  ## Parameters
@@ -80,7 +79,7 @@ Build a registry of objects:
80
79
 
81
80
  Split the commit registry into batches of `batch_size` commits each.
82
81
 
83
- Spawn **haiku** sub-agents via the Task tool with `subagent_type: "general-purpose"` and `model: "haiku"`. Launch up to 5 batches in parallel.
82
+ Dispatch one **independent subagent per batch** on the **cheapest capable tier** — classification is mechanical, so the lowest-cost model that can follow the output format is the right one. Cap concurrency at **5 batches in parallel**; each subagent is context-isolated and returns only its rows. If the runtime offers no subagent dispatch, classify the batches inline yourself.
84
83
 
85
84
  Before building the prompt template, check if `docs/memory/change-review.md` contains **Module Taxonomy** and **Feature Taxonomy** sections. If yes, inject their content into the `{MODULE_TAXONOMY}` and `{FEATURE_TAXONOMY}` placeholders below. If no memory file exists, use these generic fallbacks:
86
85
 
@@ -241,7 +240,7 @@ Per module, list which authors made the most commits in the 3-month window. Surf
241
240
  ## Rules
242
241
 
243
242
  - Never modify source code — read-only except for writing the output CSV, `docs/memory/change-review.md`, and `docs/history/backlog.md`
244
- - Use `model: "haiku"` and `subagent_type: "general-purpose"` for all classification sub-agents to minimize cost
243
+ - Run all classification subagents on the cheapest capable tier to minimize cost; they are independent and context-isolated
245
244
  - Always report progress: "Dispatching batch 1-5 of 12..." etc.
246
245
  - If git commands fail (bad branch, bad date range), stop and report the error clearly
247
246
  - Pipe characters in commit messages must be replaced with dashes before writing to CSV
@@ -135,6 +135,7 @@ docs/
135
135
  ├── history/
136
136
  │ ├── CHANGELOG.md # Master changelog index
137
137
  │ ├── changelog/ # Individual entries
138
+ │ ├── ledger/ # Refinement Ledger — append-only per-commit refinement journal (YYYY-MM.md; written at commit time, NOT scaffolded empty)
138
139
  │ ├── plans/ # Implementation plans (Status field required)
139
140
  │ ├── brainstorms/ # Harvested brainstorm sessions ({topic}/) — user decides kept (committed) or deleted; Readme.md is the index
140
141
  │ └── backlog.md # Tracked work items by priority (P0-P3)
@@ -197,7 +198,7 @@ Re-running you is safe — the structure work is idempotent, so a second pass is
197
198
  ## Rules
198
199
 
199
200
  - **Never delete feature docs** — only update or flag for review.
200
- - **Never modify `docs/history/plans/` or `docs/history/changelog/`** (frozen records).
201
+ - **Never modify `docs/history/plans/`, `docs/history/changelog/`, or `docs/history/ledger/`** (frozen/append-only records — ledger blocks are written only at commit time, distilled only by the `retro` agent).
201
202
  - **Preserve existing doc structure and formatting** when updating.
202
203
  - **When in doubt, create a backlog item** rather than making a wrong update.
203
204
  - **Be specific:** include file paths, line numbers, and what changed.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.1.11",
4
+ "version": "1.1.13",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "3e3243936008"
10
+ "digest": "27b25f41633e"
11
11
  }
@@ -61,6 +61,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
61
61
  | Document feature/module (LLD) | [`templates/feature.md`](templates/feature.md) |
62
62
  | Document data flow / state / Redux | [`templates/feature.md`](templates/feature.md) — they are sections of the feature doc |
63
63
  | Create/update changelog | [`templates/changelog-entry.md`](templates/changelog-entry.md) (record) |
64
+ | Record per-commit refinement (ledger block) | [`templates/refinement-ledger.md`](templates/refinement-ledger.md) (record — written at commit time, distilled by the `retro` agent) |
64
65
  | Create/update plan | [`templates/plan.md`](templates/plan.md) (record) |
65
66
  | Record/revise architecture decision | [`templates/architecture-decision.md`](templates/architecture-decision.md) — one AD per subject; revisions update it in place |
66
67
  | System at a glance | [`templates/system-overview.md`](templates/system-overview.md) |
@@ -0,0 +1,33 @@
1
+ # Refinement Ledger
2
+
3
+ Record. Use for: the append-only per-commit journal of session refinement — one block per non-mechanical commit, written by the committing agent at commit time and staged **inside the commit it describes**. Blocks are immutable once committed: never edited, renumbered, or reconciled; a correction is a new block referencing the old. The ledger is exempt from doc-health passes by genre — it is history and can never be "wrong".
4
+
5
+ Monthly files: `docs/history/ledger/YYYY-MM.md`, created on first block — the folder is never scaffolded empty. Distillation is the `retro` agent's job; this template owns only the capture format.
6
+
7
+ ## Block format
8
+
9
+ ```
10
+ ### {YYYY-MM-DD} · {surface} — {one-line outcome}
11
+ - **Ask:** {what the user asked, compressed — taste and preference statements quoted verbatim}
12
+ - **Iterations:** {N} · rejected: {what was rejected and why} | none
13
+ - **Skills:** {skill}@{version} — survived | partially survived | overridden ({why, briefly}) | none used
14
+ - **Change:** {added/changed X, which does Y, because Z — behavior first, mechanism second}
15
+ - **Discovered:** {non-obvious fact learned by running the product — environment quirk, platform ceiling, seam behavior, dependency constraint} | none
16
+ - **Class:** {one or more keywords — suggestive vocabulary, not a closed enum; see Field rules}
17
+ - **Pin:** {enforced artifact written in this commit: test / token / guardrail / schema} | none — candidate: {what should hold it}
18
+ ```
19
+
20
+ ## Field rules
21
+
22
+ - **Every field present; "none" stated explicitly.** An absent field is a template violation. Sessions assert success by default — the required `rejected:` and `overridden` entries are the counterweight; an honestly empty failure field is information too.
23
+ - **`surface`** uses the repo's ownership vocabulary — the owning feature doc's topic or an `owns:` path stem — so the ledger is queryable per surface.
24
+ - **`Ask` quotes taste verbatim.** The user's preference wording is the one thing no skill can infer and no rebuild recovers; the ledger is its durable home.
25
+ - **`Skills` records efficacy honestly** — survived / partially survived / overridden with a reason. These verdicts are the raw data the `retro` agent aggregates into skill-evolution findings; sanitizing them into success stories destroys the mechanism's value.
26
+ - **`Discovered` holds what only running the product teaches.** If it was learned in use, it is recorded here the same day — this field feeds the pin candidates.
27
+ - **`Class` is a suggestive vocabulary, not a closed enum.** Tag with one or more keywords, preferring an existing term over a synonym so the `retro` agent can cluster; coin a new one when nothing fits — a recurring coinage is itself retro signal that the vocabulary (or the doc set) is missing a category. Suggested keywords: `ux` · `ui` · `visual` · `taste` · `layout` · `motion` · `interaction` · `copy` · `a11y` · `responsiveness` · `navigation` · `state` · `structure` · `architecture` · `seam` · `contract` · `schema` · `data` · `content` · `seed` · `performance` · `caching` · `resilience` · `error-handling` · `security` · `auth` · `observability` · `logging` · `testing` · `tooling` · `dependency` · `ops` · `deploy` · `env` · `ci` · `i18n` · `docs`.
28
+ - **`Pin` closes the loop:** the enforced artifact written in this commit, or the named candidate for the `retro` agent to flag. A taste-verified behavior with `Pin: none` and no candidate is a template smell.
29
+ - **Out-of-session commits** (the committing session did not do the work): fill what the diff and conversation evidence support; mark the rest `unknown — committed out-of-session`.
30
+
31
+ ## Exemptions (closed list)
32
+
33
+ No block for: release/bot commits, harness installs and bumps, lockfile-only changes, pure renames/moves, and ledger housekeeping itself. Everything else gets a block, however short.
@@ -30,6 +30,7 @@ Each topic has one canonical location. Read from here, not from plans or changel
30
30
  | Testing strategy | `docs/tests/` | strategy.md |
31
31
  | Historical plans | `docs/history/plans/` | YYYY-MM-DD-name.md |
32
32
  | Change records | `docs/history/changelog/` | Individual entries |
33
+ | Refinement Ledger | `docs/history/ledger/` | YYYY-MM.md (append-only, written at commit time) |
33
34
  | Migration runbooks | `docs/history/migrations/` | YYYY-MM-DD-name.md |
34
35
  | Deep-research artifacts | `docs/history/research/{topic}/` | README.md + findings.md + sources.md |
35
36
  | Brainstorm design records | `docs/research/{topic}-brainstorm/` (active; legacy `-brainstorming/`) → `docs/history/brainstorms/{topic}/` (harvested; user deletes or commits) | thesis.md + forks.md + ledger.md + state.md |
@@ -51,6 +52,7 @@ Every doc is a **truth document** (states current absolutes, topic-named, update
51
52
  - `docs/tests/`: Testing strategy and test docs. Present when the repo has a test posture worth documenting.
52
53
  - `docs/history/plans/`: Records. Must have `Status:` field. Never modify after completion.
53
54
  - `docs/history/changelog/`: Records. Append-only.
55
+ - `docs/history/ledger/`: Records. The Refinement Ledger — an append-only per-commit refinement journal (monthly `YYYY-MM.md` files) written by the committing agent, each block staged inside the commit it describes; block format is the `documentation` skill's `templates/refinement-ledger.md`, distillation is the `retro` agent. Blocks are immutable — never edited, never reconciled; the folder is never scaffolded empty.
54
56
  - `docs/history/migrations/`: Records. One-shot migration/upgrade runbooks — authored before execution, outcome-stamped and frozen after.
55
57
  - `docs/history/research/{topic}/`: Deep-research artifacts — the dumped output of a research pass, one folder per topic (conventionally `README.md` synthesis + `findings.md` verified claims & citations + `sources.md`). A point-in-time, cited record — **not** a living authority doc: it owns no code (no `owns:` frontmatter) and is never edited to "stay current." A spec/plan links to it for grounding; a new question gets a new topic folder.
56
58
  - `docs/research/{topic}-brainstorm/` → `docs/history/brainstorms/{topic}/`: Brainstorm design records — the Brainstorming-Mode session for a topic (working surface `thesis.md` + `forks.md` + `ledger.md` + `state.md` + `basis/` + `fragments/`, plus an untracked `ops/` capture layer deleted at stop), written by the `brainstorm` skill and its twin agent. An active session lives under `docs/research/` (legacy folders carry the older `-brainstorming/` suffix); on a **passed harvest** it **moves** to `docs/history/brainstorms/` — the completion marker — and the **user decides whether the moved folder is deleted or committed** as provenance. Each of the two folders carries a `Readme.md` index (the skill reads it at start instead of scanning). Author-created (never scaffolded empty), one folder per design conversation. Owns no code (no `owns:` frontmatter); the harvest routes its conclusions into `architecture/`/`features/`/`history/`. (Distinct from `docs/history/research/{topic}/` above — that is a frozen deep-research record, not a brainstorm session.)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "hr",
3
3
  "name": "hr",
4
- "version": "0.1.8",
4
+ "version": "0.1.9",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -8,24 +8,34 @@ Owning doc: [`docs/features/codex-support.md`](../../../../docs/features/codex-s
8
8
 
9
9
  | Source | Target | File |
10
10
  | --- | --- | --- |
11
- | `CLAUDE.md` | `AGENTS.md` (exact copy) | `agents-md.mjs` |
11
+ | `CLAUDE.md` | `AGENTS.md` (byte-for-byte copy + generated adapter) | `agents-md.mjs` |
12
12
  | `.claude/skills/` | `.agents/skills/` (mirror) | `skills.mjs` |
13
13
  | bundle `codex/agents/*.toml` | `.codex/agents/*.toml` (copy) | `subagents.mjs` |
14
- | `hooks.json` | `.codex/hooks.json` (`type:"command"` only) | `hooks.mjs` |
14
+ | `hooks.json` | `.codex/hooks.json` (`type:"command"`, command translated) | `hooks.mjs` |
15
15
  | `.claude-plugin/*` | `.codex-plugin/plugin.json` + `.agents/plugins/marketplace.json` | `plugin-manifest.mjs` |
16
16
 
17
17
  `index.mjs` orchestrates the install-time projectors and returns `{installed, notices, warnings, owned}`; `owned` is recorded in `.claude/harness.json` → `owned.codex` for prune-on-refresh.
18
18
 
19
19
  **Out of scope (owner decision 2026-07-13):** MCP projection and any Codex `settings.json`/`config.toml` equivalent. There is no user-editable Codex file the harness co-owns, so every Codex path is a clean full-overwrite — no managed-block merge.
20
20
 
21
+ ## Runtime adaptation the projection performs
22
+
23
+ The Claude source is runtime-neutral in intent, but three edges do not survive a mechanical copy — the projectors adapt them so a Codex install works out of the box:
24
+
25
+ - **Policy adapter (`agents-md.mjs`).** `AGENTS.md` is the `CLAUDE.md` policy copied byte-for-byte, then a compact **generated Codex runtime adapter** appended: it names the native skill (`.agents/skills/`) and agent (`.codex/agents/`) locations and, crucially, routes Codex to the `.claude/rules/*.md` files Codex does **not** auto-load. The list is derived from the bundled harness inventory (`bundledHarnessDir`), carries no user state, and is replaced whole on reinstall. It stays well under Codex's 32 KiB combined-instruction cap.
26
+ - **Hook-path translation (`hooks.mjs`, install) & (`publish.mjs`, plugin).** A hook command locating its script via `$CLAUDE_PROJECT_DIR` fails under Codex (Codex sets no such variable). For a **CLI install**, `toCodexCommand` prefixes a runtime-neutral root resolution (`git rev-parse --show-toplevel`, `pwd` fallback) and re-exports `CLAUDE_PROJECT_DIR`, so the hook resolves from the project root or any nested dir. For a **marketplace plugin**, `publish.mjs` rewrites the prefix to `$CLAUDE_PLUGIN_ROOT/hooks/` — the plugin-root variable both runtimes export.
27
+ - **Agent validation (`validate-codex-agents.mjs`).** A committed twin must be valid TOML with the required keys and carry no Claude-only primitive (`Task tool`, `subagent_type`, or a fixed model tier). Because a twin ships to consumers unparsed, this standalone validator is the gate — the authoring procedure mandates it.
28
+
29
+ ## CLI vs plugin capability surface
30
+
31
+ The two channels do not install the same set. The **CLI** writes the full Codex footprint — `AGENTS.md`, `.agents/skills/`, `.codex/agents/*.toml`, `.codex/hooks.json`. The **marketplace plugin** carries only what `.codex-plugin/plugin.json` supports — **skills + hooks**; it exposes no project custom agents (Codex custom agents are a CLI-only feature) and no root policy. See [`codex-support.md`](../../../../docs/features/codex-support.md) → _Supported Codex plugin surface_.
32
+
21
33
  ## Subagents are authored, not derived at install
22
34
 
23
- `.claude/agents/*.md → .codex/agents/*.toml` is lossy/semantic, so it is done by the **real Codex CLI** at authoring time, not a parser at install:
35
+ `.claude/agents/*.md → .codex/agents/*.toml` is a semantic conversion, so the twin is **authored from the `.md` by following** [`docs/architecture/harness/authoring-agents.md`](../../../../docs/architecture/harness/authoring-agents.md) — the model that writes or edits the agent produces the twin inline (frontmatter → `name`/`description`, body verbatim → `developer_instructions`, `tools:` dropped to a comment, Claude model aliases omitted), with **no external CLI**. The committed `agents/<name>.codex.toml` is checked by the validation gate:
24
36
 
25
37
  ```bash
26
- node packages/cli/lib/codex/gen-agents.mjs # shared catalog (agents/*.md), stale/missing only
27
- node packages/cli/lib/codex/gen-agents.mjs --force # regenerate all shared agents
28
- node packages/cli/lib/codex/gen-agents.mjs --include-harness # + harness/*/agents/*.md
38
+ node packages/cli/lib/codex/validate-codex-agents.mjs # or: npm run validate:codex-agents
29
39
  ```
30
40
 
31
- `gen-agents.mjs` runs `codex exec` per agent and writes a committed `agents/<name>.codex.toml` sibling. It runs **only on a laptop with `codex` installed + authenticated — never in CI**. `build-data.js` and `publish.mjs` then ship those committed TOML; the installer copies them, so **consumers need no Codex CLI**. Regenerate an agent's TOML in the same change that edits its `.md` (source↔generated parity).
41
+ `build-data.js` and `publish.mjs` then ship the committed TOML; the installer copies it, so **consumers need no Codex CLI**. Re-author an agent's TOML in the same change that edits its `.md` (source↔twin parity).
@@ -1,9 +1,14 @@
1
1
  // packages/cli/lib/codex/agents-md.mjs
2
- // CLAUDE.md → AGENTS.md — an exact copy (Codex's confirmed CLAUDE.md analog).
3
- // Mirrors the installer's CLAUDE.md safety net: overwrite in place when git can
4
- // recover it, otherwise back up the existing file and warn.
2
+ // CLAUDE.md → AGENTS.md — the authored policy copied byte-for-byte, then a small GENERATED
3
+ // Codex runtime adapter appended. The policy is runtime-neutral; the adapter maps its
4
+ // capability hooks to Codex's native locations and, crucially, routes Codex to the markdown
5
+ // rules under .claude/rules/ that Codex does NOT auto-load. The adapter is derived from the
6
+ // bundled harness inventory (rules/skills/agents actually shipped), carries no user state, and
7
+ // is regenerated whole on every reinstall alongside the rest of the harness-owned AGENTS.md.
8
+ // Mirrors the installer's CLAUDE.md safety net: overwrite in place when git can recover it,
9
+ // otherwise back up the existing file and warn.
5
10
 
6
- import { existsSync, lstatSync, renameSync, cpSync } from 'node:fs';
11
+ import { existsSync, lstatSync, renameSync, readdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
7
12
  import { join } from 'node:path';
8
13
 
9
14
  function isSymlink(path) {
@@ -14,7 +19,75 @@ function isSymlink(path) {
14
19
  }
15
20
  }
16
21
 
17
- export function projectAgentsMd({ targetDir, bundledClaudeMd, projectHasGit }) {
22
+ // Sorted base names of *.<ext> files directly under dir (e.g. rules/*.md → rule names).
23
+ function listByExt(dir, ext) {
24
+ if (!existsSync(dir)) return [];
25
+ return readdirSync(dir)
26
+ .filter((f) => f.endsWith(ext))
27
+ .map((f) => f.slice(0, -ext.length))
28
+ .sort();
29
+ }
30
+
31
+ // Sorted names of immediate subdirectories of dir (e.g. skills/<name>/).
32
+ function listSubdirs(dir) {
33
+ if (!existsSync(dir)) return [];
34
+ return readdirSync(dir)
35
+ .filter((f) => {
36
+ try {
37
+ return statSync(join(dir, f)).isDirectory();
38
+ } catch {
39
+ return false;
40
+ }
41
+ })
42
+ .sort();
43
+ }
44
+
45
+ // Render an inventory list as inline code names, or a runtime-neutral fallback when empty.
46
+ function nameList(names) {
47
+ return names.length ? names.map((n) => `\`${n}\``).join(', ') : '_(none installed)_';
48
+ }
49
+
50
+ // Compose the generated Codex runtime adapter from the bundled harness inventory. Returns "" when
51
+ // the bundle carries no rules, skills, or codex agents — nothing to adapt, so nothing is appended.
52
+ export function buildCodexAdapter(bundledHarnessDir) {
53
+ const rules = listByExt(join(bundledHarnessDir, 'rules'), '.md');
54
+ const skills = listSubdirs(join(bundledHarnessDir, 'skills'));
55
+ const agents = listByExt(join(bundledHarnessDir, 'codex', 'agents'), '.toml');
56
+ if (!rules.length && !skills.length && !agents.length) return '';
57
+
58
+ const lines = [
59
+ '---',
60
+ '',
61
+ '## Codex runtime adapter (generated)',
62
+ '',
63
+ 'This project is also installed for OpenAI Codex. The runtime-neutral policy above applies unchanged;',
64
+ "this section maps its capability hooks to Codex's native locations. It is generated from the installed",
65
+ 'harness inventory and replaced whole on every reinstall — do not hand-edit.',
66
+ '',
67
+ ];
68
+ if (skills.length) {
69
+ lines.push(
70
+ `- **Skills** — Codex scans \`.agents/skills/\` (a mirror of \`.claude/skills/\`); invoke a skill by its name. Installed: ${nameList(skills)}.`,
71
+ );
72
+ }
73
+ if (agents.length) {
74
+ lines.push(
75
+ `- **Custom agents** — Codex reads \`.codex/agents/*.toml\`; dispatch a subagent by its name. Installed: ${nameList(agents)}.`,
76
+ );
77
+ }
78
+ if (rules.length) {
79
+ lines.push(
80
+ `- **Project rules** — Codex does NOT auto-load the markdown rules the policy references. Read the relevant file under \`.claude/rules/\` before the work it governs: ${nameList(rules)}.`,
81
+ );
82
+ }
83
+ lines.push(
84
+ '- **Ownership index** — regenerated each session by the `.codex/hooks.json` SessionStart hook; if it is absent from context, derive it by scanning `docs/features/` frontmatter.',
85
+ );
86
+ lines.push('');
87
+ return lines.join('\n');
88
+ }
89
+
90
+ export function projectAgentsMd({ targetDir, bundledClaudeMd, bundledHarnessDir, projectHasGit }) {
18
91
  if (!existsSync(bundledClaudeMd)) return null;
19
92
  const dest = join(targetDir, 'AGENTS.md');
20
93
  const existed = existsSync(dest) || isSymlink(dest);
@@ -30,6 +103,12 @@ export function projectAgentsMd({ targetDir, bundledClaudeMd, projectHasGit }) {
30
103
  warning = `No .git here, so no version-control safety net — backed up existing AGENTS.md → ${backup}`;
31
104
  }
32
105
  }
33
- cpSync(bundledClaudeMd, dest, { force: true });
106
+ // Policy copied byte-for-byte; the generated adapter appended after a blank-line separator.
107
+ // The policy prefix is left exactly as authored — only a separator + adapter follow it.
108
+ const policy = readFileSync(bundledClaudeMd, 'utf8');
109
+ const adapter = bundledHarnessDir ? buildCodexAdapter(bundledHarnessDir) : '';
110
+ const separator = policy.endsWith('\n') ? '\n' : '\n\n';
111
+ const body = adapter ? policy + separator + adapter : policy;
112
+ writeFileSync(dest, body);
34
113
  return { installed: 'AGENTS.md → ./', notice, warning };
35
114
  }
@@ -3,10 +3,32 @@
3
3
  // filtered to type:"command" (the only hook type Codex executes — "prompt"/"agent"
4
4
  // entries are parsed but skipped). A standalone file, not a config.toml [hooks]
5
5
  // table, since the settings/config surface is out of scope for Codex here.
6
+ //
7
+ // Command translation: the source command locates its script via $CLAUDE_PROJECT_DIR,
8
+ // which Claude Code exports but Codex does not — under Codex the token expands empty
9
+ // and `node "/.claude/hooks/…"` exits 1, failing SessionStart. So every command that
10
+ // references the token is prefixed with a runtime-neutral root resolution: `git
11
+ // rev-parse` (walks up from any nested working directory) with a `pwd` fallback for a
12
+ // non-Git project root. The resolved root is exported back as CLAUDE_PROJECT_DIR so the
13
+ // script's own project-dir read (generate-ownership-index.mjs resolveProjectDir) also
14
+ // points at the project root, not the session's nested cwd. The Claude source hooks.json
15
+ // is left untouched — it stays valid for the CLI-installed .claude/settings.json path.
6
16
 
7
17
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
18
  import { join, dirname } from 'node:path';
9
19
 
20
+ const PROJECT_DIR_TOKEN = /\$\{?CLAUDE_PROJECT_DIR\}?/;
21
+
22
+ // Prefix a $CLAUDE_PROJECT_DIR-dependent command with a Codex-safe root resolution.
23
+ // Commands that don't reference the token pass through unchanged.
24
+ export function toCodexCommand(command) {
25
+ if (typeof command !== 'string' || !PROJECT_DIR_TOKEN.test(command)) return command;
26
+ return (
27
+ 'export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; ' +
28
+ command
29
+ );
30
+ }
31
+
10
32
  export function projectHooks({ targetDir, bundledHarnessDir }) {
11
33
  const srcJson = join(bundledHarnessDir, 'hooks', 'hooks.json');
12
34
  if (!existsSync(srcJson)) return null;
@@ -20,7 +42,9 @@ export function projectHooks({ targetDir, bundledHarnessDir }) {
20
42
  for (const [event, groups] of Object.entries(parsed.hooks || {})) {
21
43
  const kept = [];
22
44
  for (const group of groups) {
23
- const hooks = (group.hooks || []).filter((h) => h.type === 'command');
45
+ const hooks = (group.hooks || [])
46
+ .filter((h) => h.type === 'command')
47
+ .map((h) => ({ ...h, command: toCodexCommand(h.command) }));
24
48
  if (hooks.length) kept.push({ ...group, hooks });
25
49
  }
26
50
  if (kept.length) out[event] = kept;
@@ -39,11 +39,12 @@ export function projectCodexArtifacts({
39
39
  owned.push(ownedPath);
40
40
  };
41
41
 
42
- // AGENTS.md — exact copy of CLAUDE.md, with the CLAUDE.md backup-if-no-git rule.
43
- collect(projectAgentsMd({ targetDir, bundledClaudeMd, projectHasGit }), 'AGENTS.md');
42
+ // AGENTS.md — CLAUDE.md copied byte-for-byte + a generated Codex runtime adapter derived from
43
+ // the bundled harness inventory (see agents-md.mjs), with the CLAUDE.md backup-if-no-git rule.
44
+ collect(projectAgentsMd({ targetDir, bundledClaudeMd, bundledHarnessDir, projectHasGit }), 'AGENTS.md');
44
45
  // .agents/skills — full-rebuilt mirror of the installed (already ship-filtered) .claude/skills.
45
46
  collect(projectSkills({ targetDir, projectHasGit }), '.agents/skills');
46
- // .codex/agents — copy the shipped, pre-generated *.toml (see gen-agents.mjs).
47
+ // .codex/agents — copy the shipped, pre-authored *.toml (see docs/architecture/harness/authoring-agents.md).
47
48
  collect(copyCodexAgents({ targetDir, bundledHarnessDir, projectHasGit }), '.codex/agents');
48
49
  // .codex/hooks.json — Claude hooks.json filtered to type:"command".
49
50
  collect(projectHooks({ targetDir, bundledHarnessDir }), '.codex/hooks.json');
@@ -19,8 +19,13 @@ export function buildCodexPluginJson(fm, { hasHooks = false } = {}) {
19
19
  return out;
20
20
  }
21
21
 
22
- // The repo marketplace catalog Codex reads at .agents/plugins/marketplace.json.
23
- // Each plugin is a local source; the first keyword (if any) seeds a category.
22
+ // The repo marketplace catalog Codex reads at .agents/plugins/marketplace.json. Each plugin is a
23
+ // LOCAL source (source.path relative to the catalog root) carrying an explicit install policy:
24
+ // · installation AVAILABLE — offered, not force-installed (INSTALLED_BY_DEFAULT / NOT_AVAILABLE
25
+ // are the other documented tokens; these plugins are opt-in).
26
+ // · authentication NONE — they bundle only skills + hooks, no credentials or external auth.
27
+ // The first keyword (if any) seeds a category. (Enum tokens per codex-support.md → Open Questions;
28
+ // AVAILABLE/NONE are the correct pair for an opt-in, no-auth plugin.)
24
29
  export function buildCodexMarketplace(plugins) {
25
30
  return {
26
31
  name: 'nurixlabs',
@@ -29,7 +34,8 @@ export function buildCodexMarketplace(plugins) {
29
34
  .sort((a, b) => a.name.localeCompare(b.name))
30
35
  .map((p) => ({
31
36
  name: p.name,
32
- source: `./plugins/${p.name}`,
37
+ source: { path: `./plugins/${p.name}` },
38
+ policy: { installation: 'AVAILABLE', authentication: 'NONE' },
33
39
  ...(p.frontmatter?.keywords?.length ? { category: p.frontmatter.keywords[0] } : {}),
34
40
  })),
35
41
  };
@@ -1,6 +1,6 @@
1
1
  // packages/cli/lib/codex/subagents.mjs
2
- // Copy the harness's pre-generated Codex subagent TOML — bundle codex/agents/*.toml,
3
- // authored on a Codex-authenticated laptop by gen-agents.mjs — into the project's
2
+ // Copy the harness's pre-authored Codex subagent TOML — bundle codex/agents/*.toml,
3
+ // authored per docs/architecture/harness/authoring-agents.md — into the project's
4
4
  // .codex/agents/. Full rebuild each run. The consumer needs NO Codex CLI: the TOML
5
5
  // ships ready, so a missing local Codex never blocks an install.
6
6
 
@@ -0,0 +1,126 @@
1
+ // packages/cli/lib/codex/validate-codex-agents.mjs
2
+ // The Codex-twin validation gate — decoupled from any generator. A `<name>.codex.toml`
3
+ // twin is now authored by the AI following docs/architecture/harness/authoring-agents.md,
4
+ // so this is the check that keeps a malformed or Claude-only twin from shipping: the twins
5
+ // are copied UNPARSED into the bundle (build-data.js) and a consumer's .codex/agents/, so
6
+ // nothing else catches a bad one before Codex fails to load it in that repo. Run it after
7
+ // authoring/editing a twin (the authoring-agents.md checklist mandates it) and, ideally, as
8
+ // a fail-loud publish gate.
9
+ //
10
+ // Checks each twin for: valid TOML (python3 tomllib when present), the required keys
11
+ // (name/description/developer_instructions), and runtime-neutrality (no Claude-only dispatch
12
+ // primitives or fixed model tiers). Also flags a source agent with NO twin (out of sync).
13
+ //
14
+ // Usage:
15
+ // node packages/cli/lib/codex/validate-codex-agents.mjs # shared catalog + all harnesses
16
+ // node packages/cli/lib/codex/validate-codex-agents.mjs --quiet # errors only
17
+ // Exits 0 when every twin is valid, 1 otherwise.
18
+
19
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
20
+ import { dirname, join, resolve, basename } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { spawnSync } from 'node:child_process';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const repoRoot = resolve(__dirname, '..', '..', '..', '..');
26
+ const quiet = process.argv.slice(2).includes('--quiet');
27
+
28
+ // Every source agent .md and its expected twin path: the shared catalog plus every harness's
29
+ // owned agents. The .md is the authoritative set — a listed agent with no twin is out of sync.
30
+ function collectAgents() {
31
+ const out = [];
32
+ const push = (dir) => {
33
+ if (!existsSync(dir)) return;
34
+ for (const f of readdirSync(dir)) {
35
+ if (!f.endsWith('.md')) continue;
36
+ const name = basename(f, '.md');
37
+ out.push({ name, mdPath: join(dir, f), tomlPath: join(dir, `${name}.codex.toml`) });
38
+ }
39
+ };
40
+ push(join(repoRoot, 'agents'));
41
+ const harnessDir = join(repoRoot, 'harness');
42
+ if (existsSync(harnessDir)) {
43
+ for (const harness of readdirSync(harnessDir)) {
44
+ if (harness === 'common') continue; // not a harness — the shared root-files folder
45
+ push(join(harnessDir, harness, 'agents'));
46
+ }
47
+ }
48
+ return out.sort((a, b) => a.tomlPath.localeCompare(b.tomlPath));
49
+ }
50
+
51
+ // TOML parse via python3's tomllib when present; '' = valid, a message = invalid, null =
52
+ // python3 unavailable (the key + runtime-neutral regex checks still run, so the gate degrades
53
+ // gracefully rather than passing blind — but CI/macOS both ship python3, so parse runs there).
54
+ function tomlError(toml) {
55
+ const res = spawnSync('python3', ['-c', 'import tomllib,sys; tomllib.loads(sys.stdin.read())'], {
56
+ input: toml,
57
+ encoding: 'utf8',
58
+ });
59
+ if (res.error && res.error.code === 'ENOENT') return null;
60
+ return res.status === 0 ? '' : (res.stderr || 'invalid TOML').trim();
61
+ }
62
+
63
+ // A Codex twin runs under Codex, which has neither the Task tool nor Claude model aliases. The
64
+ // md body is copied verbatim into developer_instructions, so a hit here is a hit in the SOURCE
65
+ // agent. The dropped-tools comment lists bare names (`Task`, `Skill`) which are fine — only the
66
+ // prose forms (`Task tool`/`Skill tool`) and model-tier aliases are rejected.
67
+ const CLAUDE_ONLY_PATTERNS = [
68
+ { re: /Task tool/i, label: '"Task tool" (name the runtime-neutral outcome, not the Claude tool)' },
69
+ { re: /Skill tool/i, label: '"Skill tool" (refer to the skill by name, not the Claude tool)' },
70
+ { re: /subagent_type/i, label: '`subagent_type` (a Claude Agent-tool parameter)' },
71
+ { re: /\bhaiku\b/i, label: '"haiku" (a fixed Claude model tier — say "cheapest capable tier")' },
72
+ { re: /\bopus\b/i, label: '"opus" (a fixed Claude model tier — say "strongest tier")' },
73
+ { re: /\bsonnet\b/i, label: '"sonnet" (a fixed Claude model tier)' },
74
+ ];
75
+
76
+ const REQUIRED_KEYS = ['name', 'description', 'developer_instructions'];
77
+
78
+ // Return the list of problems with one twin (empty ⇒ valid).
79
+ function validateTwin(tomlPath) {
80
+ const problems = [];
81
+ const toml = readFileSync(tomlPath, 'utf8');
82
+ for (const key of REQUIRED_KEYS) {
83
+ if (!new RegExp(`^\\s*${key}\\s*=`, 'm').test(toml)) problems.push(`missing \`${key} =\` key`);
84
+ }
85
+ const err = tomlError(toml);
86
+ if (err) problems.push(`invalid TOML: ${err.split('\n').pop().slice(0, 160)}`);
87
+ for (const { re, label } of CLAUDE_ONLY_PATTERNS) {
88
+ if (re.test(toml)) problems.push(`Claude-only primitive: ${label}`);
89
+ }
90
+ return problems;
91
+ }
92
+
93
+ function main() {
94
+ const agents = collectAgents();
95
+ if (!agents.length) {
96
+ console.error('validate-codex-agents: no agents/*.md found.');
97
+ process.exit(1);
98
+ }
99
+ let ok = 0;
100
+ const failures = [];
101
+ for (const { name, tomlPath } of agents) {
102
+ if (!existsSync(tomlPath)) {
103
+ failures.push(`${name}: no twin at ${relPath(tomlPath)} (source .md has no .codex.toml — out of sync)`);
104
+ continue;
105
+ }
106
+ const problems = validateTwin(tomlPath);
107
+ if (problems.length) {
108
+ failures.push(`${relPath(tomlPath)}:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
109
+ } else {
110
+ ok += 1;
111
+ if (!quiet) console.log(` ok ${relPath(tomlPath)}`);
112
+ }
113
+ }
114
+ if (failures.length) {
115
+ console.error(`\nvalidate-codex-agents: ${failures.length} invalid, ${ok} ok of ${agents.length} twin(s):\n`);
116
+ for (const f of failures) console.error(` ✗ ${f}`);
117
+ process.exit(1);
118
+ }
119
+ console.log(`\nvalidate-codex-agents: all ${ok} twin(s) valid.`);
120
+ }
121
+
122
+ function relPath(p) {
123
+ return p.startsWith(repoRoot) ? p.slice(repoRoot.length + 1) : p;
124
+ }
125
+
126
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.22",
3
+ "version": "0.19.23",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "build:data": "node scripts/build-data.js",
23
23
  "prepack": "node scripts/build-data.js",
24
- "gen:codex-agents": "node lib/codex/gen-agents.mjs"
24
+ "validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
25
25
  },
26
26
  "repository": {
27
27
  "type": "git",