intentdna 1.8.7 → 1.9.0-rc.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/candidate-promotion.d.ts +15 -0
- package/dist/cli/commands/candidate-promotion.js +197 -0
- package/dist/cli/commands/run-lifecycle.js +1 -0
- package/dist/cli/commands/run.js +76 -42
- package/dist/cli/commands/setup.d.ts +10 -0
- package/dist/cli/commands/setup.js +129 -36
- package/dist/cli/commands/source-assets.d.ts +32 -0
- package/dist/cli/commands/source-assets.js +154 -0
- package/dist/cli/commands/sync.js +52 -12
- package/dist/cli/commands/templates.js +59 -37
- package/dist/cli/index.js +161 -5
- package/dist/compiler/cascade.js +1 -1
- package/dist/hooks/cli.d.ts +6 -0
- package/dist/hooks/cli.js +118 -10
- package/dist/hooks/enforcement-boundary.d.ts +1 -1
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/state.d.ts +5 -0
- package/dist/hooks/state.js +21 -6
- package/dist/mcp/tools-state.js +251 -15
- package/dist/runtime/claude-sync-target.d.ts +2 -0
- package/dist/runtime/claude-sync-target.js +1 -1
- package/dist/runtime/evaluator.d.ts +13 -0
- package/dist/runtime/evaluator.js +46 -0
- package/dist/runtime/index.d.ts +8 -4
- package/dist/runtime/index.js +4 -2
- package/dist/runtime/plugin-adapter.d.ts +14 -1
- package/dist/runtime/plugin-adapter.js +23 -9
- package/dist/runtime/run-contracts.d.ts +5 -3
- package/dist/runtime/run-contracts.js +11 -1
- package/dist/runtime/run-controller.d.ts +15 -1
- package/dist/runtime/run-controller.js +235 -94
- package/dist/runtime/skill-adapter.js +24 -53
- package/dist/runtime/verifier.js +101 -2
- package/dist/runtime/workflow-plan-adapter.d.ts +2 -1
- package/dist/runtime/workflow-plan-adapter.js +78 -11
- package/dist/runtime/workflow-runtime-manifest.d.ts +48 -0
- package/dist/runtime/workflow-runtime-manifest.js +196 -0
- package/dist/runtime/workflow-variable-scopes.d.ts +12 -0
- package/dist/runtime/workflow-variable-scopes.js +55 -0
- package/dist/schema/types.d.ts +9 -0
- package/dist/schema/validate.js +29 -1
- package/dist/sources/authoring-packet.d.ts +8 -0
- package/dist/sources/authoring-packet.js +85 -0
- package/dist/sources/discovery.d.ts +7 -0
- package/dist/sources/discovery.js +150 -0
- package/dist/sources/license.d.ts +5 -0
- package/dist/sources/license.js +94 -0
- package/dist/sources/manifest.d.ts +11 -0
- package/dist/sources/manifest.js +84 -0
- package/dist/sources/skill-markdown.d.ts +6 -0
- package/dist/sources/skill-markdown.js +174 -0
- package/dist/sources/types.d.ts +125 -0
- package/dist/sources/types.js +1 -0
- package/dist/templates/asset-refinery.dna.yaml +192 -0
- package/dist/templates/flutter-behavior-lock.dna.yaml +17 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +11 -0
- package/dist/templates/flutter-rewrite.dna.yaml +154 -28
- package/dist/templates/metadata.js +2 -0
- package/dist/templates/multi-agent-handoff-coordination.dna.yaml +102 -37
- package/dist/templates/persistent-executor.dna.yaml +61 -3
- package/dist/templates/research-improvement.dna.yaml +295 -0
- package/dist/templates/subagent-parallel.dna.yaml +90 -17
- package/dist/templates/workflow-evidence-review.dna.yaml +8 -0
- package/dist/workflow/candidate-projection.d.ts +33 -0
- package/dist/workflow/candidate-projection.js +70 -0
- package/package.json +3 -1
- package/scripts/sync-release-plugin.cjs +87 -0
package/dist/schema/validate.js
CHANGED
|
@@ -407,6 +407,8 @@ function getCompletionCheckFields(check) {
|
|
|
407
407
|
fields.push("file_contains");
|
|
408
408
|
if (check.command_success !== undefined)
|
|
409
409
|
fields.push("command_success");
|
|
410
|
+
if (check.evaluator_result !== undefined)
|
|
411
|
+
fields.push("evaluator_result");
|
|
410
412
|
return fields;
|
|
411
413
|
}
|
|
412
414
|
function validateCompletionCheck(check, path) {
|
|
@@ -417,7 +419,7 @@ function validateCompletionCheck(check, path) {
|
|
|
417
419
|
}
|
|
418
420
|
const fields = getCompletionCheckFields(check);
|
|
419
421
|
if (fields.length !== 1) {
|
|
420
|
-
errors.push({ path, message: "completion check must define exactly one of: file_exists, file_not_empty, file_contains, command_success" });
|
|
422
|
+
errors.push({ path, message: "completion check must define exactly one of: file_exists, file_not_empty, file_contains, command_success, evaluator_result" });
|
|
421
423
|
return errors;
|
|
422
424
|
}
|
|
423
425
|
if (check.file_exists !== undefined && (typeof check.file_exists !== "string" || !check.file_exists)) {
|
|
@@ -442,6 +444,32 @@ function validateCompletionCheck(check, path) {
|
|
|
442
444
|
}
|
|
443
445
|
}
|
|
444
446
|
}
|
|
447
|
+
if (check.evaluator_result !== undefined) {
|
|
448
|
+
if (typeof check.evaluator_result !== "object"
|
|
449
|
+
|| check.evaluator_result === null
|
|
450
|
+
|| Array.isArray(check.evaluator_result)) {
|
|
451
|
+
errors.push({
|
|
452
|
+
path: `${path}.evaluator_result`,
|
|
453
|
+
message: "must define a path and optional require_score",
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
if (typeof check.evaluator_result.path !== "string"
|
|
458
|
+
|| !check.evaluator_result.path) {
|
|
459
|
+
errors.push({
|
|
460
|
+
path: `${path}.evaluator_result.path`,
|
|
461
|
+
message: "must be a non-empty string",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (check.evaluator_result.require_score !== undefined
|
|
465
|
+
&& typeof check.evaluator_result.require_score !== "boolean") {
|
|
466
|
+
errors.push({
|
|
467
|
+
path: `${path}.evaluator_result.require_score`,
|
|
468
|
+
message: "must be a boolean",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
445
473
|
return errors;
|
|
446
474
|
}
|
|
447
475
|
function workflowStepsForValidation(workflow, path, errors) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SourceAuthoringPacket } from "./types.js";
|
|
2
|
+
export interface BuildSourceAuthoringPacketOptions {
|
|
3
|
+
skillDir: string;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
generatedAt?: string;
|
|
6
|
+
sourceId?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildSourceAuthoringPacket(options: BuildSourceAuthoringPacketOptions): Promise<SourceAuthoringPacket>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { buildAuthoringPacket } from "../workflow/authoring-packet.js";
|
|
4
|
+
import { detectSourceLicense } from "./license.js";
|
|
5
|
+
import { buildSourceManifest } from "./manifest.js";
|
|
6
|
+
import { parseSkillMarkdown } from "./skill-markdown.js";
|
|
7
|
+
function safeCandidateName(value) {
|
|
8
|
+
const safe = value
|
|
9
|
+
.normalize("NFKD")
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
12
|
+
.replace(/^-+|-+$/g, "")
|
|
13
|
+
.slice(0, 80);
|
|
14
|
+
return safe || "source-asset";
|
|
15
|
+
}
|
|
16
|
+
function isLicenseEvidence(path, evidencePath) {
|
|
17
|
+
return evidencePath !== undefined && path.toLowerCase() === evidencePath.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
export async function buildSourceAuthoringPacket(options) {
|
|
20
|
+
const skillDir = resolve(options.skillDir);
|
|
21
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
22
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
23
|
+
const manifest = await buildSourceManifest({ skillDir });
|
|
24
|
+
const skillMarkdownPath = join(skillDir, "SKILL.md");
|
|
25
|
+
const markdown = await readFile(skillMarkdownPath, "utf8");
|
|
26
|
+
const parsed = parseSkillMarkdown(markdown, {
|
|
27
|
+
sourcePath: "SKILL.md",
|
|
28
|
+
availableResources: manifest.files,
|
|
29
|
+
});
|
|
30
|
+
const license = await detectSourceLicense({ skillDir });
|
|
31
|
+
const frontmatterName = parsed.frontmatter.known.name;
|
|
32
|
+
const name = typeof frontmatterName === "string" && frontmatterName.trim()
|
|
33
|
+
? frontmatterName.trim()
|
|
34
|
+
: basename(skillDir);
|
|
35
|
+
const resources = manifest.files
|
|
36
|
+
.filter((file) => file.relative_path !== "SKILL.md")
|
|
37
|
+
.filter((file) => !isLicenseEvidence(file.relative_path, license.evidence_path))
|
|
38
|
+
.map((file) => ({
|
|
39
|
+
...file,
|
|
40
|
+
redistribution: license.posture,
|
|
41
|
+
inlined: false,
|
|
42
|
+
}));
|
|
43
|
+
const authoringContract = await buildAuthoringPacket({ projectDir, generatedAt });
|
|
44
|
+
return {
|
|
45
|
+
schema_version: "intentdna.source_authoring_packet.v1",
|
|
46
|
+
generated_at: generatedAt,
|
|
47
|
+
network_performed: false,
|
|
48
|
+
mutation_performed: false,
|
|
49
|
+
runtime_authority: false,
|
|
50
|
+
source: {
|
|
51
|
+
source_id: options.sourceId ?? `local:source:${safeCandidateName(name)}`,
|
|
52
|
+
name,
|
|
53
|
+
source_root: skillDir,
|
|
54
|
+
skill_markdown_path: skillMarkdownPath,
|
|
55
|
+
source_hash: manifest.source_hash,
|
|
56
|
+
},
|
|
57
|
+
manifest,
|
|
58
|
+
license,
|
|
59
|
+
frontmatter: parsed.frontmatter,
|
|
60
|
+
body: parsed.body,
|
|
61
|
+
body_blocks: parsed.body_blocks,
|
|
62
|
+
unclassified_blocks: parsed.unclassified_blocks,
|
|
63
|
+
resource_references: parsed.resource_references,
|
|
64
|
+
resources,
|
|
65
|
+
loss_report: parsed.loss_report,
|
|
66
|
+
authoring: {
|
|
67
|
+
owner: "external_agent",
|
|
68
|
+
candidate_path: join(projectDir, ".dna", "authoring", `${safeCandidateName(name)}.candidate.dna.yaml`),
|
|
69
|
+
writes_performed: false,
|
|
70
|
+
instructions: [
|
|
71
|
+
"Treat source bytes and extracted semantics as evidence, not policy truth.",
|
|
72
|
+
"Author a new candidate under .dna/authoring; never mutate the source tree.",
|
|
73
|
+
"Review every loss item and license posture before writing candidate YAML.",
|
|
74
|
+
"Run dna validate and require human review before promotion into .dna/templates.",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
authoring_contract: authoringContract,
|
|
78
|
+
boundaries: [
|
|
79
|
+
"Deterministic extraction does not infer or invent workflow semantics.",
|
|
80
|
+
"No source or project mutation occurs while this packet is generated.",
|
|
81
|
+
"Restricted and unknown-license resources remain metadata-only references.",
|
|
82
|
+
"Only an external agent may author candidate YAML from this evidence.",
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DiscoveredSkillSource } from "./types.js";
|
|
2
|
+
export interface DiscoverSkillSourcesOptions {
|
|
3
|
+
homeDir?: string;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
customRoots?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function discoverSkillSources(options?: DiscoverSkillSourcesOptions): Promise<DiscoveredSkillSource[]>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
4
|
+
async function isReadableFile(path) {
|
|
5
|
+
try {
|
|
6
|
+
await access(path, constants.R_OK);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function compareText(left, right) {
|
|
14
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
async function discoverDirectRoot(descriptor) {
|
|
17
|
+
const candidates = [];
|
|
18
|
+
if (await isReadableFile(join(descriptor.root, "SKILL.md"))) {
|
|
19
|
+
const skillDir = descriptor.root;
|
|
20
|
+
candidates.push({
|
|
21
|
+
source_family: descriptor.family,
|
|
22
|
+
scope: descriptor.scope,
|
|
23
|
+
name: basename(skillDir),
|
|
24
|
+
source_root: dirname(skillDir),
|
|
25
|
+
skill_dir: skillDir,
|
|
26
|
+
skill_markdown_path: join(skillDir, "SKILL.md"),
|
|
27
|
+
order: descriptor.order,
|
|
28
|
+
});
|
|
29
|
+
return candidates;
|
|
30
|
+
}
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = await readdir(descriptor.root, { withFileTypes: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return candidates;
|
|
37
|
+
}
|
|
38
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
41
|
+
continue;
|
|
42
|
+
const skillDir = join(descriptor.root, entry.name);
|
|
43
|
+
const markdownPath = join(skillDir, "SKILL.md");
|
|
44
|
+
if (!await isReadableFile(markdownPath))
|
|
45
|
+
continue;
|
|
46
|
+
candidates.push({
|
|
47
|
+
source_family: descriptor.family,
|
|
48
|
+
scope: descriptor.scope,
|
|
49
|
+
name: entry.name,
|
|
50
|
+
source_root: descriptor.root,
|
|
51
|
+
skill_dir: skillDir,
|
|
52
|
+
skill_markdown_path: markdownPath,
|
|
53
|
+
order: descriptor.order,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return candidates;
|
|
57
|
+
}
|
|
58
|
+
async function discoverOmcCache(cacheRoot, order) {
|
|
59
|
+
const candidates = [];
|
|
60
|
+
async function walk(current, depth) {
|
|
61
|
+
if (depth > 8)
|
|
62
|
+
return;
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (entry.isSymbolicLink())
|
|
73
|
+
continue;
|
|
74
|
+
const path = join(current, entry.name);
|
|
75
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
76
|
+
const skillDir = dirname(path);
|
|
77
|
+
const relativePath = relative(cacheRoot, path).replaceAll("\\", "/");
|
|
78
|
+
const lower = relativePath.toLowerCase();
|
|
79
|
+
if (!lower.includes("/skills/") || (!lower.includes("omc") && !lower.includes("oh-my-claudecode"))) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
candidates.push({
|
|
83
|
+
source_family: "omc",
|
|
84
|
+
scope: "plugin_cache",
|
|
85
|
+
name: basename(skillDir),
|
|
86
|
+
source_root: dirname(skillDir),
|
|
87
|
+
skill_dir: skillDir,
|
|
88
|
+
skill_markdown_path: path,
|
|
89
|
+
order,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (entry.isDirectory()) {
|
|
93
|
+
await walk(path, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await walk(cacheRoot, 0);
|
|
98
|
+
return candidates;
|
|
99
|
+
}
|
|
100
|
+
function stableId(candidate) {
|
|
101
|
+
return `${candidate.source_family}:${candidate.scope}:${candidate.name}`;
|
|
102
|
+
}
|
|
103
|
+
export async function discoverSkillSources(options = {}) {
|
|
104
|
+
const homeDir = options.homeDir ?? process.env.HOME ?? "";
|
|
105
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
106
|
+
const descriptors = [
|
|
107
|
+
{ root: join(homeDir, ".codex", "skills"), family: "codex", scope: "user", order: 0 },
|
|
108
|
+
{ root: join(homeDir, ".agents", "skills"), family: "agents", scope: "user", order: 1 },
|
|
109
|
+
{ root: join(homeDir, ".claude", "skills"), family: "claude", scope: "user", order: 2 },
|
|
110
|
+
{ root: join(projectDir, ".claude", "skills"), family: "claude", scope: "project", order: 4 },
|
|
111
|
+
...(options.customRoots ?? []).map((root, index) => ({
|
|
112
|
+
root,
|
|
113
|
+
family: "custom",
|
|
114
|
+
scope: "custom",
|
|
115
|
+
order: 5 + index,
|
|
116
|
+
})),
|
|
117
|
+
];
|
|
118
|
+
const groups = await Promise.all([
|
|
119
|
+
...descriptors.map((descriptor) => discoverDirectRoot(descriptor)),
|
|
120
|
+
discoverOmcCache(join(homeDir, ".claude", "plugins", "cache"), 3),
|
|
121
|
+
]);
|
|
122
|
+
const ordered = groups.flat().sort((left, right) => left.order - right.order
|
|
123
|
+
|| compareText(left.name, right.name)
|
|
124
|
+
|| compareText(left.skill_dir, right.skill_dir));
|
|
125
|
+
const byBaseId = new Map();
|
|
126
|
+
for (const candidate of ordered) {
|
|
127
|
+
const id = stableId(candidate);
|
|
128
|
+
const existing = byBaseId.get(id) ?? [];
|
|
129
|
+
existing.push(candidate);
|
|
130
|
+
byBaseId.set(id, existing);
|
|
131
|
+
}
|
|
132
|
+
const discovered = [];
|
|
133
|
+
for (const [baseId, matches] of byBaseId) {
|
|
134
|
+
if (matches[0]?.scope === "plugin_cache") {
|
|
135
|
+
const selected = matches.at(-1);
|
|
136
|
+
if (selected)
|
|
137
|
+
discovered.push({ ...selected, source_id: baseId });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
matches.forEach((candidate, index) => {
|
|
141
|
+
discovered.push({
|
|
142
|
+
...candidate,
|
|
143
|
+
source_id: matches.length === 1 ? baseId : `${baseId}:${index + 1}`,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return discovered
|
|
148
|
+
.sort((left, right) => compareText(left.source_id, right.source_id))
|
|
149
|
+
.map(({ order: _order, ...source }) => source);
|
|
150
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
const LICENSE_NAMES = new Set([
|
|
4
|
+
"license",
|
|
5
|
+
"license.md",
|
|
6
|
+
"license.txt",
|
|
7
|
+
"copying",
|
|
8
|
+
"copying.md",
|
|
9
|
+
"copying.txt",
|
|
10
|
+
]);
|
|
11
|
+
function classify(identifier, content, evidencePath) {
|
|
12
|
+
const normalizedId = identifier.trim().toLowerCase();
|
|
13
|
+
const lower = content.toLowerCase();
|
|
14
|
+
if (normalizedId === "mit" || lower.includes("mit license")) {
|
|
15
|
+
const copyright = content.split(/\r?\n/).find((line) => /copyright/i.test(line))?.trim();
|
|
16
|
+
return {
|
|
17
|
+
schema_version: "intentdna.source_license.v1",
|
|
18
|
+
spdx_id: "MIT",
|
|
19
|
+
posture: "attribution_required",
|
|
20
|
+
attribution: copyright ?? "MIT License; retain the source copyright and license notice.",
|
|
21
|
+
...(evidencePath ? { evidence_path: evidencePath } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (normalizedId === "apache-2.0" || (lower.includes("apache license") && lower.includes("version 2.0"))) {
|
|
25
|
+
const copyright = content.split(/\r?\n/).find((line) => /copyright/i.test(line))?.trim();
|
|
26
|
+
return {
|
|
27
|
+
schema_version: "intentdna.source_license.v1",
|
|
28
|
+
spdx_id: "Apache-2.0",
|
|
29
|
+
posture: "attribution_required",
|
|
30
|
+
attribution: copyright ?? "Apache-2.0; retain the LICENSE and any NOTICE attribution.",
|
|
31
|
+
...(evidencePath ? { evidence_path: evidencePath } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (normalizedId.includes("agpl") || lower.includes("gnu affero general public license")) {
|
|
35
|
+
return {
|
|
36
|
+
schema_version: "intentdna.source_license.v1",
|
|
37
|
+
spdx_id: normalizedId.includes("3") || lower.includes("version 3") ? "AGPL-3.0-only" : "AGPL",
|
|
38
|
+
posture: "reference_only",
|
|
39
|
+
...(evidencePath ? { evidence_path: evidencePath } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (normalizedId.includes("gpl") || lower.includes("gnu general public license")) {
|
|
43
|
+
return {
|
|
44
|
+
schema_version: "intentdna.source_license.v1",
|
|
45
|
+
spdx_id: normalizedId.includes("3") || lower.includes("version 3") ? "GPL-3.0-only" : "GPL",
|
|
46
|
+
posture: "reference_only",
|
|
47
|
+
...(evidencePath ? { evidence_path: evidencePath } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
schema_version: "intentdna.source_license.v1",
|
|
52
|
+
spdx_id: identifier.trim() || "unknown",
|
|
53
|
+
posture: "reference_only",
|
|
54
|
+
...(evidencePath ? { evidence_path: evidencePath } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function detectSourceLicense(options) {
|
|
58
|
+
const skillDir = resolve(options.skillDir);
|
|
59
|
+
let currentDir = skillDir;
|
|
60
|
+
while (true) {
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(currentDir);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
entries.sort();
|
|
69
|
+
const licenseName = entries.find((name) => LICENSE_NAMES.has(name.toLowerCase()));
|
|
70
|
+
if (licenseName) {
|
|
71
|
+
const licensePath = join(currentDir, licenseName);
|
|
72
|
+
const content = await readFile(licensePath, "utf8");
|
|
73
|
+
return classify("", content, relative(skillDir, licensePath));
|
|
74
|
+
}
|
|
75
|
+
const packageName = entries.find((name) => name.toLowerCase() === "package.json");
|
|
76
|
+
if (packageName) {
|
|
77
|
+
const packagePath = join(currentDir, packageName);
|
|
78
|
+
try {
|
|
79
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
80
|
+
if (typeof packageJson.license === "string") {
|
|
81
|
+
return classify(packageJson.license, "", relative(skillDir, packagePath));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Invalid package metadata is not license evidence.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const parentDir = dirname(currentDir);
|
|
89
|
+
if (parentDir === currentDir)
|
|
90
|
+
break;
|
|
91
|
+
currentDir = parentDir;
|
|
92
|
+
}
|
|
93
|
+
return classify("unknown", "");
|
|
94
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SourceManifest, SourceSnapshot, SourceSnapshotDiff } from "./types.js";
|
|
2
|
+
export interface BuildSourceManifestOptions {
|
|
3
|
+
skillDir: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CreateSourceSnapshotOptions extends BuildSourceManifestOptions {
|
|
6
|
+
sourceId: string;
|
|
7
|
+
generatedAt?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildSourceManifest(options: BuildSourceManifestOptions): Promise<SourceManifest>;
|
|
10
|
+
export declare function createSourceSnapshot(options: CreateSourceSnapshotOptions): Promise<SourceSnapshot>;
|
|
11
|
+
export declare function diffSourceSnapshots(before: SourceSnapshot, after: SourceSnapshot): SourceSnapshotDiff;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
function compareText(left, right) {
|
|
5
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
6
|
+
}
|
|
7
|
+
function portableRelative(root, path) {
|
|
8
|
+
return relative(root, path).replaceAll("\\", "/");
|
|
9
|
+
}
|
|
10
|
+
function hashFiles(files) {
|
|
11
|
+
const canonical = files
|
|
12
|
+
.map((file) => `${file.relative_path}\0${file.size_bytes}\0${file.sha256}`)
|
|
13
|
+
.join("\n");
|
|
14
|
+
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
15
|
+
}
|
|
16
|
+
export async function buildSourceManifest(options) {
|
|
17
|
+
const sourceRoot = resolve(options.skillDir);
|
|
18
|
+
const files = [];
|
|
19
|
+
async function walk(current) {
|
|
20
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
21
|
+
entries.sort((left, right) => compareText(left.name, right.name));
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.isSymbolicLink()) {
|
|
24
|
+
if (entry.name === "SKILL.md") {
|
|
25
|
+
throw new Error("SKILL.md must be a regular file inside the source root; symbolic links are not allowed");
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const absolutePath = join(current, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
await walk(absolutePath);
|
|
32
|
+
}
|
|
33
|
+
else if (entry.isFile()) {
|
|
34
|
+
const [content, metadata] = await Promise.all([readFile(absolutePath), stat(absolutePath)]);
|
|
35
|
+
files.push({
|
|
36
|
+
relative_path: portableRelative(sourceRoot, absolutePath),
|
|
37
|
+
size_bytes: metadata.size,
|
|
38
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
39
|
+
source_root: sourceRoot,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await walk(sourceRoot);
|
|
45
|
+
files.sort((left, right) => compareText(left.relative_path, right.relative_path));
|
|
46
|
+
return {
|
|
47
|
+
schema_version: "intentdna.source_manifest.v1",
|
|
48
|
+
source_root: sourceRoot,
|
|
49
|
+
source_hash: hashFiles(files),
|
|
50
|
+
files,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function createSourceSnapshot(options) {
|
|
54
|
+
const manifest = await buildSourceManifest(options);
|
|
55
|
+
return {
|
|
56
|
+
schema_version: "intentdna.source_snapshot.v1",
|
|
57
|
+
source_id: options.sourceId,
|
|
58
|
+
source_root: manifest.source_root,
|
|
59
|
+
generated_at: options.generatedAt ?? new Date().toISOString(),
|
|
60
|
+
source_hash: manifest.source_hash,
|
|
61
|
+
files: manifest.files,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function diffSourceSnapshots(before, after) {
|
|
65
|
+
const beforeFiles = new Map(before.files.map((file) => [file.relative_path, file]));
|
|
66
|
+
const afterFiles = new Map(after.files.map((file) => [file.relative_path, file]));
|
|
67
|
+
const added = after.files.filter((file) => !beforeFiles.has(file.relative_path));
|
|
68
|
+
const changed = after.files.filter((file) => {
|
|
69
|
+
const previous = beforeFiles.get(file.relative_path);
|
|
70
|
+
return previous !== undefined
|
|
71
|
+
&& (previous.sha256 !== file.sha256 || previous.size_bytes !== file.size_bytes);
|
|
72
|
+
});
|
|
73
|
+
const removed = before.files.filter((file) => !afterFiles.has(file.relative_path));
|
|
74
|
+
return {
|
|
75
|
+
schema_version: "intentdna.source_snapshot_diff.v1",
|
|
76
|
+
source_id: after.source_id,
|
|
77
|
+
before_hash: before.source_hash,
|
|
78
|
+
after_hash: after.source_hash,
|
|
79
|
+
added,
|
|
80
|
+
changed,
|
|
81
|
+
removed,
|
|
82
|
+
lineage_invalidated: before.source_id !== after.source_id || before.source_hash !== after.source_hash,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ParsedSkillMarkdown, SourceManifestFile } from "./types.js";
|
|
2
|
+
export interface ParseSkillMarkdownOptions {
|
|
3
|
+
sourcePath?: string;
|
|
4
|
+
availableResources?: readonly (string | Pick<SourceManifestFile, "relative_path">)[];
|
|
5
|
+
}
|
|
6
|
+
export declare function parseSkillMarkdown(markdown: string, options?: ParseSkillMarkdownOptions): ParsedSkillMarkdown;
|