@moreih29/nexus-core 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +76 -57
  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/assets/hooks/agent-bootstrap/handler.d.ts +4 -0
  51. package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -0
  52. package/dist/assets/hooks/agent-bootstrap/handler.js +100 -0
  53. package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -0
  54. package/dist/assets/hooks/agent-finalize/handler.d.ts +4 -0
  55. package/dist/assets/hooks/agent-finalize/handler.d.ts.map +1 -0
  56. package/dist/assets/hooks/agent-finalize/handler.js +63 -0
  57. package/dist/assets/hooks/agent-finalize/handler.js.map +1 -0
  58. package/dist/assets/hooks/post-tool-telemetry/handler.d.ts +4 -0
  59. package/dist/assets/hooks/post-tool-telemetry/handler.d.ts.map +1 -0
  60. package/dist/assets/hooks/post-tool-telemetry/handler.js +40 -0
  61. package/dist/assets/hooks/post-tool-telemetry/handler.js.map +1 -0
  62. package/dist/assets/hooks/prompt-router/handler.d.ts +4 -0
  63. package/dist/assets/hooks/prompt-router/handler.d.ts.map +1 -0
  64. package/dist/assets/hooks/prompt-router/handler.js +204 -0
  65. package/dist/assets/hooks/prompt-router/handler.js.map +1 -0
  66. package/dist/assets/hooks/session-init/handler.d.ts +4 -0
  67. package/dist/assets/hooks/session-init/handler.d.ts.map +1 -0
  68. package/dist/assets/hooks/session-init/handler.js +23 -0
  69. package/dist/assets/hooks/session-init/handler.js.map +1 -0
  70. package/dist/hooks/agent-bootstrap.js +105 -0
  71. package/dist/hooks/agent-finalize.js +164 -0
  72. package/dist/hooks/post-tool-telemetry.js +55 -0
  73. package/dist/hooks/prompt-router.js +7300 -0
  74. package/dist/hooks/session-init.js +21 -0
  75. package/dist/manifests/claude-hooks.json +52 -0
  76. package/dist/manifests/codex-hooks.json +28 -0
  77. package/dist/manifests/opencode-manifest.json +44 -0
  78. package/dist/manifests/portability-report.json +87 -0
  79. package/dist/scripts/build-agents.d.ts +157 -0
  80. package/dist/scripts/build-agents.d.ts.map +1 -0
  81. package/dist/scripts/build-agents.js +737 -0
  82. package/dist/scripts/build-agents.js.map +1 -0
  83. package/dist/scripts/build-hooks.d.ts +16 -0
  84. package/dist/scripts/build-hooks.d.ts.map +1 -0
  85. package/dist/scripts/build-hooks.js +388 -0
  86. package/dist/scripts/build-hooks.js.map +1 -0
  87. package/dist/scripts/cli.d.ts +54 -0
  88. package/dist/scripts/cli.d.ts.map +1 -0
  89. package/dist/scripts/cli.js +467 -0
  90. package/dist/scripts/cli.js.map +1 -0
  91. package/dist/src/hooks/opencode-mount.d.ts +35 -0
  92. package/dist/src/hooks/opencode-mount.d.ts.map +1 -0
  93. package/dist/src/hooks/opencode-mount.js +352 -0
  94. package/dist/src/hooks/opencode-mount.js.map +1 -0
  95. package/dist/src/hooks/runtime.d.ts +37 -0
  96. package/dist/src/hooks/runtime.d.ts.map +1 -0
  97. package/dist/src/hooks/runtime.js +274 -0
  98. package/dist/src/hooks/runtime.js.map +1 -0
  99. package/dist/src/hooks/types.d.ts +196 -0
  100. package/dist/src/hooks/types.d.ts.map +1 -0
  101. package/dist/src/hooks/types.js +85 -0
  102. package/dist/src/hooks/types.js.map +1 -0
  103. package/dist/src/lsp/cache.d.ts +9 -0
  104. package/dist/src/lsp/cache.d.ts.map +1 -0
  105. package/dist/src/lsp/cache.js +216 -0
  106. package/dist/src/lsp/cache.js.map +1 -0
  107. package/dist/src/lsp/client.d.ts +24 -0
  108. package/dist/src/lsp/client.d.ts.map +1 -0
  109. package/dist/src/lsp/client.js +166 -0
  110. package/dist/src/lsp/client.js.map +1 -0
  111. package/dist/src/lsp/detect.d.ts +77 -0
  112. package/dist/src/lsp/detect.d.ts.map +1 -0
  113. package/dist/src/lsp/detect.js +116 -0
  114. package/dist/src/lsp/detect.js.map +1 -0
  115. package/dist/src/mcp/server.d.ts +5 -0
  116. package/dist/src/mcp/server.d.ts.map +1 -0
  117. package/dist/src/mcp/server.js +34 -0
  118. package/dist/src/mcp/server.js.map +1 -0
  119. package/dist/src/mcp/tools/artifact.d.ts +4 -0
  120. package/dist/src/mcp/tools/artifact.d.ts.map +1 -0
  121. package/dist/src/mcp/tools/artifact.js +36 -0
  122. package/dist/src/mcp/tools/artifact.js.map +1 -0
  123. package/dist/src/mcp/tools/history.d.ts +3 -0
  124. package/dist/src/mcp/tools/history.d.ts.map +1 -0
  125. package/dist/src/mcp/tools/history.js +29 -0
  126. package/dist/src/mcp/tools/history.js.map +1 -0
  127. package/dist/src/mcp/tools/lsp.d.ts +13 -0
  128. package/dist/src/mcp/tools/lsp.d.ts.map +1 -0
  129. package/dist/src/mcp/tools/lsp.js +225 -0
  130. package/dist/src/mcp/tools/lsp.js.map +1 -0
  131. package/dist/src/mcp/tools/plan.d.ts +3 -0
  132. package/dist/src/mcp/tools/plan.d.ts.map +1 -0
  133. package/dist/src/mcp/tools/plan.js +317 -0
  134. package/dist/src/mcp/tools/plan.js.map +1 -0
  135. package/dist/src/mcp/tools/task.d.ts +3 -0
  136. package/dist/src/mcp/tools/task.d.ts.map +1 -0
  137. package/dist/src/mcp/tools/task.js +252 -0
  138. package/dist/src/mcp/tools/task.js.map +1 -0
  139. package/dist/src/shared/invocations.d.ts +74 -0
  140. package/dist/src/shared/invocations.d.ts.map +1 -0
  141. package/dist/src/shared/invocations.js +247 -0
  142. package/dist/src/shared/invocations.js.map +1 -0
  143. package/dist/src/shared/json-store.d.ts +37 -0
  144. package/dist/src/shared/json-store.d.ts.map +1 -0
  145. package/dist/src/shared/json-store.js +163 -0
  146. package/dist/src/shared/json-store.js.map +1 -0
  147. package/dist/src/shared/mcp-utils.d.ts +3 -0
  148. package/dist/src/shared/mcp-utils.d.ts.map +1 -0
  149. package/dist/src/shared/mcp-utils.js +6 -0
  150. package/dist/src/shared/mcp-utils.js.map +1 -0
  151. package/dist/src/shared/paths.d.ts +21 -0
  152. package/dist/src/shared/paths.d.ts.map +1 -0
  153. package/dist/src/shared/paths.js +81 -0
  154. package/dist/src/shared/paths.js.map +1 -0
  155. package/dist/src/shared/tool-log.d.ts +8 -0
  156. package/dist/src/shared/tool-log.d.ts.map +1 -0
  157. package/dist/src/shared/tool-log.js +22 -0
  158. package/dist/src/shared/tool-log.js.map +1 -0
  159. package/dist/src/types/state.d.ts +862 -0
  160. package/dist/src/types/state.d.ts.map +1 -0
  161. package/dist/src/types/state.js +66 -0
  162. package/dist/src/types/state.js.map +1 -0
  163. package/docs/consuming/codex-lead-merge.md +106 -0
  164. package/docs/plugin-guide.md +396 -0
  165. package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
  166. package/docs/plugin-template/claude/README.md +110 -0
  167. package/docs/plugin-template/claude/package.json +16 -0
  168. package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
  169. package/docs/plugin-template/codex/README.md +147 -0
  170. package/docs/plugin-template/codex/package.json +17 -0
  171. package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
  172. package/docs/plugin-template/opencode/README.md +121 -0
  173. package/docs/plugin-template/opencode/package.json +25 -0
  174. package/package.json +36 -28
  175. package/agents/architect/meta.yml +0 -13
  176. package/agents/designer/meta.yml +0 -13
  177. package/agents/engineer/meta.yml +0 -11
  178. package/agents/postdoc/meta.yml +0 -13
  179. package/agents/researcher/meta.yml +0 -12
  180. package/agents/reviewer/meta.yml +0 -12
  181. package/agents/strategist/meta.yml +0 -13
  182. package/agents/tester/meta.yml +0 -12
  183. package/agents/writer/meta.yml +0 -11
  184. package/conformance/README.md +0 -311
  185. package/conformance/examples/plan.extension.schema.example.json +0 -25
  186. package/conformance/lifecycle/README.md +0 -48
  187. package/conformance/lifecycle/agent-complete.json +0 -44
  188. package/conformance/lifecycle/agent-resume.json +0 -43
  189. package/conformance/lifecycle/agent-spawn.json +0 -36
  190. package/conformance/lifecycle/memory-access-record.json +0 -27
  191. package/conformance/lifecycle/session-end.json +0 -48
  192. package/conformance/scenarios/full-plan-cycle.json +0 -147
  193. package/conformance/scenarios/task-deps-ordering.json +0 -95
  194. package/conformance/schema/fixture.schema.json +0 -354
  195. package/conformance/state-schemas/agent-tracker.schema.json +0 -63
  196. package/conformance/state-schemas/history.schema.json +0 -134
  197. package/conformance/state-schemas/memory-access.schema.json +0 -36
  198. package/conformance/state-schemas/plan.schema.json +0 -77
  199. package/conformance/state-schemas/tasks.schema.json +0 -98
  200. package/conformance/tools/artifact-write.json +0 -97
  201. package/conformance/tools/context.json +0 -172
  202. package/conformance/tools/history-search.json +0 -219
  203. package/conformance/tools/plan-decide.json +0 -139
  204. package/conformance/tools/plan-start.json +0 -81
  205. package/conformance/tools/plan-status.json +0 -127
  206. package/conformance/tools/plan-update.json +0 -341
  207. package/conformance/tools/task-add.json +0 -156
  208. package/conformance/tools/task-close.json +0 -161
  209. package/conformance/tools/task-list.json +0 -177
  210. package/conformance/tools/task-update.json +0 -167
  211. package/docs/behavioral-contracts.md +0 -145
  212. package/docs/consumer-implementation-guide.md +0 -840
  213. package/docs/memory-lifecycle-contract.md +0 -119
  214. package/docs/nexus-layout.md +0 -224
  215. package/docs/nexus-outputs-contract.md +0 -344
  216. package/docs/nexus-state-overview.md +0 -170
  217. package/docs/nexus-tools-contract.md +0 -438
  218. package/manifest.json +0 -448
  219. package/schema/README.md +0 -69
  220. package/schema/agent.schema.json +0 -23
  221. package/schema/common.schema.json +0 -17
  222. package/schema/manifest.schema.json +0 -78
  223. package/schema/memory-policy.schema.json +0 -98
  224. package/schema/skill.schema.json +0 -54
  225. package/schema/task-exceptions.schema.json +0 -40
  226. package/schema/vocabulary.schema.json +0 -167
  227. package/scripts/.gitkeep +0 -0
  228. package/scripts/conformance-coverage.ts +0 -466
  229. package/scripts/import-from-claude-nexus.ts +0 -403
  230. package/scripts/lib/frontmatter.ts +0 -71
  231. package/scripts/lib/lint.ts +0 -348
  232. package/scripts/lib/structure.ts +0 -159
  233. package/scripts/lib/validate.ts +0 -796
  234. package/scripts/validate.ts +0 -90
  235. package/skills/nx-init/meta.yml +0 -8
  236. package/skills/nx-plan/meta.yml +0 -10
  237. package/skills/nx-run/meta.yml +0 -8
  238. package/skills/nx-sync/meta.yml +0 -7
  239. package/vocabulary/capabilities.yml +0 -65
  240. package/vocabulary/categories.yml +0 -11
  241. package/vocabulary/invocations.yml +0 -147
  242. package/vocabulary/memory_policy.yml +0 -88
  243. package/vocabulary/resume-tiers.yml +0 -11
  244. package/vocabulary/tags.yml +0 -60
  245. 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
- }