genesis-compiler 1.2.11 → 1.2.12

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 CHANGED
@@ -122,9 +122,12 @@ Genesis does not install substitute generic `nodejs`, `vue`, `php`, or similar
122
122
  skills. Official, user, or host skills retain their normal names. A Stack piece
123
123
  may instead name one authoritative technology skill; Genesis copies that
124
124
  complete directory into `.agents/skills/`, including its `references/`,
125
- `scripts/`, `assets/`, and agent metadata. The agent loads those resources only
126
- when the skill requires them. For example, the `genesis-stack` catalog's JSKIT
127
- piece installs the JSKIT package's own `jskit` skill. A piece's optional
125
+ `scripts/`, `assets/`, and agent metadata. Every relative Markdown link and image
126
+ must resolve within that same Skill directory after relocation; a missing
127
+ target or path that escapes the Skill root makes the Skill invalid. The agent
128
+ loads valid resources only when the Skill requires them. For example, the
129
+ `genesis-stack` catalog's JSKIT piece installs the JSKIT package's own `jskit`
130
+ skill. A piece's optional
128
131
  `## Guidance` is concise
129
132
  supplemental project-work guidance; it does not create or replace a generic
130
133
  technology skill.
@@ -126,7 +126,10 @@ selects one complete [Agent Skills](https://agentskills.io) directory, either
126
126
  from Genesis or from the declared npm package. Its `SKILL.md`, `references/`,
127
127
  `scripts/`, `assets/`, and `agents/` metadata are copied together to
128
128
  `.agents/skills/<skill-name>/`; the agent loads them progressively instead of
129
- Genesis expanding every manual into every prompt. `Resources` declares generic
129
+ Genesis expanding every manual into every prompt. Relative Markdown links and
130
+ images must resolve within that copied Skill root. Missing targets and paths
131
+ that escape the root are invalid, even if an escaped path happens to exist in
132
+ the source package or project. `Resources` declares generic
130
133
  alternative environment-name sets reported by prompt generation and checked
131
134
  before verification. `allowEmpty` may name a required variable whose empty
132
135
  string is valid. `Environment defaults` declares public non-secret constants
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with optional Codex hooks.",
6
6
  "repository": {
@@ -57,6 +57,7 @@
57
57
  "@ast-grep/lang-ruby": "^0.0.7",
58
58
  "@ast-grep/lang-rust": "^0.0.7",
59
59
  "@ast-grep/napi": "^0.45.1",
60
+ "mdast-util-from-markdown": "^2.0.3",
60
61
  "yaml": "^2.9.0"
61
62
  }
62
63
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -12,9 +12,10 @@ import {
12
12
  import path from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
 
15
+ import { fromMarkdown } from 'mdast-util-from-markdown';
15
16
  import { parse as parseYaml } from 'yaml';
16
17
 
17
- import { GenesisError } from './errors.js';
18
+ import { asDiagnostic, GenesisError } from './errors.js';
18
19
  import { sha256, stableJson, writeFileAtomic } from './utils.js';
19
20
 
20
21
  const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
@@ -76,6 +77,90 @@ function frontmatter(source, location) {
76
77
  return metadata;
77
78
  }
78
79
 
80
+ function markdownDestinations(tree) {
81
+ const destinations = [];
82
+ const nodes = [tree];
83
+ while (nodes.length > 0) {
84
+ const node = nodes.pop();
85
+ if (['definition', 'image', 'link'].includes(node?.type) && typeof node.url === 'string') {
86
+ destinations.push({ line: node.position?.start?.line, url: node.url });
87
+ }
88
+ if (Array.isArray(node?.children)) nodes.push(...node.children);
89
+ }
90
+ return destinations.sort((left, right) => (left.line || 0) - (right.line || 0));
91
+ }
92
+
93
+ function localReferencePath(url) {
94
+ const value = String(url || '').trim();
95
+ if (
96
+ !value
97
+ || value.startsWith('#')
98
+ || value.startsWith('?')
99
+ || value.startsWith('/')
100
+ || /^[a-z][a-z\d+.-]*:/iu.test(value)
101
+ ) return null;
102
+ const separator = value.search(/[?#]/u);
103
+ const encodedPath = separator === -1 ? value : value.slice(0, separator);
104
+ if (!encodedPath) return null;
105
+ try {
106
+ return { path: decodeURIComponent(encodedPath) };
107
+ } catch {
108
+ return { error: 'malformed', path: encodedPath };
109
+ }
110
+ }
111
+
112
+ function outsideDirectory(directory, target) {
113
+ const relative = path.relative(directory, target);
114
+ return relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
115
+ }
116
+
117
+ async function validateMarkdownReferences(directory, files) {
118
+ const root = path.resolve(directory);
119
+ const problems = [];
120
+ for (const file of files.filter(({ path: filePath }) => filePath.endsWith('.md'))) {
121
+ const location = path.join(root, file.path);
122
+ let tree;
123
+ try {
124
+ tree = fromMarkdown(await readFile(location, 'utf8'));
125
+ } catch (error) {
126
+ invalidSkill(`Agent Skill Markdown is invalid: ${location}.`, {
127
+ cause: error.message,
128
+ path: file.path,
129
+ });
130
+ }
131
+ for (const destination of markdownDestinations(tree)) {
132
+ const local = localReferencePath(destination.url);
133
+ if (!local) continue;
134
+ const problem = {
135
+ path: file.path,
136
+ target: destination.url,
137
+ ...(destination.line ? { line: destination.line } : {}),
138
+ };
139
+ if (local.error) {
140
+ problems.push({ ...problem, reason: local.error });
141
+ continue;
142
+ }
143
+ const target = path.resolve(path.dirname(location), local.path);
144
+ if (outsideDirectory(root, target)) {
145
+ problems.push({ ...problem, reason: 'outside-skill' });
146
+ continue;
147
+ }
148
+ try {
149
+ await lstat(target);
150
+ } catch (error) {
151
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
152
+ problems.push({ ...problem, reason: 'missing' });
153
+ }
154
+ }
155
+ }
156
+ if (problems.length > 0) {
157
+ invalidSkill(
158
+ `Agent Skill contains ${problems.length} unresolved or non-portable local Markdown reference${problems.length === 1 ? '' : 's'}: ${directory}.`,
159
+ { references: problems },
160
+ );
161
+ }
162
+ }
163
+
79
164
  async function skillTree(directory, relative = '') {
80
165
  const entries = await readdir(path.join(directory, relative), { withFileTypes: true });
81
166
  const files = [];
@@ -146,6 +231,7 @@ async function describeSkill(directory, source) {
146
231
  )
147
232
  ) invalidSkill(`Agent Skill metadata must map strings to strings: ${skillFile}.`);
148
233
  const files = await skillTree(directory);
234
+ await validateMarkdownReferences(directory, files);
149
235
  return {
150
236
  name,
151
237
  description,
@@ -395,7 +481,7 @@ export async function inspectProjectSkills({ projectRoot, stack } = {}) {
395
481
  let installed;
396
482
  try { installed = await describeSkill(target, `project:${expected.name}`); } catch (error) {
397
483
  invalid = true;
398
- diagnostics.push({ code: error.code || 'AGENT_SKILL_INVALID', message: error.message });
484
+ diagnostics.push(asDiagnostic(error));
399
485
  continue;
400
486
  }
401
487
  if (!installed) {