@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.
Files changed (34) hide show
  1. package/conformance/README.md +15 -18
  2. package/conformance/examples/plan.extension.schema.example.json +25 -0
  3. package/conformance/lifecycle/README.md +1 -3
  4. package/conformance/lifecycle/agent-complete.json +2 -1
  5. package/conformance/lifecycle/agent-resume.json +2 -1
  6. package/conformance/lifecycle/agent-spawn.json +5 -8
  7. package/conformance/scenarios/full-plan-cycle.json +3 -3
  8. package/conformance/schema/fixture.schema.json +6 -6
  9. package/conformance/state-schemas/agent-tracker.schema.json +10 -5
  10. package/conformance/state-schemas/history.schema.json +11 -1
  11. package/conformance/state-schemas/plan.schema.json +5 -0
  12. package/conformance/state-schemas/tasks.schema.json +5 -0
  13. package/conformance/tools/plan-decide.json +7 -7
  14. package/conformance/tools/plan-start.json +1 -1
  15. package/conformance/tools/task-add.json +1 -1
  16. package/conformance/tools/task-close.json +2 -0
  17. package/docs/consumer-implementation-guide.md +7 -11
  18. package/docs/nexus-layout.md +0 -15
  19. package/docs/nexus-outputs-contract.md +15 -25
  20. package/docs/nexus-state-overview.md +0 -19
  21. package/docs/nexus-tools-contract.md +12 -2
  22. package/manifest.json +26 -26
  23. package/package.json +5 -1
  24. package/scripts/.gitkeep +0 -0
  25. package/scripts/conformance-coverage.ts +466 -0
  26. package/scripts/import-from-claude-nexus.ts +403 -0
  27. package/scripts/lib/frontmatter.ts +71 -0
  28. package/scripts/lib/lint.ts +216 -0
  29. package/scripts/lib/structure.ts +159 -0
  30. package/scripts/lib/validate.ts +668 -0
  31. package/scripts/validate.ts +90 -0
  32. package/conformance/lifecycle/session-end.json +0 -31
  33. package/conformance/lifecycle/session-start.json +0 -36
  34. 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
+ }