@marcomigozzi/trailhead 0.3.0 → 0.5.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.
- package/README.it.md +359 -0
- package/README.md +35 -22
- package/bin/lib/codex-projection.js +421 -0
- package/bin/lib/codex-projection.test.js +266 -0
- package/bin/lib/host-descriptor.js +404 -0
- package/bin/lib/host-descriptor.test.js +172 -0
- package/bin/trailhead.js +518 -67
- package/bin/trailhead.test.js +399 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +8 -3
- package/plugins/trailhead/commands/adopt.md +1 -1
- package/plugins/trailhead/commands/bug.md +1 -1
- package/plugins/trailhead/commands/config.md +1 -1
- package/plugins/trailhead/commands/dashboard.md +1 -1
- package/plugins/trailhead/commands/grill.md +1 -1
- package/plugins/trailhead/commands/idea.md +1 -1
- package/plugins/trailhead/commands/inbox.md +1 -1
- package/plugins/trailhead/commands/map.md +1 -1
- package/plugins/trailhead/commands/new.md +1 -1
- package/plugins/trailhead/commands/note.md +1 -1
- package/plugins/trailhead/commands/pause.md +1 -1
- package/plugins/trailhead/commands/quick.md +3 -1
- package/plugins/trailhead/commands/resume.md +1 -1
- package/plugins/trailhead/commands/seed.md +1 -1
- package/plugins/trailhead/commands/split.md +1 -1
- package/plugins/trailhead/commands/ticket.md +1 -1
- package/plugins/trailhead/commands/todo.md +1 -1
- package/plugins/trailhead/commands/update.md +1 -1
- package/plugins/trailhead/commands/whiteboard.md +1 -1
- package/plugins/trailhead/commands/work.md +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +74 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +83 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +54 -24
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +7 -10
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +19 -4
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +41 -1
- package/plugins/trailhead/skills/{trailhead/references/configuration.md → _shared/configuration-reference.md} +15 -3
- package/plugins/trailhead/skills/_shared/configuration.md +8 -0
- package/plugins/trailhead/skills/_shared/multi-map.md +27 -0
- package/plugins/trailhead/skills/_shared/out-of-scope.md +11 -0
- package/plugins/trailhead/skills/_shared/principles.md +8 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +22 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +159 -0
- package/plugins/trailhead/skills/_shared/substrate.md +35 -0
- package/plugins/trailhead/skills/{trailhead/references → _shared}/teamwork.md +8 -5
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/acceptance-testing.md +3 -2
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/code-review.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +21 -0
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +36 -0
- package/plugins/trailhead/skills/_shared/techniques.md +20 -0
- package/plugins/trailhead/skills/_shared/ticket-anatomy.md +32 -0
- package/plugins/trailhead/skills/_shared/ticket-language.md +3 -0
- package/plugins/trailhead/skills/trailhead/SKILL.md +25 -450
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +28 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-capture}/references/capture.md +5 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +51 -0
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +24 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-chart}/references/inbox.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +29 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +112 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +65 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +52 -0
- package/plugins/trailhead/templates/statusline-trailhead.sh +40 -8
- package/plugins/trailhead/templates/trailhead-commit-msg +57 -0
- package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +0 -24
- package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +0 -18
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/codebase-map.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/debug.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/domain-vocabulary.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/grilling.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/research.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/tdd.md +0 -0
- /package/plugins/trailhead/skills/{trailhead → trailhead-manage}/references/updating.md +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for codex-projection.js. Run: node codex-projection.test.js
|
|
3
|
+
// No framework: plain asserts, mirrors the style of host-descriptor.test.js.
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const {
|
|
6
|
+
codexLayout,
|
|
7
|
+
convertToCodex,
|
|
8
|
+
codexSkillAdapterHeader,
|
|
9
|
+
injectCodexAdapterHeader,
|
|
10
|
+
codexAgentsYaml,
|
|
11
|
+
codexAgentToml,
|
|
12
|
+
codexAgentTomlPlan,
|
|
13
|
+
codexVerbSkillContent,
|
|
14
|
+
codexVerbSkillAgentsYaml,
|
|
15
|
+
codexVerbSkillPlan,
|
|
16
|
+
codexHookEntries,
|
|
17
|
+
enableCodexHooksFeature,
|
|
18
|
+
enableCodexMultiAgentV2Feature,
|
|
19
|
+
} = require('./codex-projection.js');
|
|
20
|
+
|
|
21
|
+
let passed = 0;
|
|
22
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
23
|
+
|
|
24
|
+
// --- convertToCodex: command surface ---
|
|
25
|
+
ok('convertToCodex rewrites /trailhead:work to $trailhead work', convertToCodex('/trailhead:work') === '$trailhead work');
|
|
26
|
+
ok('convertToCodex rewrites bare /trailhead to $trailhead', (() => {
|
|
27
|
+
const out = convertToCodex('run /trailhead now');
|
|
28
|
+
return out.includes('$trailhead') && !out.includes('/trailhead ');
|
|
29
|
+
})());
|
|
30
|
+
ok('convertToCodex leaves bare labels untouched', convertToCodex('label trailhead:build stays').includes('trailhead:build'));
|
|
31
|
+
// Regression: the bare-command rule must NOT eat `/trailhead` inside a path
|
|
32
|
+
// segment (a \b boundary before `/` or `-` used to corrupt these).
|
|
33
|
+
ok('convertToCodex preserves a /trailhead/ path segment',
|
|
34
|
+
convertToCodex('read ~/.codex/skills/trailhead/references/foo.md').includes('/skills/trailhead/references/'));
|
|
35
|
+
ok('convertToCodex preserves a /trailhead- filename',
|
|
36
|
+
convertToCodex('copy templates/trailhead-commit-msg here').includes('templates/trailhead-commit-msg'));
|
|
37
|
+
|
|
38
|
+
// --- convertToCodex: /clear -> /new ---
|
|
39
|
+
ok('convertToCodex rewrites /clear to /new', (() => {
|
|
40
|
+
const out = convertToCodex('run `/clear` now');
|
|
41
|
+
return out.includes('/new') && !out.includes('/clear');
|
|
42
|
+
})());
|
|
43
|
+
|
|
44
|
+
// --- convertToCodex: paths ---
|
|
45
|
+
ok('convertToCodex rewrites ~/.claude/foo', convertToCodex('~/.claude/foo') === '~/.codex/foo');
|
|
46
|
+
ok('convertToCodex rewrites $HOME/.claude', convertToCodex('$HOME/.claude') === '$HOME/.codex');
|
|
47
|
+
ok('convertToCodex rewrites bare .claude/', convertToCodex('see .claude/skills/').includes('.codex/skills/'));
|
|
48
|
+
|
|
49
|
+
// --- convertToCodex: plugin root ---
|
|
50
|
+
ok('convertToCodex rewrites CLAUDE_PLUGIN_ROOT', convertToCodex('${CLAUDE_PLUGIN_ROOT}/templates').includes('~/.codex/skills/trailhead/templates'));
|
|
51
|
+
|
|
52
|
+
// --- codexLayout ---
|
|
53
|
+
const layout = codexLayout('/c');
|
|
54
|
+
ok('codexLayout skillDir', layout.skillDir === '/c/skills/trailhead');
|
|
55
|
+
ok('codexLayout skillMain', layout.skillMain === '/c/skills/trailhead/SKILL.md');
|
|
56
|
+
ok('codexLayout versionFile', layout.versionFile === '/c/skills/trailhead/VERSION');
|
|
57
|
+
ok('codexLayout templatesDir', layout.templatesDir === '/c/skills/trailhead/templates');
|
|
58
|
+
ok('codexLayout agentsYaml', layout.agentsYaml === '/c/skills/trailhead/agents/openai.yaml');
|
|
59
|
+
ok('codexLayout hooksScriptsDir', layout.hooksScriptsDir === '/c/skills/trailhead/hooks');
|
|
60
|
+
ok('codexLayout hooksJson', layout.hooksJson === '/c/hooks.json');
|
|
61
|
+
ok('codexLayout configToml', layout.configToml === '/c/config.toml');
|
|
62
|
+
ok('codexLayout codexAgentsDir', layout.codexAgentsDir === '/c/agents');
|
|
63
|
+
|
|
64
|
+
// --- codexSkillAdapterHeader ---
|
|
65
|
+
const header = codexSkillAdapterHeader();
|
|
66
|
+
ok('codexSkillAdapterHeader has adapter tag', header.includes('<codex_skill_adapter>'));
|
|
67
|
+
ok('codexSkillAdapterHeader mentions request_user_input', header.includes('request_user_input'));
|
|
68
|
+
ok('codexSkillAdapterHeader mentions $trailhead <verb> form', header.includes('$trailhead <verb>'));
|
|
69
|
+
ok('codexSkillAdapterHeader does not mention the legacy hyphen form', !header.includes('/trailhead-<verb>'));
|
|
70
|
+
ok('codexSkillAdapterHeader §D describes native multi_agent spawning',
|
|
71
|
+
header.includes('spawn_agent') && header.includes('multi_agent'));
|
|
72
|
+
ok('codexSkillAdapterHeader §D says subagents inherit the one session model',
|
|
73
|
+
header.includes('inherit the one session model'));
|
|
74
|
+
ok('codexSkillAdapterHeader §D dispatches by agent_type under multi_agent_v2',
|
|
75
|
+
header.includes('agent_type') && header.includes('multi_agent_v2'));
|
|
76
|
+
ok('codexSkillAdapterHeader §D detects the registry by spawn_agent schema introspection',
|
|
77
|
+
header.includes('spawn_agent') && header.includes('schema'));
|
|
78
|
+
ok('codexSkillAdapterHeader §D no longer calls per-subagent model pinning deferred',
|
|
79
|
+
!header.includes('deferred'));
|
|
80
|
+
ok('codexSkillAdapterHeader §D pins the exact agent_type dispatch call shape',
|
|
81
|
+
header.includes('spawn_agent(agent_type="trailhead-<technique>"'));
|
|
82
|
+
ok('codexSkillAdapterHeader §D keeps the base spawn_agent v1 fallback path',
|
|
83
|
+
header.includes('base multi_agent v1') && header.includes('inherit the one session model'));
|
|
84
|
+
ok('codexSkillAdapterHeader §D warns against a single blocking wait_agent with a poll loop',
|
|
85
|
+
header.includes('bounded wait') && header.includes('poll'));
|
|
86
|
+
ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling review',
|
|
87
|
+
header.includes('background job'));
|
|
88
|
+
ok('codexSkillAdapterHeader §D makes background+poll the hard cross-host default for reviews',
|
|
89
|
+
header.includes('always run this way') && header.includes('hard cross-host default'));
|
|
90
|
+
ok('codexSkillAdapterHeader §F mentions real Codex hooks', header.includes('real Codex hooks'));
|
|
91
|
+
ok('codexSkillAdapterHeader §F no longer says Codex has no hook bus', !header.includes('Codex has no hook bus'));
|
|
92
|
+
ok('codexSkillAdapterHeader §H maps plan-review external-CLI Bash timeout/background onto Codex shell',
|
|
93
|
+
header.includes('## H.') && header.includes('Cross-AI plan review') && header.includes('run_in_background') && header.includes('shell/exec'));
|
|
94
|
+
ok('codexSkillAdapterHeader §H distinguishes the external-CLI shell path from the §D subagent path',
|
|
95
|
+
header.includes('external-CLI') && header.includes('not a spawned Codex subagent'));
|
|
96
|
+
ok('codexSkillAdapterHeader §H makes backgrounding reviewers the default',
|
|
97
|
+
header.includes('the default is to background the reviewers'));
|
|
98
|
+
ok('codexSkillAdapterHeader §H closes reviewer stdin to avoid the EOF hang',
|
|
99
|
+
header.includes('/dev/null') && header.toLowerCase().includes('stdin'));
|
|
100
|
+
|
|
101
|
+
// --- codexVerbSkillContent (#46) ---
|
|
102
|
+
ok('codexVerbSkillContent: SKILL.md frontmatter is at byte 0 with name+description', (() => {
|
|
103
|
+
const out = codexVerbSkillContent({ verb: 'work', description: 'Work the next frontier ticket' });
|
|
104
|
+
return out.startsWith('---\nname: trailhead-work\n') && out.includes('description: "Work the next frontier ticket"');
|
|
105
|
+
})());
|
|
106
|
+
ok('codexVerbSkillContent: body delegates to $trailhead <verb>', (() => {
|
|
107
|
+
const out = codexVerbSkillContent({ verb: 'work', description: 'x' });
|
|
108
|
+
return out.includes('`$trailhead work`') && out.includes('single source of truth');
|
|
109
|
+
})());
|
|
110
|
+
ok('codexVerbSkillContent: falls back to a description when none given', (() => {
|
|
111
|
+
const out = codexVerbSkillContent({ verb: 'update' });
|
|
112
|
+
return out.includes('description: "trailhead update"');
|
|
113
|
+
})());
|
|
114
|
+
ok('codexVerbSkillContent: YAML-quotes a description with a colon/quote', (() => {
|
|
115
|
+
const out = codexVerbSkillContent({ verb: 'bug', description: 'Capture a bug: "regression"' });
|
|
116
|
+
return out.includes('description: "Capture a bug: \\"regression\\""');
|
|
117
|
+
})());
|
|
118
|
+
|
|
119
|
+
// --- codexVerbSkillAgentsYaml (#46) ---
|
|
120
|
+
ok('codexVerbSkillAgentsYaml: explicit-only invocation, no auto-trigger', (() => {
|
|
121
|
+
const out = codexVerbSkillAgentsYaml({ verb: 'work', description: 'x' });
|
|
122
|
+
return out.includes('allow_implicit_invocation: false') && out.includes('display_name: "Trailhead: work"');
|
|
123
|
+
})());
|
|
124
|
+
|
|
125
|
+
// --- codexVerbSkillPlan (#46) ---
|
|
126
|
+
ok('codexVerbSkillPlan: one skills/trailhead-<verb>/ dir per verb (SKILL.md + openai.yaml)', (() => {
|
|
127
|
+
const plan = codexVerbSkillPlan('/c', [{ verb: 'work' }, { verb: 'bug' }]);
|
|
128
|
+
return plan.dirs.length === 2 &&
|
|
129
|
+
plan.dirs.includes('/c/skills/trailhead-work') &&
|
|
130
|
+
plan.writes.some((w) => w.path === '/c/skills/trailhead-work/SKILL.md') &&
|
|
131
|
+
plan.writes.some((w) => w.path === '/c/skills/trailhead-work/agents/openai.yaml') &&
|
|
132
|
+
plan.writes.some((w) => w.path === '/c/skills/trailhead-bug/SKILL.md');
|
|
133
|
+
})());
|
|
134
|
+
ok('codexVerbSkillPlan: never emits a bare trailhead skill ($trailhead already is smart entry)', (() => {
|
|
135
|
+
const plan = codexVerbSkillPlan('/c', [{ verb: 'work' }]);
|
|
136
|
+
return !plan.dirs.includes('/c/skills/trailhead') && !plan.writes.some((w) => w.path === '/c/skills/trailhead/SKILL.md');
|
|
137
|
+
})());
|
|
138
|
+
ok('codexVerbSkillPlan: accepts a bare-string verb and skips empty/invalid', (() => {
|
|
139
|
+
const plan = codexVerbSkillPlan('/c', [{ verb: '' }, { verb: null }, 'work']);
|
|
140
|
+
return plan.dirs.length === 1 && plan.dirs[0] === '/c/skills/trailhead-work';
|
|
141
|
+
})());
|
|
142
|
+
ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', null).dirs.length === 0 && codexVerbSkillPlan('/c').dirs.length === 0);
|
|
143
|
+
|
|
144
|
+
// --- codexHookEntries ---
|
|
145
|
+
const entries = codexHookEntries('/h');
|
|
146
|
+
ok('codexHookEntries returns 4 entries', Array.isArray(entries) && entries.length === 4);
|
|
147
|
+
ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
|
|
148
|
+
e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
|
|
149
|
+
ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
|
|
150
|
+
e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-secret-guard.js')));
|
|
151
|
+
ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
|
|
152
|
+
e.event === 'PostToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-issue-injection-scanner.js')));
|
|
153
|
+
ok('codexHookEntries: check-update is SessionStart', entries.some((e) =>
|
|
154
|
+
e.event === 'SessionStart' && e.command.includes('trailhead-check-update.js')));
|
|
155
|
+
|
|
156
|
+
// --- enableCodexHooksFeature ---
|
|
157
|
+
ok('enableCodexHooksFeature(empty) appends [features] table with hooks = true', (() => {
|
|
158
|
+
const out = enableCodexHooksFeature('');
|
|
159
|
+
return typeof out === 'string' && out.includes('[features]') && out.includes('hooks = true');
|
|
160
|
+
})());
|
|
161
|
+
ok('enableCodexHooksFeature: already enabled -> null', enableCodexHooksFeature('[features]\nhooks = true\n') === null);
|
|
162
|
+
ok('enableCodexHooksFeature: inserts hooks = true into existing [features]', (() => {
|
|
163
|
+
const out = enableCodexHooksFeature('[features]\nfoo = 1\n');
|
|
164
|
+
return typeof out === 'string' && out.includes('hooks = true') && (out.match(/\[features\]/g) || []).length === 1;
|
|
165
|
+
})());
|
|
166
|
+
ok('enableCodexHooksFeature: flips hooks = false to true', (() => {
|
|
167
|
+
const out = enableCodexHooksFeature('[features]\nhooks = false\n');
|
|
168
|
+
return typeof out === 'string' && out.includes('hooks = true') && !out.includes('hooks = false');
|
|
169
|
+
})());
|
|
170
|
+
ok('enableCodexHooksFeature: appends [features] table when none exists', (() => {
|
|
171
|
+
const out = enableCodexHooksFeature('[other]\nk = 1\n');
|
|
172
|
+
return typeof out === 'string' && out.includes('[other]') && out.includes('[features]') && out.includes('hooks = true');
|
|
173
|
+
})());
|
|
174
|
+
ok('enableCodexHooksFeature: dotted top-level form already true -> null', enableCodexHooksFeature('features.hooks = true\n') === null);
|
|
175
|
+
|
|
176
|
+
// --- codexAgentToml ---
|
|
177
|
+
ok('codexAgentToml with effort includes model + model_reasoning_effort + developer_instructions block', (() => {
|
|
178
|
+
const out = codexAgentToml({
|
|
179
|
+
name: 'trailhead-execute',
|
|
180
|
+
description: 'desc',
|
|
181
|
+
developerInstructions: 'do the thing',
|
|
182
|
+
model: 'gpt-5.6-sol',
|
|
183
|
+
effort: 'high',
|
|
184
|
+
});
|
|
185
|
+
return out.includes('name = "trailhead-execute"') &&
|
|
186
|
+
out.includes('model = "gpt-5.6-sol"') &&
|
|
187
|
+
out.includes('model_reasoning_effort = "high"') &&
|
|
188
|
+
out.includes("developer_instructions = '''") &&
|
|
189
|
+
out.includes('do the thing');
|
|
190
|
+
})());
|
|
191
|
+
ok('codexAgentToml without effort omits model_reasoning_effort', (() => {
|
|
192
|
+
const out = codexAgentToml({
|
|
193
|
+
name: 'trailhead-plan',
|
|
194
|
+
description: 'desc',
|
|
195
|
+
developerInstructions: 'plan the thing',
|
|
196
|
+
model: 'gpt-5.6-terra',
|
|
197
|
+
effort: null,
|
|
198
|
+
});
|
|
199
|
+
return out.includes('model = "gpt-5.6-terra"') && !out.includes('model_reasoning_effort');
|
|
200
|
+
})());
|
|
201
|
+
|
|
202
|
+
// --- codexAgentTomlPlan ---
|
|
203
|
+
ok('codexAgentTomlPlan: string value produces one write with the right path/name/model', (() => {
|
|
204
|
+
const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' });
|
|
205
|
+
return plan.writes.length === 1 &&
|
|
206
|
+
plan.writes[0].path === '/c/agents/trailhead-execute.toml' &&
|
|
207
|
+
plan.writes[0].name === 'trailhead-execute' &&
|
|
208
|
+
plan.writes[0].content.includes('model = "gpt-5.6-terra"');
|
|
209
|
+
})());
|
|
210
|
+
ok('codexAgentTomlPlan: object value includes model + effort', (() => {
|
|
211
|
+
const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' } });
|
|
212
|
+
const c = plan.writes[0].content;
|
|
213
|
+
return c.includes('model = "gpt-5.6-sol"') && c.includes('model_reasoning_effort = "high"');
|
|
214
|
+
})());
|
|
215
|
+
ok('codexAgentTomlPlan: empty object -> no writes', codexAgentTomlPlan('/c', {}).writes.length === 0);
|
|
216
|
+
ok('codexAgentTomlPlan: null -> no writes', codexAgentTomlPlan('/c', null).writes.length === 0);
|
|
217
|
+
ok('codexAgentTomlPlan: unknown key skipped', codexAgentTomlPlan('/c', { bogus: 'x' }).writes.length === 0);
|
|
218
|
+
ok('codexAgentTomlPlan: object value with no model is skipped', codexAgentTomlPlan('/c', { execute: { effort: 'high' } }).writes.length === 0);
|
|
219
|
+
|
|
220
|
+
// --- enableCodexMultiAgentV2Feature ---
|
|
221
|
+
ok('enableCodexMultiAgentV2Feature: dotted form already true -> null',
|
|
222
|
+
enableCodexMultiAgentV2Feature('features.multi_agent_v2 = true\n') === null);
|
|
223
|
+
ok('enableCodexMultiAgentV2Feature: inserts into existing [features] table', (() => {
|
|
224
|
+
const out = enableCodexMultiAgentV2Feature('[features]\nfoo = 1\n');
|
|
225
|
+
return typeof out === 'string' && out.includes('multi_agent_v2 = true') && (out.match(/\[features\]/g) || []).length === 1;
|
|
226
|
+
})());
|
|
227
|
+
ok('enableCodexMultiAgentV2Feature: appends a table when none exists', (() => {
|
|
228
|
+
const out = enableCodexMultiAgentV2Feature('[other]\nk = 1\n');
|
|
229
|
+
return typeof out === 'string' && out.includes('[other]') && out.includes('[features]') && out.includes('multi_agent_v2 = true');
|
|
230
|
+
})());
|
|
231
|
+
ok('enableCodexMultiAgentV2Feature: foreign explicit value is unsafe',
|
|
232
|
+
(() => {
|
|
233
|
+
const out = enableCodexMultiAgentV2Feature('features.multi_agent_v2 = "x"\n');
|
|
234
|
+
return out && out.unsafe === true;
|
|
235
|
+
})());
|
|
236
|
+
|
|
237
|
+
// --- enableCodexHooksFeature regression (still works after generalisation) ---
|
|
238
|
+
ok('enableCodexHooksFeature regression: still yields hooks = true', (() => {
|
|
239
|
+
const out = enableCodexHooksFeature('[features]\nfoo = 1\n');
|
|
240
|
+
return typeof out === 'string' && out.includes('hooks = true');
|
|
241
|
+
})());
|
|
242
|
+
|
|
243
|
+
// --- injectCodexAdapterHeader (frontmatter stays at the top: #40) ---
|
|
244
|
+
// A realistic converted SKILL.md: YAML frontmatter first, then body.
|
|
245
|
+
const skillMain = '---\nname: trailhead\ndescription: "chart & work a map"\nargument-hint: "[work] [ticket]"\n---\n\nA loose idea has arrived.\n';
|
|
246
|
+
const injected = injectCodexAdapterHeader(skillMain);
|
|
247
|
+
ok('injectCodexAdapterHeader keeps the projected SKILL.md starting with ---', injected.startsWith('---\n'));
|
|
248
|
+
ok('injectCodexAdapterHeader keeps the frontmatter before the adapter header',
|
|
249
|
+
injected.indexOf('name: trailhead') < injected.indexOf('<codex_skill_adapter>'));
|
|
250
|
+
ok('injectCodexAdapterHeader still injects the adapter header', injected.includes('<codex_skill_adapter>'));
|
|
251
|
+
ok('injectCodexAdapterHeader preserves the body', injected.includes('A loose idea has arrived.'));
|
|
252
|
+
ok('injectCodexAdapterHeader places the header after the closing --- of the frontmatter',
|
|
253
|
+
injected.indexOf('<codex_skill_adapter>') > injected.indexOf('\n---\n'));
|
|
254
|
+
// Fallback: no frontmatter -> header still prepended (nothing to protect).
|
|
255
|
+
ok('injectCodexAdapterHeader falls back to prepend when there is no frontmatter', (() => {
|
|
256
|
+
const out = injectCodexAdapterHeader('just a body, no frontmatter\n');
|
|
257
|
+
return out.startsWith('<codex_skill_adapter>') && out.includes('just a body');
|
|
258
|
+
})());
|
|
259
|
+
|
|
260
|
+
// --- codexAgentsYaml ---
|
|
261
|
+
const agentsYaml = codexAgentsYaml();
|
|
262
|
+
ok('codexAgentsYaml disallows implicit invocation', agentsYaml.includes('allow_implicit_invocation: false'));
|
|
263
|
+
ok('codexAgentsYaml mentions $trailhead', agentsYaml.includes('$trailhead'));
|
|
264
|
+
ok('codexAgentsYaml has the display name', agentsYaml.includes('display_name: "Trailhead"'));
|
|
265
|
+
|
|
266
|
+
console.log(`✓ codex-projection: ${passed} assertions passed`);
|