@moreih29/nexus-core 0.11.0 → 0.13.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.
Files changed (210) hide show
  1. package/README.md +48 -63
  2. package/assets/agents/architect/body.ko.md +177 -0
  3. package/{agents → assets/agents}/architect/body.md +16 -0
  4. package/assets/agents/designer/body.ko.md +125 -0
  5. package/{agents → assets/agents}/designer/body.md +16 -0
  6. package/assets/agents/engineer/body.ko.md +106 -0
  7. package/{agents → assets/agents}/engineer/body.md +14 -0
  8. package/assets/agents/lead/body.ko.md +70 -0
  9. package/assets/agents/lead/body.md +70 -0
  10. package/assets/agents/postdoc/body.ko.md +122 -0
  11. package/{agents → assets/agents}/postdoc/body.md +16 -0
  12. package/assets/agents/researcher/body.ko.md +137 -0
  13. package/{agents → assets/agents}/researcher/body.md +15 -0
  14. package/assets/agents/reviewer/body.ko.md +138 -0
  15. package/{agents → assets/agents}/reviewer/body.md +15 -0
  16. package/assets/agents/strategist/body.ko.md +116 -0
  17. package/{agents → assets/agents}/strategist/body.md +16 -0
  18. package/assets/agents/tester/body.ko.md +195 -0
  19. package/{agents → assets/agents}/tester/body.md +15 -0
  20. package/assets/agents/writer/body.ko.md +122 -0
  21. package/{agents → assets/agents}/writer/body.md +14 -0
  22. package/assets/capability-matrix.yml +198 -0
  23. package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
  24. package/assets/hooks/agent-bootstrap/handler.ts +119 -0
  25. package/assets/hooks/agent-bootstrap/meta.yml +10 -0
  26. package/assets/hooks/agent-finalize/handler.test.ts +368 -0
  27. package/assets/hooks/agent-finalize/handler.ts +76 -0
  28. package/assets/hooks/agent-finalize/meta.yml +10 -0
  29. package/assets/hooks/capability-matrix.yml +313 -0
  30. package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
  31. package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
  32. package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
  33. package/assets/hooks/prompt-router/handler.test.ts +801 -0
  34. package/assets/hooks/prompt-router/handler.ts +261 -0
  35. package/assets/hooks/prompt-router/meta.yml +11 -0
  36. package/assets/hooks/session-init/handler.test.ts +274 -0
  37. package/assets/hooks/session-init/handler.ts +30 -0
  38. package/assets/hooks/session-init/meta.yml +9 -0
  39. package/assets/lsp-servers.json +55 -0
  40. package/assets/schema/lsp-servers.schema.json +67 -0
  41. package/assets/skills/nx-init/body.ko.md +197 -0
  42. package/{skills → assets/skills}/nx-init/body.md +11 -0
  43. package/assets/skills/nx-plan/body.ko.md +361 -0
  44. package/{skills → assets/skills}/nx-plan/body.md +13 -0
  45. package/assets/skills/nx-run/body.ko.md +161 -0
  46. package/{skills → assets/skills}/nx-run/body.md +11 -0
  47. package/assets/skills/nx-sync/body.ko.md +92 -0
  48. package/{skills → assets/skills}/nx-sync/body.md +10 -0
  49. package/assets/tools/tool-name-map.yml +353 -0
  50. package/dist/hooks/opencode-mount.d.ts +35 -0
  51. package/dist/hooks/opencode-mount.d.ts.map +1 -0
  52. package/dist/hooks/opencode-mount.js +332 -0
  53. package/dist/hooks/opencode-mount.js.map +1 -0
  54. package/dist/hooks/runtime.d.ts +37 -0
  55. package/dist/hooks/runtime.d.ts.map +1 -0
  56. package/dist/hooks/runtime.js +274 -0
  57. package/dist/hooks/runtime.js.map +1 -0
  58. package/dist/hooks/types.d.ts +196 -0
  59. package/dist/hooks/types.d.ts.map +1 -0
  60. package/dist/hooks/types.js +85 -0
  61. package/dist/hooks/types.js.map +1 -0
  62. package/dist/lsp/cache.d.ts +9 -0
  63. package/dist/lsp/cache.d.ts.map +1 -0
  64. package/dist/lsp/cache.js +216 -0
  65. package/dist/lsp/cache.js.map +1 -0
  66. package/dist/lsp/client.d.ts +24 -0
  67. package/dist/lsp/client.d.ts.map +1 -0
  68. package/dist/lsp/client.js +166 -0
  69. package/dist/lsp/client.js.map +1 -0
  70. package/dist/lsp/detect.d.ts +77 -0
  71. package/dist/lsp/detect.d.ts.map +1 -0
  72. package/dist/lsp/detect.js +116 -0
  73. package/dist/lsp/detect.js.map +1 -0
  74. package/dist/mcp/server.d.ts +5 -0
  75. package/dist/mcp/server.d.ts.map +1 -0
  76. package/dist/mcp/server.js +34 -0
  77. package/dist/mcp/server.js.map +1 -0
  78. package/dist/mcp/tools/artifact.d.ts +4 -0
  79. package/dist/mcp/tools/artifact.d.ts.map +1 -0
  80. package/dist/mcp/tools/artifact.js +36 -0
  81. package/dist/mcp/tools/artifact.js.map +1 -0
  82. package/dist/mcp/tools/history.d.ts +3 -0
  83. package/dist/mcp/tools/history.d.ts.map +1 -0
  84. package/dist/mcp/tools/history.js +29 -0
  85. package/dist/mcp/tools/history.js.map +1 -0
  86. package/dist/mcp/tools/lsp.d.ts +13 -0
  87. package/dist/mcp/tools/lsp.d.ts.map +1 -0
  88. package/dist/mcp/tools/lsp.js +225 -0
  89. package/dist/mcp/tools/lsp.js.map +1 -0
  90. package/dist/mcp/tools/plan.d.ts +3 -0
  91. package/dist/mcp/tools/plan.d.ts.map +1 -0
  92. package/dist/mcp/tools/plan.js +317 -0
  93. package/dist/mcp/tools/plan.js.map +1 -0
  94. package/dist/mcp/tools/task.d.ts +3 -0
  95. package/dist/mcp/tools/task.d.ts.map +1 -0
  96. package/dist/mcp/tools/task.js +252 -0
  97. package/dist/mcp/tools/task.js.map +1 -0
  98. package/dist/shared/invocations.d.ts +74 -0
  99. package/dist/shared/invocations.d.ts.map +1 -0
  100. package/dist/shared/invocations.js +247 -0
  101. package/dist/shared/invocations.js.map +1 -0
  102. package/dist/shared/json-store.d.ts +37 -0
  103. package/dist/shared/json-store.d.ts.map +1 -0
  104. package/dist/shared/json-store.js +163 -0
  105. package/dist/shared/json-store.js.map +1 -0
  106. package/dist/shared/mcp-utils.d.ts +3 -0
  107. package/dist/shared/mcp-utils.d.ts.map +1 -0
  108. package/dist/shared/mcp-utils.js +6 -0
  109. package/dist/shared/mcp-utils.js.map +1 -0
  110. package/dist/shared/paths.d.ts +21 -0
  111. package/dist/shared/paths.d.ts.map +1 -0
  112. package/dist/shared/paths.js +81 -0
  113. package/dist/shared/paths.js.map +1 -0
  114. package/dist/shared/tool-log.d.ts +8 -0
  115. package/dist/shared/tool-log.d.ts.map +1 -0
  116. package/dist/shared/tool-log.js +22 -0
  117. package/dist/shared/tool-log.js.map +1 -0
  118. package/dist/types/state.d.ts +862 -0
  119. package/dist/types/state.d.ts.map +1 -0
  120. package/dist/types/state.js +66 -0
  121. package/dist/types/state.js.map +1 -0
  122. package/docs/consuming/codex-lead-merge.md +106 -0
  123. package/docs/plugin-guide.md +360 -0
  124. package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
  125. package/docs/plugin-template/claude/README.md +110 -0
  126. package/docs/plugin-template/claude/package.json +16 -0
  127. package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
  128. package/docs/plugin-template/codex/README.md +147 -0
  129. package/docs/plugin-template/codex/package.json +17 -0
  130. package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
  131. package/docs/plugin-template/opencode/README.md +121 -0
  132. package/docs/plugin-template/opencode/package.json +25 -0
  133. package/package.json +21 -21
  134. package/scripts/build-agents.test.ts +1279 -0
  135. package/scripts/build-agents.ts +978 -0
  136. package/scripts/build-hooks.test.ts +1385 -0
  137. package/scripts/build-hooks.ts +584 -0
  138. package/scripts/cli.test.ts +367 -0
  139. package/scripts/cli.ts +547 -0
  140. package/agents/architect/meta.yml +0 -13
  141. package/agents/designer/meta.yml +0 -13
  142. package/agents/engineer/meta.yml +0 -11
  143. package/agents/postdoc/meta.yml +0 -13
  144. package/agents/researcher/meta.yml +0 -12
  145. package/agents/reviewer/meta.yml +0 -12
  146. package/agents/strategist/meta.yml +0 -13
  147. package/agents/tester/meta.yml +0 -12
  148. package/agents/writer/meta.yml +0 -11
  149. package/conformance/README.md +0 -311
  150. package/conformance/examples/plan.extension.schema.example.json +0 -25
  151. package/conformance/lifecycle/README.md +0 -48
  152. package/conformance/lifecycle/agent-complete.json +0 -44
  153. package/conformance/lifecycle/agent-resume.json +0 -43
  154. package/conformance/lifecycle/agent-spawn.json +0 -36
  155. package/conformance/lifecycle/memory-access-record.json +0 -27
  156. package/conformance/lifecycle/session-end.json +0 -48
  157. package/conformance/scenarios/full-plan-cycle.json +0 -147
  158. package/conformance/scenarios/task-deps-ordering.json +0 -95
  159. package/conformance/schema/fixture.schema.json +0 -354
  160. package/conformance/state-schemas/agent-tracker.schema.json +0 -63
  161. package/conformance/state-schemas/history.schema.json +0 -134
  162. package/conformance/state-schemas/memory-access.schema.json +0 -36
  163. package/conformance/state-schemas/plan.schema.json +0 -77
  164. package/conformance/state-schemas/tasks.schema.json +0 -98
  165. package/conformance/tools/artifact-write.json +0 -97
  166. package/conformance/tools/context.json +0 -172
  167. package/conformance/tools/history-search.json +0 -219
  168. package/conformance/tools/plan-decide.json +0 -139
  169. package/conformance/tools/plan-start.json +0 -81
  170. package/conformance/tools/plan-status.json +0 -127
  171. package/conformance/tools/plan-update.json +0 -341
  172. package/conformance/tools/task-add.json +0 -156
  173. package/conformance/tools/task-close.json +0 -161
  174. package/conformance/tools/task-list.json +0 -177
  175. package/conformance/tools/task-update.json +0 -167
  176. package/docs/behavioral-contracts.md +0 -145
  177. package/docs/consumer-implementation-guide.md +0 -852
  178. package/docs/memory-lifecycle-contract.md +0 -119
  179. package/docs/nexus-layout.md +0 -224
  180. package/docs/nexus-outputs-contract.md +0 -344
  181. package/docs/nexus-state-overview.md +0 -170
  182. package/docs/nexus-tools-contract.md +0 -438
  183. package/manifest.json +0 -449
  184. package/schema/README.md +0 -69
  185. package/schema/agent.schema.json +0 -23
  186. package/schema/common.schema.json +0 -17
  187. package/schema/manifest.schema.json +0 -78
  188. package/schema/memory-policy.schema.json +0 -98
  189. package/schema/skill.schema.json +0 -54
  190. package/schema/task-exceptions.schema.json +0 -40
  191. package/schema/vocabulary.schema.json +0 -167
  192. package/scripts/.gitkeep +0 -0
  193. package/scripts/conformance-coverage.ts +0 -466
  194. package/scripts/import-from-claude-nexus.ts +0 -403
  195. package/scripts/lib/frontmatter.ts +0 -71
  196. package/scripts/lib/lint.ts +0 -348
  197. package/scripts/lib/structure.ts +0 -159
  198. package/scripts/lib/validate.ts +0 -794
  199. package/scripts/validate.ts +0 -90
  200. package/skills/nx-init/meta.yml +0 -8
  201. package/skills/nx-plan/meta.yml +0 -10
  202. package/skills/nx-run/meta.yml +0 -9
  203. package/skills/nx-sync/meta.yml +0 -7
  204. package/vocabulary/capabilities.yml +0 -65
  205. package/vocabulary/categories.yml +0 -11
  206. package/vocabulary/invocations.yml +0 -147
  207. package/vocabulary/memory_policy.yml +0 -88
  208. package/vocabulary/resume-tiers.yml +0 -11
  209. package/vocabulary/tags.yml +0 -60
  210. package/vocabulary/task-exceptions.yml +0 -29
@@ -1,348 +0,0 @@
1
- import { glob } from 'tinyglobby';
2
- import { readFile } from 'node:fs/promises';
3
- import { parse as parseYaml } from 'yaml';
4
- import path from 'node:path';
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
-
70
- /**
71
- * Common ValidationResult type. Imported from ./validate.ts for consistency,
72
- * but declared here as well for isolation.
73
- */
74
- export interface ValidationResult {
75
- file: string;
76
- gate: string;
77
- severity: 'error' | 'warning';
78
- line?: number;
79
- message: string;
80
- }
81
-
82
- /** Paths excluded from all lint checks. */
83
- const LINT_EXCLUDE: string[] = [
84
- 'scripts/**',
85
- 'node_modules/**',
86
- '.git/**',
87
- 'dist/**',
88
- '.nexus/**',
89
- 'schema/**',
90
- // capabilities.yml prose_guidance naturally uses English words (Read, edit, write)
91
- // that match tool-name regexes. After v0.2.0 harness-agnostic redesign, this file
92
- // contains zero harness tool names — only semantic descriptions. Excluding is safe.
93
- 'vocabulary/capabilities.yml',
94
- ];
95
-
96
- /**
97
- * Patterns to scan — only prompt-injection sources and canonical vocabulary.
98
- *
99
- * Intentionally excluded: README.md, CONSUMING.md, CHANGELOG.md, MIGRATIONS/*,
100
- * schema/README.md — these are human-facing documentation where harness tool
101
- * names and model names may legitimately appear in prose explanations.
102
- */
103
- const LINT_INCLUDE: string[] = [
104
- 'agents/**/meta.yml',
105
- 'agents/**/body.md',
106
- 'skills/**/meta.yml',
107
- 'skills/**/body.md',
108
- 'vocabulary/*.yml',
109
- ];
110
-
111
- // G6: harness-specific tool names
112
- // Distinctive tools — unambiguous, safe to scan in ALL files including body.md prose
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;
114
- // Ambiguous tools — also common English words (Read, Write, Edit, Bash, Task, Monitor)
115
- // Only scanned in meta.yml and vocabulary where they are clearly tool references, not prose.
116
- const CLAUDE_CODE_TOOLS_AMBIGUOUS = /\b(Read|Write|Edit|Bash|Task|Monitor)\b/g;
117
- const OPENCODE_TOOLS = /\b(edit|write|patch|multiedit|bash)\b/g;
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
-
126
- // G7: concrete model names
127
- const CONCRETE_MODELS = /\b(opus|sonnet|haiku|gpt-[0-9][a-z0-9.-]*|claude-[0-9][a-z0-9.-]*)\b/gi;
128
-
129
- // G8: non-TS/JS file allowed extensions
130
- const PROMPT_ONLY_BAD_EXT = /\.(ts|tsx|js|jsx|cjs|mjs)$/;
131
-
132
- async function* iterFiles(root: string): AsyncGenerator<string> {
133
- const files = await glob(LINT_INCLUDE, {
134
- cwd: root,
135
- ignore: LINT_EXCLUDE,
136
- absolute: true,
137
- onlyFiles: true,
138
- });
139
- for (const f of files) yield f;
140
- }
141
-
142
- function lineOfMatch(source: string, index: number): number {
143
- return source.slice(0, index).split('\n').length;
144
- }
145
-
146
- function scanRegex(
147
- source: string,
148
- regex: RegExp,
149
- file: string,
150
- gate: string,
151
- makeMessage: (match: string) => string
152
- ): ValidationResult[] {
153
- const results: ValidationResult[] = [];
154
- regex.lastIndex = 0;
155
- let m: RegExpExecArray | null;
156
- // eslint-disable-next-line no-cond-assign
157
- while ((m = regex.exec(source)) !== null) {
158
- results.push({
159
- file,
160
- gate,
161
- severity: 'error',
162
- line: lineOfMatch(source, m.index),
163
- message: makeMessage(m[0]),
164
- });
165
- if (m.index === regex.lastIndex) regex.lastIndex++;
166
- }
167
- return results;
168
- }
169
-
170
- /** G6: harness-specific tool names forbidden in body/meta/vocabulary.
171
- *
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).
188
- */
189
- export async function checkHarnessSpecific(root: string): Promise<ValidationResult[]> {
190
- const invocationIds = await loadInvocationIds(root);
191
- const results: ValidationResult[] = [];
192
- for await (const file of iterFiles(root)) {
193
- const source = await readFile(file, 'utf8');
194
- const rel = path.relative(root, file);
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()
209
- results.push(
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',
236
- (m) => `Harness-specific tool name forbidden: '${m}'. Use abstract capability or remove.`)
237
- );
238
-
239
- results.push(
240
- ...scanRegex(source, CALL_PATTERN_TOOLS, rel, 'G6-harness-lint',
241
- (m) => `Harness-specific tool call syntax forbidden: '${m}'. Use abstract capability or remove.`)
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
- }
259
- }
260
- }
261
- return results;
262
- }
263
-
264
- /** G7: concrete model names forbidden; use model_tier abstraction. */
265
- export async function checkConcreteModel(root: string): Promise<ValidationResult[]> {
266
- const results: ValidationResult[] = [];
267
- for await (const file of iterFiles(root)) {
268
- const source = await readFile(file, 'utf8');
269
- const rel = path.relative(root, file);
270
- results.push(
271
- ...scanRegex(source, CONCRETE_MODELS, rel, 'G7-model-lint',
272
- (m) => `Concrete model name forbidden: '${m}'. Use 'model_tier: high | standard'.`)
273
- );
274
- }
275
- return results;
276
- }
277
-
278
- /**
279
- * G11: tag trigger consistency — each tag's trigger must equal "[" + id.replace(/-/g, ":") + "]".
280
- */
281
- export async function checkTagTriggerConsistency(root: string): Promise<ValidationResult[]> {
282
- const tagsPath = path.join(root, 'vocabulary', 'tags.yml');
283
- const rel = path.join('vocabulary', 'tags.yml');
284
- let source: string;
285
- try {
286
- source = await readFile(tagsPath, 'utf8');
287
- } catch (err) {
288
- return [{
289
- file: rel,
290
- gate: 'G11-tag-trigger',
291
- severity: 'error',
292
- message: `Cannot read tags.yml: ${(err as Error).message}`,
293
- }];
294
- }
295
-
296
- let data: unknown;
297
- try {
298
- data = parseYaml(source);
299
- } catch (err) {
300
- return [{
301
- file: rel,
302
- gate: 'G11-tag-trigger',
303
- severity: 'error',
304
- message: `YAML parse error in tags.yml: ${(err as Error).message}`,
305
- }];
306
- }
307
-
308
- const tags = (data as { tags?: Array<{ id: string; trigger: string }> })?.tags ?? [];
309
- const results: ValidationResult[] = [];
310
- for (const tag of tags) {
311
- const expected = '[' + tag.id.replace(/-/g, ':') + ']';
312
- if (tag.trigger !== expected) {
313
- results.push({
314
- file: rel,
315
- gate: 'G11-tag-trigger',
316
- severity: 'error',
317
- message: `Tag '${tag.id}': trigger mismatch — expected '${expected}', got '${tag.trigger}'`,
318
- });
319
- }
320
- }
321
- return results;
322
- }
323
-
324
- /**
325
- * G8: prompt-only enforcement — no .ts/.js/.cjs/.mjs outside scripts/.
326
- * Published artifact must not contain runtime code.
327
- */
328
- export async function checkPromptOnly(root: string): Promise<ValidationResult[]> {
329
- const results: ValidationResult[] = [];
330
- const allFiles = await glob(['**/*'], {
331
- cwd: root,
332
- ignore: ['node_modules/**', '.git/**', 'dist/**', '.nexus/**', 'scripts/**'],
333
- absolute: true,
334
- onlyFiles: true,
335
- });
336
- for (const file of allFiles) {
337
- if (PROMPT_ONLY_BAD_EXT.test(file)) {
338
- const rel = path.relative(root, file);
339
- results.push({
340
- file: rel,
341
- gate: 'G8-prompt-only',
342
- severity: 'error',
343
- message: `Runtime code file outside scripts/: ${rel}. nexus-core is a prompt-only library.`,
344
- });
345
- }
346
- }
347
- return results;
348
- }
@@ -1,159 +0,0 @@
1
- import { glob } from 'tinyglobby';
2
- import { readFile, readdir } from 'node:fs/promises';
3
- import { parse as parseYaml } from 'yaml';
4
- import path from 'node:path';
5
-
6
- export interface ValidationResult {
7
- file: string;
8
- gate: string;
9
- severity: 'error' | 'warning';
10
- line?: number;
11
- message: string;
12
- }
13
-
14
- const KEBAB_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
15
- const ALLOWED_FILES = new Set(['body.md', 'meta.yml']);
16
-
17
- /**
18
- * G9: Strict directory contents.
19
- * agents/{id}/ and skills/{id}/ must contain exactly body.md + meta.yml, nothing else.
20
- */
21
- export async function checkDirectoryStrict(root: string): Promise<ValidationResult[]> {
22
- const results: ValidationResult[] = [];
23
- const targets: Array<{ kind: string; base: string }> = [
24
- { kind: 'agent', base: 'agents' },
25
- { kind: 'skill', base: 'skills' },
26
- ];
27
-
28
- for (const { kind, base } of targets) {
29
- const baseDir = path.join(root, base);
30
- let entries: Array<{ name: string; isDirectory: () => boolean }>;
31
- try {
32
- entries = await readdir(baseDir, { withFileTypes: true });
33
- } catch {
34
- // base directory absent — not an error at this gate
35
- continue;
36
- }
37
-
38
- for (const entry of entries) {
39
- if (!entry.isDirectory()) {
40
- const rel = path.join(base, entry.name);
41
- results.push({
42
- file: rel,
43
- gate: 'G9-directory-strict',
44
- severity: 'error',
45
- message: `Unexpected non-directory entry in ${base}/: '${entry.name}'. Only ${kind} directories allowed.`,
46
- });
47
- continue;
48
- }
49
-
50
- const dirPath = path.join(baseDir, entry.name);
51
- const files = await readdir(dirPath);
52
- const fileSet = new Set(files);
53
-
54
- // Must contain exactly body.md + meta.yml
55
- if (!fileSet.has('body.md')) {
56
- results.push({
57
- file: path.join(base, entry.name),
58
- gate: 'G9-directory-strict',
59
- severity: 'error',
60
- message: `Missing required file: ${base}/${entry.name}/body.md`,
61
- });
62
- }
63
- if (!fileSet.has('meta.yml')) {
64
- results.push({
65
- file: path.join(base, entry.name),
66
- gate: 'G9-directory-strict',
67
- severity: 'error',
68
- message: `Missing required file: ${base}/${entry.name}/meta.yml`,
69
- });
70
- }
71
- for (const f of files) {
72
- if (!ALLOWED_FILES.has(f)) {
73
- results.push({
74
- file: path.join(base, entry.name, f),
75
- gate: 'G9-directory-strict',
76
- severity: 'error',
77
- message: `Unexpected file in ${base}/${entry.name}/: '${f}'. Only body.md + meta.yml allowed (Strict).`,
78
- });
79
- }
80
- }
81
- }
82
- }
83
-
84
- return results;
85
- }
86
-
87
- /**
88
- * G10: id <-> directory name match + kebab-case pattern.
89
- * meta.yml.id must equal path.basename(path.dirname(file)) and match ^[a-z][a-z0-9-]*$.
90
- */
91
- export async function checkIdMatch(root: string): Promise<ValidationResult[]> {
92
- const results: ValidationResult[] = [];
93
- const metaFiles = await glob(['agents/*/meta.yml', 'skills/*/meta.yml'], {
94
- cwd: root,
95
- absolute: true,
96
- onlyFiles: true,
97
- });
98
-
99
- for (const metaPath of metaFiles) {
100
- const rel = path.relative(root, metaPath);
101
- const dirName = path.basename(path.dirname(metaPath));
102
-
103
- // Directory name must itself be kebab-case
104
- if (!KEBAB_ID_PATTERN.test(dirName)) {
105
- results.push({
106
- file: rel,
107
- gate: 'G10-id-match',
108
- severity: 'error',
109
- message: `Directory name '${dirName}' violates kebab-case pattern ^[a-z][a-z0-9-]*$`,
110
- });
111
- // Continue to also check id field — don't skip
112
- }
113
-
114
- let data: Record<string, unknown>;
115
- try {
116
- const content = await readFile(metaPath, 'utf8');
117
- data = (parseYaml(content) ?? {}) as Record<string, unknown>;
118
- } catch (err) {
119
- results.push({
120
- file: rel,
121
- gate: 'G10-id-match',
122
- severity: 'error',
123
- message: `Failed to parse meta.yml: ${(err as Error).message}`,
124
- });
125
- continue;
126
- }
127
-
128
- const id = data.id;
129
- if (typeof id !== 'string') {
130
- results.push({
131
- file: rel,
132
- gate: 'G10-id-match',
133
- severity: 'error',
134
- message: `meta.yml.id is missing or not a string`,
135
- });
136
- continue;
137
- }
138
-
139
- if (!KEBAB_ID_PATTERN.test(id)) {
140
- results.push({
141
- file: rel,
142
- gate: 'G10-id-match',
143
- severity: 'error',
144
- message: `meta.yml.id '${id}' violates kebab-case pattern ^[a-z][a-z0-9-]*$`,
145
- });
146
- }
147
-
148
- if (id !== dirName) {
149
- results.push({
150
- file: rel,
151
- gate: 'G10-id-match',
152
- severity: 'error',
153
- message: `meta.yml.id '${id}' does not match directory name '${dirName}'`,
154
- });
155
- }
156
- }
157
-
158
- return results;
159
- }