@nurix/etna 0.19.92 → 0.20.0

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 (61) hide show
  1. package/bin/etna.js +0 -11
  2. package/data/catalog/rules/frontend.md +1 -1
  3. package/data/catalog/rules/general.md +3 -3
  4. package/data/catalog/rules/nomenclature.md +0 -4
  5. package/data/catalog/rules/pnpm.md +1 -1
  6. package/data/catalog/rules/testing.md +1 -0
  7. package/data/harness-index.json +1 -1
  8. package/data/harnesses/code/CLAUDE.md +14 -14
  9. package/data/harnesses/code/agents/bookkeeper.md +3 -0
  10. package/data/harnesses/code/agents/maintenance.md +3 -14
  11. package/data/harnesses/code/agents/performance-review.md +1 -1
  12. package/data/harnesses/code/codex/agents/performance-review.toml +1 -1
  13. package/data/harnesses/code/harness.json +1 -1
  14. package/data/harnesses/code/hooks/awareness.mjs +5 -169
  15. package/data/harnesses/code/hooks/compliance.mjs +21 -17
  16. package/data/harnesses/code/hooks/lib/ledger.mjs +36 -13
  17. package/data/harnesses/code/hooks/lib/sweep.mjs +172 -0
  18. package/data/harnesses/code/rules/frontend.md +1 -1
  19. package/data/harnesses/code/rules/general.md +3 -3
  20. package/data/harnesses/code/rules/nomenclature.md +0 -4
  21. package/data/harnesses/code/rules/pnpm.md +1 -1
  22. package/data/harnesses/code/rules/testing.md +1 -0
  23. package/data/harnesses/code/skills/boundary/SKILL.md +1 -1
  24. package/data/harnesses/code/skills/documentation/SKILL.md +1 -1
  25. package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +1 -1
  26. package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +12 -8
  27. package/data/harnesses/code/skills/documentation/templates/plan.md +9 -16
  28. package/data/harnesses/code/skills/testing/SKILL.md +1 -2
  29. package/data/harnesses/delivery/agents/maintenance.md +3 -14
  30. package/data/harnesses/delivery/harness.json +1 -1
  31. package/data/harnesses/delivery/hooks/generate-ownership-index.mjs +3 -1
  32. package/data/harnesses/delivery/rules/nomenclature.md +0 -4
  33. package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -1
  34. package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +1 -1
  35. package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +12 -8
  36. package/data/harnesses/delivery/skills/documentation/templates/plan.md +9 -16
  37. package/data/harnesses/etna/agents/maintenance.md +3 -14
  38. package/data/harnesses/etna/agents/performance-review.md +1 -1
  39. package/data/harnesses/etna/codex/agents/performance-review.toml +1 -1
  40. package/data/harnesses/etna/harness.json +1 -1
  41. package/data/harnesses/etna/hooks/generate-ownership-index.mjs +3 -1
  42. package/data/harnesses/etna/rules/general.md +3 -3
  43. package/data/harnesses/etna/rules/nomenclature.md +0 -4
  44. package/data/harnesses/etna/rules/testing.md +1 -0
  45. package/data/harnesses/etna/skills/boundary/SKILL.md +1 -1
  46. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -1
  47. package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +1 -1
  48. package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +12 -8
  49. package/data/harnesses/etna/skills/documentation/templates/plan.md +9 -16
  50. package/lib/codex/agents-md.mjs +1 -35
  51. package/lib/codex/hooks.mjs +0 -29
  52. package/lib/install/materialize.mjs +4 -34
  53. package/lib/install/plan.mjs +54 -10
  54. package/package.json +3 -3
  55. package/data/harnesses/code/hooks/check-bookkeeping-clean.mjs +0 -28
  56. package/data/harnesses/code/hooks/check-changelog-presence.mjs +0 -36
  57. package/data/harnesses/code/hooks/check-completion-integrity.mjs +0 -72
  58. package/data/harnesses/code/hooks/check-ownership.mjs +0 -55
  59. package/lib/codex/index.mjs +0 -63
  60. package/lib/codex/plugin-manifest.mjs +0 -42
  61. package/lib/codex/subagents.mjs +0 -26
@@ -45,7 +45,7 @@ export const PLUGIN_SUBDIRS = ['skills', 'agents', 'rules', 'scripts', 'hooks'];
45
45
  export const PRUNABLE_SUBDIRS = ['skills', 'agents', 'rules', 'scripts'];
46
46
 
47
47
  // Codex targets rebuilt wholesale on every install rather than merged, so a refresh must delete
48
- // them first. Recorded in harness.json's `owned.codex` so a later version can prune one it drops.
48
+ // them first.
49
49
  export const CODEX_REBUILT_DIRS = ['.agents/skills', '.codex/agents'];
50
50
 
51
51
  // Every file under dir, as posix paths relative to it. Posix regardless of host, because these
@@ -165,7 +165,49 @@ function codexHooks(bundleDir) {
165
165
  * Throws when the bundle has no CLAUDE.md: that is the one file every harness must carry, and a
166
166
  * harness without a policy is not a harness.
167
167
  */
168
- export function planHarnessFiles(bundleDir) {
168
+ // The one capability-gated rule (owner ruling 2026-08-15): `cloudflare` ships only where a
169
+ // Worker exists — wrangler.jsonc/wrangler.toml at the target root or one directory level deep
170
+ // (a monorepo's worker package), node_modules excluded. Every other rule ships everywhere by
171
+ // decision. Detection re-runs on every refresh, so `owned.rules` records the filtered truth and
172
+ // the prune/add diff converges a repo whose Worker appears or disappears.
173
+ const CAPABILITY_RULES = { 'cloudflare.md': targetHasWorkerConfig };
174
+
175
+ function targetHasWorkerConfig(targetDir) {
176
+ const names = ['wrangler.jsonc', 'wrangler.toml'];
177
+ if (names.some((n) => existsSync(join(targetDir, n)))) return true;
178
+ let entries = [];
179
+ try {
180
+ entries = readdirSync(targetDir, { withFileTypes: true });
181
+ } catch {
182
+ return false;
183
+ }
184
+ for (const e of entries) {
185
+ if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
186
+ if (names.some((n) => existsSync(join(targetDir, e.name, n)))) return true;
187
+ // one more level: packages/*/wrangler.jsonc — the common monorepo shape
188
+ let sub = [];
189
+ try {
190
+ sub = readdirSync(join(targetDir, e.name), { withFileTypes: true });
191
+ } catch {
192
+ continue;
193
+ }
194
+ for (const s of sub) {
195
+ if (!s.isDirectory() || s.name === 'node_modules' || s.name.startsWith('.')) continue;
196
+ if (names.some((n) => existsSync(join(targetDir, e.name, s.name, n)))) return true;
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+
202
+ // A rule the target's capabilities exclude. No targetDir (payload verification, byte-identity
203
+ // tests) means no filtering — the plan then describes the full bundle.
204
+ function ruleExcluded(sub, rel, targetDir) {
205
+ if (!targetDir || sub !== 'rules') return false;
206
+ const gate = CAPABILITY_RULES[rel];
207
+ return !!gate && !gate(targetDir);
208
+ }
209
+
210
+ export function planHarnessFiles(bundleDir, targetDir = null) {
169
211
  const claudeMdPath = join(bundleDir, 'CLAUDE.md');
170
212
  if (!existsSync(claudeMdPath)) {
171
213
  throw new Error(`Bundled CLAUDE.md missing at ${claudeMdPath}. Rebuild package data.`);
@@ -177,8 +219,13 @@ export function planHarnessFiles(bundleDir) {
177
219
 
178
220
  // The component subtrees. This is the mapping the whole module exists for: a bundle says
179
221
  // `skills/design/SKILL.md`, a project says `.claude/skills/design/SKILL.md`.
222
+ const excludedRules = [];
180
223
  for (const sub of PLUGIN_SUBDIRS) {
181
224
  for (const rel of filesUnder(join(bundleDir, sub))) {
225
+ if (ruleExcluded(sub, rel, targetDir)) {
226
+ excludedRules.push(rel);
227
+ continue;
228
+ }
182
229
  files.push({
183
230
  path: `.claude/${sub}/${rel}`,
184
231
  content: readFileSync(join(bundleDir, sub, rel), 'utf8'),
@@ -191,7 +238,10 @@ export function planHarnessFiles(bundleDir) {
191
238
  // installer, so if these are not in the payload a downloaded project has no Codex support at
192
239
  // all and no way to generate it.
193
240
  const owned = bundledOwned(bundleDir);
194
- const codexOwned = [];
241
+ if (excludedRules.length) owned.rules = owned.rules.filter((r) => !excludedRules.includes(r));
242
+ const capabilityNotes = excludedRules.map(
243
+ (r) => `Rule ${r} not installed — no wrangler.jsonc/wrangler.toml found (capability-gated; a refresh after one appears adds it).`
244
+ );
195
245
 
196
246
  const adapter = buildCodexAdapter(bundleDir);
197
247
  const separator = claudeMd.endsWith('\n') ? '\n' : '\n\n';
@@ -200,7 +250,6 @@ export function planHarnessFiles(bundleDir) {
200
250
  content: adapter ? claudeMd + separator + adapter : claudeMd,
201
251
  policy: 'own',
202
252
  });
203
- codexOwned.push('AGENTS.md');
204
253
 
205
254
  // `.agents/skills/` is Codex's official scan path and a byte mirror of `.claude/skills/`. Both
206
255
  // runtimes require exactly name+description in SKILL.md frontmatter, so the mirrored files are
@@ -214,7 +263,6 @@ export function planHarnessFiles(bundleDir) {
214
263
  policy: 'own',
215
264
  });
216
265
  }
217
- codexOwned.push('.agents/skills');
218
266
  }
219
267
 
220
268
  const tomls = listByExt(join(bundleDir, 'codex', 'agents'), '.toml');
@@ -226,17 +274,13 @@ export function planHarnessFiles(bundleDir) {
226
274
  policy: 'own',
227
275
  });
228
276
  }
229
- codexOwned.push('.codex/agents');
230
277
  }
231
278
 
232
279
  const hooks = codexHooks(bundleDir);
233
280
  if (hooks) {
234
281
  files.push({ path: '.codex/hooks.json', content: hooks, policy: 'own' });
235
- codexOwned.push('.codex/hooks.json');
236
282
  }
237
283
 
238
- owned.codex = codexOwned;
239
-
240
284
  // The marker, at the path the CLI writes and the maintenance agent resolves the installed
241
285
  // version from — never at the bundle root, where nothing reads it.
242
286
  const markerPath = join(bundleDir, 'harness.json');
@@ -267,5 +311,5 @@ export function planHarnessFiles(bundleDir) {
267
311
  seen.add(file.path);
268
312
  }
269
313
 
270
- return { files, owned, meta };
314
+ return { files, owned, meta, capabilityNotes };
271
315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.92",
3
+ "version": "0.20.0",
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": {
@@ -19,7 +19,7 @@
19
19
  "node": ">=18.0.0"
20
20
  },
21
21
  "scripts": {
22
- "test": "node --test 'tests/**/*.test.mjs'",
22
+ "test": "node --test 'tests/**/*.test.mjs' '../../harness/code/hooks/tests/*.test.mjs'",
23
23
  "build:data": "node scripts/build-data.js",
24
24
  "prepack": "node scripts/build-data.js",
25
25
  "validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "a1cf8eb35f57"
45
+ "etnaFingerprint": "067206dbe15a"
46
46
  }
@@ -1,28 +0,0 @@
1
- // harness/code/hooks/check-bookkeeping-clean.mjs — CI backstop: no unresolved bookkeeping rows
2
- // (auto:/note:/needs-session:) may reach the remote. waived: rows pass — they are the recorded,
3
- // PR-reviewable exceptions. Exit 1 when unresolved rows exist.
4
- //
5
- // THIS CHECK IS GLOBAL AND STAYS GLOBAL. The commit gate scopes to one commit's staged set so
6
- // concurrent agents in one checkout do not wall each other; CI judges the branch as a whole, where
7
- // there is no staged set to scope by and every row has had every commit to be drained in.
8
-
9
- import { readQueue, countRows } from "./lib/state.mjs";
10
-
11
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
12
- const counts = countRows(readQueue(projectDir));
13
-
14
- if (counts.unresolved === 0) {
15
- console.log(`check-bookkeeping-clean: OK (${counts.waived} waived row(s))`);
16
- process.exit(0);
17
- }
18
- console.error(
19
- `check-bookkeeping-clean: ${counts.unresolved} unresolved row(s) in docs/operations/bookkeeping.md ` +
20
- `(${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session, ` +
21
- `${counts.expiredWaived} expired-waived). ` +
22
- "Drain them (bookkeeper agent), resolve them, or convert to waived: with reason + expires=. " +
23
- (counts.expiredWaived > 0
24
- ? "An expired waiver is a deferral that ran out: re-judge whether its reason still holds — " +
25
- "re-waive with a fresh date only if it does, and never by default."
26
- : "")
27
- );
28
- process.exit(1);
@@ -1,36 +0,0 @@
1
- // harness/code/hooks/check-changelog-presence.mjs — CI backstop: the commit under test (HEAD)
2
- // carries a docs/history/changelog/ entry unless it is exempt by the changelog rule's closed list,
3
- // judged mechanically: lockfile-only, harness-file-only, pure renames, changelog housekeeping,
4
- // or a bot author. Exit 1 when a non-exempt HEAD stages no entry.
5
-
6
- import { changelogExemptFile } from "./lib/exempt.mjs";
7
- import { git as gitAt } from "./lib/repo.mjs";
8
-
9
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
10
- const git = (args) => gitAt(projectDir, args);
11
-
12
- const nameStatus = git(["diff-tree", "--root", "--no-commit-id", "--name-status", "-r", "HEAD"]) || "";
13
- const entries = nameStatus
14
- .split("\n")
15
- .filter(Boolean)
16
- .map((line) => {
17
- const [status, ...files] = line.split("\t");
18
- return { status, file: files[files.length - 1] };
19
- });
20
-
21
-
22
- const author = git(["log", "-1", "--format=%an <%ae>"]) || "";
23
- const isBot = /\[bot\]|actions@github\.com/i.test(author);
24
- const allRenames = entries.length > 0 && entries.every((e) => e.status.startsWith("R"));
25
- const allExempt = entries.length === 0 || entries.every((e) => changelogExemptFile(e.file));
26
- const hasEntry = entries.some((e) => e.file.startsWith("docs/history/changelog/") && e.file.endsWith(".md"));
27
-
28
- if (isBot || allRenames || allExempt || hasEntry) {
29
- console.log("check-changelog-presence: OK");
30
- process.exit(0);
31
- }
32
- console.error(
33
- "check-changelog-presence: HEAD is a non-exempt commit carrying no docs/history/changelog/ entry. " +
34
- "Every non-exempt commit ships its entry inside it (exempt: release/bot, harness installs and bumps, lockfile-only, pure renames, changelog housekeeping)."
35
- );
36
- process.exit(1);
@@ -1,72 +0,0 @@
1
- // harness/code/hooks/check-completion-integrity.mjs — CI backstop for plan completion. Scope:
2
- // plans in the Gates: format only (legacy plans predate the contract and are history). A Complete
3
- // plan must show a fully discharged surface: no unchecked Done Criteria boxes, the Spec'd LLD
4
- // flipped active, and the behavioral checklist present. An Abandoned plan must reference the
5
- // backlog (undischarged obligations outlive the frozen file there). Exit 1 on any violation.
6
-
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { frontmatter, field } from "./lib/ownership.mjs";
10
- import { undischargedRows } from "./lib/ledger.mjs";
11
- import { readSafe } from "./lib/repo.mjs";
12
-
13
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
- const plansDir = path.join(projectDir, "docs", "history", "plans");
15
-
16
- let failures = 0;
17
- const fail = (msg) => {
18
- failures += 1;
19
- console.error(`check-completion-integrity: ${msg}`);
20
- };
21
-
22
- let plans = [];
23
- try {
24
- plans = fs.readdirSync(plansDir).filter((f) => f.endsWith(".md"));
25
- } catch (e) {
26
- plans = [];
27
- }
28
-
29
- for (const file of plans) {
30
- const text = readSafe(path.join(plansDir, file));
31
- if (!/^\*\*Gates:\*\*/m.test(text)) continue; // legacy plan — out of contract
32
- const status = (/^\*\*Status:\*\*\s*(\S+)/m.exec(text) || [])[1];
33
-
34
- if (status === "Complete") {
35
- const unchecked = (text.match(/^- \[ \]/gm) || []).length;
36
- if (unchecked > 0) fail(`${file} is Complete with ${unchecked} unchecked Done Criteria box(es)`);
37
-
38
- // The ledger is discharged per row, in the file — a Done Criteria checkbox asserting "every
39
- // row discharged" is exactly the checkbox-nothing-reads failure the doc ledger existed to fix.
40
- const rows = undischargedRows(text);
41
- if (rows.length > 0) {
42
- fail(`${file} is Complete with ${rows.length} Test Obligations row(s) missing a discharge outcome (#${rows.join(", #")})`);
43
- }
44
-
45
- // EVERY Spec line, not the first. A plan may implement against several feature docs, and a
46
- // single-match parse silently exempts all but one — the same singular assumption the Doc
47
- // Obligations ledger exists to break, reintroduced here where nothing would surface it.
48
- const specs = [...text.matchAll(/^\*\*Spec:\*\*\s*\[[^\]]*\]\(([^)]+)\)/gm)].map((m) => m[1]);
49
- for (const spec of specs) {
50
- const specPath = path.resolve(plansDir, spec);
51
- const specText = readSafe(specPath);
52
- const specStatus = (field(frontmatter(specText), "status") || "").toString();
53
- if (specText && specStatus !== "active") {
54
- fail(`${file} is Complete but its Spec LLD ${spec} is status: ${specStatus || "unknown"} (expected active)`);
55
- }
56
- const feature = path.basename(specPath, ".md");
57
- if (!fs.existsSync(path.join(projectDir, "docs", "tests", `${feature}.md`))) {
58
- fail(`${file} is Complete but docs/tests/${feature}.md (the behavioral checklist) is missing`);
59
- }
60
- }
61
- }
62
-
63
- if (status === "Abandoned" && !/backlog/i.test(text)) {
64
- fail(`${file} is Abandoned without referencing the backlog — undischarged obligations must outlive the frozen plan`);
65
- }
66
- }
67
-
68
- if (failures === 0) {
69
- console.log("check-completion-integrity: OK");
70
- process.exit(0);
71
- }
72
- process.exit(1);
@@ -1,55 +0,0 @@
1
- // harness/code/hooks/check-ownership.mjs — CI backstop for docs/features/ ownership. Two checks,
2
- // one walk of the tree:
3
- //
4
- // 1. dead-glob — every owns: glob matches at least one file
5
- // 2. ownership-overlap — no two feature docs claim a common file at EQUAL specificity, which
6
- // leaves "most specific wins" unable to pick an owner
7
- //
8
- // Either is silenced by an unexpired `waived:` row in docs/operations/bookkeeping.md naming its
9
- // kind plus the offending glob (dead-glob) or the shared path (ownership-overlap) in backticks.
10
- // Exit 1 on anything unwaived.
11
- //
12
- // A glob at strictly HIGHER specificity carving a sub-path out of a broader `**` is the designed
13
- // layering, not a conflict — only an exact tie is reported.
14
-
15
- import { deadGlobs, listRepoFiles, overlaps } from "./lib/ownership.mjs";
16
- import { readQueue, waiverCovers } from "./lib/state.mjs";
17
-
18
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
19
- const today = new Date().toISOString().slice(0, 10);
20
-
21
- const files = listRepoFiles(projectDir);
22
- const queue = readQueue(projectDir);
23
-
24
- const dead = deadGlobs(projectDir, files);
25
- const unwaivedDead = dead.filter((d) => !waiverCovers(queue, "dead-glob", d.glob, today));
26
-
27
- const conflicts = overlaps(projectDir, files);
28
- const unwaivedOverlaps = conflicts.filter((c) => !waiverCovers(queue, "ownership-overlap", c.example, today));
29
-
30
- if (unwaivedDead.length === 0 && unwaivedOverlaps.length === 0) {
31
- console.log(
32
- `check-ownership: OK (${dead.length} dead glob(s), ${conflicts.length} equal-specificity overlap(s), all waived or none)`,
33
- );
34
- process.exit(0);
35
- }
36
-
37
- if (unwaivedDead.length > 0) {
38
- console.error(`check-ownership: ${unwaivedDead.length} unwaived dead owns: glob(s):`);
39
- for (const d of unwaivedDead) console.error(` ${d.glob} → ${d.doc}`);
40
- console.error("Fix the owning doc's owns: frontmatter, or add a `waived:` dead-glob row naming the glob in backticks, with reason + expires=.");
41
- }
42
-
43
- if (unwaivedOverlaps.length > 0) {
44
- console.error(`check-ownership: ${unwaivedOverlaps.length} unwaived equal-specificity overlap(s):`);
45
- for (const c of unwaivedOverlaps) {
46
- console.error(` ${c.example}`);
47
- console.error(` ${c.a.glob} → ${c.a.doc}`);
48
- console.error(` ${c.b.glob} → ${c.b.doc}`);
49
- }
50
- console.error(
51
- "Narrow one doc's glob so a single owner wins, or add a `waived:` ownership-overlap row naming the shared path in backticks with reason + expires=.",
52
- );
53
- }
54
-
55
- process.exit(1);
@@ -1,63 +0,0 @@
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 — 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');
45
- // .agents/skills — full-rebuilt mirror of the installed (already ship-filtered) .claude/skills.
46
- collect(projectSkills({ targetDir, projectHasGit }), '.agents/skills');
47
- // .codex/agents — copy the shipped, pre-authored *.toml (see docs/architecture/harness/authoring-agents.md).
48
- collect(copyCodexAgents({ targetDir, bundledHarnessDir, projectHasGit }), '.codex/agents');
49
- // .codex/hooks.json — Claude hooks.json filtered to type:"command".
50
- collect(projectHooks({ targetDir, bundledHarnessDir }), '.codex/hooks.json');
51
-
52
- // Prune a Codex target a prior version owned but this bundle no longer emits.
53
- for (const rel of priorCodexOwned) {
54
- if (owned.includes(rel)) continue;
55
- const target = join(targetDir, rel);
56
- if (existsSync(target)) {
57
- rmSync(target, { recursive: true, force: true });
58
- notices.push(`removed orphaned codex ${rel}`);
59
- }
60
- }
61
-
62
- return { installed, notices, warnings, owned };
63
- }
@@ -1,42 +0,0 @@
1
- // packages/cli/lib/codex/plugin-manifest.mjs
2
- // Pure builders for the Codex plugin + marketplace manifests, emitted by the
3
- // marketplace shipper (.github/scripts/publish.mjs) alongside the Claude ones.
4
- // A Codex plugin reuses the same skills/ and hooks/ dirs as a Claude plugin — only
5
- // the manifest differs (.codex-plugin/plugin.json) — plus a Codex marketplace
6
- // catalog (.agents/plugins/marketplace.json). Codex also reads the legacy
7
- // .claude-plugin/marketplace.json but for DISCOVERY only, so native ones are still emitted.
8
-
9
- // The required Codex plugin.json fields + component pointers. `hasHooks` adds the
10
- // hooks pointer (Codex auto-detects hooks/hooks.json, but naming it is explicit).
11
- export function buildCodexPluginJson(fm, { hasHooks = false } = {}) {
12
- const out = {
13
- name: fm.name,
14
- version: fm.version,
15
- description: fm.description,
16
- skills: './skills/',
17
- };
18
- if (hasHooks) out.hooks = './hooks/hooks.json';
19
- return out;
20
- }
21
-
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.)
29
- export function buildCodexMarketplace(plugins) {
30
- return {
31
- name: 'nurixlabs',
32
- interface: { displayName: 'Nurix Labs' },
33
- plugins: [...plugins]
34
- .sort((a, b) => a.name.localeCompare(b.name))
35
- .map((p) => ({
36
- name: p.name,
37
- source: { path: `./plugins/${p.name}` },
38
- policy: { installation: 'AVAILABLE', authentication: 'NONE' },
39
- ...(p.frontmatter?.keywords?.length ? { category: p.frontmatter.keywords[0] } : {}),
40
- })),
41
- };
42
- }
@@ -1,26 +0,0 @@
1
- // packages/cli/lib/codex/subagents.mjs
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
- // .codex/agents/. Full rebuild each run. The consumer needs NO Codex CLI: the TOML
5
- // ships ready, so a missing local Codex never blocks an install.
6
-
7
- import { existsSync, readdirSync, cpSync, rmSync, mkdirSync } from 'node:fs';
8
- import { join } from 'node:path';
9
-
10
- export function copyCodexAgents({ targetDir, bundledHarnessDir, projectHasGit }) {
11
- const src = join(bundledHarnessDir, 'codex', 'agents');
12
- if (!existsSync(src)) return null;
13
- const tomls = readdirSync(src).filter((f) => f.endsWith('.toml'));
14
- if (!tomls.length) return null;
15
- const dest = join(targetDir, '.codex', 'agents');
16
- if (existsSync(dest)) {
17
- if (!projectHasGit) {
18
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
19
- cpSync(dest, `${dest}.bak.${ts}`, { recursive: true });
20
- }
21
- rmSync(dest, { recursive: true, force: true });
22
- }
23
- mkdirSync(dest, { recursive: true });
24
- for (const f of tomls) cpSync(join(src, f), join(dest, f), { force: true });
25
- return { installed: `${tomls.length} codex agent(s) → .codex/agents/` };
26
- }