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.
Files changed (71) hide show
  1. package/core/.claude/commands/Ansuz.md +36 -35
  2. package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
  3. package/core/.claude/hooks/_ansuz-conversation.js +1 -0
  4. package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
  5. package/core/.claude/hooks/_run-user-prompt.js +0 -4
  6. package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
  7. package/core/.claude/hooks/handoff-staleness.js +2 -2
  8. package/core/.claude/hooks/turn-context.js +50 -2
  9. package/core/.claude/settings.json +0 -37
  10. package/core/.collaboration/core/task-api.js +4 -12
  11. package/core/.formation/README.md +21 -146
  12. package/core/.formation/bifrost/codex-doctor.js +37 -2
  13. package/core/.formation/bifrost/codex-doctor.test.js +18 -0
  14. package/core/.formation/bifrost/launch.js +1 -1
  15. package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
  16. package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
  17. package/core/.formation/bifrost/setup-codex.js +3 -0
  18. package/core/.formation/spec/task_operations.parity.test.js +0 -1
  19. package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
  20. package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
  21. package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
  22. package/core/.formation/tools/dispatch.js +28 -549
  23. package/core/.formation/tools/mcp-allowlist.test.js +0 -1
  24. package/core/.formation/tools/post-complete/task2345.test.js +0 -5
  25. package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
  26. package/core/.formation/tools/prompt.js +66 -1607
  27. package/core/.formation/tools/prompt.test.js +43 -582
  28. package/core/.kenaz/claude-hooks-manifest.json +16 -16
  29. package/core/.specialist/Ansuz/RULES.md +6 -10
  30. package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
  31. package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
  32. package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
  33. package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
  34. package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
  35. package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
  36. package/core/KENAZ_CORE_VERSION +1 -1
  37. package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
  38. package/core/plugins/kenaz/commands/Amelia.md +1 -1
  39. package/core/plugins/kenaz/commands/Ansuz.md +36 -35
  40. package/core/plugins/kenaz/hooks/hooks.json +1 -5
  41. package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
  42. package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
  43. package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
  44. package/core/plugins/kenaz/scripts/sync.js +6 -2
  45. package/core/plugins/kenaz/scripts/ygg.js +13 -1
  46. package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
  47. package/lib/cli.js +47 -6
  48. package/lib/cli.test.js +24 -0
  49. package/lib/dashboard.js +10 -0
  50. package/package.json +11 -7
  51. package/vendor/install-kenaz-cli.js +18 -7
  52. package/core/.formation/tools/broadcast.js +0 -103
  53. package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
  54. package/core/.formation/tools/dispatch-notifier.js +0 -84
  55. package/core/.formation/tools/formations.js +0 -1203
  56. package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
  57. package/core/.formation/tools/headless-dryrun.test.js +0 -79
  58. package/core/.formation/tools/headless-v2.js +0 -659
  59. package/core/.formation/tools/headless.js +0 -1071
  60. package/core/.formation/tools/legacy-scheduler.js +0 -1994
  61. package/core/.formation/tools/logger.js +0 -273
  62. package/core/.formation/tools/memory-loader.js +0 -422
  63. package/core/.formation/tools/process-state.js +0 -33
  64. package/core/.formation/tools/remote-conflict.js +0 -98
  65. package/core/.formation/tools/runner.js +0 -1332
  66. package/core/.formation/tools/task-utils.js +0 -344
  67. package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
  68. package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
  69. package/core/.formation/tools/worktree-leak.test.js +0 -162
  70. package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
  71. 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
- return { file: f, skill, action: a === 'unchanged' && b === 'unchanged' ? 'unchanged' : 'written' };
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 so headless.js knows to route through Bifröst
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
- if (id === 'http') return { request: () => { effects.http.push('request'); assert.fail('unexpected peer request'); } };
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 (kuzu's CMake
31
- alone can run 10+ minutes) gets killed mid-compile. Running it backgrounded
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