@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.
- package/README.md +6 -5
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +10 -0
- package/data/harnesses/code/agents/brainstorm.md +11 -11
- package/data/harnesses/code/agents/change-review.md +4 -4
- package/data/harnesses/code/agents/maintenance.md +2 -1
- package/data/harnesses/code/agents/retro.md +45 -0
- package/data/harnesses/code/codex/agents/brainstorm.toml +11 -11
- package/data/harnesses/code/codex/agents/change-review.toml +4 -5
- package/data/harnesses/code/codex/agents/maintenance.toml +2 -1
- package/data/harnesses/code/codex/agents/retro.toml +43 -0
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/skills/documentation/SKILL.md +1 -0
- package/data/harnesses/code/skills/documentation/templates/refinement-ledger.md +33 -0
- package/data/harnesses/code/skills/documentation/templates/structure.md +2 -0
- package/data/harnesses/etna/CLAUDE.md +2 -0
- package/data/harnesses/etna/agents/brainstorm.md +11 -11
- package/data/harnesses/etna/agents/change-review.md +4 -4
- package/data/harnesses/etna/agents/maintenance.md +2 -1
- package/data/harnesses/etna/codex/agents/brainstorm.toml +11 -11
- package/data/harnesses/etna/codex/agents/change-review.toml +4 -5
- package/data/harnesses/etna/codex/agents/maintenance.toml +2 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/SKILL.md +1 -0
- package/data/harnesses/etna/skills/documentation/templates/refinement-ledger.md +33 -0
- package/data/harnesses/etna/skills/documentation/templates/structure.md +2 -0
- package/data/harnesses/hr/harness.json +1 -1
- package/lib/codex/README.md +17 -7
- package/lib/codex/agents-md.mjs +85 -6
- package/lib/codex/hooks.mjs +25 -1
- package/lib/codex/index.mjs +4 -3
- package/lib/codex/plugin-manifest.mjs +9 -3
- package/lib/codex/subagents.mjs +2 -2
- package/lib/codex/validate-codex-agents.mjs +126 -0
- package/package.json +2 -2
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
@@ -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.)
|
package/lib/codex/README.md
CHANGED
|
@@ -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` (
|
|
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"
|
|
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
|
|
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/
|
|
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
|
-
`
|
|
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).
|
package/lib/codex/agents-md.mjs
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// packages/cli/lib/codex/agents-md.mjs
|
|
2
|
-
// CLAUDE.md → AGENTS.md —
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/codex/hooks.mjs
CHANGED
|
@@ -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 || [])
|
|
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;
|
package/lib/codex/index.mjs
CHANGED
|
@@ -39,11 +39,12 @@ export function projectCodexArtifacts({
|
|
|
39
39
|
owned.push(ownedPath);
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
// AGENTS.md —
|
|
43
|
-
|
|
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-
|
|
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
|
-
//
|
|
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
|
};
|
package/lib/codex/subagents.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/cli/lib/codex/subagents.mjs
|
|
2
|
-
// Copy the harness's pre-
|
|
3
|
-
// authored
|
|
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.
|
|
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
|
-
"
|
|
24
|
+
"validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
|
|
25
25
|
},
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|