@moreih29/nexus-core 0.12.0 → 0.14.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.md +76 -57
- package/assets/agents/architect/body.ko.md +177 -0
- package/{agents → assets/agents}/architect/body.md +16 -0
- package/assets/agents/designer/body.ko.md +125 -0
- package/{agents → assets/agents}/designer/body.md +16 -0
- package/assets/agents/engineer/body.ko.md +106 -0
- package/{agents → assets/agents}/engineer/body.md +14 -0
- package/assets/agents/lead/body.ko.md +70 -0
- package/assets/agents/lead/body.md +70 -0
- package/assets/agents/postdoc/body.ko.md +122 -0
- package/{agents → assets/agents}/postdoc/body.md +16 -0
- package/assets/agents/researcher/body.ko.md +137 -0
- package/{agents → assets/agents}/researcher/body.md +15 -0
- package/assets/agents/reviewer/body.ko.md +138 -0
- package/{agents → assets/agents}/reviewer/body.md +15 -0
- package/assets/agents/strategist/body.ko.md +116 -0
- package/{agents → assets/agents}/strategist/body.md +16 -0
- package/assets/agents/tester/body.ko.md +195 -0
- package/{agents → assets/agents}/tester/body.md +15 -0
- package/assets/agents/writer/body.ko.md +122 -0
- package/{agents → assets/agents}/writer/body.md +14 -0
- package/assets/capability-matrix.yml +198 -0
- package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
- package/assets/hooks/agent-bootstrap/handler.ts +119 -0
- package/assets/hooks/agent-bootstrap/meta.yml +10 -0
- package/assets/hooks/agent-finalize/handler.test.ts +368 -0
- package/assets/hooks/agent-finalize/handler.ts +76 -0
- package/assets/hooks/agent-finalize/meta.yml +10 -0
- package/assets/hooks/capability-matrix.yml +313 -0
- package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
- package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
- package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
- package/assets/hooks/prompt-router/handler.test.ts +801 -0
- package/assets/hooks/prompt-router/handler.ts +261 -0
- package/assets/hooks/prompt-router/meta.yml +11 -0
- package/assets/hooks/session-init/handler.test.ts +274 -0
- package/assets/hooks/session-init/handler.ts +30 -0
- package/assets/hooks/session-init/meta.yml +9 -0
- package/assets/lsp-servers.json +55 -0
- package/assets/schema/lsp-servers.schema.json +67 -0
- package/assets/skills/nx-init/body.ko.md +197 -0
- package/{skills → assets/skills}/nx-init/body.md +11 -0
- package/assets/skills/nx-plan/body.ko.md +361 -0
- package/{skills → assets/skills}/nx-plan/body.md +13 -0
- package/assets/skills/nx-run/body.ko.md +161 -0
- package/{skills → assets/skills}/nx-run/body.md +11 -0
- package/assets/skills/nx-sync/body.ko.md +92 -0
- package/{skills → assets/skills}/nx-sync/body.md +10 -0
- package/assets/tools/tool-name-map.yml +353 -0
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts +4 -0
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -0
- package/dist/assets/hooks/agent-bootstrap/handler.js +100 -0
- package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -0
- package/dist/assets/hooks/agent-finalize/handler.d.ts +4 -0
- package/dist/assets/hooks/agent-finalize/handler.d.ts.map +1 -0
- package/dist/assets/hooks/agent-finalize/handler.js +63 -0
- package/dist/assets/hooks/agent-finalize/handler.js.map +1 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.d.ts +4 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.d.ts.map +1 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.js +40 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.js.map +1 -0
- package/dist/assets/hooks/prompt-router/handler.d.ts +4 -0
- package/dist/assets/hooks/prompt-router/handler.d.ts.map +1 -0
- package/dist/assets/hooks/prompt-router/handler.js +204 -0
- package/dist/assets/hooks/prompt-router/handler.js.map +1 -0
- package/dist/assets/hooks/session-init/handler.d.ts +4 -0
- package/dist/assets/hooks/session-init/handler.d.ts.map +1 -0
- package/dist/assets/hooks/session-init/handler.js +23 -0
- package/dist/assets/hooks/session-init/handler.js.map +1 -0
- package/dist/hooks/agent-bootstrap.js +105 -0
- package/dist/hooks/agent-finalize.js +164 -0
- package/dist/hooks/post-tool-telemetry.js +55 -0
- package/dist/hooks/prompt-router.js +7300 -0
- package/dist/hooks/session-init.js +21 -0
- package/dist/manifests/claude-hooks.json +52 -0
- package/dist/manifests/codex-hooks.json +28 -0
- package/dist/manifests/opencode-manifest.json +44 -0
- package/dist/manifests/portability-report.json +87 -0
- package/dist/scripts/build-agents.d.ts +157 -0
- package/dist/scripts/build-agents.d.ts.map +1 -0
- package/dist/scripts/build-agents.js +737 -0
- package/dist/scripts/build-agents.js.map +1 -0
- package/dist/scripts/build-hooks.d.ts +16 -0
- package/dist/scripts/build-hooks.d.ts.map +1 -0
- package/dist/scripts/build-hooks.js +388 -0
- package/dist/scripts/build-hooks.js.map +1 -0
- package/dist/scripts/cli.d.ts +54 -0
- package/dist/scripts/cli.d.ts.map +1 -0
- package/dist/scripts/cli.js +467 -0
- package/dist/scripts/cli.js.map +1 -0
- package/dist/src/hooks/opencode-mount.d.ts +35 -0
- package/dist/src/hooks/opencode-mount.d.ts.map +1 -0
- package/dist/src/hooks/opencode-mount.js +352 -0
- package/dist/src/hooks/opencode-mount.js.map +1 -0
- package/dist/src/hooks/runtime.d.ts +37 -0
- package/dist/src/hooks/runtime.d.ts.map +1 -0
- package/dist/src/hooks/runtime.js +274 -0
- package/dist/src/hooks/runtime.js.map +1 -0
- package/dist/src/hooks/types.d.ts +196 -0
- package/dist/src/hooks/types.d.ts.map +1 -0
- package/dist/src/hooks/types.js +85 -0
- package/dist/src/hooks/types.js.map +1 -0
- package/dist/src/lsp/cache.d.ts +9 -0
- package/dist/src/lsp/cache.d.ts.map +1 -0
- package/dist/src/lsp/cache.js +216 -0
- package/dist/src/lsp/cache.js.map +1 -0
- package/dist/src/lsp/client.d.ts +24 -0
- package/dist/src/lsp/client.d.ts.map +1 -0
- package/dist/src/lsp/client.js +166 -0
- package/dist/src/lsp/client.js.map +1 -0
- package/dist/src/lsp/detect.d.ts +77 -0
- package/dist/src/lsp/detect.d.ts.map +1 -0
- package/dist/src/lsp/detect.js +116 -0
- package/dist/src/lsp/detect.js.map +1 -0
- package/dist/src/mcp/server.d.ts +5 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +34 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/artifact.d.ts +4 -0
- package/dist/src/mcp/tools/artifact.d.ts.map +1 -0
- package/dist/src/mcp/tools/artifact.js +36 -0
- package/dist/src/mcp/tools/artifact.js.map +1 -0
- package/dist/src/mcp/tools/history.d.ts +3 -0
- package/dist/src/mcp/tools/history.d.ts.map +1 -0
- package/dist/src/mcp/tools/history.js +29 -0
- package/dist/src/mcp/tools/history.js.map +1 -0
- package/dist/src/mcp/tools/lsp.d.ts +13 -0
- package/dist/src/mcp/tools/lsp.d.ts.map +1 -0
- package/dist/src/mcp/tools/lsp.js +225 -0
- package/dist/src/mcp/tools/lsp.js.map +1 -0
- package/dist/src/mcp/tools/plan.d.ts +3 -0
- package/dist/src/mcp/tools/plan.d.ts.map +1 -0
- package/dist/src/mcp/tools/plan.js +317 -0
- package/dist/src/mcp/tools/plan.js.map +1 -0
- package/dist/src/mcp/tools/task.d.ts +3 -0
- package/dist/src/mcp/tools/task.d.ts.map +1 -0
- package/dist/src/mcp/tools/task.js +252 -0
- package/dist/src/mcp/tools/task.js.map +1 -0
- package/dist/src/shared/invocations.d.ts +74 -0
- package/dist/src/shared/invocations.d.ts.map +1 -0
- package/dist/src/shared/invocations.js +247 -0
- package/dist/src/shared/invocations.js.map +1 -0
- package/dist/src/shared/json-store.d.ts +37 -0
- package/dist/src/shared/json-store.d.ts.map +1 -0
- package/dist/src/shared/json-store.js +163 -0
- package/dist/src/shared/json-store.js.map +1 -0
- package/dist/src/shared/mcp-utils.d.ts +3 -0
- package/dist/src/shared/mcp-utils.d.ts.map +1 -0
- package/dist/src/shared/mcp-utils.js +6 -0
- package/dist/src/shared/mcp-utils.js.map +1 -0
- package/dist/src/shared/paths.d.ts +21 -0
- package/dist/src/shared/paths.d.ts.map +1 -0
- package/dist/src/shared/paths.js +81 -0
- package/dist/src/shared/paths.js.map +1 -0
- package/dist/src/shared/tool-log.d.ts +8 -0
- package/dist/src/shared/tool-log.d.ts.map +1 -0
- package/dist/src/shared/tool-log.js +22 -0
- package/dist/src/shared/tool-log.js.map +1 -0
- package/dist/src/types/state.d.ts +862 -0
- package/dist/src/types/state.d.ts.map +1 -0
- package/dist/src/types/state.js +66 -0
- package/dist/src/types/state.js.map +1 -0
- package/docs/consuming/codex-lead-merge.md +106 -0
- package/docs/plugin-guide.md +396 -0
- package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
- package/docs/plugin-template/claude/README.md +110 -0
- package/docs/plugin-template/claude/package.json +16 -0
- package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
- package/docs/plugin-template/codex/README.md +147 -0
- package/docs/plugin-template/codex/package.json +17 -0
- package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
- package/docs/plugin-template/opencode/README.md +121 -0
- package/docs/plugin-template/opencode/package.json +25 -0
- package/package.json +36 -28
- package/agents/architect/meta.yml +0 -13
- package/agents/designer/meta.yml +0 -13
- package/agents/engineer/meta.yml +0 -11
- package/agents/postdoc/meta.yml +0 -13
- package/agents/researcher/meta.yml +0 -12
- package/agents/reviewer/meta.yml +0 -12
- package/agents/strategist/meta.yml +0 -13
- package/agents/tester/meta.yml +0 -12
- package/agents/writer/meta.yml +0 -11
- package/conformance/README.md +0 -311
- package/conformance/examples/plan.extension.schema.example.json +0 -25
- package/conformance/lifecycle/README.md +0 -48
- package/conformance/lifecycle/agent-complete.json +0 -44
- package/conformance/lifecycle/agent-resume.json +0 -43
- package/conformance/lifecycle/agent-spawn.json +0 -36
- package/conformance/lifecycle/memory-access-record.json +0 -27
- package/conformance/lifecycle/session-end.json +0 -48
- package/conformance/scenarios/full-plan-cycle.json +0 -147
- package/conformance/scenarios/task-deps-ordering.json +0 -95
- package/conformance/schema/fixture.schema.json +0 -354
- package/conformance/state-schemas/agent-tracker.schema.json +0 -63
- package/conformance/state-schemas/history.schema.json +0 -134
- package/conformance/state-schemas/memory-access.schema.json +0 -36
- package/conformance/state-schemas/plan.schema.json +0 -77
- package/conformance/state-schemas/tasks.schema.json +0 -98
- package/conformance/tools/artifact-write.json +0 -97
- package/conformance/tools/context.json +0 -172
- package/conformance/tools/history-search.json +0 -219
- package/conformance/tools/plan-decide.json +0 -139
- package/conformance/tools/plan-start.json +0 -81
- package/conformance/tools/plan-status.json +0 -127
- package/conformance/tools/plan-update.json +0 -341
- package/conformance/tools/task-add.json +0 -156
- package/conformance/tools/task-close.json +0 -161
- package/conformance/tools/task-list.json +0 -177
- package/conformance/tools/task-update.json +0 -167
- package/docs/behavioral-contracts.md +0 -145
- package/docs/consumer-implementation-guide.md +0 -840
- package/docs/memory-lifecycle-contract.md +0 -119
- package/docs/nexus-layout.md +0 -224
- package/docs/nexus-outputs-contract.md +0 -344
- package/docs/nexus-state-overview.md +0 -170
- package/docs/nexus-tools-contract.md +0 -438
- package/manifest.json +0 -448
- package/schema/README.md +0 -69
- package/schema/agent.schema.json +0 -23
- package/schema/common.schema.json +0 -17
- package/schema/manifest.schema.json +0 -78
- package/schema/memory-policy.schema.json +0 -98
- package/schema/skill.schema.json +0 -54
- package/schema/task-exceptions.schema.json +0 -40
- package/schema/vocabulary.schema.json +0 -167
- package/scripts/.gitkeep +0 -0
- package/scripts/conformance-coverage.ts +0 -466
- package/scripts/import-from-claude-nexus.ts +0 -403
- package/scripts/lib/frontmatter.ts +0 -71
- package/scripts/lib/lint.ts +0 -348
- package/scripts/lib/structure.ts +0 -159
- package/scripts/lib/validate.ts +0 -796
- package/scripts/validate.ts +0 -90
- package/skills/nx-init/meta.yml +0 -8
- package/skills/nx-plan/meta.yml +0 -10
- package/skills/nx-run/meta.yml +0 -8
- package/skills/nx-sync/meta.yml +0 -7
- package/vocabulary/capabilities.yml +0 -65
- package/vocabulary/categories.yml +0 -11
- package/vocabulary/invocations.yml +0 -147
- package/vocabulary/memory_policy.yml +0 -88
- package/vocabulary/resume-tiers.yml +0 -11
- package/vocabulary/tags.yml +0 -60
- package/vocabulary/task-exceptions.yml +0 -29
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# capability-matrix.yml
|
|
2
|
+
# SSOT for agent capability[] -> per-harness enforcement mapping
|
|
3
|
+
#
|
|
4
|
+
# Purpose:
|
|
5
|
+
# Build scripts read this file to emit harness-specific agent configuration
|
|
6
|
+
# from the canonical `capabilities:` array in each agent's meta.yml.
|
|
7
|
+
# This is NOT the hook capability matrix (that lives in assets/hooks/capability-matrix.yml);
|
|
8
|
+
# this file exclusively concerns agent definition denylist enforcement.
|
|
9
|
+
#
|
|
10
|
+
# Schema:
|
|
11
|
+
# Each top-level key under `capabilities:` is a capability ID that may appear
|
|
12
|
+
# in assets/agents/*/meta.yml `capabilities:` arrays.
|
|
13
|
+
#
|
|
14
|
+
# Per-harness sub-keys:
|
|
15
|
+
# claude:
|
|
16
|
+
# disallowedTools: [...] # Appended to the agent .md frontmatter disallowedTools list
|
|
17
|
+
# # Source: https://code.claude.com/docs/en/sub-agents
|
|
18
|
+
# # Verified in: .nexus/memory/external-harness-agent-permissions.md §2
|
|
19
|
+
#
|
|
20
|
+
# opencode:
|
|
21
|
+
# permission: {} # Merged into the agent .md frontmatter `permission:` block
|
|
22
|
+
# # Source: https://opencode.ai/docs/agents/
|
|
23
|
+
# # Verified in: .nexus/memory/external-harness-agent-permissions.md §3
|
|
24
|
+
#
|
|
25
|
+
# codex:
|
|
26
|
+
# sandbox_mode: ... # If set, written as sandbox_mode key in agent TOML
|
|
27
|
+
# # Source: https://developers.openai.com/codex/config-reference
|
|
28
|
+
# # Verified in: .nexus/memory/external-harness-agent-permissions.md §4
|
|
29
|
+
# disabled_tools: [...] # Appended to [mcp_servers.nx] disabled_tools in agent TOML
|
|
30
|
+
#
|
|
31
|
+
# MCP tool name convention:
|
|
32
|
+
# Claude: mcp__plugin_claude-nexus_nx__<tool>
|
|
33
|
+
# OpenCode: permission key uses tool name directly (server-namespaced wildcard supported)
|
|
34
|
+
# Codex: disabled_tools uses bare tool names as registered in [mcp_servers.nx]
|
|
35
|
+
# Source: .nexus/memory/external-harness-agent-permissions.md §2-4
|
|
36
|
+
#
|
|
37
|
+
# Decision basis:
|
|
38
|
+
# "Agent definition denylist is the single portable enforcement mechanism
|
|
39
|
+
# across all 3 harnesses — caller propagation is not reliable on any."
|
|
40
|
+
# Source: .nexus/memory/external-harness-agent-permissions.md §5-6
|
|
41
|
+
#
|
|
42
|
+
# Updated: 2026-04-19
|
|
43
|
+
# Maintained by: nexus-core team
|
|
44
|
+
|
|
45
|
+
capabilities:
|
|
46
|
+
|
|
47
|
+
# ── no_file_edit ────────────────────────────────────────────────────────────
|
|
48
|
+
# Blocks all file creation and modification by the agent.
|
|
49
|
+
# Applied to: HOW agents (architect, designer, postdoc, strategist),
|
|
50
|
+
# CHECK agents (reviewer), DO agents (researcher).
|
|
51
|
+
# These agents are advisory/analysis roles that must never write to disk.
|
|
52
|
+
#
|
|
53
|
+
# Claude disallowedTools:
|
|
54
|
+
# Edit, Write, MultiEdit, NotebookEdit cover all first-class file-write tools.
|
|
55
|
+
# Source: https://code.claude.com/docs/en/sub-agents (disallowedTools field)
|
|
56
|
+
# Verified: .nexus/memory/external-claude-code-hooks-tools.md §6 (tool catalog)
|
|
57
|
+
# Note: LSP-based rename/write is blocked via the Edit/Write entries.
|
|
58
|
+
#
|
|
59
|
+
# OpenCode permission:
|
|
60
|
+
# edit: deny blocks the `edit` and `write` built-in tools (OpenCode v1.1.1+).
|
|
61
|
+
# apply_patch also requires edit permission; denied transitively.
|
|
62
|
+
# Source: https://opencode.ai/docs/agents/ (permission field)
|
|
63
|
+
# Verified: .nexus/memory/external-harness-agent-permissions.md §3
|
|
64
|
+
#
|
|
65
|
+
# Codex sandbox_mode:
|
|
66
|
+
# "read-only" activates an OS-level sandbox that blocks all file writes.
|
|
67
|
+
# This is the only reliable mechanism in Codex — there are no independent
|
|
68
|
+
# Edit/Write tools to deny (Codex uses apply_patch, which goes through shell).
|
|
69
|
+
# Source: https://developers.openai.com/codex/config-reference
|
|
70
|
+
# Verified: .nexus/memory/external-harness-agent-permissions.md §4
|
|
71
|
+
|
|
72
|
+
no_file_edit:
|
|
73
|
+
claude:
|
|
74
|
+
disallowedTools:
|
|
75
|
+
- Edit
|
|
76
|
+
- Write
|
|
77
|
+
- MultiEdit
|
|
78
|
+
- NotebookEdit
|
|
79
|
+
opencode:
|
|
80
|
+
permission:
|
|
81
|
+
edit: deny
|
|
82
|
+
codex:
|
|
83
|
+
sandbox_mode: read-only
|
|
84
|
+
disabled_tools: [] # OS sandbox is sufficient; no tool-level entries needed
|
|
85
|
+
|
|
86
|
+
# ── no_task_create ──────────────────────────────────────────────────────────
|
|
87
|
+
# Blocks the agent from creating new tasks via nx_task_add.
|
|
88
|
+
# Applied to: all non-Lead agents (9 agents total).
|
|
89
|
+
# Task creation is a Lead-only operation. Soft-gating via denylist is the
|
|
90
|
+
# only portable enforcement; caller propagation is unreliable across harnesses.
|
|
91
|
+
# Source: .nexus/memory/external-harness-agent-permissions.md §5-6
|
|
92
|
+
#
|
|
93
|
+
# Claude disallowedTools:
|
|
94
|
+
# Full MCP tool name with plugin namespace prefix.
|
|
95
|
+
# Wildcard mcp__* syntax is supported in disallowedTools but not confirmed
|
|
96
|
+
# stable across all Claude versions; explicit name is used here for safety.
|
|
97
|
+
# Source: https://code.claude.com/docs/en/sub-agents
|
|
98
|
+
# Verified: .nexus/memory/external-harness-agent-permissions.md §2
|
|
99
|
+
#
|
|
100
|
+
# OpenCode permission:
|
|
101
|
+
# MCP tool names are used directly as permission keys.
|
|
102
|
+
# Wildcard mymcp_*: false pattern is supported per OpenCode agent permission docs.
|
|
103
|
+
# Source: https://opencode.ai/docs/agents/
|
|
104
|
+
# Verified: .nexus/memory/external-harness-agent-permissions.md §3
|
|
105
|
+
#
|
|
106
|
+
# Codex disabled_tools:
|
|
107
|
+
# Bare tool name as registered in [mcp_servers.nx] block of agent TOML.
|
|
108
|
+
# Source: https://developers.openai.com/codex/config-reference (disabled_tools key)
|
|
109
|
+
# Verified: .nexus/memory/external-harness-agent-permissions.md §4
|
|
110
|
+
|
|
111
|
+
no_task_create:
|
|
112
|
+
claude:
|
|
113
|
+
disallowedTools:
|
|
114
|
+
- mcp__plugin_claude-nexus_nx__nx_task_add
|
|
115
|
+
opencode:
|
|
116
|
+
# UNVERIFIED: OpenCode's permission block is documented for built-in tools
|
|
117
|
+
# (edit, bash) and wildcard patterns (mymcp_*). Using a bare MCP tool name
|
|
118
|
+
# as a key is plausible but unconfirmed. Consider switching to a wildcard
|
|
119
|
+
# (nx_*: deny) if this proves ineffective at runtime.
|
|
120
|
+
permission:
|
|
121
|
+
nx_task_add: deny
|
|
122
|
+
codex:
|
|
123
|
+
sandbox_mode: null # No sandbox needed; tool-level block is sufficient
|
|
124
|
+
disabled_tools:
|
|
125
|
+
- nx_task_add
|
|
126
|
+
|
|
127
|
+
# ── no_task_update ──────────────────────────────────────────────────────────
|
|
128
|
+
# Blocks the agent from updating task state via nx_task_update.
|
|
129
|
+
# Applied to: HOW agents (architect, designer, postdoc, strategist).
|
|
130
|
+
# These advisory agents must not modify task lifecycle — they advise Lead,
|
|
131
|
+
# who then updates tasks. DO/CHECK agents may update (engineer can update
|
|
132
|
+
# tasks it works on; reviewer/tester/researcher report to Lead).
|
|
133
|
+
# Source: .nexus/memory/external-harness-agent-permissions.md §2 (table)
|
|
134
|
+
#
|
|
135
|
+
# See no_task_create for per-harness source citations.
|
|
136
|
+
|
|
137
|
+
no_task_update:
|
|
138
|
+
claude:
|
|
139
|
+
disallowedTools:
|
|
140
|
+
- mcp__plugin_claude-nexus_nx__nx_task_update
|
|
141
|
+
opencode:
|
|
142
|
+
permission:
|
|
143
|
+
nx_task_update: deny
|
|
144
|
+
codex:
|
|
145
|
+
sandbox_mode: null
|
|
146
|
+
disabled_tools:
|
|
147
|
+
- nx_task_update
|
|
148
|
+
|
|
149
|
+
# ── model_tier ──────────────────────────────────────────────────────────────
|
|
150
|
+
# Maps the abstract `model_tier` field in meta.yml to concrete model slugs
|
|
151
|
+
# per harness. The build script substitutes these into agent definitions at
|
|
152
|
+
# build time.
|
|
153
|
+
#
|
|
154
|
+
# Rationale:
|
|
155
|
+
# Agents declare `model_tier: high | standard | low` rather than hard-coding
|
|
156
|
+
# model IDs so that model upgrades require only this file to change, not
|
|
157
|
+
# every agent definition.
|
|
158
|
+
#
|
|
159
|
+
# opencode: null means "inherit from user config" (opencode has no forced model
|
|
160
|
+
# override in the agent definition by default — the user or project config
|
|
161
|
+
# supplies the active model).
|
|
162
|
+
# Source: https://opencode.ai/docs/agents/ (model field is optional)
|
|
163
|
+
# Verified: .nexus/memory/external-opencode-plugin.md §5
|
|
164
|
+
#
|
|
165
|
+
# claude model slugs (partial-match basis — Claude Code resolves to latest in series):
|
|
166
|
+
# Source: https://code.claude.com/docs/en/sub-agents (model field)
|
|
167
|
+
# Verified: .nexus/memory/external-claude-code-hooks-tools.md §6 (Agent tool: model param)
|
|
168
|
+
#
|
|
169
|
+
# codex model slugs:
|
|
170
|
+
# Source: https://developers.openai.com/codex/config-reference (model key in agent TOML)
|
|
171
|
+
# Verified: .nexus/memory/external-harness-agent-permissions.md §4 (model field example)
|
|
172
|
+
# Note: gpt-5.4 / gpt-5.3-codex / gpt-5.4-mini reflect current Codex-tier naming
|
|
173
|
+
# as documented in config-reference. Update this section when OpenAI releases
|
|
174
|
+
# new model versions.
|
|
175
|
+
|
|
176
|
+
model_tier:
|
|
177
|
+
|
|
178
|
+
# high — used by: architect, designer, postdoc, strategist
|
|
179
|
+
# Reasoning-intensive advisory roles that require deep analysis.
|
|
180
|
+
high:
|
|
181
|
+
claude: claude-opus-4
|
|
182
|
+
codex: gpt-5.4 # UNVERIFIED slug — confirm against current Codex model list
|
|
183
|
+
opencode: null # inherits user config
|
|
184
|
+
|
|
185
|
+
# standard — used by: engineer, researcher, reviewer, tester, writer
|
|
186
|
+
# Execution and verification roles where throughput matters more than
|
|
187
|
+
# top-tier reasoning.
|
|
188
|
+
standard:
|
|
189
|
+
claude: claude-sonnet-4
|
|
190
|
+
codex: gpt-5.3-codex # UNVERIFIED slug — confirm against current Codex model list
|
|
191
|
+
opencode: null # inherits user config
|
|
192
|
+
|
|
193
|
+
# low — reserved tier; no agents currently assigned.
|
|
194
|
+
# Intended for lightweight utility agents (fast, cheap, simple tasks).
|
|
195
|
+
low:
|
|
196
|
+
claude: claude-haiku-4
|
|
197
|
+
codex: gpt-5.4-mini # UNVERIFIED slug — confirm against current Codex model list
|
|
198
|
+
opencode: null # inherits user config
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-bootstrap handler tests
|
|
3
|
+
*
|
|
4
|
+
* Scenarios:
|
|
5
|
+
* (1) fresh + registered role (assets/agents/architect exists) → additional_context contains core index + rules
|
|
6
|
+
* (2) fresh + unregistered role ("general") → silent skip (no additional_context)
|
|
7
|
+
* (3) resume_count > 0 → skip (not fresh)
|
|
8
|
+
* (4) .nexus/rules/<role>.md absent → core index only
|
|
9
|
+
* (5) core index > 2KB → truncated to recent-modified N entries
|
|
10
|
+
* (6) tracker write side-effect absent (agent-tracker.json unchanged before/after)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
14
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, utimesSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
import handler from "./handler.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Fixture helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Create a temp directory tree suitable for agent-bootstrap tests.
|
|
25
|
+
* Returns the cwd (root of the fixture).
|
|
26
|
+
*
|
|
27
|
+
* Layout:
|
|
28
|
+
* <root>/
|
|
29
|
+
* assets/agents/architect/ ← registered role
|
|
30
|
+
* .nexus/memory/ ← memory .md files
|
|
31
|
+
* .nexus/context/ ← context .md files
|
|
32
|
+
* .nexus/rules/architect.md ← role rule
|
|
33
|
+
* .nexus/state/sessions/<sid>/ ← session dir (no tracker by default)
|
|
34
|
+
*/
|
|
35
|
+
function makeFixture(opts: {
|
|
36
|
+
withRule?: boolean;
|
|
37
|
+
memoryFiles?: number;
|
|
38
|
+
contextFiles?: number;
|
|
39
|
+
withTracker?: { agentId: string; resumeCount: number };
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
} = {}): { cwd: string; sessionId: string; cleanup: () => void } {
|
|
42
|
+
const {
|
|
43
|
+
withRule = true,
|
|
44
|
+
memoryFiles = 1,
|
|
45
|
+
contextFiles = 1,
|
|
46
|
+
withTracker,
|
|
47
|
+
sessionId = "sess-test",
|
|
48
|
+
} = opts;
|
|
49
|
+
|
|
50
|
+
const cwd = mkdtempSync(join(tmpdir(), "nexus-bootstrap-"));
|
|
51
|
+
|
|
52
|
+
// Registered role directory
|
|
53
|
+
mkdirSync(join(cwd, "assets/agents/architect"), { recursive: true });
|
|
54
|
+
|
|
55
|
+
// Memory files
|
|
56
|
+
const memDir = join(cwd, ".nexus/memory");
|
|
57
|
+
mkdirSync(memDir, { recursive: true });
|
|
58
|
+
for (let i = 0; i < memoryFiles; i++) {
|
|
59
|
+
writeFileSync(join(memDir, `mem-${i}.md`), `# Memory file ${i}\nsome content`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Context files
|
|
63
|
+
const ctxDir = join(cwd, ".nexus/context");
|
|
64
|
+
mkdirSync(ctxDir, { recursive: true });
|
|
65
|
+
for (let i = 0; i < contextFiles; i++) {
|
|
66
|
+
writeFileSync(join(ctxDir, `ctx-${i}.md`), `# Context file ${i}\nsome content`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Rules
|
|
70
|
+
const rulesDir = join(cwd, ".nexus/rules");
|
|
71
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
72
|
+
if (withRule) {
|
|
73
|
+
writeFileSync(join(rulesDir, "architect.md"), "Always think in systems.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Session dir
|
|
77
|
+
const sessionDir = join(cwd, ".nexus/state", sessionId);
|
|
78
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
// Optional tracker
|
|
81
|
+
if (withTracker) {
|
|
82
|
+
writeFileSync(
|
|
83
|
+
join(sessionDir, "agent-tracker.json"),
|
|
84
|
+
JSON.stringify([
|
|
85
|
+
{ agent_id: withTracker.agentId, resume_count: withTracker.resumeCount },
|
|
86
|
+
])
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
cwd,
|
|
92
|
+
sessionId,
|
|
93
|
+
cleanup: () => rmSync(cwd, { recursive: true, force: true }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build a SubagentStart input */
|
|
98
|
+
function makeInput(
|
|
99
|
+
cwd: string,
|
|
100
|
+
sessionId: string,
|
|
101
|
+
agentType: string,
|
|
102
|
+
agentId = "agent-001"
|
|
103
|
+
) {
|
|
104
|
+
return {
|
|
105
|
+
hook_event_name: "SubagentStart" as const,
|
|
106
|
+
session_id: sessionId,
|
|
107
|
+
cwd,
|
|
108
|
+
agent_type: agentType,
|
|
109
|
+
agent_id: agentId,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Scenario (1): fresh + registered role → additional_context with core index + rules
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
describe("scenario 1: fresh + registered role", () => {
|
|
118
|
+
let cleanup: () => void;
|
|
119
|
+
|
|
120
|
+
afterAll(() => cleanup?.());
|
|
121
|
+
|
|
122
|
+
test("additional_context contains core index and role rule", async () => {
|
|
123
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({ withRule: true });
|
|
124
|
+
cleanup = c;
|
|
125
|
+
|
|
126
|
+
const result = await handler(makeInput(cwd, sessionId, "architect"));
|
|
127
|
+
|
|
128
|
+
expect(result).toBeDefined();
|
|
129
|
+
expect(result!.additional_context).toBeDefined();
|
|
130
|
+
const ctx = result!.additional_context!;
|
|
131
|
+
|
|
132
|
+
// Core index header
|
|
133
|
+
expect(ctx).toContain("Available memory/context:");
|
|
134
|
+
// At least one .md entry
|
|
135
|
+
expect(ctx).toMatch(/\.nexus\/(memory|context)\/.*\.md/);
|
|
136
|
+
// Role rule injection
|
|
137
|
+
expect(ctx).toContain("Custom rule for architect:");
|
|
138
|
+
expect(ctx).toContain("Always think in systems.");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Scenario (2): fresh + unregistered role → silent skip
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
describe("scenario 2: fresh + unregistered role", () => {
|
|
147
|
+
let cleanup: () => void;
|
|
148
|
+
|
|
149
|
+
afterAll(() => cleanup?.());
|
|
150
|
+
|
|
151
|
+
test("returns undefined (no additional_context) for unknown role", async () => {
|
|
152
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({ withRule: false });
|
|
153
|
+
cleanup = c;
|
|
154
|
+
|
|
155
|
+
const result = await handler(makeInput(cwd, sessionId, "general"));
|
|
156
|
+
|
|
157
|
+
// Silent skip: handler returns void / undefined
|
|
158
|
+
expect(result == null).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Scenario (3): resume_count > 0 → skip entirely
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe("scenario 3: resume — skip on resume_count > 0", () => {
|
|
167
|
+
let cleanup: () => void;
|
|
168
|
+
|
|
169
|
+
afterAll(() => cleanup?.());
|
|
170
|
+
|
|
171
|
+
test("returns undefined when agent has been resumed", async () => {
|
|
172
|
+
const agentId = "agent-resume";
|
|
173
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({
|
|
174
|
+
withTracker: { agentId, resumeCount: 1 },
|
|
175
|
+
});
|
|
176
|
+
cleanup = c;
|
|
177
|
+
|
|
178
|
+
const result = await handler(makeInput(cwd, sessionId, "architect", agentId));
|
|
179
|
+
|
|
180
|
+
expect(result == null).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Scenario (4): no .nexus/rules/<role>.md → core index only (no rule block)
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe("scenario 4: no role rule file → core index only", () => {
|
|
189
|
+
let cleanup: () => void;
|
|
190
|
+
|
|
191
|
+
afterAll(() => cleanup?.());
|
|
192
|
+
|
|
193
|
+
test("additional_context has core index but no rule block", async () => {
|
|
194
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({ withRule: false });
|
|
195
|
+
cleanup = c;
|
|
196
|
+
|
|
197
|
+
const result = await handler(makeInput(cwd, sessionId, "architect"));
|
|
198
|
+
|
|
199
|
+
expect(result).toBeDefined();
|
|
200
|
+
const ctx = result!.additional_context!;
|
|
201
|
+
|
|
202
|
+
expect(ctx).toContain("Available memory/context:");
|
|
203
|
+
expect(ctx).not.toContain("Custom rule for architect:");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Scenario (5): core index > 2KB → truncated to recent N entries
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
describe("scenario 5: 2KB truncation of core index", () => {
|
|
212
|
+
let cleanup: () => void;
|
|
213
|
+
|
|
214
|
+
afterAll(() => cleanup?.());
|
|
215
|
+
|
|
216
|
+
test("core index is truncated when total size exceeds 2KB", async () => {
|
|
217
|
+
// Create enough files to exceed 2KB.
|
|
218
|
+
// Each entry line looks like:
|
|
219
|
+
// "- .nexus/memory/mem-XX.md: Memory file XX" (~45 chars + newline)
|
|
220
|
+
// 2048 / 46 ≈ 44 entries needed; create 60 to be safe.
|
|
221
|
+
const LOTS = 60;
|
|
222
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({
|
|
223
|
+
withRule: false,
|
|
224
|
+
memoryFiles: LOTS,
|
|
225
|
+
contextFiles: 0,
|
|
226
|
+
});
|
|
227
|
+
cleanup = c;
|
|
228
|
+
|
|
229
|
+
// Spread mtimes so "recent" ordering is deterministic:
|
|
230
|
+
// give each file a distinct mtime (oldest → newest = index 0 → LOTS-1)
|
|
231
|
+
for (let i = 0; i < LOTS; i++) {
|
|
232
|
+
const filePath = join(cwd, ".nexus/memory", `mem-${i}.md`);
|
|
233
|
+
const t = new Date(Date.now() - (LOTS - i) * 10_000); // older files have smaller index
|
|
234
|
+
utimesSync(filePath, t, t);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result = await handler(makeInput(cwd, sessionId, "architect"));
|
|
238
|
+
|
|
239
|
+
expect(result).toBeDefined();
|
|
240
|
+
const ctx = result!.additional_context!;
|
|
241
|
+
expect(ctx).toContain("Available memory/context:");
|
|
242
|
+
|
|
243
|
+
// The raw section between <system-notice> tags for the core index
|
|
244
|
+
const indexSection = ctx.split("</system-notice>")[0];
|
|
245
|
+
|
|
246
|
+
// Should NOT contain all 60 files — truncation must have kicked in
|
|
247
|
+
const entryCount = (indexSection.match(/- \.nexus\/memory\/mem-/g) ?? []).length;
|
|
248
|
+
expect(entryCount).toBeGreaterThan(0);
|
|
249
|
+
expect(entryCount).toBeLessThan(LOTS);
|
|
250
|
+
|
|
251
|
+
// The index section must not exceed 2KB (plus small wrapper overhead)
|
|
252
|
+
const indexBytes = new TextEncoder().encode(indexSection).length;
|
|
253
|
+
// Allow a generous margin for the header line and wrapping
|
|
254
|
+
expect(indexBytes).toBeLessThan(2048 + 200);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Scenario (6): no tracker write side-effects
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
describe("scenario 6: handler produces no file write side-effects", () => {
|
|
263
|
+
let cleanup: () => void;
|
|
264
|
+
|
|
265
|
+
afterAll(() => cleanup?.());
|
|
266
|
+
|
|
267
|
+
test("agent-tracker.json is not created or modified by handler", async () => {
|
|
268
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({ withTracker: undefined });
|
|
269
|
+
cleanup = c;
|
|
270
|
+
|
|
271
|
+
const trackerPath = join(cwd, ".nexus/state", sessionId, "agent-tracker.json");
|
|
272
|
+
|
|
273
|
+
// Tracker must not exist before the call
|
|
274
|
+
expect(existsSync(trackerPath)).toBe(false);
|
|
275
|
+
|
|
276
|
+
await handler(makeInput(cwd, sessionId, "architect"));
|
|
277
|
+
|
|
278
|
+
// Tracker must still not exist after the call
|
|
279
|
+
expect(existsSync(trackerPath)).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("pre-existing agent-tracker.json is not modified by handler", async () => {
|
|
283
|
+
const agentId = "agent-side-effect";
|
|
284
|
+
const { cwd, sessionId, cleanup: c } = makeFixture({
|
|
285
|
+
withTracker: { agentId, resumeCount: 0 },
|
|
286
|
+
});
|
|
287
|
+
cleanup = c;
|
|
288
|
+
|
|
289
|
+
const trackerPath = join(cwd, ".nexus/state", sessionId, "agent-tracker.json");
|
|
290
|
+
const before = readFileSync(trackerPath, "utf-8");
|
|
291
|
+
|
|
292
|
+
await handler(makeInput(cwd, sessionId, "architect", agentId));
|
|
293
|
+
|
|
294
|
+
const after = readFileSync(trackerPath, "utf-8");
|
|
295
|
+
expect(after).toBe(before);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// 모듈 전역 상태 격리
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
describe("모듈 전역 상태 격리", () => {
|
|
304
|
+
// Test A: two different cwds with different agent sets produce independent results
|
|
305
|
+
test("Test A: different cwds return only their own roles", async () => {
|
|
306
|
+
const tmpDir1 = mkdtempSync(join(tmpdir(), "nexus-isolation-a1-"));
|
|
307
|
+
const tmpDir2 = mkdtempSync(join(tmpdir(), "nexus-isolation-a2-"));
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
// tmpDir1: only "architect"
|
|
311
|
+
mkdirSync(join(tmpDir1, "assets/agents/architect"), { recursive: true });
|
|
312
|
+
mkdirSync(join(tmpDir1, ".nexus/memory"), { recursive: true });
|
|
313
|
+
writeFileSync(join(tmpDir1, ".nexus/memory/mem.md"), "# mem\ncontent");
|
|
314
|
+
mkdirSync(join(tmpDir1, ".nexus/state/sess1"), { recursive: true });
|
|
315
|
+
|
|
316
|
+
// tmpDir2: only "engineer"
|
|
317
|
+
mkdirSync(join(tmpDir2, "assets/agents/engineer"), { recursive: true });
|
|
318
|
+
mkdirSync(join(tmpDir2, ".nexus/memory"), { recursive: true });
|
|
319
|
+
writeFileSync(join(tmpDir2, ".nexus/memory/mem.md"), "# mem\ncontent");
|
|
320
|
+
mkdirSync(join(tmpDir2, ".nexus/state/sess2"), { recursive: true });
|
|
321
|
+
|
|
322
|
+
// Call with tmpDir1 as architect → should return context
|
|
323
|
+
const r1a = await handler(makeInput(tmpDir1, "sess1", "architect"));
|
|
324
|
+
expect(r1a).toBeDefined();
|
|
325
|
+
|
|
326
|
+
// Call with tmpDir1 as engineer → should be skipped (engineer not in tmpDir1)
|
|
327
|
+
const r1b = await handler(makeInput(tmpDir1, "sess1", "engineer"));
|
|
328
|
+
expect(r1b == null).toBe(true);
|
|
329
|
+
|
|
330
|
+
// Call with tmpDir2 as engineer → should return context
|
|
331
|
+
const r2a = await handler(makeInput(tmpDir2, "sess2", "engineer"));
|
|
332
|
+
expect(r2a).toBeDefined();
|
|
333
|
+
|
|
334
|
+
// Call with tmpDir2 as architect → should be skipped (architect not in tmpDir2)
|
|
335
|
+
const r2b = await handler(makeInput(tmpDir2, "sess2", "architect"));
|
|
336
|
+
expect(r2b == null).toBe(true);
|
|
337
|
+
} finally {
|
|
338
|
+
rmSync(tmpDir1, { recursive: true, force: true });
|
|
339
|
+
rmSync(tmpDir2, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Test B: adding a new role directory is picked up on the next call
|
|
344
|
+
test("Test B: newly added role is reflected in subsequent call", async () => {
|
|
345
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "nexus-isolation-b-"));
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// Start with only "architect"
|
|
349
|
+
mkdirSync(join(tmpDir, "assets/agents/architect"), { recursive: true });
|
|
350
|
+
mkdirSync(join(tmpDir, ".nexus/memory"), { recursive: true });
|
|
351
|
+
writeFileSync(join(tmpDir, ".nexus/memory/mem.md"), "# mem\ncontent");
|
|
352
|
+
mkdirSync(join(tmpDir, ".nexus/state/sess"), { recursive: true });
|
|
353
|
+
|
|
354
|
+
// "foo" does not exist yet → should be skipped
|
|
355
|
+
const r1 = await handler(makeInput(tmpDir, "sess", "foo"));
|
|
356
|
+
expect(r1 == null).toBe(true);
|
|
357
|
+
|
|
358
|
+
// Add "foo" role directory
|
|
359
|
+
mkdirSync(join(tmpDir, "assets/agents/foo"), { recursive: true });
|
|
360
|
+
|
|
361
|
+
// Now "foo" should be recognized on the next call
|
|
362
|
+
const r2 = await handler(makeInput(tmpDir, "sess", "foo"));
|
|
363
|
+
expect(r2).toBeDefined();
|
|
364
|
+
} finally {
|
|
365
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { HookHandler } from "../../../src/hooks/types.js";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const CORE_INDEX_SIZE_LIMIT = 2 * 1024; // 2KB
|
|
6
|
+
|
|
7
|
+
function loadValidRoles(cwd: string): string[] {
|
|
8
|
+
const agentsDir = join(cwd, "assets/agents");
|
|
9
|
+
const roles: string[] = [];
|
|
10
|
+
if (existsSync(agentsDir)) {
|
|
11
|
+
for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
|
|
12
|
+
if (entry.isDirectory()) roles.push(entry.name);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return roles;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readFirstLine(path: string): string {
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(path, "utf-8");
|
|
21
|
+
const firstNonEmpty =
|
|
22
|
+
content.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
23
|
+
return firstNonEmpty.replace(/^#+\s*/, "").slice(0, 80);
|
|
24
|
+
} catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildCoreIndex(cwd: string): string {
|
|
30
|
+
const entries: Array<{ path: string; mtime: number; line: string }> = [];
|
|
31
|
+
|
|
32
|
+
for (const sub of [".nexus/memory", ".nexus/context"]) {
|
|
33
|
+
const absDir = join(cwd, sub);
|
|
34
|
+
if (!existsSync(absDir)) continue;
|
|
35
|
+
for (const f of readdirSync(absDir, { withFileTypes: true })) {
|
|
36
|
+
if (!f.isFile() || !f.name.endsWith(".md")) continue;
|
|
37
|
+
const full = join(absDir, f.name);
|
|
38
|
+
entries.push({
|
|
39
|
+
path: `${sub}/${f.name}`,
|
|
40
|
+
mtime: statSync(full).mtimeMs,
|
|
41
|
+
line: readFirstLine(full),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
47
|
+
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const formatted = `- ${e.path}: ${e.line}`;
|
|
52
|
+
if (bytes + formatted.length + 1 > CORE_INDEX_SIZE_LIMIT) break;
|
|
53
|
+
lines.push(formatted);
|
|
54
|
+
bytes += formatted.length + 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines.length > 0
|
|
58
|
+
? "Available memory/context:\n" + lines.join("\n")
|
|
59
|
+
: "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getResumeCount(
|
|
63
|
+
cwd: string,
|
|
64
|
+
sessionId: string,
|
|
65
|
+
agentId: string
|
|
66
|
+
): number {
|
|
67
|
+
const trackerPath = join(
|
|
68
|
+
cwd,
|
|
69
|
+
".nexus/state",
|
|
70
|
+
sessionId,
|
|
71
|
+
"agent-tracker.json"
|
|
72
|
+
);
|
|
73
|
+
if (!existsSync(trackerPath)) return 0;
|
|
74
|
+
try {
|
|
75
|
+
const tracker = JSON.parse(readFileSync(trackerPath, "utf-8"));
|
|
76
|
+
const entry = Array.isArray(tracker)
|
|
77
|
+
? tracker.find((e: { agent_id?: string }) => e.agent_id === agentId)
|
|
78
|
+
: null;
|
|
79
|
+
return (entry as { resume_count?: number } | null)?.resume_count ?? 0;
|
|
80
|
+
} catch {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handler: HookHandler = async (input) => {
|
|
86
|
+
if (input.hook_event_name !== "SubagentStart") return;
|
|
87
|
+
|
|
88
|
+
const { cwd, session_id, agent_type, agent_id } = input;
|
|
89
|
+
|
|
90
|
+
// fresh only — skip on resume
|
|
91
|
+
const resumeCount = getResumeCount(cwd, session_id, agent_id);
|
|
92
|
+
if (resumeCount > 0) return;
|
|
93
|
+
|
|
94
|
+
// unregistered role: silent skip
|
|
95
|
+
const validRoles = loadValidRoles(cwd);
|
|
96
|
+
if (!validRoles.includes(agent_type)) return;
|
|
97
|
+
|
|
98
|
+
const parts: string[] = [];
|
|
99
|
+
|
|
100
|
+
const coreIndex = buildCoreIndex(cwd);
|
|
101
|
+
if (coreIndex) {
|
|
102
|
+
parts.push(`<system-notice>\n${coreIndex}\n</system-notice>`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rulePath = join(cwd, ".nexus/rules", `${agent_type}.md`);
|
|
106
|
+
if (existsSync(rulePath)) {
|
|
107
|
+
const ruleContent = readFileSync(rulePath, "utf-8").trim();
|
|
108
|
+
if (ruleContent) {
|
|
109
|
+
parts.push(
|
|
110
|
+
`<system-notice>\nCustom rule for ${agent_type}:\n${ruleContent}\n</system-notice>`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (parts.length === 0) return;
|
|
116
|
+
return { additional_context: parts.join("\n\n") };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default handler;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
name: agent-bootstrap
|
|
2
|
+
description: Inject core memory index and role-specific rules on fresh subagent spawn
|
|
3
|
+
events: [SubagentStart]
|
|
4
|
+
matcher: "*"
|
|
5
|
+
timeout: 10
|
|
6
|
+
fallback: warn
|
|
7
|
+
priority: 0
|
|
8
|
+
requires_capabilities:
|
|
9
|
+
- event.subagent_start
|
|
10
|
+
- output.additional_context.session_start
|