@moreih29/nexus-core 0.4.0 → 0.6.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/conformance/README.md +15 -18
- package/conformance/examples/plan.extension.schema.example.json +25 -0
- package/conformance/lifecycle/README.md +1 -3
- package/conformance/lifecycle/agent-complete.json +2 -1
- package/conformance/lifecycle/agent-resume.json +2 -1
- package/conformance/lifecycle/agent-spawn.json +5 -8
- package/conformance/scenarios/full-plan-cycle.json +3 -3
- package/conformance/schema/fixture.schema.json +6 -6
- package/conformance/state-schemas/agent-tracker.schema.json +10 -5
- package/conformance/state-schemas/history.schema.json +11 -1
- package/conformance/state-schemas/plan.schema.json +5 -0
- package/conformance/state-schemas/tasks.schema.json +5 -0
- package/conformance/tools/plan-decide.json +7 -7
- package/conformance/tools/plan-start.json +1 -1
- package/conformance/tools/task-add.json +1 -1
- package/conformance/tools/task-close.json +2 -0
- package/docs/consumer-implementation-guide.md +7 -11
- package/docs/nexus-layout.md +0 -15
- package/docs/nexus-outputs-contract.md +15 -25
- package/docs/nexus-state-overview.md +0 -19
- package/docs/nexus-tools-contract.md +12 -2
- package/manifest.json +26 -26
- package/package.json +5 -1
- package/scripts/.gitkeep +0 -0
- package/scripts/conformance-coverage.ts +466 -0
- package/scripts/import-from-claude-nexus.ts +403 -0
- package/scripts/lib/frontmatter.ts +71 -0
- package/scripts/lib/lint.ts +216 -0
- package/scripts/lib/structure.ts +159 -0
- package/scripts/lib/validate.ts +668 -0
- package/scripts/validate.ts +90 -0
- package/conformance/lifecycle/session-end.json +0 -31
- package/conformance/lifecycle/session-start.json +0 -36
- package/conformance/state-schemas/runtime.schema.json +0 -25
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
}
|