@nurix/etna 0.19.22 → 0.19.24
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/.vscode/settings.json +9 -0
- package/data/harnesses/code/CLAUDE.md +16 -1
- 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 +4 -3
- 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/rules/database.md +29 -1
- package/data/harnesses/code/skills/documentation/SKILL.md +1 -0
- package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +3 -1
- 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/code/skills/test-plan/SKILL.md +82 -0
- package/data/harnesses/discover/.vscode/settings.json +9 -0
- package/data/harnesses/discover/harness.json +1 -1
- package/data/harnesses/discussion/.vscode/settings.json +9 -0
- package/data/harnesses/discussion/harness.json +2 -2
- package/data/harnesses/discussion/rules/artifacts.md +1 -1
- package/data/harnesses/discussion/rules/method.md +2 -0
- package/data/harnesses/etna/.vscode/settings.json +9 -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 +4 -3
- 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/scripts/doc-lint.mjs +3 -1
- 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/.vscode/settings.json +9 -0
- package/data/harnesses/hr/harness.json +2 -2
- package/data/harnesses/kb-agi/.vscode/settings.json +9 -0
- package/data/harnesses/kb-agi/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
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.24",
|
|
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",
|
package/lib/codex/gen-agents.mjs
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
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 harness/*/agents/*.md with --include-harness) 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-harness # + harness/*/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 includeHarness = args.includes('--include-harness');
|
|
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 harness-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 (includeHarness) {
|
|
73
|
-
const harnessDir = join(repoRoot, 'harness');
|
|
74
|
-
if (existsSync(harnessDir)) {
|
|
75
|
-
for (const harness of readdirSync(harnessDir)) {
|
|
76
|
-
if (harness === 'common') continue; // not a harness — the shared root-files folder
|
|
77
|
-
const agentsDir = join(harnessDir, harness, 'agents');
|
|
78
|
-
if (!existsSync(agentsDir)) continue;
|
|
79
|
-
for (const f of readdirSync(agentsDir)) {
|
|
80
|
-
if (f.endsWith('.md')) files.push(join(agentsDir, f));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return files.sort();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// A regen is due when forced, the toml is missing, or the md is newer than the toml.
|
|
89
|
-
function needsRegen(mdPath, tomlPath) {
|
|
90
|
-
if (force || !existsSync(tomlPath)) return true;
|
|
91
|
-
return statSync(mdPath).mtimeMs > statSync(tomlPath).mtimeMs;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Strip an accidental ```toml fence if the model wrapped its output despite the prompt.
|
|
95
|
-
function stripFences(text) {
|
|
96
|
-
const trimmed = text.trim();
|
|
97
|
-
const fence = trimmed.match(/^```(?:toml)?\n([\s\S]*?)\n```$/);
|
|
98
|
-
return (fence ? fence[1] : trimmed) + '\n';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Validate that the produced TOML parses. Uses python3's tomllib when present
|
|
102
|
-
// (returns true/error string); returns null when python3 is unavailable so the
|
|
103
|
-
// caller proceeds best-effort rather than hard-failing on a missing dev tool.
|
|
104
|
-
function tomlError(toml) {
|
|
105
|
-
const res = spawnSync('python3', ['-c', 'import tomllib,sys; tomllib.loads(sys.stdin.read())'], {
|
|
106
|
-
input: toml,
|
|
107
|
-
encoding: 'utf8',
|
|
108
|
-
});
|
|
109
|
-
if (res.error && res.error.code === 'ENOENT') return null;
|
|
110
|
-
return res.status === 0 ? '' : (res.stderr || 'invalid TOML').trim();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function convert(mdPath) {
|
|
114
|
-
const md = readFileSync(mdPath, 'utf8');
|
|
115
|
-
const res = spawnSync('codex', ['exec', '--sandbox', 'read-only', CONVERSION_PROMPT], {
|
|
116
|
-
input: md,
|
|
117
|
-
encoding: 'utf8',
|
|
118
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
119
|
-
});
|
|
120
|
-
if (res.status !== 0) {
|
|
121
|
-
throw new Error(`codex exec failed for ${mdPath}: ${(res.stderr || '').slice(-400)}`);
|
|
122
|
-
}
|
|
123
|
-
const toml = stripFences(res.stdout || '');
|
|
124
|
-
if (!/^\s*name\s*=/m.test(toml)) {
|
|
125
|
-
throw new Error(`codex output for ${mdPath} has no \`name =\` key — refusing to write. Got:\n${toml.slice(0, 200)}`);
|
|
126
|
-
}
|
|
127
|
-
const err = tomlError(toml);
|
|
128
|
-
if (err) {
|
|
129
|
-
throw new Error(`codex output for ${mdPath} is not valid TOML — refusing to write:\n${err.slice(0, 300)}`);
|
|
130
|
-
}
|
|
131
|
-
return toml;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
assertCodexReady();
|
|
135
|
-
const files = collectAgentFiles();
|
|
136
|
-
if (!files.length) die('no agents/*.md found to convert.');
|
|
137
|
-
|
|
138
|
-
let wrote = 0;
|
|
139
|
-
let skipped = 0;
|
|
140
|
-
for (const mdPath of files) {
|
|
141
|
-
const name = basename(mdPath, '.md');
|
|
142
|
-
const tomlPath = join(dirname(mdPath), `${name}.codex.toml`);
|
|
143
|
-
if (!needsRegen(mdPath, tomlPath)) {
|
|
144
|
-
skipped += 1;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
process.stdout.write(`converting ${name} … `);
|
|
148
|
-
try {
|
|
149
|
-
writeFileSync(tomlPath, convert(mdPath));
|
|
150
|
-
wrote += 1;
|
|
151
|
-
console.log('ok');
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.log('FAILED');
|
|
154
|
-
console.error(` ${e.message}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
console.log(`\ngen-codex-agents: wrote ${wrote}, skipped ${skipped} (up-to-date) of ${files.length} agent(s).`);
|