@moreih29/nexus-core 0.7.0 → 0.8.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 +5 -6
- package/docs/consumer-implementation-guide.md +1 -1
- package/docs/nexus-outputs-contract.md +4 -5
- package/docs/nexus-tools-contract.md +1 -1
- package/manifest.json +83 -56
- package/package.json +1 -1
- package/schema/manifest.schema.json +19 -4
- package/schema/vocabulary.schema.json +45 -0
- package/scripts/lib/lint.ts +148 -16
- package/scripts/lib/validate.ts +91 -3
- package/skills/nx-init/body.md +6 -16
- package/skills/nx-init/meta.yml +1 -0
- package/skills/nx-plan/body.md +3 -3
- package/skills/nx-run/body.md +5 -5
- package/skills/nx-sync/body.md +3 -2
- package/vocabulary/capabilities.yml +1 -1
- package/vocabulary/invocations.yml +116 -0
package/README.md
CHANGED
|
@@ -9,26 +9,25 @@
|
|
|
9
9
|
Nexus 생태계는 세 층위로 나뉩니다. `nexus-core`는 가장 아래, **Authoring layer**에 위치합니다.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
Supervision
|
|
12
|
+
Supervision (reserved)
|
|
13
13
|
│ read-only
|
|
14
14
|
Execution claude-nexus ↔ opencode-nexus
|
|
15
15
|
│ read-only
|
|
16
16
|
Authoring nexus-core ← 이 저장소
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
현재 active 소비자는 두 Execution layer 하네스(`claude-nexus`, `opencode-nexus`)이며, 모두 `nexus-core`를 **read-only**로 참조합니다. Supervision layer는 외부 감독자 consumer를 위해 예약된 자리입니다(과거 nexus-code 프로젝트가 이 layer를 구현했으나 2026-04-14 archived).
|
|
20
20
|
|
|
21
21
|
| Consumer | Layer | 하는 일 |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| [`claude-nexus`](https://github.com/moreih29/claude-nexus) | Execution | Claude Code 하네스 위에서 에이전트 조립·디스패치 |
|
|
24
24
|
| [`opencode-nexus`](https://github.com/moreih29/opencode-nexus) | Execution | OpenCode 하네스 위에서 에이전트 조립·디스패치 |
|
|
25
|
-
| `nexus-code` | Supervision | Execution 세션 감독·Policy Enforcement·시각화 |
|
|
26
25
|
|
|
27
26
|
## For Consumer Repositories
|
|
28
27
|
|
|
29
28
|
> 이 저장소는 **외부 사용자가 직접 설치하는 플러그인이 아닙니다**. Nexus 하네스(`claude-nexus`, `opencode-nexus`)를 사용하려면 해당 저장소의 안내를 따르세요.
|
|
30
29
|
|
|
31
|
-
Consumer 저장소(`claude-nexus`, `opencode-nexus
|
|
30
|
+
Consumer 저장소(`claude-nexus`, `opencode-nexus`)의 LLM 에이전트가 `@moreih29/nexus-core` 버전 업그레이드를 처리해야 하는 경우, **[CONSUMING.md](./CONSUMING.md)**의 Upgrade Protocol을 참조하세요.
|
|
32
31
|
|
|
33
32
|
CONSUMING.md는 LLM 에이전트 전용 문서입니다. 사람 독자는 이 README가 더 유용합니다.
|
|
34
33
|
|
|
@@ -56,7 +55,7 @@ CONSUMING.md는 LLM 에이전트 전용 문서입니다. 사람 독자는 이 RE
|
|
|
56
55
|
- MCP server 구현 — 각 하네스 내부
|
|
57
56
|
- TypeScript 런타임 타입 — 각 하네스 내부
|
|
58
57
|
- 런타임 I/O 로직 — 각 하네스 내부
|
|
59
|
-
- Supervision 집행 로직 (`ApprovalBridge` 등) —
|
|
58
|
+
- Supervision 집행 로직 (`ApprovalBridge` 등) — 외부 Supervision consumer 내부 (현재 active consumer 없음)
|
|
60
59
|
- UI hint 필드 (`icon`, `color` 등) — 특정 소비자 결합 금지
|
|
61
60
|
|
|
62
61
|
## 원칙
|
|
@@ -70,7 +69,7 @@ CONSUMING.md는 LLM 에이전트 전용 문서입니다. 사람 독자는 이 RE
|
|
|
70
69
|
|
|
71
70
|
## Status
|
|
72
71
|
|
|
73
|
-
v0.
|
|
72
|
+
v0.7.1 (2026-04-14). 최신 release: agent-tracker namespace isolation (v0.7.0, GH #16) + nexus-code archived cleanup (v0.7.1). 상세 변경 이력은 [CHANGELOG.md](./CHANGELOG.md) 참조.
|
|
74
73
|
|
|
75
74
|
## References
|
|
76
75
|
|
|
@@ -382,7 +382,7 @@ For each agent in `manifest.json`:
|
|
|
382
382
|
|
|
383
383
|
### Neutral body + harness context synthesis
|
|
384
384
|
|
|
385
|
-
`body.md` is deliberately harness-neutral. It says things like "report to Lead" without specifying the concrete mechanism. This is intentional: the same body works across claude-nexus
|
|
385
|
+
`body.md` is deliberately harness-neutral. It says things like "report to Lead" without specifying the concrete mechanism. This is intentional: the same body works across claude-nexus and opencode-nexus because each harness injects the mechanism alongside the body.
|
|
386
386
|
|
|
387
387
|
Your harness must inject harness-specific tool awareness when activating an agent. Concretely:
|
|
388
388
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nexus 산출물 제어 계약
|
|
2
2
|
|
|
3
|
-
이 문서는 nexus-core를 소비하는 하네스(claude-nexus, opencode-nexus
|
|
3
|
+
이 문서는 nexus-core를 소비하는 하네스(claude-nexus, opencode-nexus)가 준수해야 할 **산출물 제어 normative 계약**이다. 산출물이란 Nexus 세션 또는 프로젝트 사이클에서 생성·수정·삭제되는 모든 파일을 의미하며, 그 책임 주체·생성 조건·삭제 조건·상호운용 의무를 선언적으로 기술한다.
|
|
4
4
|
|
|
5
5
|
이 문서는 `docs/nexus-state-overview.md`와 역할을 명확히 분담한다.
|
|
6
6
|
|
|
@@ -174,7 +174,6 @@ Nexus 산출물은 생성 책임 주체에 따라 세 카테고리로 분류된
|
|
|
174
174
|
- **MUST**: `{harness-id}`는 하네스 npm package name의 마지막 segment를 사용한다.
|
|
175
175
|
- `@moreih29/claude-nexus` → `claude-nexus`
|
|
176
176
|
- `@moreih29/opencode-nexus` → `opencode-nexus`
|
|
177
|
-
- `@moreih29/nexus-code` → `nexus-code`
|
|
178
177
|
- **MUST NOT**: nexus-core는 하네스 id 레지스트리를 소유하지 않는다. 규약은 각 하네스 `CONSUMING.md` / `README.md`에서 자기 id를 선언한다.
|
|
179
178
|
|
|
180
179
|
### Extension 파일 의무
|
|
@@ -251,7 +250,7 @@ consumer는 이 파일을 starting point로 삼아 자신의 실제 extension
|
|
|
251
250
|
|
|
252
251
|
**shared-purpose file**이란 두 개 이상의 하네스가 **같은 목적**(예: agent lifecycle tracking)을 위해 **같은 파일명**으로 **각자의 harness-local namespace**에 두는 파일이다. 공통 state(`plan.json`, `tasks.json`)와 달리 단일 공유 경로가 아니라 하네스별 namespace 하위에 각각 존재한다.
|
|
253
252
|
|
|
254
|
-
nexus-core는 shared-purpose file에 대해 **최소 공통 schema contract**를 선언한다. 이 contract는 cross-harness aggregator
|
|
253
|
+
nexus-core는 shared-purpose file에 대해 **최소 공통 schema contract**를 선언한다. 이 contract는 cross-harness aggregator가 여러 하네스의 파일을 일관되게 읽을 수 있도록 보장한다.
|
|
255
254
|
|
|
256
255
|
### 최소 공통 필드 contract
|
|
257
256
|
|
|
@@ -275,9 +274,9 @@ nexus-core는 shared-purpose file에 대해 **최소 공통 schema contract**를
|
|
|
275
274
|
|
|
276
275
|
향후 신규 shared-purpose file을 추가할 때는 반드시 이 섹션에 등록해야 한다. 등록 없이 여러 하네스가 동일 파일명을 독립적으로 사용하는 것은 MUST NOT 허용되어서는 안 된다.
|
|
277
276
|
|
|
278
|
-
### Supervision
|
|
277
|
+
### Supervision aggregation 전제 (reserved)
|
|
279
278
|
|
|
280
|
-
|
|
279
|
+
향후 Supervision consumer가 cross-harness aggregation을 구현할 경우, `.nexus/state/*/agent-tracker.json` glob 모델로 각 하네스의 파일을 개별적으로 읽는다. 단일 공통 경로 파일 모델이 아니다. 이 glob 모델이 동작하려면 각 하네스가 정확히 `{harness-id}/agent-tracker.json` 경로에 파일을 기록해야 한다. (과거 nexus-code가 이 역할을 할 예정이었으나 2026-04-14 archived.)
|
|
281
280
|
|
|
282
281
|
---
|
|
283
282
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nexus MCP Tool Contracts
|
|
2
2
|
|
|
3
|
-
This document is the normative specification for the eleven Nexus MCP tools. Implementations in all consumer harnesses (claude-nexus, opencode-nexus
|
|
3
|
+
This document is the normative specification for the eleven Nexus MCP tools. Implementations in all consumer harnesses (claude-nexus, opencode-nexus) must conform to the parameter names, types, return shapes, side effects, and error conditions defined here. Harness-specific registration names (prefixes such as `nx_` or `mcp__plugin_*`) are implementation details and are not part of this specification.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"nexus_core_version": "0.
|
|
3
|
-
"nexus_core_commit": "
|
|
2
|
+
"nexus_core_version": "0.8.0",
|
|
3
|
+
"nexus_core_commit": "254efc7d8f4f52e45b548706dd42389fdb9801b2",
|
|
4
4
|
"schema_contract_version": "2.0",
|
|
5
5
|
"agents": [
|
|
6
6
|
{
|
|
@@ -19,20 +19,6 @@
|
|
|
19
19
|
"id": "architect",
|
|
20
20
|
"body_hash": "sha256:85f9a3de419f32cdae284436eb1d902bff19a2230c50fe3068ffc642949a63b7"
|
|
21
21
|
},
|
|
22
|
-
{
|
|
23
|
-
"name": "engineer",
|
|
24
|
-
"description": "Implementation — writes code, debugs issues, follows specifications from Lead and architect",
|
|
25
|
-
"task": "Code implementation, edits, debugging",
|
|
26
|
-
"alias_ko": "엔지니어",
|
|
27
|
-
"category": "do",
|
|
28
|
-
"resume_tier": "bounded",
|
|
29
|
-
"model_tier": "standard",
|
|
30
|
-
"capabilities": [
|
|
31
|
-
"no_task_create"
|
|
32
|
-
],
|
|
33
|
-
"id": "engineer",
|
|
34
|
-
"body_hash": "sha256:3d58b1b490c2f93cace2eedd0f04ec000f84514388eb086768cf53f8fa33db01"
|
|
35
|
-
},
|
|
36
22
|
{
|
|
37
23
|
"name": "reviewer",
|
|
38
24
|
"description": "Content verification — validates accuracy, checks facts, confirms grammar and format of non-code deliverables",
|
|
@@ -48,21 +34,6 @@
|
|
|
48
34
|
"id": "reviewer",
|
|
49
35
|
"body_hash": "sha256:f04d15249601b14046e7e40a4475defb289436c4474afbd89986964f8c3e7c2f"
|
|
50
36
|
},
|
|
51
|
-
{
|
|
52
|
-
"name": "researcher",
|
|
53
|
-
"description": "Independent investigation — conducts web searches, gathers evidence, and reports findings with citations",
|
|
54
|
-
"task": "Web search, independent investigation",
|
|
55
|
-
"alias_ko": "리서처",
|
|
56
|
-
"category": "do",
|
|
57
|
-
"resume_tier": "persistent",
|
|
58
|
-
"model_tier": "standard",
|
|
59
|
-
"capabilities": [
|
|
60
|
-
"no_file_edit",
|
|
61
|
-
"no_task_create"
|
|
62
|
-
],
|
|
63
|
-
"id": "researcher",
|
|
64
|
-
"body_hash": "sha256:fc79bafec05503327bd51a0b84b6e642d304bd79c45b78db6448b112793c143e"
|
|
65
|
-
},
|
|
66
37
|
{
|
|
67
38
|
"name": "strategist",
|
|
68
39
|
"description": "Business strategy — evaluates market positioning, competitive landscape, and business viability of decisions",
|
|
@@ -95,6 +66,35 @@
|
|
|
95
66
|
"id": "designer",
|
|
96
67
|
"body_hash": "sha256:88ac56147d0e5bdf23fa591ce570a9c2d0eb1338df4ec2219f6238ddfcb65df4"
|
|
97
68
|
},
|
|
69
|
+
{
|
|
70
|
+
"name": "researcher",
|
|
71
|
+
"description": "Independent investigation — conducts web searches, gathers evidence, and reports findings with citations",
|
|
72
|
+
"task": "Web search, independent investigation",
|
|
73
|
+
"alias_ko": "리서처",
|
|
74
|
+
"category": "do",
|
|
75
|
+
"resume_tier": "persistent",
|
|
76
|
+
"model_tier": "standard",
|
|
77
|
+
"capabilities": [
|
|
78
|
+
"no_file_edit",
|
|
79
|
+
"no_task_create"
|
|
80
|
+
],
|
|
81
|
+
"id": "researcher",
|
|
82
|
+
"body_hash": "sha256:fc79bafec05503327bd51a0b84b6e642d304bd79c45b78db6448b112793c143e"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "engineer",
|
|
86
|
+
"description": "Implementation — writes code, debugs issues, follows specifications from Lead and architect",
|
|
87
|
+
"task": "Code implementation, edits, debugging",
|
|
88
|
+
"alias_ko": "엔지니어",
|
|
89
|
+
"category": "do",
|
|
90
|
+
"resume_tier": "bounded",
|
|
91
|
+
"model_tier": "standard",
|
|
92
|
+
"capabilities": [
|
|
93
|
+
"no_task_create"
|
|
94
|
+
],
|
|
95
|
+
"id": "engineer",
|
|
96
|
+
"body_hash": "sha256:3d58b1b490c2f93cace2eedd0f04ec000f84514388eb086768cf53f8fa33db01"
|
|
97
|
+
},
|
|
98
98
|
{
|
|
99
99
|
"name": "postdoc",
|
|
100
100
|
"description": "Research methodology and synthesis — designs investigation approach, evaluates evidence quality, writes synthesis documents",
|
|
@@ -143,17 +143,26 @@
|
|
|
143
143
|
],
|
|
144
144
|
"skills": [
|
|
145
145
|
{
|
|
146
|
-
"name": "nx-
|
|
147
|
-
"description": "
|
|
148
|
-
"summary": "
|
|
149
|
-
"
|
|
150
|
-
"run"
|
|
151
|
-
],
|
|
146
|
+
"name": "nx-init",
|
|
147
|
+
"description": "Project onboarding — scan, mission, essentials, context generation",
|
|
148
|
+
"summary": "Project onboarding — scan, mission, essentials, context generation",
|
|
149
|
+
"manual_only": true,
|
|
152
150
|
"harness_docs_refs": [
|
|
153
|
-
"
|
|
151
|
+
"instruction_file",
|
|
152
|
+
"slash_command_display"
|
|
154
153
|
],
|
|
155
|
-
"id": "nx-
|
|
156
|
-
"body_hash": "sha256:
|
|
154
|
+
"id": "nx-init",
|
|
155
|
+
"body_hash": "sha256:b828a974ab4722dd7f1d15a4338d1380fdae47cd42c1bd4a5539277075efb6fc"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "nx-sync",
|
|
159
|
+
"description": "Context knowledge synchronization — scans project state and updates .nexus/context/ design documents",
|
|
160
|
+
"summary": "Context knowledge synchronization",
|
|
161
|
+
"triggers": [
|
|
162
|
+
"sync"
|
|
163
|
+
],
|
|
164
|
+
"id": "nx-sync",
|
|
165
|
+
"body_hash": "sha256:3ee8dd780d53f2e04472de6c701e16bc1fbde7f2ce9ed4e680b7cd2010530a22"
|
|
157
166
|
},
|
|
158
167
|
{
|
|
159
168
|
"name": "nx-plan",
|
|
@@ -166,28 +175,20 @@
|
|
|
166
175
|
"resume_invocation"
|
|
167
176
|
],
|
|
168
177
|
"id": "nx-plan",
|
|
169
|
-
"body_hash": "sha256:
|
|
178
|
+
"body_hash": "sha256:cd7a5fd1815530be6ffad18358a1295d1f74ef8a24132e75b73522c106eb6ae5"
|
|
170
179
|
},
|
|
171
180
|
{
|
|
172
|
-
"name": "nx-
|
|
173
|
-
"description": "
|
|
174
|
-
"summary": "
|
|
181
|
+
"name": "nx-run",
|
|
182
|
+
"description": "Execution — user-directed agent composition.",
|
|
183
|
+
"summary": "Execution — user-directed agent composition",
|
|
175
184
|
"triggers": [
|
|
176
|
-
"
|
|
185
|
+
"run"
|
|
177
186
|
],
|
|
178
|
-
"id": "nx-sync",
|
|
179
|
-
"body_hash": "sha256:a7b0ae8f13ebcd10e52361d0ada1570ff0c47933f731deec07e95539c63e6946"
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
"name": "nx-init",
|
|
183
|
-
"description": "Project onboarding — scan, mission, essentials, context generation",
|
|
184
|
-
"summary": "Project onboarding — scan, mission, essentials, context generation",
|
|
185
|
-
"manual_only": true,
|
|
186
187
|
"harness_docs_refs": [
|
|
187
|
-
"
|
|
188
|
+
"resume_invocation"
|
|
188
189
|
],
|
|
189
|
-
"id": "nx-
|
|
190
|
-
"body_hash": "sha256:
|
|
190
|
+
"id": "nx-run",
|
|
191
|
+
"body_hash": "sha256:0e2c443efceeab4621709a85cd4e2ba50471d2e850680c655d776cbb62814549"
|
|
191
192
|
}
|
|
192
193
|
],
|
|
193
194
|
"vocabulary": {
|
|
@@ -320,6 +321,32 @@
|
|
|
320
321
|
"*"
|
|
321
322
|
]
|
|
322
323
|
}
|
|
324
|
+
],
|
|
325
|
+
"invocations": [
|
|
326
|
+
{
|
|
327
|
+
"id": "skill_activation",
|
|
328
|
+
"description": "Activate another skill within the current conversation.",
|
|
329
|
+
"intent": "skill_entry_dispatch",
|
|
330
|
+
"fallback_behavior": "If the harness lacks a live skill activation primitive, re-emit the\nskill's trigger tag (e.g., '[plan:auto]') as a self-dispatch signal,\nrelying on tag detection to re-enter the skill. The skill id must be\nmapped to its canonical trigger tag by the harness's own docs.\n"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"id": "subagent_spawn",
|
|
334
|
+
"description": "Spawn a new subagent session with a specific role and prompt.",
|
|
335
|
+
"intent": "subagent_session_create",
|
|
336
|
+
"fallback_behavior": "If the harness lacks an explicit subagent spawn primitive (e.g.,\nhooks-based implicit routing), inject the target_role as a routing\nhint and structure the prompt so the harness's own delegation rules\ncatch it. A harness that cannot spawn agents must document this\nlimitation and treat the invocation as a no-op with a warning.\n"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"id": "task_register",
|
|
340
|
+
"description": "Register a task for user-visible progress tracking.",
|
|
341
|
+
"intent": "execution_visibility_register",
|
|
342
|
+
"fallback_behavior": "If the harness has no TUI task tracker, omit the call entirely. This\nprimitive is best-effort — failure or absence must not block\nexecution. Logging the label and state to the conversation transcript\nis acceptable as a degraded fallback for auditability.\n"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"id": "user_question",
|
|
346
|
+
"description": "Ask the user a structured question with selectable options.",
|
|
347
|
+
"intent": "structured_user_prompt",
|
|
348
|
+
"fallback_behavior": "If the harness lacks a structured question tool (e.g., opencode-nexus),\npresent the question as prose followed by the options enumerated as a\nnumbered list, then await the user's free-form reply. The LLM is\nexpected to map the reply to the most appropriate option or treat it\nas a free-form answer if no options were given.\n"
|
|
349
|
+
}
|
|
323
350
|
]
|
|
324
351
|
}
|
|
325
352
|
}
|
package/package.json
CHANGED
|
@@ -34,27 +34,42 @@
|
|
|
34
34
|
"items": {
|
|
35
35
|
"type": "object",
|
|
36
36
|
"additionalProperties": false,
|
|
37
|
-
"required": ["id", "name", "description", "
|
|
37
|
+
"required": ["id", "name", "description", "body_hash"],
|
|
38
38
|
"properties": {
|
|
39
39
|
"id": { "$ref": "common.schema.json#/$defs/id" },
|
|
40
40
|
"name": { "type": "string" },
|
|
41
41
|
"description": { "type": "string" },
|
|
42
|
+
"summary": { "type": "string", "minLength": 10, "maxLength": 120 },
|
|
42
43
|
"triggers": { "type": "array", "items": { "type": "string" } },
|
|
44
|
+
"harness_docs_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
43
45
|
"alias_ko": { "type": "string" },
|
|
44
46
|
"manual_only": { "type": "boolean" },
|
|
45
47
|
"body_hash": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
|
|
46
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"allOf": [
|
|
50
|
+
{
|
|
51
|
+
"if": {
|
|
52
|
+
"properties": { "manual_only": { "const": true } },
|
|
53
|
+
"required": ["manual_only"]
|
|
54
|
+
},
|
|
55
|
+
"then": {},
|
|
56
|
+
"else": {
|
|
57
|
+
"required": ["triggers"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
47
61
|
}
|
|
48
62
|
},
|
|
49
63
|
"vocabulary": {
|
|
50
64
|
"type": "object",
|
|
51
65
|
"additionalProperties": false,
|
|
52
|
-
"required": ["capabilities", "categories", "resume_tiers", "tags"],
|
|
66
|
+
"required": ["capabilities", "categories", "resume_tiers", "tags", "invocations"],
|
|
53
67
|
"properties": {
|
|
54
68
|
"capabilities": { "type": "array" },
|
|
55
69
|
"categories": { "type": "array" },
|
|
56
70
|
"resume_tiers": { "type": "array" },
|
|
57
|
-
"tags": { "type": "array" }
|
|
71
|
+
"tags": { "type": "array" },
|
|
72
|
+
"invocations": { "type": "array" }
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
}
|
|
@@ -92,6 +92,51 @@
|
|
|
92
92
|
"properties": {
|
|
93
93
|
"tags": { "type": "array", "items": { "$ref": "#/$defs/tagEntry" } }
|
|
94
94
|
}
|
|
95
|
+
},
|
|
96
|
+
"invocationParam": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"required": ["name", "description", "required"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"name": { "type": "string", "minLength": 1, "pattern": "^[a-z][a-z0-9_]*$" },
|
|
102
|
+
"description": { "$ref": "common.schema.json#/$defs/description" },
|
|
103
|
+
"required": { "type": "boolean" },
|
|
104
|
+
"value_type": { "type": "string", "minLength": 1 }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"invocationEntry": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"required": ["id", "description", "intent", "semantic_params", "prose_guidance", "fallback_behavior"],
|
|
111
|
+
"properties": {
|
|
112
|
+
"id": { "$ref": "common.schema.json#/$defs/id" },
|
|
113
|
+
"description": { "$ref": "common.schema.json#/$defs/description" },
|
|
114
|
+
"intent": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"minLength": 1,
|
|
117
|
+
"pattern": "^[a-z][a-z0-9_]*$"
|
|
118
|
+
},
|
|
119
|
+
"semantic_params": {
|
|
120
|
+
"type": "array",
|
|
121
|
+
"items": { "$ref": "#/$defs/invocationParam" }
|
|
122
|
+
},
|
|
123
|
+
"prose_guidance": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"minLength": 40
|
|
126
|
+
},
|
|
127
|
+
"fallback_behavior": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"minLength": 20
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"invocationFile": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"required": ["invocations"],
|
|
137
|
+
"properties": {
|
|
138
|
+
"invocations": { "type": "array", "items": { "$ref": "#/$defs/invocationEntry" } }
|
|
139
|
+
}
|
|
95
140
|
}
|
|
96
141
|
}
|
|
97
142
|
}
|
package/scripts/lib/lint.ts
CHANGED
|
@@ -3,6 +3,70 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
// ─── Invocation ID cache ──────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
let _invocationIds: Set<string> | null = null;
|
|
9
|
+
|
|
10
|
+
async function loadInvocationIds(root: string): Promise<Set<string>> {
|
|
11
|
+
if (_invocationIds !== null) return _invocationIds;
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(path.join(root, 'vocabulary', 'invocations.yml'), 'utf8');
|
|
14
|
+
const data = parseYaml(raw) as { invocations?: Array<{ id: string }> };
|
|
15
|
+
_invocationIds = new Set((data.invocations ?? []).map((e) => e.id));
|
|
16
|
+
} catch {
|
|
17
|
+
_invocationIds = new Set();
|
|
18
|
+
}
|
|
19
|
+
return _invocationIds;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Pre-processing helpers ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mask heredoc blocks (>>LABEL ... <<LABEL) with spaces, preserving newlines
|
|
26
|
+
* so line numbers remain accurate. Returns masked source.
|
|
27
|
+
*
|
|
28
|
+
* Per spec: heredoc internals are opaque for DISTINCTIVE/AMBIGUOUS G6 scanning.
|
|
29
|
+
* Note: the spec also says tool call patterns inside heredocs should still be
|
|
30
|
+
* caught. We do that via a separate scanRegex pass on the original source for
|
|
31
|
+
* the CALL-PATTERN-ONLY regexes (Cat 2) and NAMESPACE regexes (Cat 3), which
|
|
32
|
+
* are applied to the unmasked source.
|
|
33
|
+
*/
|
|
34
|
+
function maskHeredocs(source: string): string {
|
|
35
|
+
// Match >>LABEL (optionally preceded by = or whitespace) through <<LABEL
|
|
36
|
+
return source.replace(
|
|
37
|
+
/>>([A-Z][A-Z0-9_]*)([\s\S]*?)<<\1/g,
|
|
38
|
+
(_match, _label: string, body: string) => {
|
|
39
|
+
// Replace non-newline chars with spaces
|
|
40
|
+
const masked = body.replace(/[^\n]/g, ' ');
|
|
41
|
+
return `>>${_label}${masked}<<${_label}`;
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mask macro invocations {{ ... }} with spaces, preserving newlines.
|
|
48
|
+
* The primitive_id token immediately after {{ is preserved for validation;
|
|
49
|
+
* everything else inside the braces is replaced with spaces.
|
|
50
|
+
*
|
|
51
|
+
* Returns { masked, macros } where macros is a list of { id, line }.
|
|
52
|
+
*/
|
|
53
|
+
function maskMacros(
|
|
54
|
+
source: string
|
|
55
|
+
): { masked: string; macros: Array<{ id: string; line: number }> } {
|
|
56
|
+
const macros: Array<{ id: string; line: number }> = [];
|
|
57
|
+
const masked = source.replace(
|
|
58
|
+
/\{\{([a-z_][a-z0-9_]*)([^}]*)\}\}/g,
|
|
59
|
+
(match, id: string, rest: string, offset: number) => {
|
|
60
|
+
const line = source.slice(0, offset).split('\n').length;
|
|
61
|
+
macros.push({ id, line });
|
|
62
|
+
// Replace the entire macro token with spaces (preserve newlines)
|
|
63
|
+
const inner = (id as string) + (rest as string);
|
|
64
|
+
return '{{' + inner.replace(/[^\n]/g, ' ') + '}}';
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
return { masked, macros };
|
|
68
|
+
}
|
|
69
|
+
|
|
6
70
|
/**
|
|
7
71
|
* Common ValidationResult type. Imported from ./validate.ts for consistency,
|
|
8
72
|
* but declared here as well for isolation.
|
|
@@ -46,12 +110,19 @@ const LINT_INCLUDE: string[] = [
|
|
|
46
110
|
|
|
47
111
|
// G6: harness-specific tool names
|
|
48
112
|
// Distinctive tools — unambiguous, safe to scan in ALL files including body.md prose
|
|
49
|
-
const CLAUDE_CODE_TOOLS_DISTINCTIVE = /\b(NotebookEdit|BashOutput|KillShell|Glob|Grep|WebFetch|WebSearch|TodoWrite|SendMessage|TeamCreate|AskUserQuestion|mcp__plugin_[a-z0-9_]
|
|
113
|
+
const CLAUDE_CODE_TOOLS_DISTINCTIVE = /\b(NotebookEdit|BashOutput|KillShell|Glob|Grep|WebFetch|WebSearch|TodoWrite|SendMessage|TeamCreate|AskUserQuestion|mcp__plugin_[a-z0-9_]+|TaskCreate|TaskUpdate|TaskList|TaskGet|TaskStop|TaskOutput|subagent_type|prompt_user)\b/g;
|
|
50
114
|
// Ambiguous tools — also common English words (Read, Write, Edit, Bash, Task, Monitor)
|
|
51
115
|
// Only scanned in meta.yml and vocabulary where they are clearly tool references, not prose.
|
|
52
116
|
const CLAUDE_CODE_TOOLS_AMBIGUOUS = /\b(Read|Write|Edit|Bash|Task|Monitor)\b/g;
|
|
53
117
|
const OPENCODE_TOOLS = /\b(edit|write|patch|multiedit|bash)\b/g;
|
|
54
118
|
|
|
119
|
+
// G6 Category 2: Call-pattern only (prose words that become violations only with open-paren)
|
|
120
|
+
// "Agent role", "Skill activation" etc. are fine; "Agent(", "Skill(" are forbidden.
|
|
121
|
+
const CALL_PATTERN_TOOLS = /\b(Skill|Agent)\s*\(/g;
|
|
122
|
+
|
|
123
|
+
// G6 Category 3: Harness namespace slash-command patterns
|
|
124
|
+
const HARNESS_NAMESPACE = /\/(?:claude-nexus|opencode-nexus):/g;
|
|
125
|
+
|
|
55
126
|
// G7: concrete model names
|
|
56
127
|
const CONCRETE_MODELS = /\b(opus|sonnet|haiku|gpt-[0-9][a-z0-9.-]*|claude-[0-9][a-z0-9.-]*)\b/gi;
|
|
57
128
|
|
|
@@ -98,32 +169,93 @@ function scanRegex(
|
|
|
98
169
|
|
|
99
170
|
/** G6: harness-specific tool names forbidden in body/meta/vocabulary.
|
|
100
171
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
172
|
+
* CLAUDE_CODE_TOOLS_DISTINCTIVE — unambiguous, scanned in ALL lint-included files.
|
|
173
|
+
* For body.md: source is pre-processed (heredoc + macro masking) so that
|
|
174
|
+
* macro internals and heredoc bodies do not produce false positives.
|
|
175
|
+
*
|
|
176
|
+
* CLAUDE_CODE_TOOLS_AMBIGUOUS (Read/Write/Edit/Bash/Task/Monitor) + OPENCODE_TOOLS —
|
|
177
|
+
* scanned ONLY in meta.yml and vocabulary/*.yml, not in body.md prose.
|
|
178
|
+
*
|
|
179
|
+
* CALL_PATTERN_TOOLS (Skill(, Agent() — scanned in ALL files on raw source
|
|
180
|
+
* (after macro+heredoc masking). Prose words without parens are never flagged.
|
|
181
|
+
*
|
|
182
|
+
* HARNESS_NAMESPACE (/claude-nexus:, /opencode-nexus:) — scanned in ALL files.
|
|
183
|
+
* For body.md: applied to macro/heredoc-masked source.
|
|
184
|
+
*
|
|
185
|
+
* Cat 4 (Macro whitelist): {{primitive_id}} macros in body.md are extracted and
|
|
186
|
+
* their primitive_id is validated against vocabulary/invocations.yml enum.
|
|
187
|
+
* Unknown primitive_ids emit a warning (consumer expander cannot handle them).
|
|
106
188
|
*/
|
|
107
189
|
export async function checkHarnessSpecific(root: string): Promise<ValidationResult[]> {
|
|
190
|
+
const invocationIds = await loadInvocationIds(root);
|
|
108
191
|
const results: ValidationResult[] = [];
|
|
109
192
|
for await (const file of iterFiles(root)) {
|
|
110
193
|
const source = await readFile(file, 'utf8');
|
|
111
194
|
const rel = path.relative(root, file);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
195
|
+
const isBody = rel.endsWith('body.md');
|
|
196
|
+
|
|
197
|
+
if (isBody) {
|
|
198
|
+
// Pre-process: mask heredocs first, then macros
|
|
199
|
+
const heredocMasked = maskHeredocs(source);
|
|
200
|
+
const { masked, macros } = maskMacros(heredocMasked);
|
|
201
|
+
|
|
202
|
+
// Cat 1 (Distinctive) — on masked source
|
|
203
|
+
results.push(
|
|
204
|
+
...scanRegex(masked, CLAUDE_CODE_TOOLS_DISTINCTIVE, rel, 'G6-harness-lint',
|
|
205
|
+
(m) => `Harness-specific tool name forbidden: '${m}'. Use abstract capability or remove.`)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Cat 2 (Call-pattern) — on masked source (macros/heredocs won't contain Agent(/Skill()
|
|
119
209
|
results.push(
|
|
120
|
-
...scanRegex(
|
|
210
|
+
...scanRegex(masked, CALL_PATTERN_TOOLS, rel, 'G6-harness-lint',
|
|
211
|
+
(m) => `Harness-specific tool call syntax forbidden: '${m}'. Use abstract capability or remove.`)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Cat 3 (Namespace) — on masked source
|
|
215
|
+
results.push(
|
|
216
|
+
...scanRegex(masked, HARNESS_NAMESPACE, rel, 'G6-harness-lint',
|
|
217
|
+
(m) => `Harness namespace slash-command forbidden: '${m}'. Use capability abstraction.`)
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Cat 4 (Macro whitelist) — validate primitive_ids against invocations.yml
|
|
221
|
+
for (const macro of macros) {
|
|
222
|
+
if (!invocationIds.has(macro.id)) {
|
|
223
|
+
results.push({
|
|
224
|
+
file: rel,
|
|
225
|
+
gate: 'G6-harness-lint',
|
|
226
|
+
severity: 'warning',
|
|
227
|
+
line: macro.line,
|
|
228
|
+
message: `Macro primitive_id '${macro.id}' is not registered in vocabulary/invocations.yml — consumer expander cannot handle it.`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// meta.yml and vocabulary files — scan raw source
|
|
234
|
+
results.push(
|
|
235
|
+
...scanRegex(source, CLAUDE_CODE_TOOLS_DISTINCTIVE, rel, 'G6-harness-lint',
|
|
121
236
|
(m) => `Harness-specific tool name forbidden: '${m}'. Use abstract capability or remove.`)
|
|
122
237
|
);
|
|
238
|
+
|
|
123
239
|
results.push(
|
|
124
|
-
...scanRegex(source,
|
|
125
|
-
(m) => `
|
|
240
|
+
...scanRegex(source, CALL_PATTERN_TOOLS, rel, 'G6-harness-lint',
|
|
241
|
+
(m) => `Harness-specific tool call syntax forbidden: '${m}'. Use abstract capability or remove.`)
|
|
126
242
|
);
|
|
243
|
+
|
|
244
|
+
results.push(
|
|
245
|
+
...scanRegex(source, HARNESS_NAMESPACE, rel, 'G6-harness-lint',
|
|
246
|
+
(m) => `Harness namespace slash-command forbidden: '${m}'. Use capability abstraction.`)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (rel.endsWith('meta.yml') || rel.startsWith('vocabulary/')) {
|
|
250
|
+
results.push(
|
|
251
|
+
...scanRegex(source, CLAUDE_CODE_TOOLS_AMBIGUOUS, rel, 'G6-harness-lint',
|
|
252
|
+
(m) => `Harness-specific tool name forbidden: '${m}'. Use abstract capability or remove.`)
|
|
253
|
+
);
|
|
254
|
+
results.push(
|
|
255
|
+
...scanRegex(source, OPENCODE_TOOLS, rel, 'G6-harness-lint',
|
|
256
|
+
(m) => `OpenCode tool name forbidden: '${m}'. Use abstract capability or remove.`)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
127
259
|
}
|
|
128
260
|
}
|
|
129
261
|
return results;
|
package/scripts/lib/validate.ts
CHANGED
|
@@ -65,11 +65,28 @@ interface TagEntry {
|
|
|
65
65
|
variants?: string[];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
interface InvocationParam {
|
|
69
|
+
name: string;
|
|
70
|
+
description: string;
|
|
71
|
+
required: boolean;
|
|
72
|
+
value_type?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface InvocationEntry {
|
|
76
|
+
id: string;
|
|
77
|
+
description: string;
|
|
78
|
+
intent: string;
|
|
79
|
+
semantic_params: InvocationParam[];
|
|
80
|
+
prose_guidance: string;
|
|
81
|
+
fallback_behavior: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
interface Vocab {
|
|
69
85
|
capabilities: CapabilityEntry[];
|
|
70
86
|
categories: SimpleEntry[];
|
|
71
87
|
resume_tiers: SimpleEntry[];
|
|
72
88
|
tags: TagEntry[];
|
|
89
|
+
invocations: InvocationEntry[];
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
interface ManifestAgent extends AgentMeta {
|
|
@@ -80,6 +97,13 @@ interface ManifestSkill extends SkillMeta {
|
|
|
80
97
|
body_hash: string;
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
interface ManifestInvocationEntry {
|
|
101
|
+
id: string;
|
|
102
|
+
description: string;
|
|
103
|
+
intent: string;
|
|
104
|
+
fallback_behavior: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
interface Manifest {
|
|
84
108
|
nexus_core_version: string;
|
|
85
109
|
nexus_core_commit: string;
|
|
@@ -91,6 +115,7 @@ interface Manifest {
|
|
|
91
115
|
categories: SimpleEntry[];
|
|
92
116
|
resume_tiers: SimpleEntry[];
|
|
93
117
|
tags: TagEntry[];
|
|
118
|
+
invocations: ManifestInvocationEntry[];
|
|
94
119
|
};
|
|
95
120
|
}
|
|
96
121
|
|
|
@@ -173,11 +198,18 @@ export async function loadSchemas(root: string): Promise<void> {
|
|
|
173
198
|
$id: 'vocabulary-tag-file',
|
|
174
199
|
$defs: vocabDefs,
|
|
175
200
|
};
|
|
201
|
+
const invocationFileSchema = {
|
|
202
|
+
...vocabDefs['invocationFile'],
|
|
203
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
204
|
+
$id: 'vocabulary-invocation-file',
|
|
205
|
+
$defs: vocabDefs,
|
|
206
|
+
};
|
|
176
207
|
|
|
177
208
|
const capabilityValidator = await ajv.compileAsync(capabilityFileSchema);
|
|
178
209
|
const categoryValidator = await ajv.compileAsync(categoryFileSchema);
|
|
179
210
|
const resumeTierValidator = await ajv.compileAsync(resumeTierFileSchema);
|
|
180
211
|
const tagValidator = await ajv.compileAsync(tagFileSchema);
|
|
212
|
+
const invocationValidator = await ajv.compileAsync(invocationFileSchema);
|
|
181
213
|
|
|
182
214
|
const manifestAjv = new Ajv2020({
|
|
183
215
|
strict: false,
|
|
@@ -204,7 +236,7 @@ export async function loadSchemas(root: string): Promise<void> {
|
|
|
204
236
|
};
|
|
205
237
|
|
|
206
238
|
// Store all vocab validators for internal use
|
|
207
|
-
_vocabValidators = { capabilityValidator, categoryValidator, resumeTierValidator, tagValidator };
|
|
239
|
+
_vocabValidators = { capabilityValidator, categoryValidator, resumeTierValidator, tagValidator, invocationValidator };
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
interface VocabValidators {
|
|
@@ -212,6 +244,7 @@ interface VocabValidators {
|
|
|
212
244
|
categoryValidator: ValidateFunction;
|
|
213
245
|
resumeTierValidator: ValidateFunction;
|
|
214
246
|
tagValidator: ValidateFunction;
|
|
247
|
+
invocationValidator: ValidateFunction;
|
|
215
248
|
}
|
|
216
249
|
|
|
217
250
|
let _vocabValidators: VocabValidators | null = null;
|
|
@@ -440,6 +473,49 @@ export function checkCapabilityEntryIntegrity(capabilities: CapabilityEntry[]):
|
|
|
440
473
|
return results;
|
|
441
474
|
}
|
|
442
475
|
|
|
476
|
+
// ─── G6': Invocation entry integrity ─────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
export function checkInvocationEntryIntegrity(invocations: InvocationEntry[]): ValidationResult[] {
|
|
479
|
+
const results: ValidationResult[] = [];
|
|
480
|
+
for (const inv of invocations) {
|
|
481
|
+
if (!SNAKE_CASE_RE.test(inv.intent)) {
|
|
482
|
+
results.push({
|
|
483
|
+
file: 'vocabulary/invocations.yml',
|
|
484
|
+
gate: 'G6-invocation-integrity',
|
|
485
|
+
severity: 'error',
|
|
486
|
+
message: `Invocation '${inv.id}': 'intent' must match snake_case /^[a-z][a-z0-9_]*$/, got '${inv.intent}'`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (!inv.prose_guidance || inv.prose_guidance.trim().length < 40) {
|
|
490
|
+
results.push({
|
|
491
|
+
file: 'vocabulary/invocations.yml',
|
|
492
|
+
gate: 'G6-invocation-integrity',
|
|
493
|
+
severity: 'error',
|
|
494
|
+
message: `Invocation '${inv.id}': 'prose_guidance' must be at least 40 characters`,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (!inv.fallback_behavior || inv.fallback_behavior.trim().length < 20) {
|
|
498
|
+
results.push({
|
|
499
|
+
file: 'vocabulary/invocations.yml',
|
|
500
|
+
gate: 'G6-invocation-integrity',
|
|
501
|
+
severity: 'error',
|
|
502
|
+
message: `Invocation '${inv.id}': 'fallback_behavior' must be at least 20 characters`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
for (const param of inv.semantic_params ?? []) {
|
|
506
|
+
if (!SNAKE_CASE_RE.test(param.name)) {
|
|
507
|
+
results.push({
|
|
508
|
+
file: 'vocabulary/invocations.yml',
|
|
509
|
+
gate: 'G6-invocation-integrity',
|
|
510
|
+
severity: 'error',
|
|
511
|
+
message: `Invocation '${inv.id}': param name '${param.name}' must match snake_case /^[a-z][a-z0-9_]*$/`,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return results;
|
|
517
|
+
}
|
|
518
|
+
|
|
443
519
|
// ─── Vocabulary loading ───────────────────────────────────────────────────────
|
|
444
520
|
|
|
445
521
|
async function loadVocab(root: string): Promise<{ vocab: Vocab | null; results: ValidationResult[] }> {
|
|
@@ -483,14 +559,15 @@ async function loadVocab(root: string): Promise<{ vocab: Vocab | null; results:
|
|
|
483
559
|
return data as T;
|
|
484
560
|
}
|
|
485
561
|
|
|
486
|
-
const [capData, catData, resumeData, tagData] = await Promise.all([
|
|
562
|
+
const [capData, catData, resumeData, tagData, invocationData] = await Promise.all([
|
|
487
563
|
loadYaml<{ capabilities: CapabilityEntry[] }>('capabilities.yml', _vocabValidators.capabilityValidator),
|
|
488
564
|
loadYaml<{ categories: SimpleEntry[] }>('categories.yml', _vocabValidators.categoryValidator),
|
|
489
565
|
loadYaml<{ resume_tiers: SimpleEntry[] }>('resume-tiers.yml', _vocabValidators.resumeTierValidator),
|
|
490
566
|
loadYaml<{ tags: TagEntry[] }>('tags.yml', _vocabValidators.tagValidator),
|
|
567
|
+
loadYaml<{ invocations: InvocationEntry[] }>('invocations.yml', _vocabValidators.invocationValidator),
|
|
491
568
|
]);
|
|
492
569
|
|
|
493
|
-
if (!capData || !catData || !resumeData || !tagData) {
|
|
570
|
+
if (!capData || !catData || !resumeData || !tagData || !invocationData) {
|
|
494
571
|
return { vocab: null, results };
|
|
495
572
|
}
|
|
496
573
|
|
|
@@ -500,6 +577,7 @@ async function loadVocab(root: string): Promise<{ vocab: Vocab | null; results:
|
|
|
500
577
|
categories: catData.categories,
|
|
501
578
|
resume_tiers: resumeData.resume_tiers,
|
|
502
579
|
tags: tagData.tags,
|
|
580
|
+
invocations: invocationData.invocations,
|
|
503
581
|
},
|
|
504
582
|
results,
|
|
505
583
|
};
|
|
@@ -539,6 +617,13 @@ export async function generateManifest(
|
|
|
539
617
|
})
|
|
540
618
|
);
|
|
541
619
|
|
|
620
|
+
const invocationSummaries: ManifestInvocationEntry[] = vocab.invocations.map((inv) => ({
|
|
621
|
+
id: inv.id,
|
|
622
|
+
description: inv.description,
|
|
623
|
+
intent: inv.intent,
|
|
624
|
+
fallback_behavior: inv.fallback_behavior,
|
|
625
|
+
}));
|
|
626
|
+
|
|
542
627
|
return {
|
|
543
628
|
nexus_core_version: version,
|
|
544
629
|
nexus_core_commit: commit,
|
|
@@ -550,6 +635,7 @@ export async function generateManifest(
|
|
|
550
635
|
categories: vocab.categories,
|
|
551
636
|
resume_tiers: vocab.resume_tiers,
|
|
552
637
|
tags: vocab.tags,
|
|
638
|
+
invocations: invocationSummaries,
|
|
553
639
|
},
|
|
554
640
|
};
|
|
555
641
|
}
|
|
@@ -624,6 +710,8 @@ export async function runAll(root: string): Promise<ValidationResult[]> {
|
|
|
624
710
|
allResults.push(...checkTagIntegrity(validSkills, tags));
|
|
625
711
|
// G5': capability entry field integrity
|
|
626
712
|
allResults.push(...checkCapabilityEntryIntegrity(vocab.capabilities));
|
|
713
|
+
// G6': invocation entry field integrity
|
|
714
|
+
allResults.push(...checkInvocationEntryIntegrity(vocab.invocations));
|
|
627
715
|
}
|
|
628
716
|
|
|
629
717
|
// Manifest generation — only on full success (no errors)
|
package/skills/nx-init/body.md
CHANGED
|
@@ -16,9 +16,9 @@ Scans the project and builds Nexus knowledge in the flat .nexus/ structure. On f
|
|
|
16
16
|
|
|
17
17
|
## Trigger
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
19
|
+
- Manual trigger — full onboarding (or resume). See harness docs: slash_command_display.
|
|
20
|
+
- Manual trigger with `--reset` flag — back up existing `.nexus/` knowledge and re-onboard. See harness docs: slash_command_display.
|
|
21
|
+
- Manual trigger with `--reset --cleanup` flags — show backup list + selective deletion. See harness docs: slash_command_display.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -51,9 +51,7 @@ Show backup directory list, let user select backups to delete.
|
|
|
51
51
|
```
|
|
52
52
|
IF --reset --cleanup flag:
|
|
53
53
|
Show list of .nexus/bak.*/ directories
|
|
54
|
-
Prompt user with options
|
|
55
|
-
question: "Select a backup to delete (or cancel)"
|
|
56
|
-
options: [...backup list..., { label: "Cancel", description: "Exit without changes" }]
|
|
54
|
+
Prompt user with options via `{{user_question question="Select a backup to delete (or cancel)" options=[<backup list...>, {label: Cancel, description: "Exit without changes"}]}}`.
|
|
57
55
|
Delete selected backup and exit
|
|
58
56
|
|
|
59
57
|
ELSE IF --reset flag:
|
|
@@ -162,15 +160,7 @@ On completion: "context knowledge N files generated"
|
|
|
162
160
|
Check whether team custom rules are needed.
|
|
163
161
|
|
|
164
162
|
```
|
|
165
|
-
|
|
166
|
-
questions: [{
|
|
167
|
-
question: "Do you want to set up development rules now?",
|
|
168
|
-
options: [
|
|
169
|
-
{ label: "Set up", description: "Coding conventions, test policy, commit rules, etc." },
|
|
170
|
-
{ label: "Skip", description: "Can be added later via [rule] tag" }
|
|
171
|
-
]
|
|
172
|
-
}]
|
|
173
|
-
})
|
|
163
|
+
{{user_question question="Do you want to set up development rules now?" options=[{label: "Set up", description: "Coding conventions, test policy, commit rules, etc."}, {label: Skip, description: "Can be added later via [rule] tag"}]}}
|
|
174
164
|
```
|
|
175
165
|
|
|
176
166
|
If "Set up": present a draft based on scan results → user confirms → save via the harness's file-creation primitive to `.nexus/rules/{topic}.md`.
|
|
@@ -192,5 +182,5 @@ Output a summary of the onboarding results.
|
|
|
192
182
|
### Next Steps
|
|
193
183
|
- [plan] — research, analyze, and plan before execution
|
|
194
184
|
- [run] — execute from a plan
|
|
195
|
-
-
|
|
185
|
+
- Manual re-run trigger with `--reset` flag — re-run onboarding (existing knowledge will be backed up). See harness docs: slash_command_display.
|
|
196
186
|
```
|
package/skills/nx-init/meta.yml
CHANGED
package/skills/nx-plan/body.md
CHANGED
|
@@ -35,7 +35,7 @@ Facilitate structured multi-perspective analysis using subagents to decompose is
|
|
|
35
35
|
|
|
36
36
|
## Auto Mode (`[plan:auto]`)
|
|
37
37
|
|
|
38
|
-
When triggered with `[plan:auto]` or invoked via `
|
|
38
|
+
When triggered with `[plan:auto]` or invoked via `{{skill_activation skill=nx-plan mode=auto}}`, run the full planning process **without user interaction**:
|
|
39
39
|
|
|
40
40
|
1. **Research** — spawn researcher+Explore subagents (same as interactive)
|
|
41
41
|
2. **Issue derivation** — Lead identifies issues from research
|
|
@@ -93,8 +93,8 @@ Understand code, core knowledge, and prior decisions before forming a planning a
|
|
|
93
93
|
|
|
94
94
|
| Scenario | Approach |
|
|
95
95
|
|----------|----------|
|
|
96
|
-
| Codebase orientation |
|
|
97
|
-
| External research needed |
|
|
96
|
+
| Codebase orientation | `{{subagent_spawn target_role=explore prompt="<file/code search task>"}}` for codebase exploration |
|
|
97
|
+
| External research needed | `{{subagent_spawn target_role=researcher prompt="<research question>"}}` for web search |
|
|
98
98
|
| Both codebase and external | Spawn Explore + Researcher in parallel |
|
|
99
99
|
|
|
100
100
|
- NEVER call `nx_plan_start` before research is complete.
|
package/skills/nx-run/body.md
CHANGED
|
@@ -24,16 +24,16 @@ Execution norm that Lead follows when the user invokes the [run] tag. Composes s
|
|
|
24
24
|
- **Branch Guard**: if on main/master, create a branch appropriate to the task type before proceeding (prefix: `feat/`, `fix/`, `chore/`, `research/`, etc. — Lead's judgment). Auto-create without user confirmation.
|
|
25
25
|
- Check for `tasks.json`:
|
|
26
26
|
- **Exists** → read it and proceed to Step 2.
|
|
27
|
-
- **Absent** → auto-invoke `
|
|
27
|
+
- **Absent** → auto-invoke `{{skill_activation skill=nx-plan mode=auto}}` to generate tasks.json. Do NOT ask — `[run]` implies execution intent. After plan generation, proceed to Step 2.
|
|
28
28
|
- If tasks.json exists, check prior decisions with `nx_plan_status`.
|
|
29
29
|
|
|
30
30
|
### Step 1.5: TUI Progress
|
|
31
31
|
|
|
32
32
|
Register tasks for visual progress tracking (Ctrl+T):
|
|
33
33
|
|
|
34
|
-
- **≤ 10 tasks**: `
|
|
35
|
-
- **> 10 tasks**: group by `plan_issue`, `
|
|
36
|
-
-
|
|
34
|
+
- **≤ 10 tasks**: `{{task_register label="<per-task label>" state=pending}}` per task
|
|
35
|
+
- **> 10 tasks**: group by `plan_issue`, `{{task_register label="<group label>" state=pending}}` per group
|
|
36
|
+
- Update the registered entry via `{{task_register label="<label>" state=in_progress}}` / `{{task_register label="<label>" state=completed}}` as execution proceeds
|
|
37
37
|
- **Skip only if**: non-TTY environment (VSCode, headless)
|
|
38
38
|
- **Known issue**: TUI may freeze during auto-compact (#27919) — task data on disk remains correct
|
|
39
39
|
|
|
@@ -90,7 +90,7 @@ For each task, Lead chooses between fresh spawn and resume based on the `owner`'
|
|
|
90
90
|
|
|
91
91
|
Execute in order:
|
|
92
92
|
|
|
93
|
-
1. **nx-sync**: invoke `
|
|
93
|
+
1. **nx-sync**: invoke `{{skill_activation skill=nx-sync}}` if code changes were made in this cycle. Best effort — failure does not block cycle completion.
|
|
94
94
|
2. **nx_task_close**: call to archive plan+tasks to history.json. This updates `.nexus/history.json`.
|
|
95
95
|
3. **git commit**: stage and commit source changes, build artifacts (`bridge/`, `scripts/`), `.nexus/history.json`, and any modified `.nexus/memory/` or `.nexus/context/`. Use explicit `git add` with paths (not `git add -A`) and a HEREDOC commit message with `Co-Authored-By`. This ensures the cycle's history archive lands in the same commit as the code changes, giving a 1:1 cycle-commit mapping.
|
|
96
96
|
4. **Report**: summarize to user — changed files, key decisions applied, and suggested next steps. Merge/push is the user's decision and outside this skill's scope.
|
package/skills/nx-sync/body.md
CHANGED
|
@@ -44,8 +44,9 @@ Only update files where a concrete change is detected. If no staleness is found,
|
|
|
44
44
|
Spawn Writer agent to update affected context documents:
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
{{subagent_spawn target_role=writer name=writer-sync-context prompt=>>WRITER_SYNC_PROMPT}}
|
|
48
|
+
Update .nexus/context/ documents based on the following changes. Read current files with the harness's file-reading primitive, then write updates with the harness's file-creation primitive. Changes: {change_manifest}
|
|
49
|
+
<<WRITER_SYNC_PROMPT
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
The Writer agent:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Harness-neutral capability definitions.
|
|
2
2
|
# Each entry describes WHAT is denied in semantic terms.
|
|
3
|
-
# Consumers (claude-nexus, opencode-nexus
|
|
3
|
+
# Consumers (claude-nexus, opencode-nexus) maintain their own
|
|
4
4
|
# local map from these ids/classes to concrete tool names in their own repo.
|
|
5
5
|
# nexus-core does not and must not know those tool names.
|
|
6
6
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Harness-neutral invocation semantic definitions.
|
|
2
|
+
# Each entry describes WHAT is invoked and what params are semantically required.
|
|
3
|
+
# Consumers (claude-nexus, opencode-nexus) maintain their own
|
|
4
|
+
# local map from these semantic primitives to concrete tool call syntax in their own repo.
|
|
5
|
+
# nexus-core does not and must not know those tool names.
|
|
6
|
+
#
|
|
7
|
+
# body.md uses macro syntax: {{primitive_id key1=val1 key2=val2}}
|
|
8
|
+
# See Spec γ (plan session #4, Issue #2) for macro grammar.
|
|
9
|
+
|
|
10
|
+
invocations:
|
|
11
|
+
- id: skill_activation
|
|
12
|
+
description: "Activate another skill within the current conversation."
|
|
13
|
+
intent: skill_entry_dispatch
|
|
14
|
+
semantic_params:
|
|
15
|
+
- name: skill
|
|
16
|
+
description: "Canonical skill id from manifest.json skills list."
|
|
17
|
+
required: true
|
|
18
|
+
- name: mode
|
|
19
|
+
description: "Activation mode (e.g., 'auto' for non-interactive)."
|
|
20
|
+
required: false
|
|
21
|
+
prose_guidance: |
|
|
22
|
+
Invoke a skill whose logic should be inlined into the current session.
|
|
23
|
+
The target skill must exist in the current harness's skill registry.
|
|
24
|
+
The invocation transfers execution context to the named skill; the
|
|
25
|
+
calling skill yields until the activated skill completes or signals
|
|
26
|
+
a return. Optional mode parameters modify activation behavior (e.g.,
|
|
27
|
+
bypassing interactive prompts for fully autonomous execution).
|
|
28
|
+
Only skill ids that appear in manifest.json are valid targets.
|
|
29
|
+
fallback_behavior: |
|
|
30
|
+
If the harness lacks a live skill activation primitive, re-emit the
|
|
31
|
+
skill's trigger tag (e.g., '[plan:auto]') as a self-dispatch signal,
|
|
32
|
+
relying on tag detection to re-enter the skill. The skill id must be
|
|
33
|
+
mapped to its canonical trigger tag by the harness's own docs.
|
|
34
|
+
|
|
35
|
+
- id: subagent_spawn
|
|
36
|
+
description: "Spawn a new subagent session with a specific role and prompt."
|
|
37
|
+
intent: subagent_session_create
|
|
38
|
+
semantic_params:
|
|
39
|
+
- name: target_role
|
|
40
|
+
description: "Canonical agent id from manifest.json agents list (e.g., 'writer', 'engineer')."
|
|
41
|
+
required: true
|
|
42
|
+
- name: prompt
|
|
43
|
+
description: "Structured task prompt. May be multiline (heredoc in body.md)."
|
|
44
|
+
required: true
|
|
45
|
+
- name: name
|
|
46
|
+
description: "Optional instance label for this subagent session."
|
|
47
|
+
required: false
|
|
48
|
+
- name: resume_tier_hint
|
|
49
|
+
description: "Optional hint from vocabulary/resume-tiers.yml (e.g., 'bounded', 'ephemeral')."
|
|
50
|
+
required: false
|
|
51
|
+
prose_guidance: |
|
|
52
|
+
Delegates a bounded unit of work to an agent with the given role.
|
|
53
|
+
The target_role must match an id in manifest.json agents list; the
|
|
54
|
+
harness resolves this to a concrete session or thread configuration.
|
|
55
|
+
The prompt provides the complete task specification for the subagent,
|
|
56
|
+
including all context the agent needs — do not rely on ambient session
|
|
57
|
+
state unless the resume_tier_hint indicates persistent context.
|
|
58
|
+
resume_tier_hint is advisory: harnesses may override based on their
|
|
59
|
+
own session management constraints.
|
|
60
|
+
fallback_behavior: |
|
|
61
|
+
If the harness lacks an explicit subagent spawn primitive (e.g.,
|
|
62
|
+
hooks-based implicit routing), inject the target_role as a routing
|
|
63
|
+
hint and structure the prompt so the harness's own delegation rules
|
|
64
|
+
catch it. A harness that cannot spawn agents must document this
|
|
65
|
+
limitation and treat the invocation as a no-op with a warning.
|
|
66
|
+
|
|
67
|
+
- id: task_register
|
|
68
|
+
description: "Register a task for user-visible progress tracking."
|
|
69
|
+
intent: execution_visibility_register
|
|
70
|
+
semantic_params:
|
|
71
|
+
- name: label
|
|
72
|
+
description: "Short human-readable task label."
|
|
73
|
+
required: true
|
|
74
|
+
- name: state
|
|
75
|
+
description: "Current state (pending / in_progress / completed)."
|
|
76
|
+
required: true
|
|
77
|
+
prose_guidance: |
|
|
78
|
+
Primarily for TUI progress rendering — allows the user to see which
|
|
79
|
+
work items are active, pending, or done during multi-step execution.
|
|
80
|
+
The label should be concise enough to display in a progress panel.
|
|
81
|
+
The state values are constrained to a three-state lifecycle: pending
|
|
82
|
+
(enqueued but not started), in_progress (currently executing), and
|
|
83
|
+
completed (done). Harnesses without TUI progress support may silently
|
|
84
|
+
no-op this primitive; failure must not block execution flow.
|
|
85
|
+
fallback_behavior: |
|
|
86
|
+
If the harness has no TUI task tracker, omit the call entirely. This
|
|
87
|
+
primitive is best-effort — failure or absence must not block
|
|
88
|
+
execution. Logging the label and state to the conversation transcript
|
|
89
|
+
is acceptable as a degraded fallback for auditability.
|
|
90
|
+
|
|
91
|
+
- id: user_question
|
|
92
|
+
description: "Ask the user a structured question with selectable options."
|
|
93
|
+
intent: structured_user_prompt
|
|
94
|
+
semantic_params:
|
|
95
|
+
- name: question
|
|
96
|
+
description: "The question text shown to the user."
|
|
97
|
+
required: true
|
|
98
|
+
- name: options
|
|
99
|
+
description: "Array of option objects, each with 'label' and 'description'. If empty, free-form response is expected."
|
|
100
|
+
required: true
|
|
101
|
+
prose_guidance: |
|
|
102
|
+
Presents the user with a structured decision point. When options is
|
|
103
|
+
non-empty, the user is expected to select one of the provided choices;
|
|
104
|
+
when empty, the user provides a free-form text response. The harness
|
|
105
|
+
is responsible for rendering options in a way appropriate to its UI
|
|
106
|
+
(e.g., numbered list, interactive picker, inline buttons). The LLM
|
|
107
|
+
should not proceed with execution until a response is received.
|
|
108
|
+
Use this primitive for branch points that require explicit user input,
|
|
109
|
+
not for informational messages or confirmations that could be inferred
|
|
110
|
+
from context.
|
|
111
|
+
fallback_behavior: |
|
|
112
|
+
If the harness lacks a structured question tool (e.g., opencode-nexus),
|
|
113
|
+
present the question as prose followed by the options enumerated as a
|
|
114
|
+
numbered list, then await the user's free-form reply. The LLM is
|
|
115
|
+
expected to map the reply to the most appropriate option or treat it
|
|
116
|
+
as a free-form answer if no options were given.
|