@nurix/etna 0.19.13 → 0.19.15
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 +2 -2
- package/bin/etna.js +18 -0
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/codex/agents/brainstorm.toml +142 -0
- package/data/harnesses/code/codex/agents/change-review.toml +250 -0
- package/data/harnesses/code/codex/agents/lld-mode.toml +52 -0
- package/data/harnesses/code/codex/agents/maintenance.toml +216 -0
- package/data/harnesses/code/codex/agents/performance-review.toml +222 -0
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/discover/codex/agents/api.toml +33 -0
- package/data/harnesses/discover/codex/agents/codebase.toml +43 -0
- package/data/harnesses/discover/codex/agents/community.toml +31 -0
- package/data/harnesses/discover/codex/agents/deployed-client-bundle.toml +53 -0
- package/data/harnesses/discover/codex/agents/distribution-artifacts.toml +58 -0
- package/data/harnesses/discover/codex/agents/docs.toml +36 -0
- package/data/harnesses/discover/codex/agents/infra-backend-fingerprint.toml +64 -0
- package/data/harnesses/discover/codex/agents/packages.toml +30 -0
- package/data/harnesses/discover/codex/agents/session.toml +41 -0
- package/data/harnesses/discover/codex/agents/website.toml +36 -0
- package/data/harnesses/discover/codex/agents/wire-capture.toml +45 -0
- package/data/harnesses/discover/harness.json +1 -1
- package/data/harnesses/discussion/codex/agents/discussion-baseline.toml +256 -0
- package/data/harnesses/discussion/codex/agents/lessons-maintenance.toml +29 -0
- package/data/harnesses/discussion/harness.json +1 -1
- package/data/harnesses/etna/codex/agents/brainstorm.toml +142 -0
- package/data/harnesses/etna/codex/agents/change-review.toml +250 -0
- package/data/harnesses/etna/codex/agents/lld-mode.toml +52 -0
- package/data/harnesses/etna/codex/agents/maintenance.toml +216 -0
- package/data/harnesses/etna/codex/agents/performance-review.toml +222 -0
- package/data/harnesses/etna/harness.json +1 -1
- package/data/harnesses/kb-agi/codex/agents/kb-apply.toml +32 -0
- package/data/harnesses/kb-agi/codex/agents/kb-audit.toml +47 -0
- package/data/harnesses/kb-agi/codex/agents/kb-init.toml +314 -0
- package/data/harnesses/kb-agi/harness.json +1 -1
- package/lib/codex/README.md +31 -0
- package/lib/codex/agents-md.mjs +35 -0
- package/lib/codex/gen-agents.mjs +156 -0
- package/lib/codex/hooks.mjs +33 -0
- package/lib/codex/index.mjs +62 -0
- package/lib/codex/plugin-manifest.mjs +36 -0
- package/lib/codex/skills.mjs +26 -0
- package/lib/codex/subagents.mjs +26 -0
- package/package.json +4 -2
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# dropped Claude tools: Read, Glob, Grep, Write, Bash
|
|
2
|
+
name = "kb-init"
|
|
3
|
+
description = "Initializes a kb-agi vault by scaffolding the meta/ control plane — the contract, genre templates and intent lanes generated from the identity-interview answers in the dispatch prompt, the gap ledger, sessions and proposals scaffolds — plus a skeleton Readme.md in every content folder lacking one. Use on a fresh vault, or when the router reports meta/contract.md missing. Re-run to check contract drift. Triggers on \"initialize the vault\", \"init kb-agi\", \"scaffold meta\", \"set up the control plane\"."
|
|
4
|
+
developer_instructions = '''
|
|
5
|
+
You are the **kb-init** agent. One job: stand up the `meta/` control plane a kb-agi vault needs, generate its reader-specific parts from the identity interview, then get out of the way. Every template you need is carried inline below — fully self-contained.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
The router conducts the identity interview (you cannot ask the user questions) and dispatches you with:
|
|
10
|
+
|
|
11
|
+
1. **Vault name and domain** — what this vault is about, in a sentence.
|
|
12
|
+
2. **Reader types** — for each: a short name, what they come for, and a default answer depth (crisp / working / deep).
|
|
13
|
+
3. **Primary intents** — 3–6 "I want to…" lanes for the intent index.
|
|
14
|
+
|
|
15
|
+
If any of these are missing from your dispatch prompt, stop and report exactly what the router must collect first. Do not invent an identity.
|
|
16
|
+
|
|
17
|
+
## Boundaries
|
|
18
|
+
|
|
19
|
+
- You write `meta/` and skeleton folder `Readme.md` files only. **Never** `CLAUDE.md` (ships with the harness; if absent, stop — the harness isn't installed), **never** `meta/_derived/` (built by `kb-export`), **never** content notes.
|
|
20
|
+
- **Never overwrite an existing file.** Create only what's missing; report every file created vs skipped.
|
|
21
|
+
- Every write ends in a git commit.
|
|
22
|
+
|
|
23
|
+
## Procedure
|
|
24
|
+
|
|
25
|
+
1. **Installed?** If `CLAUDE.md` is missing at the vault root, stop: the kb-agi harness isn't installed here.
|
|
26
|
+
2. **Already initialized?** If `meta/contract.md` exists, do not re-scaffold. Compare its `contract-version` against the template's below: same → report "initialized, current" and stop; older → report the drift and summarize what changed between the shapes, so a human can fold it in; never overwrite.
|
|
27
|
+
3. **Write the control plane** from the Templates section, filling every `{placeholder}` from your inputs: `meta/contract.md`, one `meta/genres/<type>.md` per reader type, `meta/index.md` (one lane per primary intent + the standing "add knowledge" lane), `meta/gaps.md`, `meta/sessions/README.md`, `meta/proposals/README.md`.
|
|
28
|
+
4. **Scaffold folder self-descriptions.** For every content folder (any folder outside `meta/` and dot-folders that holds `.md` notes) lacking a `Readme.md`: generate one in the skeleton shape below **from the folder's actual contents** — list what's really there, infer the About line, pick the most-linked note(s) as entry notes. Keep the review flag line; the audit's descriptions goal re-surfaces it until a human edits.
|
|
29
|
+
5. **Commit** the scaffold (one commit, message `kb-init: scaffold meta/ control plane`).
|
|
30
|
+
6. **Build the graph:** run `node .claude/scripts/kb-export.mjs` from the vault root.
|
|
31
|
+
7. **Hand off.** Point the user at what they own: `meta/contract.md` (read it), `meta/genres/*.md` (tune the generated framings), and every generated folder `Readme.md` (review the skeletons). Then: ask the vault a question, or run `kb-audit`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Templates
|
|
36
|
+
|
|
37
|
+
Fences are 4-backtick so inner 3-backtick blocks survive — strip the outer fence when writing each file. Fill `{placeholders}`; leave everything else byte-exact.
|
|
38
|
+
|
|
39
|
+
## `meta/contract.md`
|
|
40
|
+
|
|
41
|
+
````markdown
|
|
42
|
+
---
|
|
43
|
+
contract-version: 1.0.0
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<!-- contract-version is the contract-SCHEMA version: it changes only when the shape of this
|
|
47
|
+
contract changes (new field, new section semantics), never in step with the kb-agi plugin
|
|
48
|
+
version, which bumps automatically on any payload edit. Do not sync the two. -->
|
|
49
|
+
|
|
50
|
+
# {Vault name} — the contract
|
|
51
|
+
|
|
52
|
+
This vault is about: {domain, one sentence}.
|
|
53
|
+
|
|
54
|
+
This file is the single contract the router (`CLAUDE.md`) and the agents (`kb-audit`, `kb-apply`, `kb-init`) all read first. It defines what a note is, what the frontmatter hints mean, how links resolve, how a folder describes itself, and who may write what. Everything here is a **hint or a convention, not a schema enforced by code** — a note that omits a hint still works; the system degrades gracefully.
|
|
55
|
+
|
|
56
|
+
## The note model
|
|
57
|
+
|
|
58
|
+
The vault is one flat Obsidian vault. Every note is the same kind of thing — a markdown note linked to others by `[[wikilinks]]`. No document-type labels, no audience altitude. Folders exist for human convenience and describe themselves via a `Readme.md`.
|
|
59
|
+
|
|
60
|
+
- **One idea per note.** Push depth into a _linked_ note instead of bloating one; the links carry the depth.
|
|
61
|
+
- **Links are the structure.** The wikilink graph is the one graph, derived from the notes into `meta/_derived/graph.json` by `kb-export` — never hand-maintained.
|
|
62
|
+
- **Notes are the only canon.** Everything under `meta/` is control plane; the derived graph and every index are regenerable.
|
|
63
|
+
|
|
64
|
+
## Frontmatter hints
|
|
65
|
+
|
|
66
|
+
All fields optional; a missing field means its default.
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
---
|
|
70
|
+
title: Human-readable title # default: the H1, else the filename
|
|
71
|
+
maturity: synthesized # raw | captured | synthesized | published — default: captured
|
|
72
|
+
canonical: true # THE current note on its topic? default: true
|
|
73
|
+
visibility: public # public | hidden — default: public
|
|
74
|
+
---
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| maturity | meaning |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `raw` | dumped in, unprocessed — a capture, a transcript, a paste |
|
|
80
|
+
| `captured` | cleaned up and readable, not yet reconciled with neighbours |
|
|
81
|
+
| `synthesized` | reconciled into the vault's point of view, linked to its neighbours |
|
|
82
|
+
| `published` | load-bearing, citeable as the vault's current position |
|
|
83
|
+
|
|
84
|
+
`canonical: false` marks a superseded-but-kept note — still reachable, treated as background. When a note supersedes another, the demotion lands **in the same commit** as the successor.
|
|
85
|
+
|
|
86
|
+
`visibility: hidden` keeps a note in the vault and the graph but marks it for exclusion from any servable corpus (see Handover). An answer may draw on a hidden note only with explicit disclosure — never presented as the vault's position to an external-facing reader.
|
|
87
|
+
|
|
88
|
+
## Wikilink dialect
|
|
89
|
+
|
|
90
|
+
Four forms, resolved against the note set (code blocks are never links):
|
|
91
|
+
|
|
92
|
+
| form | example | resolves to |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| link | `[[note]]` | the note |
|
|
95
|
+
| alias | `[[note\|shown text]]` | the note; alias is display-only |
|
|
96
|
+
| anchor | `[[note#Heading]]` | the note (heading is a hint) |
|
|
97
|
+
| embed | `![[note]]` | the note, transcluded |
|
|
98
|
+
|
|
99
|
+
Resolution: an exact path (`[[specs/sso]]`) wins; else a unique basename; a basename shared by several notes is **ambiguous** — an error the shape goal surfaces. Keep titles and basenames unique.
|
|
100
|
+
|
|
101
|
+
## Folder self-description — `Readme.md`
|
|
102
|
+
|
|
103
|
+
Every content folder carries a `Readme.md`: the folder's landing page for humans **and** the checklist the coverage goal audits. Fixed shape:
|
|
104
|
+
|
|
105
|
+
```markdown
|
|
106
|
+
# <folder name>
|
|
107
|
+
|
|
108
|
+
**About.** One or two sentences: what this folder holds and why.
|
|
109
|
+
|
|
110
|
+
**Should contain.** The kinds of notes that belong here — the coverage checklist.
|
|
111
|
+
|
|
112
|
+
**Entry notes.** The 1–3 notes to start from. (`[[wikilinks]]`)
|
|
113
|
+
|
|
114
|
+
**Neighbours.** Folders this one links to most.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
It describes the folder rather than holding an idea; losing one costs orientation, not knowledge.
|
|
118
|
+
|
|
119
|
+
## Note shape
|
|
120
|
+
|
|
121
|
+
- One idea per note; a note pushing past ~150 lines is a split candidate (depth moves to linked notes).
|
|
122
|
+
- Unique titles and basenames — collisions break resolution and retrieval; `kb-export` reports them.
|
|
123
|
+
- Frontmatter must parse (plain `key: value` scalars).
|
|
124
|
+
|
|
125
|
+
## Who writes what
|
|
126
|
+
|
|
127
|
+
| actor | reads | writes |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| router, ask path | everything | `meta/sessions/` only |
|
|
130
|
+
| router, contribute path | everything | content notes — a human steering every write |
|
|
131
|
+
| `kb-audit` | everything | `meta/proposals/`, `meta/gaps.md`, session `processed` flags — never canon |
|
|
132
|
+
| `kb-apply` | one open proposal | exactly that proposal's items (canon + entry points), then the proposal's status |
|
|
133
|
+
| `kb-init` | — | the `meta/` scaffold + skeleton folder `Readme.md`s, once |
|
|
134
|
+
| `kb-export` | every note | `meta/_derived/graph.json` only |
|
|
135
|
+
|
|
136
|
+
Every agent mutation of canon flows **audit → proposal → human review → apply → git commit**. Every write, by anyone, is a git commit.
|
|
137
|
+
|
|
138
|
+
## Deployment model
|
|
139
|
+
|
|
140
|
+
The vault is a git repository; **one writer per clone**, sharing via push/pull. Session logs, proposals, and gap-ledger updates are commits like everything else. Session files carry a collision-safe suffix (see `meta/sessions/README.md`). This model is declared, not enforced — if you need concurrent writers in one checkout, you need a different harness.
|
|
141
|
+
|
|
142
|
+
## Genre — the reader, not the document
|
|
143
|
+
|
|
144
|
+
`genre` labels **who is asking** — never a note. Its only effect is the default depth and framing of answers, per `meta/genres/`. It never changes which notes exist or are reachable.
|
|
145
|
+
|
|
146
|
+
## The control plane
|
|
147
|
+
|
|
148
|
+
| path | what it is |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `meta/contract.md` | this file |
|
|
151
|
+
| `meta/genres/` | reader-type templates — default depth + entry points |
|
|
152
|
+
| `meta/index.md` | "Read by need" — intent-keyed entry lanes |
|
|
153
|
+
| `meta/gaps.md` | the gap ledger — the living content roadmap (owned by `kb-audit`) |
|
|
154
|
+
| `meta/sessions/` | query logs — every ask, and whether it could be answered |
|
|
155
|
+
| `meta/proposals/` | upkeep findings awaiting review, and the applied record |
|
|
156
|
+
| `meta/_derived/` | the derived graph — regenerate with `node .claude/scripts/kb-export.mjs`, never edit |
|
|
157
|
+
|
|
158
|
+
## Handover — this vault as a RAG corpus
|
|
159
|
+
|
|
160
|
+
The repo is the index package; an ingesting system should read it as:
|
|
161
|
+
|
|
162
|
+
| artifact | RAG role |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| content notes | the chunks — atomic, titled, author-bounded |
|
|
165
|
+
| note frontmatter | metadata filters (`canonical`, `maturity`); `visibility: hidden` → exclude from the servable corpus |
|
|
166
|
+
| `meta/_derived/graph.json` (`schema: kb-graph/1`, additive-only evolution) | graph expansion and re-ranking |
|
|
167
|
+
| folder `Readme.md` files | curated cluster summaries |
|
|
168
|
+
| `meta/index.md` | intent → entry-point routing hints |
|
|
169
|
+
| `meta/gaps.md` | known-unanswerable list — answer "recorded gap" instead of hallucinating |
|
|
170
|
+
| this file | the ingestion spec |
|
|
171
|
+
|
|
172
|
+
A downstream system may write its own misses into `meta/sessions/` in the standard format — the coverage goal ingests them exactly like the router's, and the vault grows in response.
|
|
173
|
+
````
|
|
174
|
+
|
|
175
|
+
## `meta/genres/<type>.md` — one per reader type, generated
|
|
176
|
+
|
|
177
|
+
Generate one file per reader type from the interview, following this shape exactly; write real content from what the reader wants, never placeholder text:
|
|
178
|
+
|
|
179
|
+
````markdown
|
|
180
|
+
# genre — {Reader type}
|
|
181
|
+
|
|
182
|
+
{Who this reader is and what they come to the vault for — one or two sentences, from the interview.}
|
|
183
|
+
|
|
184
|
+
**Default depth.** {crisp | working | deep} — {how an answer should open for this reader, one sentence.}
|
|
185
|
+
|
|
186
|
+
**Framing.** {What to lead with and what to cite for this reader, one or two sentences.}
|
|
187
|
+
|
|
188
|
+
**Start from.**
|
|
189
|
+
|
|
190
|
+
- `meta/index.md` → {the lanes matching this reader's intents}
|
|
191
|
+
- {the kinds of notes to lead with, e.g. "canonical, published positioning notes"}
|
|
192
|
+
|
|
193
|
+
**Adjust.** {When to go deeper or compress; when to hand off toward another genre's depth.}
|
|
194
|
+
````
|
|
195
|
+
|
|
196
|
+
## `meta/index.md`
|
|
197
|
+
|
|
198
|
+
Create one `##` lane per primary intent from the interview (replacing the example lane), each with the italic empty-marker line, then the standing final lane verbatim:
|
|
199
|
+
|
|
200
|
+
````markdown
|
|
201
|
+
# index — Read by need
|
|
202
|
+
|
|
203
|
+
The front door, keyed on **what you're trying to do**, not who you are. Each lane lists entry notes; the links carry you from there. Kept current by the entrances goal of `kb-audit`. An empty lane is a known gap, not an error.
|
|
204
|
+
|
|
205
|
+
## "{I want to …}"
|
|
206
|
+
|
|
207
|
+
_{one-line description of the intent}_
|
|
208
|
+
|
|
209
|
+
- _empty — populated as content lands_
|
|
210
|
+
|
|
211
|
+
## "I want to add knowledge"
|
|
212
|
+
|
|
213
|
+
_You have something the vault doesn't hold yet._
|
|
214
|
+
|
|
215
|
+
- `meta/gaps.md` — what the vault is missing and wants next
|
|
216
|
+
- `meta/contract.md` — the note model and where things belong
|
|
217
|
+
````
|
|
218
|
+
|
|
219
|
+
## `meta/gaps.md`
|
|
220
|
+
|
|
221
|
+
````markdown
|
|
222
|
+
# gaps — the content roadmap
|
|
223
|
+
|
|
224
|
+
The living ledger of what the vault should hold but doesn't. **Written only by `kb-audit`'s coverage goal**, which folds in verified session gaps and folder-promise diffs, deduplicates, ranks (reader-hit above folder-promise), and checks off entries reality now covers. Readers and contributors: read this, don't edit it — landing a note that closes a gap is how an entry gets checked off.
|
|
225
|
+
|
|
226
|
+
Entry format: `- [ ] {what's missing} — {folder it belongs in} — {why: session | promise | both}`
|
|
227
|
+
|
|
228
|
+
## Content gaps
|
|
229
|
+
|
|
230
|
+
_none recorded yet_
|
|
231
|
+
|
|
232
|
+
## Navigation gaps
|
|
233
|
+
|
|
234
|
+
_Knowledge that exists but can't be reached from the entry lanes — link work, not writing work._
|
|
235
|
+
|
|
236
|
+
_none recorded yet_
|
|
237
|
+
````
|
|
238
|
+
|
|
239
|
+
## `meta/sessions/README.md`
|
|
240
|
+
|
|
241
|
+
````markdown
|
|
242
|
+
# sessions — query logs
|
|
243
|
+
|
|
244
|
+
Every ask the router handles is logged here: it lets a reader resume later, and a question the vault **couldn't** answer feeds the gap ledger. This is the only thing the read-only ask path writes. A downstream system (e.g. a RAG service over this vault) may write its misses here in the same format — they're ingested identically.
|
|
245
|
+
|
|
246
|
+
One file per session, named `YYYY-MM-DD-<slug>-<hhmm>.md` (the time suffix keeps clones collision-safe):
|
|
247
|
+
|
|
248
|
+
```markdown
|
|
249
|
+
---
|
|
250
|
+
date: 2026-07-10
|
|
251
|
+
genre: pm # or unknown
|
|
252
|
+
answered: partial # yes | partial | no
|
|
253
|
+
processed: false # flipped by kb-audit's coverage goal once folded into the ledger
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Q: <the question, in the asker's words>
|
|
257
|
+
|
|
258
|
+
**Depth taken.** <crisp | working | deep>
|
|
259
|
+
|
|
260
|
+
**Notes used.** [[note-a]], [[note-b]]
|
|
261
|
+
|
|
262
|
+
**Gap.** <only if answered is partial/no, and only after searching the whole vault: what's truly
|
|
263
|
+
missing (content gap) — or, if the answer existed but wasn't reachable from the entry lanes,
|
|
264
|
+
say so (navigation gap).>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`processed: true` sessions have been folded into `meta/gaps.md` and may be pruned by a human.
|
|
268
|
+
````
|
|
269
|
+
|
|
270
|
+
## `meta/proposals/README.md`
|
|
271
|
+
|
|
272
|
+
````markdown
|
|
273
|
+
# proposals — upkeep findings awaiting review
|
|
274
|
+
|
|
275
|
+
Every `kb-audit` run writes its findings as one proposal file here; nothing changes in the vault until a human reviews it and `kb-apply` executes it. This folder is both the review queue (`status: open`) and the applied record (`status: applied`).
|
|
276
|
+
|
|
277
|
+
One file per audit run, named `YYYY-MM-DD-<goals>-<hhmm>.md`:
|
|
278
|
+
|
|
279
|
+
```markdown
|
|
280
|
+
---
|
|
281
|
+
status: open # open | applied
|
|
282
|
+
date: 2026-07-10
|
|
283
|
+
goals: [coverage, graph]
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Health
|
|
287
|
+
|
|
288
|
+
answered (last 10 sessions): 7 yes / 2 partial / 1 no · open gaps: 4 (2 new) · dead links: 3 · orphans: 1 · collisions: 0
|
|
289
|
+
|
|
290
|
+
## Items
|
|
291
|
+
|
|
292
|
+
- [ ] coverage · write missing note — `product/`: "pricing-tiers" (reader-hit ×2 + folder promise; link from [[positioning]])
|
|
293
|
+
- [ ] graph · fix dead link — [[roadmap]] in `product/positioning.md` → renamed to [[product-roadmap]]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Review = edit this file.** Delete any line you veto; what remains is the mandate. `kb-apply` executes the surviving unchecked items exactly, checks them off, flips `status: applied`, and commits with this file referenced. Items an agent couldn't complete stay unchecked with a note appended.
|
|
297
|
+
````
|
|
298
|
+
|
|
299
|
+
## `<content folder>/Readme.md` — skeleton, per folder lacking one
|
|
300
|
+
|
|
301
|
+
````markdown
|
|
302
|
+
# {folder name}
|
|
303
|
+
|
|
304
|
+
> review: generated skeleton — a human should edit this description. (`kb-audit` re-surfaces this flag until edited.)
|
|
305
|
+
|
|
306
|
+
**About.** {inferred from the notes actually present}
|
|
307
|
+
|
|
308
|
+
**Should contain.** {the kinds of notes observed here — this is the coverage checklist; extend it with what SHOULD be here}
|
|
309
|
+
|
|
310
|
+
**Entry notes.** {the 1–3 most-linked or most central notes, as [[wikilinks]]}
|
|
311
|
+
|
|
312
|
+
**Neighbours.** {folders these notes link to most}
|
|
313
|
+
````
|
|
314
|
+
'''
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# `lib/codex` — Claude→Codex projection
|
|
2
|
+
|
|
3
|
+
Turns a harness's already-materialized Claude artifacts into their OpenAI Codex counterparts, so one harnessed repo works under **both** Claude Code and Codex. Consumed by both shippers — the installer (`bin/etna.js`) and the marketplace publisher (`.github/scripts/publish.mjs`) — through one module, so the two channels can't drift (the same "keep in agreement" rule as the `evals/`/`fixtures/` ship-filter).
|
|
4
|
+
|
|
5
|
+
Owning doc: [`docs/features/codex-support.md`](../../../../docs/features/codex-support.md).
|
|
6
|
+
|
|
7
|
+
## What it emits
|
|
8
|
+
|
|
9
|
+
| Source | Target | File |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `CLAUDE.md` | `AGENTS.md` (exact copy) | `agents-md.mjs` |
|
|
12
|
+
| `.claude/skills/` | `.agents/skills/` (mirror) | `skills.mjs` |
|
|
13
|
+
| bundle `codex/agents/*.toml` | `.codex/agents/*.toml` (copy) | `subagents.mjs` |
|
|
14
|
+
| `hooks.json` | `.codex/hooks.json` (`type:"command"` only) | `hooks.mjs` |
|
|
15
|
+
| `.claude-plugin/*` | `.codex-plugin/plugin.json` + `.agents/plugins/marketplace.json` | `plugin-manifest.mjs` |
|
|
16
|
+
|
|
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
|
+
|
|
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
|
+
|
|
21
|
+
## Subagents are authored, not derived at install
|
|
22
|
+
|
|
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:
|
|
24
|
+
|
|
25
|
+
```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-plugins # + plugins/*/agents/*.md
|
|
29
|
+
```
|
|
30
|
+
|
|
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).
|
|
@@ -0,0 +1,35 @@
|
|
|
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.
|
|
5
|
+
|
|
6
|
+
import { existsSync, lstatSync, renameSync, cpSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
function isSymlink(path) {
|
|
10
|
+
try {
|
|
11
|
+
return lstatSync(path).isSymbolicLink();
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function projectAgentsMd({ targetDir, bundledClaudeMd, projectHasGit }) {
|
|
18
|
+
if (!existsSync(bundledClaudeMd)) return null;
|
|
19
|
+
const dest = join(targetDir, 'AGENTS.md');
|
|
20
|
+
const existed = existsSync(dest) || isSymlink(dest);
|
|
21
|
+
let notice = null;
|
|
22
|
+
let warning = null;
|
|
23
|
+
if (existed) {
|
|
24
|
+
if (projectHasGit) {
|
|
25
|
+
notice = 'Overwrote existing AGENTS.md (tracked by git — `git checkout AGENTS.md` to restore).';
|
|
26
|
+
} else {
|
|
27
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
28
|
+
const backup = `${dest}.bak.${ts}`;
|
|
29
|
+
renameSync(dest, backup);
|
|
30
|
+
warning = `No .git here, so no version-control safety net — backed up existing AGENTS.md → ${backup}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
cpSync(bundledClaudeMd, dest, { force: true });
|
|
34
|
+
return { installed: 'AGENTS.md → ./', notice, warning };
|
|
35
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// packages/cli/lib/codex/gen-agents.mjs
|
|
2
|
+
// AUTHORING-TIME generator — runs ON A LAPTOP with the Codex CLI installed and
|
|
3
|
+
// authenticated, NEVER in CI. For each Claude subagent (agents/<name>.md in the
|
|
4
|
+
// shared catalog, plus plugins/*/agents/*.md with --include-plugins) it runs
|
|
5
|
+
// `codex exec` to translate the markdown into an OpenAI Codex subagent TOML and
|
|
6
|
+
// writes a committed sibling agents/<name>.codex.toml. The shippers (build-data.js,
|
|
7
|
+
// publish.mjs) then carry those committed files; the installer copies them into a
|
|
8
|
+
// consumer's .codex/agents/ with no Codex CLI required downstream.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node packages/cli/lib/codex/gen-agents.mjs # shared catalog, only stale/missing
|
|
12
|
+
// node packages/cli/lib/codex/gen-agents.mjs --force # regenerate all shared agents
|
|
13
|
+
// node packages/cli/lib/codex/gen-agents.mjs --include-plugins # + plugins/*/agents/*.md
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
statSync,
|
|
21
|
+
} from 'node:fs';
|
|
22
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const repoRoot = resolve(__dirname, '..', '..', '..', '..');
|
|
28
|
+
|
|
29
|
+
// Pinned conversion prompt — deterministic instructions so re-runs stay stable.
|
|
30
|
+
// Codex does the semantic md→toml; the body is copied verbatim, tools are dropped
|
|
31
|
+
// (no Codex equivalent) and recorded in a leading comment.
|
|
32
|
+
const CONVERSION_PROMPT = [
|
|
33
|
+
'Convert the Claude Code subagent definition provided on stdin into an OpenAI Codex subagent TOML document.',
|
|
34
|
+
'Output ONLY the raw TOML — no prose, no markdown code fences, nothing before or after.',
|
|
35
|
+
'Set `name` from the frontmatter `name`.',
|
|
36
|
+
'Set `description` as a TOML basic string (double quotes) from the frontmatter `description`, escaping any interior double-quote as \\".',
|
|
37
|
+
'Put the ENTIRE markdown body (everything after the closing frontmatter ---) VERBATIM into a TOML multi-line',
|
|
38
|
+
"LITERAL string delimited by triple SINGLE quotes: developer_instructions = '''<body>'''.",
|
|
39
|
+
'A literal string is verbatim — do NOT escape anything inside it, and do NOT use triple double-quotes (backslashes like \\s in the body must survive unchanged).',
|
|
40
|
+
'If frontmatter `model` is a real model id, set `model`; if it is `inherit`, omit `model`.',
|
|
41
|
+
'If frontmatter has a `tools:` line, do NOT translate it — instead put a single comment line',
|
|
42
|
+
'`# dropped Claude tools: <the list>` as the first line of the file.',
|
|
43
|
+
'Emit no keys other than name, description, model (optional), developer_instructions.',
|
|
44
|
+
].join(' ');
|
|
45
|
+
|
|
46
|
+
const args = process.argv.slice(2);
|
|
47
|
+
const force = args.includes('--force');
|
|
48
|
+
const includePlugins = args.includes('--include-plugins');
|
|
49
|
+
|
|
50
|
+
function die(msg) {
|
|
51
|
+
console.error(`gen-codex-agents: ${msg}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Codex must be present + authenticated — this step cannot run in CI.
|
|
56
|
+
function assertCodexReady() {
|
|
57
|
+
const which = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
58
|
+
if (which.status !== 0) {
|
|
59
|
+
die('the `codex` CLI is not installed / not on PATH. This generator runs only on an authoring laptop.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Every agents/<name>.md to convert: the shared catalog, plus plugin-owned agents on request.
|
|
64
|
+
function collectAgentFiles() {
|
|
65
|
+
const files = [];
|
|
66
|
+
const shared = join(repoRoot, 'agents');
|
|
67
|
+
if (existsSync(shared)) {
|
|
68
|
+
for (const f of readdirSync(shared)) {
|
|
69
|
+
if (f.endsWith('.md')) files.push(join(shared, f));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (includePlugins) {
|
|
73
|
+
const pluginsDir = join(repoRoot, 'plugins');
|
|
74
|
+
if (existsSync(pluginsDir)) {
|
|
75
|
+
for (const plugin of readdirSync(pluginsDir)) {
|
|
76
|
+
const agentsDir = join(pluginsDir, plugin, 'agents');
|
|
77
|
+
if (!existsSync(agentsDir)) continue;
|
|
78
|
+
for (const f of readdirSync(agentsDir)) {
|
|
79
|
+
if (f.endsWith('.md')) files.push(join(agentsDir, f));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return files.sort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A regen is due when forced, the toml is missing, or the md is newer than the toml.
|
|
88
|
+
function needsRegen(mdPath, tomlPath) {
|
|
89
|
+
if (force || !existsSync(tomlPath)) return true;
|
|
90
|
+
return statSync(mdPath).mtimeMs > statSync(tomlPath).mtimeMs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Strip an accidental ```toml fence if the model wrapped its output despite the prompt.
|
|
94
|
+
function stripFences(text) {
|
|
95
|
+
const trimmed = text.trim();
|
|
96
|
+
const fence = trimmed.match(/^```(?:toml)?\n([\s\S]*?)\n```$/);
|
|
97
|
+
return (fence ? fence[1] : trimmed) + '\n';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate that the produced TOML parses. Uses python3's tomllib when present
|
|
101
|
+
// (returns true/error string); returns null when python3 is unavailable so the
|
|
102
|
+
// caller proceeds best-effort rather than hard-failing on a missing dev tool.
|
|
103
|
+
function tomlError(toml) {
|
|
104
|
+
const res = spawnSync('python3', ['-c', 'import tomllib,sys; tomllib.loads(sys.stdin.read())'], {
|
|
105
|
+
input: toml,
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
});
|
|
108
|
+
if (res.error && res.error.code === 'ENOENT') return null;
|
|
109
|
+
return res.status === 0 ? '' : (res.stderr || 'invalid TOML').trim();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function convert(mdPath) {
|
|
113
|
+
const md = readFileSync(mdPath, 'utf8');
|
|
114
|
+
const res = spawnSync('codex', ['exec', '--sandbox', 'read-only', CONVERSION_PROMPT], {
|
|
115
|
+
input: md,
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
118
|
+
});
|
|
119
|
+
if (res.status !== 0) {
|
|
120
|
+
throw new Error(`codex exec failed for ${mdPath}: ${(res.stderr || '').slice(-400)}`);
|
|
121
|
+
}
|
|
122
|
+
const toml = stripFences(res.stdout || '');
|
|
123
|
+
if (!/^\s*name\s*=/m.test(toml)) {
|
|
124
|
+
throw new Error(`codex output for ${mdPath} has no \`name =\` key — refusing to write. Got:\n${toml.slice(0, 200)}`);
|
|
125
|
+
}
|
|
126
|
+
const err = tomlError(toml);
|
|
127
|
+
if (err) {
|
|
128
|
+
throw new Error(`codex output for ${mdPath} is not valid TOML — refusing to write:\n${err.slice(0, 300)}`);
|
|
129
|
+
}
|
|
130
|
+
return toml;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
assertCodexReady();
|
|
134
|
+
const files = collectAgentFiles();
|
|
135
|
+
if (!files.length) die('no agents/*.md found to convert.');
|
|
136
|
+
|
|
137
|
+
let wrote = 0;
|
|
138
|
+
let skipped = 0;
|
|
139
|
+
for (const mdPath of files) {
|
|
140
|
+
const name = basename(mdPath, '.md');
|
|
141
|
+
const tomlPath = join(dirname(mdPath), `${name}.codex.toml`);
|
|
142
|
+
if (!needsRegen(mdPath, tomlPath)) {
|
|
143
|
+
skipped += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(`converting ${name} … `);
|
|
147
|
+
try {
|
|
148
|
+
writeFileSync(tomlPath, convert(mdPath));
|
|
149
|
+
wrote += 1;
|
|
150
|
+
console.log('ok');
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.log('FAILED');
|
|
153
|
+
console.error(` ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log(`\ngen-codex-agents: wrote ${wrote}, skipped ${skipped} (up-to-date) of ${files.length} agent(s).`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// packages/cli/lib/codex/hooks.mjs
|
|
2
|
+
// .claude/hooks/hooks.json → .codex/hooks.json. Same JSON shape Codex documents;
|
|
3
|
+
// filtered to type:"command" (the only hook type Codex executes — "prompt"/"agent"
|
|
4
|
+
// entries are parsed but skipped). A standalone file, not a config.toml [hooks]
|
|
5
|
+
// table, since the settings/config surface is out of scope for Codex here.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export function projectHooks({ targetDir, bundledHarnessDir }) {
|
|
11
|
+
const srcJson = join(bundledHarnessDir, 'hooks', 'hooks.json');
|
|
12
|
+
if (!existsSync(srcJson)) return null;
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(readFileSync(srcJson, 'utf8'));
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const out = {};
|
|
20
|
+
for (const [event, groups] of Object.entries(parsed.hooks || {})) {
|
|
21
|
+
const kept = [];
|
|
22
|
+
for (const group of groups) {
|
|
23
|
+
const hooks = (group.hooks || []).filter((h) => h.type === 'command');
|
|
24
|
+
if (hooks.length) kept.push({ ...group, hooks });
|
|
25
|
+
}
|
|
26
|
+
if (kept.length) out[event] = kept;
|
|
27
|
+
}
|
|
28
|
+
if (!Object.keys(out).length) return null;
|
|
29
|
+
const dest = join(targetDir, '.codex', 'hooks.json');
|
|
30
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
31
|
+
writeFileSync(dest, JSON.stringify({ hooks: out }, null, 2) + '\n');
|
|
32
|
+
return { installed: '.codex/hooks.json' };
|
|
33
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// packages/cli/lib/codex/index.mjs
|
|
2
|
+
// Claude→Codex projection entry. Derives a repo's Codex-native artifacts
|
|
3
|
+
// (AGENTS.md, .agents/skills/, .codex/agents/, .codex/hooks.json) from the
|
|
4
|
+
// just-installed Claude tree + the harness bundle. Consumed by BOTH shippers —
|
|
5
|
+
// the installer (bin/etna.js) and the marketplace publisher (publish.mjs) — so
|
|
6
|
+
// the two distribution channels can never diverge (the same "keep in agreement"
|
|
7
|
+
// discipline as the evals/fixtures ship-filter). MCP and a settings/config.toml
|
|
8
|
+
// equivalent are deliberately OUT of scope (owner decision 2026-07-13); see
|
|
9
|
+
// docs/features/codex-support.md.
|
|
10
|
+
|
|
11
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { projectAgentsMd } from './agents-md.mjs';
|
|
14
|
+
import { projectSkills } from './skills.mjs';
|
|
15
|
+
import { copyCodexAgents } from './subagents.mjs';
|
|
16
|
+
import { projectHooks } from './hooks.mjs';
|
|
17
|
+
|
|
18
|
+
// Project every Codex artifact into targetDir. Returns the same {installed,
|
|
19
|
+
// notices, warnings} accounting shape the installer already collects, plus
|
|
20
|
+
// `owned` — the top-level Codex targets, recorded in harness.json's
|
|
21
|
+
// `owned.codex` so a later version can prune one it stops shipping.
|
|
22
|
+
export function projectCodexArtifacts({
|
|
23
|
+
targetDir,
|
|
24
|
+
bundledHarnessDir,
|
|
25
|
+
bundledClaudeMd,
|
|
26
|
+
projectHasGit,
|
|
27
|
+
priorCodexOwned = [],
|
|
28
|
+
}) {
|
|
29
|
+
const installed = [];
|
|
30
|
+
const notices = [];
|
|
31
|
+
const warnings = [];
|
|
32
|
+
const owned = [];
|
|
33
|
+
|
|
34
|
+
const collect = (result, ownedPath) => {
|
|
35
|
+
if (!result) return;
|
|
36
|
+
if (result.installed) installed.push(result.installed);
|
|
37
|
+
if (result.notice) notices.push(result.notice);
|
|
38
|
+
if (result.warning) warnings.push(result.warning);
|
|
39
|
+
owned.push(ownedPath);
|
|
40
|
+
};
|
|
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');
|
|
44
|
+
// .agents/skills — full-rebuilt mirror of the installed (already ship-filtered) .claude/skills.
|
|
45
|
+
collect(projectSkills({ targetDir, projectHasGit }), '.agents/skills');
|
|
46
|
+
// .codex/agents — copy the shipped, pre-generated *.toml (see gen-agents.mjs).
|
|
47
|
+
collect(copyCodexAgents({ targetDir, bundledHarnessDir, projectHasGit }), '.codex/agents');
|
|
48
|
+
// .codex/hooks.json — Claude hooks.json filtered to type:"command".
|
|
49
|
+
collect(projectHooks({ targetDir, bundledHarnessDir }), '.codex/hooks.json');
|
|
50
|
+
|
|
51
|
+
// Prune a Codex target a prior version owned but this bundle no longer emits.
|
|
52
|
+
for (const rel of priorCodexOwned) {
|
|
53
|
+
if (owned.includes(rel)) continue;
|
|
54
|
+
const target = join(targetDir, rel);
|
|
55
|
+
if (existsSync(target)) {
|
|
56
|
+
rmSync(target, { recursive: true, force: true });
|
|
57
|
+
notices.push(`removed orphaned codex ${rel}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { installed, notices, warnings, owned };
|
|
62
|
+
}
|