kenaz 0.3.2 → 0.3.5
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/core/.claude/commands/Ansuz.md +36 -35
- package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
- package/core/.claude/hooks/_ansuz-conversation.js +1 -0
- package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
- package/core/.claude/hooks/_run-user-prompt.js +0 -4
- package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
- package/core/.claude/hooks/handoff-staleness.js +2 -2
- package/core/.claude/hooks/turn-context.js +50 -2
- package/core/.claude/settings.json +0 -37
- package/core/.collaboration/core/task-api.js +4 -12
- package/core/.formation/README.md +21 -146
- package/core/.formation/bifrost/codex-doctor.js +37 -2
- package/core/.formation/bifrost/codex-doctor.test.js +18 -0
- package/core/.formation/bifrost/launch.js +1 -1
- package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
- package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
- package/core/.formation/bifrost/setup-codex.js +3 -0
- package/core/.formation/spec/task_operations.parity.test.js +0 -1
- package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
- package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
- package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
- package/core/.formation/tools/dispatch.js +28 -549
- package/core/.formation/tools/mcp-allowlist.test.js +0 -1
- package/core/.formation/tools/post-complete/task2345.test.js +0 -5
- package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
- package/core/.formation/tools/prompt.js +66 -1607
- package/core/.formation/tools/prompt.test.js +43 -582
- package/core/.kenaz/claude-hooks-manifest.json +16 -16
- package/core/.specialist/Ansuz/RULES.md +6 -10
- package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
- package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
- package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
- package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
- package/core/plugins/kenaz/commands/Amelia.md +1 -1
- package/core/plugins/kenaz/commands/Ansuz.md +36 -35
- package/core/plugins/kenaz/hooks/hooks.json +1 -5
- package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
- package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
- package/core/plugins/kenaz/scripts/sync.js +6 -2
- package/core/plugins/kenaz/scripts/ygg.js +13 -1
- package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
- package/lib/cli.js +47 -6
- package/lib/cli.test.js +24 -0
- package/lib/dashboard.js +10 -0
- package/package.json +11 -7
- package/vendor/install-kenaz-cli.js +18 -7
- package/core/.formation/tools/broadcast.js +0 -103
- package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
- package/core/.formation/tools/dispatch-notifier.js +0 -84
- package/core/.formation/tools/formations.js +0 -1203
- package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
- package/core/.formation/tools/headless-dryrun.test.js +0 -79
- package/core/.formation/tools/headless-v2.js +0 -659
- package/core/.formation/tools/headless.js +0 -1071
- package/core/.formation/tools/legacy-scheduler.js +0 -1994
- package/core/.formation/tools/logger.js +0 -273
- package/core/.formation/tools/memory-loader.js +0 -422
- package/core/.formation/tools/process-state.js +0 -33
- package/core/.formation/tools/remote-conflict.js +0 -98
- package/core/.formation/tools/runner.js +0 -1332
- package/core/.formation/tools/task-utils.js +0 -344
- package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
- package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
- package/core/.formation/tools/worktree-leak.test.js +0 -162
- package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
- package/core/.specialist/tools/__tests__/task-1977-banner-cap.test.js +0 -129
|
@@ -40,12 +40,47 @@ function writeIfChanged(f, text) {
|
|
|
40
40
|
return 'written';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Global Codex instructions (~/.codex/AGENTS.md, read by every Codex session): a marked Kenaz block,
|
|
44
|
+
// so Codex agents know how to report Kenaz problems (wish / field report) like Claude's /Ansuz does.
|
|
45
|
+
// Only the text between the markers is ours; the user's own content is never touched.
|
|
46
|
+
const AGENTS_BEGIN = '<!-- kenaz:begin (managed by kenaz install; edits here are overwritten) -->';
|
|
47
|
+
const AGENTS_END = '<!-- kenaz:end -->';
|
|
48
|
+
function agentsBlock() {
|
|
49
|
+
return [AGENTS_BEGIN, '## Kenaz',
|
|
50
|
+
'- Kenaz tools come from the `kenaz-dashboard` MCP server (kenaz-server on 127.0.0.1:18080); `$kenaz status|init|ygg ...` runs the Kenaz CLI (needs network access to 127.0.0.1).',
|
|
51
|
+
'- When Kenaz itself blocks or degrades the work (a tool/hook/install/peer/dispatch failure, wrong data, missing capability), call `kenaz_wish_create` BEFORE working around it:',
|
|
52
|
+
' projectPath = this project; scope = `kenaz` (Kenaz problem) or `project`; wish_type = `bug` | `feature` | `question`;',
|
|
53
|
+
' content = one-line repro + expected vs actual + versions (`npx kenaz doctor` lines) + session id. Never paste keys or tokens.',
|
|
54
|
+
' A scope=kenaz wish is sent to the Kenaz team as a field report. Report each problem once, then finish the work with the workaround and tell the user the wish was filed.',
|
|
55
|
+
AGENTS_END].join('\n');
|
|
56
|
+
}
|
|
57
|
+
function withoutAgentsBlock(text) {
|
|
58
|
+
const a = text.indexOf(AGENTS_BEGIN), b = text.indexOf(AGENTS_END);
|
|
59
|
+
if (a < 0 || b < a) return text;
|
|
60
|
+
return (text.slice(0, a) + text.slice(b + AGENTS_END.length)).replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
61
|
+
}
|
|
62
|
+
function installAgentsBlock() {
|
|
63
|
+
const f = path.join(codexHome(), 'AGENTS.md');
|
|
64
|
+
const cur = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
|
|
65
|
+
const base = withoutAgentsBlock(cur).trim();
|
|
66
|
+
return writeIfChanged(f, (base ? base + '\n\n' : '') + agentsBlock() + '\n');
|
|
67
|
+
}
|
|
68
|
+
function removeAgentsBlock(dryRun) {
|
|
69
|
+
const f = path.join(codexHome(), 'AGENTS.md');
|
|
70
|
+
if (!fs.existsSync(f)) return 'absent';
|
|
71
|
+
const cur = fs.readFileSync(f, 'utf8');
|
|
72
|
+
if (!cur.includes(AGENTS_BEGIN)) return 'absent';
|
|
73
|
+
if (!dryRun) { const rest = withoutAgentsBlock(cur); if (rest.trim()) fs.writeFileSync(f, rest); else fs.rmSync(f, { force: true }); }
|
|
74
|
+
return 'removed';
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
function installPrompts(core) {
|
|
44
78
|
const f = path.join(codexHome(), 'prompts', 'kenaz.md');
|
|
45
79
|
const skill = path.join(codexHome(), 'skills', 'kenaz', 'SKILL.md');
|
|
46
80
|
const a = writeIfChanged(f, promptText(core));
|
|
47
81
|
const b = writeIfChanged(skill, skillText(core));
|
|
48
|
-
|
|
82
|
+
const c = installAgentsBlock();
|
|
83
|
+
return { file: f, skill, action: [a, b, c].every((x) => x === 'unchanged') ? 'unchanged' : 'written' };
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
// Per event: is our shim registered, and does codex hold a trusted_hash for it (hooks.state)?
|
|
@@ -95,7 +130,7 @@ function diagnose() {
|
|
|
95
130
|
return { trust, delivery_mode: deliveryMode(trust), ok: notes.length === 0, notes };
|
|
96
131
|
}
|
|
97
132
|
|
|
98
|
-
module.exports = { promptText, skillText, installPrompts, hookTrust, deliveryMode, diagnose, SHIM_REL, EVENTS };
|
|
133
|
+
module.exports = { promptText, skillText, installPrompts, installAgentsBlock, removeAgentsBlock, agentsBlock, hookTrust, deliveryMode, diagnose, SHIM_REL, EVENTS };
|
|
99
134
|
|
|
100
135
|
if (require.main === module) {
|
|
101
136
|
if (process.argv.includes('--install-prompts')) console.log(JSON.stringify(installPrompts()));
|
|
@@ -49,3 +49,21 @@ test('shim honours Kenaz mode: off silent, full control still runs', () => {
|
|
|
49
49
|
assert.strictEqual(off.stderr.includes('event='), false);
|
|
50
50
|
assert.ok(full.stderr.includes('event=SessionStart')); // control: hook does run when not off
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
test('global AGENTS.md gets one managed Kenaz block (wish / field report guidance); user text kept; removal leaves the rest', () => {
|
|
54
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cx-agents-'));
|
|
55
|
+
process.env.CODEX_HOME = home;
|
|
56
|
+
const f = path.join(home, 'AGENTS.md');
|
|
57
|
+
fs.writeFileSync(f, '# My own Codex rules\nAlways be terse.\n');
|
|
58
|
+
D.installAgentsBlock();
|
|
59
|
+
D.installAgentsBlock(); // idempotent: still one block
|
|
60
|
+
const t = fs.readFileSync(f, 'utf8');
|
|
61
|
+
assert.match(t, /Always be terse\./);
|
|
62
|
+
assert.strictEqual(t.split('kenaz:begin').length - 1, 1);
|
|
63
|
+
assert.match(t, /kenaz_wish_create/);
|
|
64
|
+
assert.strictEqual(D.removeAgentsBlock(false), 'removed');
|
|
65
|
+
const after = fs.readFileSync(f, 'utf8');
|
|
66
|
+
assert.match(after, /Always be terse\./);
|
|
67
|
+
assert.doesNotMatch(after, /kenaz:begin|kenaz_wish_create/);
|
|
68
|
+
delete process.env.CODEX_HOME;
|
|
69
|
+
});
|
|
@@ -305,7 +305,7 @@ function writeOverride(prompt, bootContext) {
|
|
|
305
305
|
|
|
306
306
|
fs.writeFileSync(OVERRIDE_PATH, content, 'utf8');
|
|
307
307
|
|
|
308
|
-
// Write adapter signal file
|
|
308
|
+
// Write adapter signal file (read by session.js and the Dashboard bifrost-active route)
|
|
309
309
|
const signalPath = path.join(ROOT, '.formation', '.bifrost-active');
|
|
310
310
|
fs.writeFileSync(signalPath, JSON.stringify({ adapter: 'openai', specialist: SPECIALIST, pid: process.pid, started: new Date().toISOString() }));
|
|
311
311
|
|
|
@@ -123,6 +123,11 @@ async function main() {
|
|
|
123
123
|
try { mode = require(path.resolve(ROOT, 'plugins/kenaz/hooks/mode.js')).resolveMode(p.cwd || process.cwd()); } catch {}
|
|
124
124
|
if (mode === 'off') process.exit(0);
|
|
125
125
|
console.error('[codex-hook-shim] event=' + ev);
|
|
126
|
+
// Register this project with kenaz-server (idempotent, no-op when known): Codex sessions never run
|
|
127
|
+
// the Claude turn-context hook, so a Codex-only machine stayed out of its room (field report).
|
|
128
|
+
const registering = (ev === 'SessionStart' || ev === 'UserPromptSubmit')
|
|
129
|
+
? (() => { try { const r = resolvePeerProjectRoot(p.cwd || process.cwd()); return r ? peerRequest('POST', '/api/projects/register', { projectPath: r }) : null; } catch { return null; } })()
|
|
130
|
+
: null;
|
|
126
131
|
const payload = normalizeHookPayload(p);
|
|
127
132
|
if (ev === 'UserPromptSubmit') {
|
|
128
133
|
payload.specialist = p.specialist || payload.specialist;
|
|
@@ -164,6 +169,7 @@ async function main() {
|
|
|
164
169
|
}).catch(() => {}).finally(() => process.exit(0));
|
|
165
170
|
return; // exit handled by finally above
|
|
166
171
|
}
|
|
172
|
+
if (registering) await registering; // bounded by PEER_TIMEOUT_MS; fail-open
|
|
167
173
|
process.exit(0);
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -33,7 +33,17 @@ async function fixture({ project, cwd = path.resolve('hook-cwd'), reject = false
|
|
|
33
33
|
readFileSync: () => { throw new Error('no real state reads'); },
|
|
34
34
|
};
|
|
35
35
|
// TASK_2299 peer pull deps; no KENAZ_PEER_PULL in these fixtures, so no request is expected.
|
|
36
|
-
|
|
36
|
+
// The only request allowed here is the idempotent project registration (SessionStart /
|
|
37
|
+
// UserPromptSubmit); the fake fails it at once, like a server that is down (fail-open).
|
|
38
|
+
if (id === 'http') return { request: (opts) => {
|
|
39
|
+
if (opts && opts.method === 'POST' && opts.path === '/api/projects/register') {
|
|
40
|
+
effects.http.push('register');
|
|
41
|
+
const handlers = {};
|
|
42
|
+
const req = { on: (ev, fn) => { handlers[ev] = fn; return req; }, destroy: () => {}, end: () => { if (handlers.error) handlers.error(new Error('down')); } };
|
|
43
|
+
return req;
|
|
44
|
+
}
|
|
45
|
+
effects.http.push('request'); assert.fail('unexpected peer request');
|
|
46
|
+
} };
|
|
37
47
|
if (id.endsWith('_peer-project-root.js')) return { resolvePeerProjectRoot: dir => dir };
|
|
38
48
|
if (id === 'child_process') return { execFileSync: () => assert.fail('unexpected child') };
|
|
39
49
|
effects.modules.push(id);
|
|
@@ -231,6 +231,9 @@ function uninstall(options = {}) {
|
|
|
231
231
|
if (res.hooks === 'removed' && !dryRun) fs.writeFileSync(cfg, out.join('\n').replace(/\n{3,}/g, '\n\n'), 'utf8');
|
|
232
232
|
const prompt = path.join(getCodexHome(), 'prompts', 'kenaz.md');
|
|
233
233
|
if (fs.existsSync(prompt)) { res.prompt = 'removed'; if (!dryRun) fs.rmSync(prompt, { force: true }); }
|
|
234
|
+
const skillDir = path.join(getCodexHome(), 'skills', 'kenaz');
|
|
235
|
+
if (fs.existsSync(skillDir)) { res.skill = 'removed'; if (!dryRun) fs.rmSync(skillDir, { recursive: true, force: true }); }
|
|
236
|
+
try { res.agents_md = require('./codex-doctor').removeAgentsBlock(dryRun); } catch (_) { /* doctor optional */ }
|
|
234
237
|
log((dryRun ? 'dry-run: ' : '') + 'codex uninstall ' + JSON.stringify(res));
|
|
235
238
|
return { success: true, ...res };
|
|
236
239
|
}
|
|
@@ -28,7 +28,6 @@ const PROJECT_ROOT = path.resolve(SPEC_DIR, '../..');
|
|
|
28
28
|
const FILES = {
|
|
29
29
|
taskApi: path.join(PROJECT_ROOT, '.collaboration/core/task-api.js'),
|
|
30
30
|
postComplete: path.join(PROJECT_ROOT, '.formation/tools/post-complete/staging.js'),
|
|
31
|
-
headless: path.join(PROJECT_ROOT, '.formation/tools/headless.js'),
|
|
32
31
|
kenazRs: path.join(PROJECT_ROOT, 'dashboard/src-tauri/src/infrastructure/http_server/routes/mcp_tools/kenaz.rs'),
|
|
33
32
|
spec: path.join(SPEC_DIR, 'task_operations.yaml'),
|
|
34
33
|
};
|
|
@@ -52,9 +52,3 @@ test('control: no project path / absent project file', () => {
|
|
|
52
52
|
assert.equal(t.utils.loadDashboardModels(t.proj).dispatchModel, 'c'); // project file absent -> core
|
|
53
53
|
} finally { fs.rmSync(t.base, { recursive: true, force: true }); }
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
test('headless.js derives dispatchModel and agent models from one loadDashboardModels object', () => {
|
|
57
|
-
const src = fs.readFileSync(path.join(__dirname, 'headless.js'), 'utf8');
|
|
58
|
-
assert.ok(!src.includes("'dashboard-models.json')"), 'no inline path loops left');
|
|
59
|
-
assert.match(src, /const _dashboardModels = loadDashboardModels\(projectPath\)/);
|
|
60
|
-
});
|
|
@@ -24,17 +24,9 @@ sub-project's shared main tree, which this dispatch must not touch. The
|
|
|
24
24
|
pre-built worktree path given directly in this prefix is the only correct
|
|
25
25
|
edit target; treat the Project Context path as informational only.
|
|
26
26
|
|
|
27
|
-
## Why Synapse calls use the main repo path, not the worktree path
|
|
28
|
-
|
|
29
|
-
Synapse's index is keyed to the project's main repo path. Passing the
|
|
30
|
-
worktree path as `projectPath` would either miss the existing index entirely
|
|
31
|
-
or fragment it across a path that will not exist after the worktree is
|
|
32
|
-
cleaned up. Always pass the main repo path shown in this prefix.
|
|
33
|
-
|
|
34
27
|
## Shared rules
|
|
35
28
|
|
|
36
29
|
Every rule in `.formation/tools/dispatch-detail/worktree-subagent-mode.md`
|
|
37
30
|
(commit-first with telemetry excludes, no long builds, no sleep polling, no
|
|
38
31
|
taskkill by image name, single-task only, no task-api complete) applies here
|
|
39
|
-
identically — the only difference is which absolute path is the edit target
|
|
40
|
-
and which path Synapse calls use.
|
|
32
|
+
identically — the only difference is which absolute path is the edit target.
|
|
@@ -27,8 +27,8 @@ that worktree hangs waiting for a lock nothing will ever release.
|
|
|
27
27
|
|
|
28
28
|
## Why no long builds / full test suites
|
|
29
29
|
|
|
30
|
-
Bash foreground calls cap at 10 minutes; a cold Rust build
|
|
31
|
-
|
|
30
|
+
Bash foreground calls cap at 10 minutes; a cold Rust build
|
|
31
|
+
(can run 10+ minutes) gets killed mid-compile. Running it backgrounded
|
|
32
32
|
instead just produces the orphan-lock problem above, because the previous
|
|
33
33
|
rule (no resuming later) means you have nothing to do but end the turn while
|
|
34
34
|
it's still running. The main thread (Ansuz) has a warm cache, can poll
|