@moreih29/nexus-core 0.7.1 → 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/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "nexus_core_version": "0.7.1",
3
- "nexus_core_commit": "d2da7dede9540a14bc5925904c2382795f383b1e",
2
+ "nexus_core_version": "0.8.0",
3
+ "nexus_core_commit": "254efc7d8f4f52e45b548706dd42389fdb9801b2",
4
4
  "schema_contract_version": "2.0",
5
5
  "agents": [
6
6
  {
@@ -20,24 +20,25 @@
20
20
  "body_hash": "sha256:85f9a3de419f32cdae284436eb1d902bff19a2230c50fe3068ffc642949a63b7"
21
21
  },
22
22
  {
23
- "name": "engineer",
24
- "description": "Implementationwrites 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",
23
+ "name": "reviewer",
24
+ "description": "Content verification validates accuracy, checks facts, confirms grammar and format of non-code deliverables",
25
+ "task": "Content verification, fact-checking, grammar review",
26
+ "alias_ko": "리뷰어",
27
+ "category": "check",
28
+ "resume_tier": "ephemeral",
29
29
  "model_tier": "standard",
30
30
  "capabilities": [
31
+ "no_file_edit",
31
32
  "no_task_create"
32
33
  ],
33
- "id": "engineer",
34
- "body_hash": "sha256:3d58b1b490c2f93cace2eedd0f04ec000f84514388eb086768cf53f8fa33db01"
34
+ "id": "reviewer",
35
+ "body_hash": "sha256:f04d15249601b14046e7e40a4475defb289436c4474afbd89986964f8c3e7c2f"
35
36
  },
36
37
  {
37
- "name": "designer",
38
- "description": "UX/UI design — evaluates user experience, interaction patterns, and how users will experience the product",
39
- "task": "UI/UX design, interaction patterns, user experience",
40
- "alias_ko": "디자이너",
38
+ "name": "strategist",
39
+ "description": "Business strategy — evaluates market positioning, competitive landscape, and business viability of decisions",
40
+ "task": "Business strategy, market analysis, competitive positioning",
41
+ "alias_ko": "전략가",
41
42
  "category": "how",
42
43
  "resume_tier": "persistent",
43
44
  "model_tier": "high",
@@ -46,14 +47,14 @@
46
47
  "no_task_create",
47
48
  "no_task_update"
48
49
  ],
49
- "id": "designer",
50
- "body_hash": "sha256:88ac56147d0e5bdf23fa591ce570a9c2d0eb1338df4ec2219f6238ddfcb65df4"
50
+ "id": "strategist",
51
+ "body_hash": "sha256:0254b4144a22c66209bd68119553d9057a4fb7f9b1ff7ebb9878687d99583465"
51
52
  },
52
53
  {
53
- "name": "strategist",
54
- "description": "Business strategy — evaluates market positioning, competitive landscape, and business viability of decisions",
55
- "task": "Business strategy, market analysis, competitive positioning",
56
- "alias_ko": "전략가",
54
+ "name": "designer",
55
+ "description": "UX/UI design — evaluates user experience, interaction patterns, and how users will experience the product",
56
+ "task": "UI/UX design, interaction patterns, user experience",
57
+ "alias_ko": "디자이너",
57
58
  "category": "how",
58
59
  "resume_tier": "persistent",
59
60
  "model_tier": "high",
@@ -62,8 +63,8 @@
62
63
  "no_task_create",
63
64
  "no_task_update"
64
65
  ],
65
- "id": "strategist",
66
- "body_hash": "sha256:0254b4144a22c66209bd68119553d9057a4fb7f9b1ff7ebb9878687d99583465"
66
+ "id": "designer",
67
+ "body_hash": "sha256:88ac56147d0e5bdf23fa591ce570a9c2d0eb1338df4ec2219f6238ddfcb65df4"
67
68
  },
68
69
  {
69
70
  "name": "researcher",
@@ -80,6 +81,20 @@
80
81
  "id": "researcher",
81
82
  "body_hash": "sha256:fc79bafec05503327bd51a0b84b6e642d304bd79c45b78db6448b112793c143e"
82
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
+ },
83
98
  {
84
99
  "name": "postdoc",
85
100
  "description": "Research methodology and synthesis — designs investigation approach, evaluates evidence quality, writes synthesis documents",
@@ -96,21 +111,6 @@
96
111
  "id": "postdoc",
97
112
  "body_hash": "sha256:da9b8c2568b8b5812abed6d6324139f814379d48dc63cdc5d0b5b263f5407814"
98
113
  },
99
- {
100
- "name": "reviewer",
101
- "description": "Content verification — validates accuracy, checks facts, confirms grammar and format of non-code deliverables",
102
- "task": "Content verification, fact-checking, grammar review",
103
- "alias_ko": "리뷰어",
104
- "category": "check",
105
- "resume_tier": "ephemeral",
106
- "model_tier": "standard",
107
- "capabilities": [
108
- "no_file_edit",
109
- "no_task_create"
110
- ],
111
- "id": "reviewer",
112
- "body_hash": "sha256:f04d15249601b14046e7e40a4475defb289436c4474afbd89986964f8c3e7c2f"
113
- },
114
114
  {
115
115
  "name": "tester",
116
116
  "description": "Testing and verification — tests, verifies, validates stability and security of implementations",
@@ -143,17 +143,26 @@
143
143
  ],
144
144
  "skills": [
145
145
  {
146
- "name": "nx-run",
147
- "description": "Executionuser-directed agent composition.",
148
- "summary": "Executionuser-directed agent composition",
149
- "triggers": [
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
- "resume_invocation"
151
+ "instruction_file",
152
+ "slash_command_display"
154
153
  ],
155
- "id": "nx-run",
156
- "body_hash": "sha256:6c8d1a36626d4034209ff83780dec6238297ec4710612441b2ef09daac714ca8"
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:85b858089bd3dc276be61baa3f5265bc107a85470f169983e710fecb404bb4b1"
178
+ "body_hash": "sha256:cd7a5fd1815530be6ffad18358a1295d1f74ef8a24132e75b73522c106eb6ae5"
170
179
  },
171
180
  {
172
- "name": "nx-sync",
173
- "description": "Context knowledge synchronization scans project state and updates .nexus/context/ design documents",
174
- "summary": "Context knowledge synchronization",
181
+ "name": "nx-run",
182
+ "description": "Executionuser-directed agent composition.",
183
+ "summary": "Execution user-directed agent composition",
175
184
  "triggers": [
176
- "sync"
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
- "instruction_file"
188
+ "resume_invocation"
188
189
  ],
189
- "id": "nx-init",
190
- "body_hash": "sha256:3c8230ecc0f87c541ec0ff80492a28f28bf173d0b9781901adadfae69a54b8ed"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moreih29/nexus-core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Nexus ecosystem Authoring layer — canonical prompts, neutral metadata, and vocabulary shared by Nexus harnesses",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,27 +34,42 @@
34
34
  "items": {
35
35
  "type": "object",
36
36
  "additionalProperties": false,
37
- "required": ["id", "name", "description", "triggers", "body_hash"],
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
  }
@@ -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_]+)\b/g;
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
- * CLAUDE_CODE_TOOLS (capitalized, distinctive) — scanned in ALL lint-included files.
102
- * OPENCODE_TOOLS (lowercase, indistinguishable from English words in prose) scanned
103
- * ONLY in meta.yml and vocabulary/*.yml, NOT in body.md or prose_guidance fields.
104
- * Rationale: "edit", "write", "bash" are common English words that legitimately appear
105
- * in descriptive body prose. Scanning body.md for these produces mass false positives.
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
- // Distinctive Claude Code tools (unambiguous) — all files
113
- results.push(
114
- ...scanRegex(source, CLAUDE_CODE_TOOLS_DISTINCTIVE, rel, 'G6-harness-lint',
115
- (m) => `Harness-specific tool name forbidden: '${m}'. Use abstract capability or remove.`)
116
- );
117
- // Ambiguous tools (Read/Write/Edit/Bash/Task/Monitor + OpenCode lowercase) — meta.yml and vocabulary only
118
- if (rel.endsWith('meta.yml') || rel.startsWith('vocabulary/')) {
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(source, CLAUDE_CODE_TOOLS_AMBIGUOUS, rel, 'G6-harness-lint',
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, OPENCODE_TOOLS, rel, 'G6-harness-lint',
125
- (m) => `OpenCode tool name forbidden: '${m}'. Use abstract capability or remove.`)
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;
@@ -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)
@@ -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
- - `/claude-nexus:nx-init` — full onboarding (or resume)
20
- - `/claude-nexus:nx-init --reset` — back up existing `.nexus/` knowledge and re-onboard
21
- - `/claude-nexus:nx-init --reset --cleanup` — show backup list + selective deletion
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 (using the harness's interactive prompt mechanism):
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
- prompt_user({
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
- - /claude-nexus:nx-init --reset — re-run onboarding (existing knowledge will be backed up)
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
  ```
@@ -4,4 +4,5 @@ summary: "Project onboarding — scan, mission, essentials, context generation"
4
4
  manual_only: true
5
5
  harness_docs_refs:
6
6
  - instruction_file
7
+ - slash_command_display
7
8
  id: nx-init
@@ -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 `Skill({ args: "auto" })`, run the full planning process **without user interaction**:
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 | Spawn Explore agent (`subagent_type: "Explore"`) for file/code search |
97
- | External research needed | Spawn Researcher agent (`subagent_type: "claude-nexus:researcher"`) for web search |
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.
@@ -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 `Skill({ skill: "claude-nexus:nx-plan", args: "auto" })` to generate tasks.json. Do NOT ask — `[run]` implies execution intent. After plan generation, proceed to Step 2.
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**: `TaskCreate` per task
35
- - **> 10 tasks**: group by `plan_issue`, `TaskCreate` per group
36
- - Use `TaskUpdate` to reflect progress (`in_progress` / `completed`) as execution proceeds
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 `Skill({ skill: "claude-nexus:nx-sync" })` if code changes were made in this cycle. Best effort — failure does not block cycle completion.
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.
@@ -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
- Agent({ subagent_type: "claude-nexus:writer", name: "writer-sync-context",
48
- prompt: "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}" })
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:
@@ -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.