@moreih29/nexus-core 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -57
- package/assets/agents/architect/body.ko.md +177 -0
- package/{agents → assets/agents}/architect/body.md +16 -0
- package/assets/agents/designer/body.ko.md +125 -0
- package/{agents → assets/agents}/designer/body.md +16 -0
- package/assets/agents/engineer/body.ko.md +106 -0
- package/{agents → assets/agents}/engineer/body.md +14 -0
- package/assets/agents/lead/body.ko.md +70 -0
- package/assets/agents/lead/body.md +70 -0
- package/assets/agents/postdoc/body.ko.md +122 -0
- package/{agents → assets/agents}/postdoc/body.md +16 -0
- package/assets/agents/researcher/body.ko.md +137 -0
- package/{agents → assets/agents}/researcher/body.md +15 -0
- package/assets/agents/reviewer/body.ko.md +138 -0
- package/{agents → assets/agents}/reviewer/body.md +15 -0
- package/assets/agents/strategist/body.ko.md +116 -0
- package/{agents → assets/agents}/strategist/body.md +16 -0
- package/assets/agents/tester/body.ko.md +195 -0
- package/{agents → assets/agents}/tester/body.md +15 -0
- package/assets/agents/writer/body.ko.md +122 -0
- package/{agents → assets/agents}/writer/body.md +14 -0
- package/assets/capability-matrix.yml +198 -0
- package/assets/hooks/agent-bootstrap/handler.test.ts +368 -0
- package/assets/hooks/agent-bootstrap/handler.ts +119 -0
- package/assets/hooks/agent-bootstrap/meta.yml +10 -0
- package/assets/hooks/agent-finalize/handler.test.ts +368 -0
- package/assets/hooks/agent-finalize/handler.ts +76 -0
- package/assets/hooks/agent-finalize/meta.yml +10 -0
- package/assets/hooks/capability-matrix.yml +313 -0
- package/assets/hooks/post-tool-telemetry/handler.test.ts +302 -0
- package/assets/hooks/post-tool-telemetry/handler.ts +49 -0
- package/assets/hooks/post-tool-telemetry/meta.yml +11 -0
- package/assets/hooks/prompt-router/handler.test.ts +801 -0
- package/assets/hooks/prompt-router/handler.ts +261 -0
- package/assets/hooks/prompt-router/meta.yml +11 -0
- package/assets/hooks/session-init/handler.test.ts +274 -0
- package/assets/hooks/session-init/handler.ts +30 -0
- package/assets/hooks/session-init/meta.yml +9 -0
- package/assets/lsp-servers.json +55 -0
- package/assets/schema/lsp-servers.schema.json +67 -0
- package/assets/skills/nx-init/body.ko.md +197 -0
- package/{skills → assets/skills}/nx-init/body.md +11 -0
- package/assets/skills/nx-plan/body.ko.md +361 -0
- package/{skills → assets/skills}/nx-plan/body.md +13 -0
- package/assets/skills/nx-run/body.ko.md +161 -0
- package/{skills → assets/skills}/nx-run/body.md +11 -0
- package/assets/skills/nx-sync/body.ko.md +92 -0
- package/{skills → assets/skills}/nx-sync/body.md +10 -0
- package/assets/tools/tool-name-map.yml +353 -0
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts +4 -0
- package/dist/assets/hooks/agent-bootstrap/handler.d.ts.map +1 -0
- package/dist/assets/hooks/agent-bootstrap/handler.js +100 -0
- package/dist/assets/hooks/agent-bootstrap/handler.js.map +1 -0
- package/dist/assets/hooks/agent-finalize/handler.d.ts +4 -0
- package/dist/assets/hooks/agent-finalize/handler.d.ts.map +1 -0
- package/dist/assets/hooks/agent-finalize/handler.js +63 -0
- package/dist/assets/hooks/agent-finalize/handler.js.map +1 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.d.ts +4 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.d.ts.map +1 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.js +40 -0
- package/dist/assets/hooks/post-tool-telemetry/handler.js.map +1 -0
- package/dist/assets/hooks/prompt-router/handler.d.ts +4 -0
- package/dist/assets/hooks/prompt-router/handler.d.ts.map +1 -0
- package/dist/assets/hooks/prompt-router/handler.js +204 -0
- package/dist/assets/hooks/prompt-router/handler.js.map +1 -0
- package/dist/assets/hooks/session-init/handler.d.ts +4 -0
- package/dist/assets/hooks/session-init/handler.d.ts.map +1 -0
- package/dist/assets/hooks/session-init/handler.js +23 -0
- package/dist/assets/hooks/session-init/handler.js.map +1 -0
- package/dist/hooks/agent-bootstrap.js +105 -0
- package/dist/hooks/agent-finalize.js +164 -0
- package/dist/hooks/post-tool-telemetry.js +55 -0
- package/dist/hooks/prompt-router.js +7300 -0
- package/dist/hooks/session-init.js +21 -0
- package/dist/manifests/claude-hooks.json +52 -0
- package/dist/manifests/codex-hooks.json +28 -0
- package/dist/manifests/opencode-manifest.json +44 -0
- package/dist/manifests/portability-report.json +87 -0
- package/dist/scripts/build-agents.d.ts +157 -0
- package/dist/scripts/build-agents.d.ts.map +1 -0
- package/dist/scripts/build-agents.js +737 -0
- package/dist/scripts/build-agents.js.map +1 -0
- package/dist/scripts/build-hooks.d.ts +16 -0
- package/dist/scripts/build-hooks.d.ts.map +1 -0
- package/dist/scripts/build-hooks.js +388 -0
- package/dist/scripts/build-hooks.js.map +1 -0
- package/dist/scripts/cli.d.ts +54 -0
- package/dist/scripts/cli.d.ts.map +1 -0
- package/dist/scripts/cli.js +467 -0
- package/dist/scripts/cli.js.map +1 -0
- package/dist/src/hooks/opencode-mount.d.ts +35 -0
- package/dist/src/hooks/opencode-mount.d.ts.map +1 -0
- package/dist/src/hooks/opencode-mount.js +352 -0
- package/dist/src/hooks/opencode-mount.js.map +1 -0
- package/dist/src/hooks/runtime.d.ts +37 -0
- package/dist/src/hooks/runtime.d.ts.map +1 -0
- package/dist/src/hooks/runtime.js +274 -0
- package/dist/src/hooks/runtime.js.map +1 -0
- package/dist/src/hooks/types.d.ts +196 -0
- package/dist/src/hooks/types.d.ts.map +1 -0
- package/dist/src/hooks/types.js +85 -0
- package/dist/src/hooks/types.js.map +1 -0
- package/dist/src/lsp/cache.d.ts +9 -0
- package/dist/src/lsp/cache.d.ts.map +1 -0
- package/dist/src/lsp/cache.js +216 -0
- package/dist/src/lsp/cache.js.map +1 -0
- package/dist/src/lsp/client.d.ts +24 -0
- package/dist/src/lsp/client.d.ts.map +1 -0
- package/dist/src/lsp/client.js +166 -0
- package/dist/src/lsp/client.js.map +1 -0
- package/dist/src/lsp/detect.d.ts +77 -0
- package/dist/src/lsp/detect.d.ts.map +1 -0
- package/dist/src/lsp/detect.js +116 -0
- package/dist/src/lsp/detect.js.map +1 -0
- package/dist/src/mcp/server.d.ts +5 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +34 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/artifact.d.ts +4 -0
- package/dist/src/mcp/tools/artifact.d.ts.map +1 -0
- package/dist/src/mcp/tools/artifact.js +36 -0
- package/dist/src/mcp/tools/artifact.js.map +1 -0
- package/dist/src/mcp/tools/history.d.ts +3 -0
- package/dist/src/mcp/tools/history.d.ts.map +1 -0
- package/dist/src/mcp/tools/history.js +29 -0
- package/dist/src/mcp/tools/history.js.map +1 -0
- package/dist/src/mcp/tools/lsp.d.ts +13 -0
- package/dist/src/mcp/tools/lsp.d.ts.map +1 -0
- package/dist/src/mcp/tools/lsp.js +225 -0
- package/dist/src/mcp/tools/lsp.js.map +1 -0
- package/dist/src/mcp/tools/plan.d.ts +3 -0
- package/dist/src/mcp/tools/plan.d.ts.map +1 -0
- package/dist/src/mcp/tools/plan.js +317 -0
- package/dist/src/mcp/tools/plan.js.map +1 -0
- package/dist/src/mcp/tools/task.d.ts +3 -0
- package/dist/src/mcp/tools/task.d.ts.map +1 -0
- package/dist/src/mcp/tools/task.js +252 -0
- package/dist/src/mcp/tools/task.js.map +1 -0
- package/dist/src/shared/invocations.d.ts +74 -0
- package/dist/src/shared/invocations.d.ts.map +1 -0
- package/dist/src/shared/invocations.js +247 -0
- package/dist/src/shared/invocations.js.map +1 -0
- package/dist/src/shared/json-store.d.ts +37 -0
- package/dist/src/shared/json-store.d.ts.map +1 -0
- package/dist/src/shared/json-store.js +163 -0
- package/dist/src/shared/json-store.js.map +1 -0
- package/dist/src/shared/mcp-utils.d.ts +3 -0
- package/dist/src/shared/mcp-utils.d.ts.map +1 -0
- package/dist/src/shared/mcp-utils.js +6 -0
- package/dist/src/shared/mcp-utils.js.map +1 -0
- package/dist/src/shared/paths.d.ts +21 -0
- package/dist/src/shared/paths.d.ts.map +1 -0
- package/dist/src/shared/paths.js +81 -0
- package/dist/src/shared/paths.js.map +1 -0
- package/dist/src/shared/tool-log.d.ts +8 -0
- package/dist/src/shared/tool-log.d.ts.map +1 -0
- package/dist/src/shared/tool-log.js +22 -0
- package/dist/src/shared/tool-log.js.map +1 -0
- package/dist/src/types/state.d.ts +862 -0
- package/dist/src/types/state.d.ts.map +1 -0
- package/dist/src/types/state.js +66 -0
- package/dist/src/types/state.js.map +1 -0
- package/docs/consuming/codex-lead-merge.md +106 -0
- package/docs/plugin-guide.md +396 -0
- package/docs/plugin-template/claude/.github/workflows/build.yml +60 -0
- package/docs/plugin-template/claude/README.md +110 -0
- package/docs/plugin-template/claude/package.json +16 -0
- package/docs/plugin-template/codex/.github/workflows/build.yml +51 -0
- package/docs/plugin-template/codex/README.md +147 -0
- package/docs/plugin-template/codex/package.json +17 -0
- package/docs/plugin-template/opencode/.github/workflows/build.yml +61 -0
- package/docs/plugin-template/opencode/README.md +121 -0
- package/docs/plugin-template/opencode/package.json +25 -0
- package/package.json +36 -28
- package/agents/architect/meta.yml +0 -13
- package/agents/designer/meta.yml +0 -13
- package/agents/engineer/meta.yml +0 -11
- package/agents/postdoc/meta.yml +0 -13
- package/agents/researcher/meta.yml +0 -12
- package/agents/reviewer/meta.yml +0 -12
- package/agents/strategist/meta.yml +0 -13
- package/agents/tester/meta.yml +0 -12
- package/agents/writer/meta.yml +0 -11
- package/conformance/README.md +0 -311
- package/conformance/examples/plan.extension.schema.example.json +0 -25
- package/conformance/lifecycle/README.md +0 -48
- package/conformance/lifecycle/agent-complete.json +0 -44
- package/conformance/lifecycle/agent-resume.json +0 -43
- package/conformance/lifecycle/agent-spawn.json +0 -36
- package/conformance/lifecycle/memory-access-record.json +0 -27
- package/conformance/lifecycle/session-end.json +0 -48
- package/conformance/scenarios/full-plan-cycle.json +0 -147
- package/conformance/scenarios/task-deps-ordering.json +0 -95
- package/conformance/schema/fixture.schema.json +0 -354
- package/conformance/state-schemas/agent-tracker.schema.json +0 -63
- package/conformance/state-schemas/history.schema.json +0 -134
- package/conformance/state-schemas/memory-access.schema.json +0 -36
- package/conformance/state-schemas/plan.schema.json +0 -77
- package/conformance/state-schemas/tasks.schema.json +0 -98
- package/conformance/tools/artifact-write.json +0 -97
- package/conformance/tools/context.json +0 -172
- package/conformance/tools/history-search.json +0 -219
- package/conformance/tools/plan-decide.json +0 -139
- package/conformance/tools/plan-start.json +0 -81
- package/conformance/tools/plan-status.json +0 -127
- package/conformance/tools/plan-update.json +0 -341
- package/conformance/tools/task-add.json +0 -156
- package/conformance/tools/task-close.json +0 -161
- package/conformance/tools/task-list.json +0 -177
- package/conformance/tools/task-update.json +0 -167
- package/docs/behavioral-contracts.md +0 -145
- package/docs/consumer-implementation-guide.md +0 -840
- package/docs/memory-lifecycle-contract.md +0 -119
- package/docs/nexus-layout.md +0 -224
- package/docs/nexus-outputs-contract.md +0 -344
- package/docs/nexus-state-overview.md +0 -170
- package/docs/nexus-tools-contract.md +0 -438
- package/manifest.json +0 -448
- package/schema/README.md +0 -69
- package/schema/agent.schema.json +0 -23
- package/schema/common.schema.json +0 -17
- package/schema/manifest.schema.json +0 -78
- package/schema/memory-policy.schema.json +0 -98
- package/schema/skill.schema.json +0 -54
- package/schema/task-exceptions.schema.json +0 -40
- package/schema/vocabulary.schema.json +0 -167
- package/scripts/.gitkeep +0 -0
- package/scripts/conformance-coverage.ts +0 -466
- package/scripts/import-from-claude-nexus.ts +0 -403
- package/scripts/lib/frontmatter.ts +0 -71
- package/scripts/lib/lint.ts +0 -348
- package/scripts/lib/structure.ts +0 -159
- package/scripts/lib/validate.ts +0 -796
- package/scripts/validate.ts +0 -90
- package/skills/nx-init/meta.yml +0 -8
- package/skills/nx-plan/meta.yml +0 -10
- package/skills/nx-run/meta.yml +0 -8
- package/skills/nx-sync/meta.yml +0 -7
- package/vocabulary/capabilities.yml +0 -65
- package/vocabulary/categories.yml +0 -11
- package/vocabulary/invocations.yml +0 -147
- package/vocabulary/memory_policy.yml +0 -88
- package/vocabulary/resume-tiers.yml +0 -11
- package/vocabulary/tags.yml +0 -60
- package/vocabulary/task-exceptions.yml +0 -29
package/scripts/lib/lint.ts
DELETED
|
@@ -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
|
-
}
|
package/scripts/lib/structure.ts
DELETED
|
@@ -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
|
-
}
|