arkgate 4.2.1 → 4.4.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 (63) hide show
  1. package/CHANGELOG.md +85 -3
  2. package/README.md +24 -8
  3. package/bin/ark-check-runtime.mjs +16 -1
  4. package/bin/ark-mcp-runtime.mjs +64 -0
  5. package/bin/ark.mjs +55 -1
  6. package/bin/lib/adapter-contract.mjs +88 -5
  7. package/bin/lib/agent-projection-command.mjs +396 -0
  8. package/bin/lib/agent-projection.mjs +319 -0
  9. package/bin/lib/agent-skills-package.mjs +266 -0
  10. package/bin/lib/baseline-key.mjs +32 -0
  11. package/bin/lib/ci-and-commands.mjs +55 -5
  12. package/bin/lib/diagnostic-catalog.mjs +155 -0
  13. package/bin/lib/doctor-plan.mjs +25 -0
  14. package/bin/lib/html-report-advisories.mjs +33 -0
  15. package/bin/lib/html-report-depth.mjs +24 -0
  16. package/bin/lib/improvement-compass-doctor.mjs +106 -0
  17. package/bin/lib/improvement-compass.mjs +630 -0
  18. package/bin/lib/status-command.mjs +369 -0
  19. package/bin/lib/status-manifest.mjs +431 -0
  20. package/dist/eslint/index.cjs +3 -3
  21. package/dist/eslint/index.js +3 -3
  22. package/dist/index.cjs +46 -11
  23. package/dist/index.d.ts +886 -6
  24. package/dist/index.js +46 -11
  25. package/docs/README.md +9 -8
  26. package/docs/agent-guide.md +128 -14
  27. package/docs/configuration.md +7 -0
  28. package/docs/develop.md +12 -1
  29. package/docs/diagnostics.md +606 -0
  30. package/docs/package-surface.md +44 -31
  31. package/docs/product-voice.md +71 -0
  32. package/docs/use.md +60 -1
  33. package/package.json +7 -1
  34. package/schemas/ark.analysis-result.schema.json +14 -1
  35. package/schemas/ark.status-manifest.schema.json +270 -0
  36. package/server.json +2 -2
  37. package/templates/agent-skills/README.md +59 -0
  38. package/templates/agent-skills/ark-adopt/SKILL.md +191 -0
  39. package/templates/agent-skills/ark-architect/SKILL.md +195 -0
  40. package/templates/agent-skills/ark-autopilot/SKILL.md +262 -0
  41. package/templates/agent-skills/ark-contract/SKILL.md +156 -0
  42. package/templates/agent-skills/ark-coverage/SKILL.md +187 -0
  43. package/templates/agent-skills/ark-explain/SKILL.md +230 -0
  44. package/templates/agent-skills/ark-explore/SKILL.md +397 -0
  45. package/templates/agent-skills/ark-fix/SKILL.md +205 -0
  46. package/templates/agent-skills/ark-loop/SKILL.md +200 -0
  47. package/templates/agent-skills/ark-place/SKILL.md +182 -0
  48. package/templates/agent-skills/ark-runtime/SKILL.md +127 -0
  49. package/templates/agent-skills/ark-think/SKILL.md +153 -0
  50. package/templates/agent-skills/ark-upgrade/SKILL.md +238 -0
  51. package/templates/skills/ark-adopt.md +20 -0
  52. package/templates/skills/ark-architect.md +21 -1
  53. package/templates/skills/ark-autopilot.md +25 -5
  54. package/templates/skills/ark-contract.md +20 -0
  55. package/templates/skills/ark-coverage.md +20 -0
  56. package/templates/skills/ark-explain.md +20 -0
  57. package/templates/skills/ark-explore.md +23 -3
  58. package/templates/skills/ark-fix.md +22 -2
  59. package/templates/skills/ark-loop.md +22 -2
  60. package/templates/skills/ark-place.md +20 -0
  61. package/templates/skills/ark-runtime.md +7 -0
  62. package/templates/skills/ark-think.md +20 -0
  63. package/templates/skills/ark-upgrade.md +20 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/agentProjection.ts
5
+ * Regenerate: node scripts/generate-cli-pure.mjs
6
+ * Drift check: node scripts/generate-cli-pure.mjs --check
7
+ *
8
+ * Pure CLI helper (bin/lib/agent-projection.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const ARK_AGENT_PROJECTION_SCHEMA_VERSION = '1.0';
12
+ /** Begin marker for the managed projection region inside AGENTS.md (or equivalent). */
13
+ export const AGENT_PROJECTION_BEGIN_MARKER = '<!-- arkgate:agent-projection:begin';
14
+ /** End marker for the managed projection region. */
15
+ export const AGENT_PROJECTION_END_MARKER = '<!-- arkgate:agent-projection:end -->';
16
+ /**
17
+ * Non-enforcement label — must appear in every generated projection body.
18
+ * Agents and humans must not treat the projection as a pass/fail authority.
19
+ */
20
+ export const AGENT_PROJECTION_NON_ENFORCEMENT_LABEL = 'This projection is **non-authoritative**. Enforcement is `ark-check` / host write hooks / required CI (`--strict-merge`), not AGENTS.md, skills, or this block.';
21
+ /** Surfaces that actually enforce (closed vocabulary for meta + docs). */
22
+ export const AGENT_PROJECTION_ENFORCEMENT_SURFACES = Object.freeze([
23
+ 'ark-check',
24
+ 'host-write-hooks',
25
+ 'ci-strict-merge',
26
+ ]);
27
+ /**
28
+ * High-signal public ruleIds for the compact catalog short list in the projection.
29
+ * Full catalog remains `docs/diagnostics.md` / `DIAGNOSTIC_CATALOG` (ACS02).
30
+ * Titles are supplied by Tooling from the catalog when available.
31
+ */
32
+ export const DEFAULT_AGENT_PROJECTION_RULE_IDS = Object.freeze([
33
+ 'LAYER_IMPORT_VIOLATION',
34
+ 'LAYER_INTENT_REFERENCE_VIOLATION',
35
+ 'CIRCULAR_DEPENDENCY',
36
+ 'CAPABILITY_VIOLATION',
37
+ 'RAW_EVENT_PUBLISH',
38
+ 'ARKRULE_STRUCTURE',
39
+ 'ATOMIC_PREFLIGHT_UNAVAILABLE',
40
+ 'ANALYSIS_PARSE_INCOMPLETE',
41
+ 'ARK_UNKNOWN',
42
+ ]);
43
+ const BEGIN_LINE_RE = /<!--\s*arkgate:agent-projection:begin\b([^>]*)-->/i;
44
+ const END_LINE_RE = /<!--\s*arkgate:agent-projection:end\s*-->/i;
45
+ // Attribute values: alnum + common version/schema tokens only (avoid open char-class ranges).
46
+ const VERSION_ATTR_RE = /\barkgateVersion=([A-Za-z0-9._+-]+)/i;
47
+ const SCHEMA_ATTR_RE = /\bschema=([A-Za-z0-9._+-]+)/i;
48
+ /** FNV-1a identity — portable, no Node crypto (same family as stableHash). */
49
+ export function agentProjectionContentIdentity(body) {
50
+ const normalized = String(body ?? '').replace(/\r\n/g, '\n');
51
+ let hash = 0x811c9dc5;
52
+ for (let index = 0; index < normalized.length; index += 1) {
53
+ hash ^= normalized.charCodeAt(index);
54
+ hash = Math.imul(hash, 0x01000193);
55
+ }
56
+ return `fnv1a-${(hash >>> 0).toString(16).padStart(8, '0')}`;
57
+ }
58
+ function normalizeNewlines(text) {
59
+ return String(text ?? '').replace(/\r\n/g, '\n');
60
+ }
61
+ function ensureTrailingNewline(text) {
62
+ const normalized = normalizeNewlines(text);
63
+ return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
64
+ }
65
+ function safeVersion(version) {
66
+ if (typeof version !== 'string' || version.trim().length === 0)
67
+ return 'unknown';
68
+ // Avoid breaking HTML comments / markdown with control chars.
69
+ return version.trim().replace(/[>\s]/g, '');
70
+ }
71
+ function resolveProfile(profile) {
72
+ return profile === 'compact' ? 'compact' : 'full';
73
+ }
74
+ /**
75
+ * Build the managed begin marker line (includes version + nonAuthoritative stamp).
76
+ */
77
+ export function buildAgentProjectionBeginMarker(facts) {
78
+ const version = safeVersion(facts.arkgateVersion);
79
+ const schema = typeof facts.schemaVersion === 'string' && facts.schemaVersion.trim()
80
+ ? facts.schemaVersion.trim()
81
+ : ARK_AGENT_PROJECTION_SCHEMA_VERSION;
82
+ return `<!-- arkgate:agent-projection:begin schema=${schema} arkgateVersion=${version} nonAuthoritative=true -->`;
83
+ }
84
+ /**
85
+ * Layer placement rows for the projection (compact markdown table).
86
+ */
87
+ export function formatAgentProjectionLayers(layers) {
88
+ if (!Array.isArray(layers) || layers.length === 0) {
89
+ return '_No project layers loaded — read `ark.config.json` or run `ark start` / `ark_manifest`._';
90
+ }
91
+ const rows = layers
92
+ .map((layer) => {
93
+ const name = layer.name?.trim() || 'Unknown';
94
+ const patternList = layer.patterns ?? [];
95
+ const prefixList = layer.intentPrefixes ?? [];
96
+ const patterns = patternList.map((pattern) => `\`${pattern}\``).join(', ') || '—';
97
+ const prefixes = prefixList.map((prefix) => `\`${prefix}\``).join(', ') || '—';
98
+ return `| ${name} | ${patterns} | ${prefixes} |`;
99
+ })
100
+ .join('\n');
101
+ return `| Layer | Patterns | Intent prefixes |
102
+ |-------|----------|-----------------|
103
+ ${rows}`;
104
+ }
105
+ /**
106
+ * Catalog short-list bullets (ruleId + title). Empty list → pointer only.
107
+ */
108
+ export function formatAgentProjectionCatalogShortList(entries, docsPath) {
109
+ const path = docsPath.trim() || 'docs/diagnostics.md';
110
+ if (!Array.isArray(entries) || entries.length === 0) {
111
+ return `Full public codes: \`${path}\` (and package \`DIAGNOSTIC_CATALOG\`).`;
112
+ }
113
+ const lines = entries
114
+ .filter((entry) => entry && typeof entry.ruleId === 'string' && entry.ruleId.length > 0)
115
+ .map((entry) => {
116
+ const title = typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : entry.ruleId;
117
+ return `- \`${entry.ruleId}\` — ${title}`;
118
+ });
119
+ return `${lines.join('\n')}
120
+
121
+ Full catalog: \`${path}\` (\`#RULE_ID\` anchors).`;
122
+ }
123
+ /**
124
+ * Projection **body** only (no begin/end markers). Used for content-identity.
125
+ */
126
+ export function buildAgentProjectionBody(facts) {
127
+ const version = safeVersion(facts.arkgateVersion);
128
+ const profile = resolveProfile(facts.profile);
129
+ const checkCommand = typeof facts.checkCommand === 'string' && facts.checkCommand.trim()
130
+ ? facts.checkCommand.trim()
131
+ : 'ark-check --strict-config';
132
+ const docsPath = typeof facts.diagnosticsDocsPath === 'string' && facts.diagnosticsDocsPath.trim()
133
+ ? facts.diagnosticsDocsPath.trim()
134
+ : 'docs/diagnostics.md';
135
+ const hostRaw = typeof facts.host === 'string' ? facts.host.trim().toLowerCase() : '';
136
+ const host = hostRaw && hostRaw !== 'unknown' ? hostRaw : null;
137
+ const layers = Array.isArray(facts.layers) ? facts.layers : [];
138
+ const catalog = Array.isArray(facts.catalogShortList) ? facts.catalogShortList : [];
139
+ const lines = [
140
+ '## ArkGate agent contract projection',
141
+ '',
142
+ AGENT_PROJECTION_NON_ENFORCEMENT_LABEL,
143
+ '',
144
+ `- **arkgateVersion:** \`${version}\` (must match the installed package; regenerate with \`ark agents-md --write\` after upgrade)`,
145
+ `- **projectionSchema:** \`${ARK_AGENT_PROJECTION_SCHEMA_VERSION}\``,
146
+ `- **profile:** \`${profile}\`${host ? ` · **host:** \`${host}\`` : ''}`,
147
+ `- **after edits:** \`${checkCommand}\``,
148
+ '',
149
+ ];
150
+ if (profile === 'compact') {
151
+ lines.push('### Primary path', '', '1. Run doctor (`ark-check --doctor`) — status light + primary next action.', '2. Read the improvement compass (not a score). Name residual lenses in plain language; never “done” on green edges alone while residual remains.', '3. Call `ark_identity` with `project.expectedRoot` at the exact project root; reuse root + `projectId` on Ark MCP calls.', '4. Read the contract with `ark_manifest` (same expectation). `ark://manifest` is compatibility-only / unverified.', '5. Place files inside configured layers; validate; run the check command above on violations — fix architecture, do not weaken the gate.', '6. Single door: edges debt → fix; design-weak / residual shape lenses → map then guided apply with user OK.', '', '### Contract layers (summary)', '', formatAgentProjectionLayers(layers), '');
152
+ }
153
+ else {
154
+ lines.push('### Contract layers', '', formatAgentProjectionLayers(layers), '', 'When creating a **new** kind of code that no layer covers, update `ark.config.json` first (`/ark-contract`), then place the file.', '', '### Diagnostic codes (short list)', '', formatAgentProjectionCatalogShortList(catalog, docsPath), '', '### Session truth', '', '- Machine snapshot: `ark status --json` (or MCP `ark_status`) — identity, activation honesty, last check, residual counts. **Not a score.**', '- Authoritative contract: `ark_manifest` after a matched `ark_identity` handshake.', '');
155
+ }
156
+ lines.push('### Enforcement surfaces (authoritative)', '', AGENT_PROJECTION_ENFORCEMENT_SURFACES.map((surface) => `- \`${surface}\``).join('\n'), '');
157
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
158
+ }
159
+ /**
160
+ * Full managed block: begin marker + body + end marker.
161
+ */
162
+ export function buildAgentProjectionBlock(facts) {
163
+ const body = buildAgentProjectionBody(facts);
164
+ const begin = buildAgentProjectionBeginMarker({
165
+ arkgateVersion: facts.arkgateVersion,
166
+ schemaVersion: ARK_AGENT_PROJECTION_SCHEMA_VERSION,
167
+ });
168
+ return `${begin}\n${body}${AGENT_PROJECTION_END_MARKER}\n`;
169
+ }
170
+ /**
171
+ * Machine meta for CLI `--json` / tests (never a gate input).
172
+ */
173
+ export function buildAgentProjectionMeta(facts) {
174
+ const body = buildAgentProjectionBody(facts);
175
+ const layers = Array.isArray(facts.layers) ? facts.layers : [];
176
+ const catalog = Array.isArray(facts.catalogShortList) ? facts.catalogShortList : [];
177
+ return {
178
+ schemaVersion: ARK_AGENT_PROJECTION_SCHEMA_VERSION,
179
+ arkgateVersion: safeVersion(facts.arkgateVersion),
180
+ nonAuthoritative: true,
181
+ enforcementSurfaces: [...AGENT_PROJECTION_ENFORCEMENT_SURFACES],
182
+ contentIdentity: agentProjectionContentIdentity(body),
183
+ layerCount: layers.length,
184
+ catalogCodeCount: catalog.filter((entry) => entry?.ruleId).length,
185
+ profile: resolveProfile(facts.profile),
186
+ };
187
+ }
188
+ /**
189
+ * Extract the managed projection block from a document (AGENTS.md or equivalent).
190
+ */
191
+ export function extractAgentProjectionBlock(document) {
192
+ const text = normalizeNewlines(document ?? '');
193
+ const beginMatch = BEGIN_LINE_RE.exec(text);
194
+ if (!beginMatch) {
195
+ return { block: null, body: null, before: text, after: '', beginAttrs: null };
196
+ }
197
+ const beginIndex = beginMatch.index;
198
+ const beginEnd = beginIndex + beginMatch[0].length;
199
+ const rest = text.slice(beginEnd);
200
+ const endMatch = END_LINE_RE.exec(rest);
201
+ if (!endMatch) {
202
+ // Unclosed block: treat as absent so merge can insert a well-formed block.
203
+ return { block: null, body: null, before: text, after: '', beginAttrs: null };
204
+ }
205
+ const endIndexInRest = endMatch.index;
206
+ const endEndInRest = endIndexInRest + endMatch[0].length;
207
+ // Strip a single leading newline after the begin marker; keep body content as-is.
208
+ let body = rest.slice(0, endIndexInRest);
209
+ if (body.startsWith('\n'))
210
+ body = body.slice(1);
211
+ const block = text.slice(beginIndex, beginEnd + endEndInRest);
212
+ const after = rest.slice(endEndInRest);
213
+ return {
214
+ block,
215
+ body,
216
+ before: text.slice(0, beginIndex),
217
+ after,
218
+ beginAttrs: beginMatch[1] ?? '',
219
+ };
220
+ }
221
+ /**
222
+ * Parse stamps from a projection begin marker or full block/document.
223
+ */
224
+ export function parseAgentProjectionStamp(source) {
225
+ const text = String(source ?? '');
226
+ const begin = BEGIN_LINE_RE.exec(text);
227
+ const attrs = begin?.[1] ?? text;
228
+ const versionMatch = VERSION_ATTR_RE.exec(attrs);
229
+ const schemaMatch = SCHEMA_ATTR_RE.exec(attrs);
230
+ const nonAuthoritative = /\bnonAuthoritative\s*=\s*true\b/i.test(attrs);
231
+ return {
232
+ arkgateVersion: versionMatch?.[1] ?? null,
233
+ schemaVersion: schemaMatch?.[1] ?? null,
234
+ nonAuthoritative,
235
+ };
236
+ }
237
+ /**
238
+ * True when the document/block stamps the given package version.
239
+ */
240
+ export function projectionMatchesPackageVersion(source, packageVersion) {
241
+ const stamped = parseAgentProjectionStamp(source).arkgateVersion;
242
+ if (!stamped)
243
+ return false;
244
+ return stamped === safeVersion(packageVersion);
245
+ }
246
+ /**
247
+ * True when body text carries the non-enforcement label (substring match).
248
+ */
249
+ export function projectionHasNonEnforcementLabel(bodyOrBlock) {
250
+ return String(bodyOrBlock ?? '').includes('non-authoritative');
251
+ }
252
+ /**
253
+ * Merge a desired projection block into an existing document without rewriting
254
+ * customized content **outside** the managed markers.
255
+ *
256
+ * - Missing document → create `# Ark Enforcement` + block
257
+ * - Existing markers → replace block when content-identity differs; else unchanged
258
+ * - No markers → insert block after the first markdown H1 (or at top)
259
+ */
260
+ export function mergeAgentProjectionDocument(existing, desiredBlock) {
261
+ const desired = ensureTrailingNewline(normalizeNewlines(desiredBlock));
262
+ const desiredExtract = extractAgentProjectionBlock(desired);
263
+ const desiredBody = desiredExtract.body ??
264
+ desired.replace(BEGIN_LINE_RE, '').replace(END_LINE_RE, '').trim() + '\n';
265
+ const contentIdentity = agentProjectionContentIdentity(desiredBody);
266
+ if (existing == null || !String(existing).trim()) {
267
+ return {
268
+ content: ensureTrailingNewline(`# Ark Enforcement\n\n${desired}`),
269
+ action: 'created',
270
+ previousBlock: null,
271
+ contentIdentity,
272
+ preservedOutsideBlock: false,
273
+ };
274
+ }
275
+ const current = normalizeNewlines(existing);
276
+ const extracted = extractAgentProjectionBlock(current);
277
+ if (extracted.block != null) {
278
+ const currentBody = extracted.body ?? '';
279
+ if (agentProjectionContentIdentity(currentBody) === contentIdentity) {
280
+ return {
281
+ content: ensureTrailingNewline(current),
282
+ action: 'unchanged',
283
+ previousBlock: extracted.block,
284
+ contentIdentity,
285
+ preservedOutsideBlock: true,
286
+ };
287
+ }
288
+ const before = extracted.before.replace(/\s*$/, '\n\n');
289
+ const after = extracted.after.replace(/^\s*/, '\n');
290
+ return {
291
+ content: ensureTrailingNewline(`${before}${desired.trimEnd()}\n${after}`),
292
+ action: 'block-replaced',
293
+ previousBlock: extracted.block,
294
+ contentIdentity,
295
+ preservedOutsideBlock: true,
296
+ };
297
+ }
298
+ // Insert after first H1 line when present.
299
+ const h1 = /^(#\s+[^\n]*\n)/m.exec(current);
300
+ if (h1 && h1.index != null) {
301
+ const insertAt = h1.index + h1[1].length;
302
+ const before = current.slice(0, insertAt).replace(/\s*$/, '\n\n');
303
+ const after = current.slice(insertAt).replace(/^\s*/, '\n');
304
+ return {
305
+ content: ensureTrailingNewline(`${before}${desired.trimEnd()}\n${after}`),
306
+ action: 'block-inserted',
307
+ previousBlock: null,
308
+ contentIdentity,
309
+ preservedOutsideBlock: true,
310
+ };
311
+ }
312
+ return {
313
+ content: ensureTrailingNewline(`${desired.trimEnd()}\n\n${current.trimStart()}`),
314
+ action: 'block-inserted',
315
+ previousBlock: null,
316
+ contentIdentity,
317
+ preservedOutsideBlock: true,
318
+ };
319
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/agentSkillsPackage.ts
5
+ * Regenerate: node scripts/generate-cli-pure.mjs
6
+ * Drift check: node scripts/generate-cli-pure.mjs --check
7
+ *
8
+ * Pure CLI helper (bin/lib/agent-skills-package.mjs). Zero Node I/O.
9
+ */
10
+
11
+ export const ARK_AGENT_SKILLS_PACKAGE_SCHEMA_VERSION = '1.0';
12
+ /**
13
+ * Relative package-root path of the Agent Skills–compatible skill package.
14
+ * Install via skills ecosystem: `npx skills add <path-to-this-dir>`.
15
+ */
16
+ export const AGENT_SKILLS_PACKAGE_RELATIVE_ROOT = 'templates/agent-skills';
17
+ /** Relative package-root path of flat skill templates (Ark install source). */
18
+ export const FLAT_SKILL_TEMPLATES_RELATIVE_ROOT = 'templates/skills';
19
+ /** Required entry filename inside each skill directory (Agent Skills standard). */
20
+ export const AGENT_SKILL_ENTRY_FILENAME = 'SKILL.md';
21
+ /**
22
+ * Closed skill-name freeze (ACS / ADR skill freeze). Exactly these 13 names ship.
23
+ * Sorted alphabetically for deterministic inventory diffs.
24
+ */
25
+ export const ARK_SKILL_NAMES = Object.freeze([
26
+ 'ark-adopt',
27
+ 'ark-architect',
28
+ 'ark-autopilot',
29
+ 'ark-contract',
30
+ 'ark-coverage',
31
+ 'ark-explain',
32
+ 'ark-explore',
33
+ 'ark-fix',
34
+ 'ark-loop',
35
+ 'ark-place',
36
+ 'ark-runtime',
37
+ 'ark-think',
38
+ 'ark-upgrade',
39
+ ]);
40
+ /** Count of frozen skill names (must stay 13 until a ROADMAP item lifts the freeze). */
41
+ export const ARK_SKILL_NAME_COUNT = ARK_SKILL_NAMES.length;
42
+ const AGENT_SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
43
+ /**
44
+ * Agent Skills `name` field rules (agentskills.io):
45
+ * - 1–64 characters
46
+ * - lowercase a–z, digits, hyphens only
47
+ * - no leading/trailing hyphen; no consecutive hyphens
48
+ */
49
+ export function isValidAgentSkillName(name) {
50
+ if (typeof name !== 'string' || name.length < 1 || name.length > 64)
51
+ return false;
52
+ return AGENT_SKILL_NAME_PATTERN.test(name);
53
+ }
54
+ /** True when `name` is one of the frozen 13 Ark skill names. */
55
+ export function isArkSkillName(name) {
56
+ return ARK_SKILL_NAMES.includes(name);
57
+ }
58
+ /**
59
+ * Parse a skill markdown document with optional YAML frontmatter.
60
+ * Supports the simple `key: value` / `key: "quoted"` form used by Ark templates
61
+ * (no nested maps, no multi-line YAML).
62
+ */
63
+ export function parseSkillDocument(content) {
64
+ const text = String(content ?? '').replace(/^\uFEFF/, '');
65
+ const newline = text.includes('\r\n') ? '\r\n' : '\n';
66
+ const lines = text.split(/\r?\n/);
67
+ if (lines[0] !== '---') {
68
+ return { hasFrontmatter: false, frontmatter: null, body: text };
69
+ }
70
+ const closeIdx = lines.indexOf('---', 1);
71
+ if (closeIdx === -1) {
72
+ return { hasFrontmatter: false, frontmatter: null, body: text };
73
+ }
74
+ const fields = {};
75
+ for (let i = 1; i < closeIdx; i += 1) {
76
+ const line = lines[i] ?? '';
77
+ if (line.trim() === '' || line.trimStart().startsWith('#'))
78
+ continue;
79
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
80
+ if (!match)
81
+ continue;
82
+ const key = match[1];
83
+ let value = match[2] ?? '';
84
+ if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
85
+ (value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
86
+ value = value.slice(1, -1);
87
+ }
88
+ fields[key] = value;
89
+ }
90
+ const body = lines.slice(closeIdx + 1).join(newline);
91
+ const name = typeof fields.name === 'string' && fields.name.length > 0 ? fields.name : null;
92
+ const description = typeof fields.description === 'string' && fields.description.length > 0
93
+ ? fields.description
94
+ : null;
95
+ const license = typeof fields.license === 'string' && fields.license.length > 0 ? fields.license : null;
96
+ return {
97
+ hasFrontmatter: true,
98
+ frontmatter: { fields, name, description, license },
99
+ body,
100
+ };
101
+ }
102
+ /**
103
+ * Validate one skill document against the Agent Skills spec (+ optional Ark freeze).
104
+ */
105
+ export function validateAgentSkillDocument(input) {
106
+ const requireArk = input.requireArkSkillName !== false;
107
+ const requireBody = input.requireBody !== false;
108
+ const issues = [];
109
+ const directoryName = String(input.directoryName ?? '');
110
+ const parsed = parseSkillDocument(input.content);
111
+ if (!parsed.hasFrontmatter || !parsed.frontmatter) {
112
+ issues.push({
113
+ code: 'MISSING_FRONTMATTER',
114
+ message: `Skill "${directoryName}" is missing YAML frontmatter fences.`,
115
+ skillName: directoryName || undefined,
116
+ });
117
+ return issues;
118
+ }
119
+ const { name, description } = parsed.frontmatter;
120
+ const skillRef = name ?? (directoryName || undefined);
121
+ if (!name || !isValidAgentSkillName(name)) {
122
+ issues.push({
123
+ code: 'INVALID_NAME',
124
+ message: `Skill name "${name ?? ''}" is invalid (Agent Skills: 1–64 chars, [a-z0-9-], no leading/trailing/consecutive hyphens).`,
125
+ skillName: skillRef,
126
+ });
127
+ }
128
+ else if (name !== directoryName) {
129
+ issues.push({
130
+ code: 'NAME_DIRECTORY_MISMATCH',
131
+ message: `Frontmatter name "${name}" must match directory name "${directoryName}".`,
132
+ skillName: name,
133
+ });
134
+ }
135
+ else if (requireArk && !isArkSkillName(name)) {
136
+ issues.push({
137
+ code: 'UNKNOWN_SKILL_NAME',
138
+ message: `Skill name "${name}" is not in the frozen Ark 13-skill catalog (no new skill names).`,
139
+ skillName: name,
140
+ });
141
+ }
142
+ if (!description) {
143
+ issues.push({
144
+ code: 'MISSING_DESCRIPTION',
145
+ message: `Skill "${directoryName}" is missing a non-empty description.`,
146
+ skillName: skillRef,
147
+ });
148
+ }
149
+ else if (description.length > 1024) {
150
+ issues.push({
151
+ code: 'DESCRIPTION_TOO_LONG',
152
+ message: `Skill "${directoryName}" description exceeds 1024 characters (${description.length}).`,
153
+ skillName: skillRef,
154
+ });
155
+ }
156
+ if (requireBody && parsed.body.trim().length === 0) {
157
+ issues.push({
158
+ code: 'EMPTY_BODY',
159
+ message: `Skill "${directoryName}" has an empty instruction body.`,
160
+ skillName: skillRef,
161
+ });
162
+ }
163
+ return issues;
164
+ }
165
+ /**
166
+ * Validate a full Agent Skills package inventory against the frozen 13-name catalog.
167
+ * Detects missing, extra, duplicate, invalid, and (when supplied) flat-template drift.
168
+ */
169
+ export function validateAgentSkillsPackage(entries) {
170
+ const issues = [];
171
+ const seen = new Set();
172
+ const names = [];
173
+ for (const entry of entries) {
174
+ const name = String(entry.name ?? '');
175
+ if (seen.has(name)) {
176
+ issues.push({
177
+ code: 'DUPLICATE_SKILL',
178
+ message: `Duplicate skill entry "${name}".`,
179
+ skillName: name,
180
+ });
181
+ continue;
182
+ }
183
+ seen.add(name);
184
+ names.push(name);
185
+ issues.push(...validateAgentSkillDocument({
186
+ directoryName: name,
187
+ content: entry.content,
188
+ requireArkSkillName: true,
189
+ requireBody: true,
190
+ }));
191
+ if (entry.flatTemplateContent != null &&
192
+ normalizeSkillContent(entry.content) !== normalizeSkillContent(entry.flatTemplateContent)) {
193
+ issues.push({
194
+ code: 'CONTENT_MISMATCH',
195
+ message: `Agent Skills SKILL.md for "${name}" does not match flat template templates/skills/${name}.md.`,
196
+ skillName: name,
197
+ });
198
+ }
199
+ }
200
+ for (const expected of ARK_SKILL_NAMES) {
201
+ if (!seen.has(expected)) {
202
+ issues.push({
203
+ code: 'MISSING_SKILL',
204
+ message: `Missing frozen skill "${expected}" from Agent Skills package.`,
205
+ skillName: expected,
206
+ });
207
+ }
208
+ }
209
+ for (const name of names) {
210
+ if (!isArkSkillName(name)) {
211
+ // UNKNOWN_SKILL_NAME may already be reported per-document; still mark package-level extra.
212
+ if (!issues.some((i) => i.code === 'UNKNOWN_SKILL_NAME' && i.skillName === name)) {
213
+ issues.push({
214
+ code: 'EXTRA_SKILL',
215
+ message: `Extra skill "${name}" is not in the frozen Ark 13-skill catalog.`,
216
+ skillName: name,
217
+ });
218
+ }
219
+ else {
220
+ issues.push({
221
+ code: 'EXTRA_SKILL',
222
+ message: `Extra skill "${name}" is not in the frozen Ark 13-skill catalog.`,
223
+ skillName: name,
224
+ });
225
+ }
226
+ }
227
+ }
228
+ names.sort();
229
+ return {
230
+ ok: issues.length === 0,
231
+ issues,
232
+ names,
233
+ expectedCount: ARK_SKILL_NAME_COUNT,
234
+ presentCount: names.length,
235
+ };
236
+ }
237
+ /**
238
+ * Normalize skill file content for identity compare (LF newlines, strip BOM).
239
+ * Does not strip or rewrite frontmatter — Agent Skills export is 1:1 with flat templates.
240
+ */
241
+ export function normalizeSkillContent(content) {
242
+ return String(content ?? '')
243
+ .replace(/^\uFEFF/, '')
244
+ .replace(/\r\n/g, '\n');
245
+ }
246
+ /**
247
+ * Relative path of one skill entry inside the Agent Skills package root.
248
+ * Example: `ark-place/SKILL.md`
249
+ */
250
+ export function agentSkillEntryRelativePath(skillName) {
251
+ return `${skillName}/${AGENT_SKILL_ENTRY_FILENAME}`;
252
+ }
253
+ /**
254
+ * Relative path from package root for one Agent Skills entry.
255
+ * Example: `templates/agent-skills/ark-place/SKILL.md`
256
+ */
257
+ export function agentSkillPackageFileRelativePath(skillName) {
258
+ return `${AGENT_SKILLS_PACKAGE_RELATIVE_ROOT}/${agentSkillEntryRelativePath(skillName)}`;
259
+ }
260
+ /**
261
+ * Relative path from package root for one flat template.
262
+ * Example: `templates/skills/ark-place.md`
263
+ */
264
+ export function flatSkillTemplateFileRelativePath(skillName) {
265
+ return `${FLAT_SKILL_TEMPLATES_RELATIVE_ROOT}/${skillName}.md`;
266
+ }
@@ -11,6 +11,8 @@
11
11
  /**
12
12
  * Stable key used by `--baseline` / `--update-baseline` to match frozen debt.
13
13
  * Field order and empty-string fallbacks are part of the CLI contract.
14
+ *
15
+ * Same string is the ACS06 finding `targetKey` (baseline-compatible).
14
16
  */
15
17
  export function baselineKey(violation) {
16
18
  return [
@@ -28,6 +30,9 @@ export function baselineKey(violation) {
28
30
  * compatible. Repeated violations with the same identity gain a `#N` suffix;
29
31
  * adding a second identical violation is therefore new debt instead of being
30
32
  * silently suppressed by the first occurrence's key.
33
+ *
34
+ * ACS06 multi-turn adapters must use these keys as `targetKey` so occurrence
35
+ * identity matches the freeze ratchet (never orphan baselines).
31
36
  */
32
37
  export function baselineOccurrenceKeys(violations) {
33
38
  const counts = new Map();
@@ -38,3 +43,30 @@ export function baselineOccurrenceKeys(violations) {
38
43
  return occurrence === 1 ? base : `${base}#${occurrence}`;
39
44
  });
40
45
  }
46
+ /**
47
+ * ACS06: baseline-compatible target key for one finding (alias of baselineKey).
48
+ * Prefer baselineOccurrenceKeys when emitting a list so duplicates stay distinct.
49
+ */
50
+ export function findingTargetKey(violation) {
51
+ return baselineKey(violation);
52
+ }
53
+ /**
54
+ * Compact stable finding ref derived from a baseline-compatible targetKey.
55
+ * FNV-1a identity only (same family as package fingerprints) — not a security hash.
56
+ * Format: `fnv1a-` + 8 zero-padded hex digits.
57
+ */
58
+ export function findingRefFromTargetKey(targetKey) {
59
+ let hash = 0x811c9dc5;
60
+ for (let index = 0; index < targetKey.length; index += 1) {
61
+ hash ^= targetKey.charCodeAt(index);
62
+ hash = Math.imul(hash, 0x01000193);
63
+ }
64
+ return `fnv1a-${(hash >>> 0).toString(16).padStart(8, '0')}`;
65
+ }
66
+ /**
67
+ * Finding ref for a single violation (first-occurrence / solo identity).
68
+ * For lists, hash baselineOccurrenceKeys(entries)[i] instead so duplicates differ.
69
+ */
70
+ export function findingRefForViolation(violation) {
71
+ return findingRefFromTargetKey(baselineKey(violation));
72
+ }