@jinn-network/jinn-layer 0.1.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 (131) hide show
  1. package/dist/adapters/contribution-adapter.d.ts +18 -0
  2. package/dist/adapters/contribution-adapter.js +134 -0
  3. package/dist/adapters/corpus-adapter.d.ts +12 -0
  4. package/dist/adapters/corpus-adapter.js +212 -0
  5. package/dist/adapters/index.d.ts +17 -0
  6. package/dist/adapters/index.js +17 -0
  7. package/dist/adapters/json-map-store.d.ts +4 -0
  8. package/dist/adapters/json-map-store.js +31 -0
  9. package/dist/adapters/local-learning-adapter.d.ts +25 -0
  10. package/dist/adapters/local-learning-adapter.js +49 -0
  11. package/dist/adapters/skills-adapter.d.ts +8 -0
  12. package/dist/adapters/skills-adapter.js +55 -0
  13. package/dist/admission-policy.d.ts +38 -0
  14. package/dist/admission-policy.js +17 -0
  15. package/dist/bin/jinn-distill-mcp.d.ts +2 -0
  16. package/dist/bin/jinn-distill-mcp.js +10 -0
  17. package/dist/bin/jinn-layer.d.ts +7 -0
  18. package/dist/bin/jinn-layer.js +15 -0
  19. package/dist/bridge-fetch-evidence.d.ts +102 -0
  20. package/dist/bridge-fetch-evidence.js +677 -0
  21. package/dist/bridge-verdict-source.d.ts +47 -0
  22. package/dist/bridge-verdict-source.js +216 -0
  23. package/dist/bridge.d.ts +247 -0
  24. package/dist/bridge.js +439 -0
  25. package/dist/capture.d.ts +98 -0
  26. package/dist/capture.js +617 -0
  27. package/dist/cli.d.ts +148 -0
  28. package/dist/cli.js +2221 -0
  29. package/dist/cluster-selection.d.ts +34 -0
  30. package/dist/cluster-selection.js +135 -0
  31. package/dist/cluster.d.ts +89 -0
  32. package/dist/cluster.js +142 -0
  33. package/dist/consume.d.ts +139 -0
  34. package/dist/consume.js +280 -0
  35. package/dist/corpus-probes.d.ts +61 -0
  36. package/dist/corpus-probes.js +120 -0
  37. package/dist/corpus-store.d.ts +16 -0
  38. package/dist/corpus-store.js +101 -0
  39. package/dist/distill-captures.d.ts +56 -0
  40. package/dist/distill-captures.js +283 -0
  41. package/dist/distill-feedback.d.ts +20 -0
  42. package/dist/distill-feedback.js +30 -0
  43. package/dist/distill-llm.d.ts +138 -0
  44. package/dist/distill-llm.js +438 -0
  45. package/dist/distill-mcp-server.d.ts +30 -0
  46. package/dist/distill-mcp-server.js +281 -0
  47. package/dist/distill-mode.d.ts +62 -0
  48. package/dist/distill-mode.js +107 -0
  49. package/dist/distill-progress.d.ts +78 -0
  50. package/dist/distill-progress.js +69 -0
  51. package/dist/distill-prompt.d.ts +34 -0
  52. package/dist/distill-prompt.js +85 -0
  53. package/dist/distill-render.d.ts +123 -0
  54. package/dist/distill-render.js +303 -0
  55. package/dist/distill-runs.d.ts +28 -0
  56. package/dist/distill-runs.js +39 -0
  57. package/dist/distill-traces.d.ts +67 -0
  58. package/dist/distill-traces.js +211 -0
  59. package/dist/distill.d.ts +180 -0
  60. package/dist/distill.js +337 -0
  61. package/dist/distiller.d.ts +118 -0
  62. package/dist/distiller.js +142 -0
  63. package/dist/envelope.d.ts +9 -0
  64. package/dist/envelope.js +9 -0
  65. package/dist/eval-prep.d.ts +129 -0
  66. package/dist/eval-prep.js +523 -0
  67. package/dist/execution-publish.d.ts +62 -0
  68. package/dist/execution-publish.js +72 -0
  69. package/dist/gate.d.ts +39 -0
  70. package/dist/gate.js +106 -0
  71. package/dist/index.d.ts +49 -0
  72. package/dist/index.js +50 -0
  73. package/dist/ipfs-cid.d.ts +18 -0
  74. package/dist/ipfs-cid.js +149 -0
  75. package/dist/layer-default.d.ts +3 -0
  76. package/dist/layer-default.js +10 -0
  77. package/dist/ledger.d.ts +62 -0
  78. package/dist/ledger.js +104 -0
  79. package/dist/measurement.d.ts +90 -0
  80. package/dist/measurement.js +134 -0
  81. package/dist/path-safety.d.ts +19 -0
  82. package/dist/path-safety.js +303 -0
  83. package/dist/pipeline.d.ts +94 -0
  84. package/dist/pipeline.js +124 -0
  85. package/dist/plugin-wiring.d.ts +4 -0
  86. package/dist/plugin-wiring.js +54 -0
  87. package/dist/preview.d.ts +31 -0
  88. package/dist/preview.js +28 -0
  89. package/dist/process-contract.d.ts +303 -0
  90. package/dist/process-contract.js +118 -0
  91. package/dist/publish-skill.d.ts +86 -0
  92. package/dist/publish-skill.js +116 -0
  93. package/dist/publish.d.ts +294 -0
  94. package/dist/publish.js +1090 -0
  95. package/dist/scripts/scan-skills-registry.d.ts +24 -0
  96. package/dist/scripts/scan-skills-registry.js +207 -0
  97. package/dist/seed-import/curated-batch.d.ts +36 -0
  98. package/dist/seed-import/curated-batch.js +103 -0
  99. package/dist/seed-import/episode-execute.d.ts +50 -0
  100. package/dist/seed-import/episode-execute.js +235 -0
  101. package/dist/seed-import/episode-fetch.d.ts +119 -0
  102. package/dist/seed-import/episode-fetch.js +137 -0
  103. package/dist/seed-import/episode-plan.d.ts +11 -0
  104. package/dist/seed-import/episode-plan.js +27 -0
  105. package/dist/seed-import/episode-report.d.ts +39 -0
  106. package/dist/seed-import/episode-report.js +36 -0
  107. package/dist/seed-import/execute.d.ts +61 -0
  108. package/dist/seed-import/execute.js +245 -0
  109. package/dist/seed-import/fetch.d.ts +62 -0
  110. package/dist/seed-import/fetch.js +82 -0
  111. package/dist/seed-import/licence.d.ts +17 -0
  112. package/dist/seed-import/licence.js +36 -0
  113. package/dist/seed-import/plan.d.ts +10 -0
  114. package/dist/seed-import/plan.js +21 -0
  115. package/dist/seed-import/report.d.ts +34 -0
  116. package/dist/seed-import/report.js +31 -0
  117. package/dist/seed-import/state.d.ts +72 -0
  118. package/dist/seed-import/state.js +120 -0
  119. package/dist/signal.d.ts +36 -0
  120. package/dist/signal.js +48 -0
  121. package/dist/signing.d.ts +8 -0
  122. package/dist/signing.js +21 -0
  123. package/dist/skill-package.d.ts +70 -0
  124. package/dist/skill-package.js +141 -0
  125. package/dist/skill.d.ts +21 -0
  126. package/dist/skill.js +77 -0
  127. package/dist/snapshot-transcript.d.ts +54 -0
  128. package/dist/snapshot-transcript.js +117 -0
  129. package/dist/three-arm-run.d.ts +27 -0
  130. package/dist/three-arm-run.js +41 -0
  131. package/package.json +60 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * `execute()` — publish the approved import rows through the Task-4 path
3
+ * (plan Task 6, spec §7).
4
+ *
5
+ * Each imported skill becomes a synthetic captured task carrying the
6
+ * SKILL.md as its single step, runs through the SAME mandatory scrub
7
+ * (`capture()`) and the SAME publish/anchor path (`publish()`) as a real
8
+ * contribution — one publish path, per the 2026-07-02 schema review — with
9
+ * `provenance: 'imported'` and attribution metadata in the step attributes.
10
+ * Seeds are thereby excluded from the demand signal and from emissions
11
+ * eligibility by every provenance-aware reader.
12
+ *
13
+ * The licence gate is structural, not advisory: execute() re-checks each
14
+ * row's licence from the freshly-fetched source. A verdict hand-edited to
15
+ * `import` over a non-allowlisted licence is refused — widening the gate is
16
+ * a code change to the disclosed allowlist, reviewable in a PR, not a
17
+ * report-file edit.
18
+ *
19
+ * Idempotent by seed identity (issue #1771, `state.ts`): re-running execute
20
+ * over an unchanged skill publishes nothing new (a `skipped` row instead);
21
+ * re-running over a CHANGED skill (different skillMd/description/licence/
22
+ * source) republishes and sets the new record's
23
+ * `provenance.supersedes` to the prior envelopeRef. `opts.state` defaults to
24
+ * a fresh in-memory store per call, so existing callers that never pass one
25
+ * see unchanged behaviour (every row always publishes) — the CLI wires a
26
+ * file-backed store for real runs (cli.ts, `seed execute`).
27
+ */
28
+ import { capture } from '../capture.js';
29
+ import { buildSeedScrubPipeline } from '@jinn-network/core/scrub';
30
+ import { publish, PublishLedgerError, } from '../publish.js';
31
+ import { checkLicence } from './licence.js';
32
+ import { createMemorySeedImportState, hashSeedContent, } from './state.js';
33
+ /** Caps from the frozen envelope schema (envelope-v0.md size limits). */
34
+ const MAX_TAG_CHARS = 64;
35
+ const MAX_TAGS = 16;
36
+ /**
37
+ * Extract `name:` from a SKILL.md YAML frontmatter block, if present.
38
+ * Deliberately a line-parser, not a YAML dependency — skill frontmatter
39
+ * names are plain scalars; anything more exotic simply yields no tag.
40
+ */
41
+ export function frontmatterName(skillMd) {
42
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
43
+ if (!match)
44
+ return null;
45
+ for (const line of match[1].split(/\r?\n/)) {
46
+ const kv = /^name:\s*(.+)$/.exec(line.trim());
47
+ if (kv) {
48
+ const value = kv[1].trim().replace(/^["']|["']$/g, '').trim();
49
+ return value || null;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ /** Last path segment of a registry-style skill id (`owner/repo/slug`). */
55
+ export function skillSlug(skillId) {
56
+ const segments = skillId.split('/').filter((s) => s.length > 0);
57
+ return segments[segments.length - 1] ?? skillId;
58
+ }
59
+ /**
60
+ * Seed tags (#1343): the seed marker, the repo, the skill slug (last path
61
+ * segment), and the SKILL.md frontmatter name when it declares one. Tags are
62
+ * freeform under the frozen schema (envelope-v0.md: no fixed taxonomy) but
63
+ * the size caps are structural — dedupe, clamp to 64 chars, cap at 16.
64
+ * The signal's authoritative seed marker stays `provenance: 'imported'`;
65
+ * `seed-import` is honest labelling on top.
66
+ */
67
+ function seedTags(skill) {
68
+ const segments = skill.skill.split('/').filter((s) => s.length > 0);
69
+ const repoName = segments[1] ?? skill.skill;
70
+ const slug = skillSlug(skill.skill);
71
+ const name = frontmatterName(skill.skillMd);
72
+ const tags = [];
73
+ for (const candidate of ['seed-import', repoName, slug, ...(name ? [name] : [])]) {
74
+ const tag = candidate.trim().slice(0, MAX_TAG_CHARS);
75
+ if (tag && !tags.includes(tag))
76
+ tags.push(tag);
77
+ }
78
+ return tags.slice(0, MAX_TAGS);
79
+ }
80
+ function toCapturedTask(skill, now) {
81
+ const nano = `${now.getTime()}000000`;
82
+ return {
83
+ session: {
84
+ sessionId: `seed:${skill.skill}`,
85
+ capturedAt: now.toISOString(),
86
+ },
87
+ task: {
88
+ summary: `Seed import: ${skill.skill}${skill.description ? ` — ${skill.description}` : ''}`,
89
+ distributionTags: seedTags(skill),
90
+ },
91
+ environment: {
92
+ harness: { name: 'jinn-layer-seed-import', version: '0.1.0' },
93
+ model: 'none',
94
+ tools: [],
95
+ },
96
+ steps: [
97
+ {
98
+ spanId: 'seed-1',
99
+ parentSpanId: null,
100
+ name: 'seed:skill-md',
101
+ startTimeUnixNano: nano,
102
+ endTimeUnixNano: nano,
103
+ attributes: {
104
+ 'skill.md': skill.skillMd,
105
+ 'seed.attribution': {
106
+ skill: skill.skill,
107
+ source: skill.source,
108
+ licence: skill.licence,
109
+ },
110
+ },
111
+ redactedKeys: [],
112
+ },
113
+ ],
114
+ outcome: {
115
+ // user-accepted is literal here: the human approved this exact list.
116
+ status: 'completed',
117
+ verifiabilityTier: 'user-accepted',
118
+ },
119
+ cost: { durationMs: 0 },
120
+ provenance: 'imported',
121
+ };
122
+ }
123
+ /** First-class skill artifact for a seed (#1394). Companion-file fetch is #1381. */
124
+ function toSkillArtifact(skill, safeAddress, supersedes) {
125
+ return {
126
+ schemaVersion: 'jinn.skill.v1',
127
+ skill: {
128
+ name: frontmatterName(skill.skillMd) ?? skillSlug(skill.skill),
129
+ ...(skill.description !== undefined ? { description: skill.description } : {}),
130
+ skillMd: skill.skillMd,
131
+ },
132
+ files: [],
133
+ provenance: {
134
+ kind: 'imported',
135
+ sourceEnvelopeCids: [],
136
+ operator: { safeAddress },
137
+ seed: { skill: skill.skill, source: skill.source, licence: skill.licence },
138
+ ...(supersedes ? { supersedes } : {}),
139
+ },
140
+ };
141
+ }
142
+ /** The content fields that define "did this seed change" for idempotency (state.ts). */
143
+ function skillContentFingerprint(skill) {
144
+ return {
145
+ skillMd: skill.skillMd,
146
+ description: skill.description ?? null,
147
+ licence: skill.licence,
148
+ source: skill.source,
149
+ };
150
+ }
151
+ export async function execute(report, source, deps, opts = {}) {
152
+ const skills = new Map((await source.list()).map((s) => [s.skill, s]));
153
+ const result = { imported: [], skipped: [], errors: [] };
154
+ const now = deps.now?.() ?? new Date();
155
+ // Seed profile (#1409): deterministic secret detectors only. Skill seeds in
156
+ // this lane are public, licence-checked prose, not operator trace data — the
157
+ // probabilistic stages false-positive on SKILL.md content and defaced
158
+ // the anchored corpus. Capture stays mandatory and fail-closed.
159
+ const seedScrubPipeline = buildSeedScrubPipeline();
160
+ const state = opts.state ?? createMemorySeedImportState();
161
+ for (const row of report) {
162
+ if (row.verdict === 'skip') {
163
+ result.skipped.push({ skill: row.skill, reason: row.reason });
164
+ continue;
165
+ }
166
+ try {
167
+ const skill = skills.get(row.skill);
168
+ if (!skill)
169
+ throw new Error(`skill ${row.skill} not found in source ${source.name}`);
170
+ const licence = checkLicence(skill.licence);
171
+ if (licence.verdict !== 'import') {
172
+ throw new Error(`licence gate refused ${row.skill}: ${licence.reason}`);
173
+ }
174
+ const identity = `skill:${skill.skill}`;
175
+ const contentHash = hashSeedContent(skillContentFingerprint(skill));
176
+ let prior;
177
+ try {
178
+ prior = state.get(identity);
179
+ }
180
+ catch (err) {
181
+ result.errors.push({
182
+ skill: row.skill,
183
+ error: err instanceof Error ? err.message : String(err),
184
+ });
185
+ break;
186
+ }
187
+ if (prior && prior.contentHash === contentHash) {
188
+ result.skipped.push({ skill: row.skill, reason: `unchanged since ${prior.envelopeRef}` });
189
+ continue;
190
+ }
191
+ const pending = await capture(toCapturedTask(skill, now), {
192
+ pipeline: seedScrubPipeline,
193
+ });
194
+ let published;
195
+ let ledgerWarning;
196
+ try {
197
+ published = await publish(pending, deps, {
198
+ skill: toSkillArtifact(skill, deps.participant.safeAddress, prior?.envelopeRef),
199
+ });
200
+ }
201
+ catch (err) {
202
+ if (err instanceof PublishLedgerError) {
203
+ published = err.result;
204
+ ledgerWarning = err.message;
205
+ }
206
+ else {
207
+ const detail = err instanceof Error ? err.message : String(err);
208
+ result.errors.push({
209
+ skill: row.skill,
210
+ error: `publication outcome unknown; do not auto-retry: ${detail}`,
211
+ });
212
+ break;
213
+ }
214
+ }
215
+ if (published.vetoed)
216
+ throw new Error('unexpected veto on seed publish');
217
+ let stateWarning;
218
+ try {
219
+ state.set(identity, { contentHash, envelopeRef: published.envelopeRef, publishedAt: now.toISOString() });
220
+ }
221
+ catch (err) {
222
+ const detail = err instanceof Error ? err.message : String(err);
223
+ stateWarning =
224
+ `published ${published.envelopeRef}, but seed-import state persistence failed: ${detail}; ` +
225
+ 'recovery required before retrying this seed';
226
+ }
227
+ result.imported.push({
228
+ skill: row.skill,
229
+ envelopeRef: published.envelopeRef,
230
+ anchorTx: published.anchorTx,
231
+ ...(ledgerWarning ? { ledgerWarning } : {}),
232
+ ...(stateWarning ? { stateWarning } : {}),
233
+ });
234
+ if (ledgerWarning || stateWarning)
235
+ break;
236
+ }
237
+ catch (err) {
238
+ result.errors.push({
239
+ skill: row.skill,
240
+ error: err instanceof Error ? err.message : String(err),
241
+ });
242
+ }
243
+ }
244
+ return result;
245
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Seed sources (plan Task 6, spec §7).
3
+ *
4
+ * skills.sh (the first registry, per spec D8) exposes no stable public
5
+ * listing API — its entries are GitHub repos. The durable listing surface is
6
+ * therefore a **disclosed input list** of `owner/repo[#path]` entries (which
7
+ * also matches D9's bounded-allowlist posture: nothing imports that was not
8
+ * on a list a human read), resolved against the GitHub API for the licence
9
+ * (`GET /repos/{owner}/{repo}` → `license.spdx_id`) and the SKILL.md
10
+ * content.
11
+ */
12
+ import { z } from 'zod';
13
+ export interface SeedSkill {
14
+ /** Registry-style id, e.g. `vercel-labs/skills/find-skills`. */
15
+ skill: string;
16
+ /** Canonical source URL (the attribution target). */
17
+ source: string;
18
+ /** SPDX id, or null when the repo declares none. */
19
+ licence: string | null;
20
+ description?: string;
21
+ /** The SKILL.md content (the layer-2 consumable, spec §5). */
22
+ skillMd: string;
23
+ }
24
+ /**
25
+ * Zod mirror of `SeedSkill` (issue #1771): the wire shape of a skill-shaped
26
+ * seed fixture on disk (e.g. `fixtures/stage1-seeds/distractor-skill-*.json`),
27
+ * used by the fixture-lint test to validate skill-shaped fixtures without a
28
+ * live GitHub source. Not consumed by `createGithubSeedSource` — that source
29
+ * still speaks `SeedSkill` directly.
30
+ */
31
+ export declare const SeedSkillSchema: z.ZodObject<{
32
+ skill: z.ZodString;
33
+ source: z.ZodString;
34
+ licence: z.ZodNullable<z.ZodString>;
35
+ description: z.ZodOptional<z.ZodString>;
36
+ skillMd: z.ZodString;
37
+ }, z.core.$strict>;
38
+ export interface SeedSource {
39
+ name: string;
40
+ list(): Promise<SeedSkill[]>;
41
+ }
42
+ /** One line of the disclosed input list: `owner/repo` or `owner/repo#sub/path`. */
43
+ export interface SeedListEntry {
44
+ owner: string;
45
+ repo: string;
46
+ /** Path to the skill directory inside the repo (contains SKILL.md). */
47
+ path?: string;
48
+ }
49
+ export declare function parseSeedListEntry(line: string): SeedListEntry;
50
+ export interface GithubSeedSourceOptions {
51
+ /** The disclosed input list. */
52
+ entries: SeedListEntry[];
53
+ /** GitHub token (raises rate limits; optional for small lists). */
54
+ token?: string;
55
+ fetchImpl?: typeof fetch;
56
+ }
57
+ /**
58
+ * GitHub-backed seed source over a disclosed entry list. Licence comes from
59
+ * the repo's declared licence; SKILL.md from `{path}/SKILL.md` (repo root
60
+ * when no path is given).
61
+ */
62
+ export declare function createGithubSeedSource(options: GithubSeedSourceOptions): SeedSource;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Seed sources (plan Task 6, spec §7).
3
+ *
4
+ * skills.sh (the first registry, per spec D8) exposes no stable public
5
+ * listing API — its entries are GitHub repos. The durable listing surface is
6
+ * therefore a **disclosed input list** of `owner/repo[#path]` entries (which
7
+ * also matches D9's bounded-allowlist posture: nothing imports that was not
8
+ * on a list a human read), resolved against the GitHub API for the licence
9
+ * (`GET /repos/{owner}/{repo}` → `license.spdx_id`) and the SKILL.md
10
+ * content.
11
+ */
12
+ import { z } from 'zod';
13
+ /**
14
+ * Zod mirror of `SeedSkill` (issue #1771): the wire shape of a skill-shaped
15
+ * seed fixture on disk (e.g. `fixtures/stage1-seeds/distractor-skill-*.json`),
16
+ * used by the fixture-lint test to validate skill-shaped fixtures without a
17
+ * live GitHub source. Not consumed by `createGithubSeedSource` — that source
18
+ * still speaks `SeedSkill` directly.
19
+ */
20
+ export const SeedSkillSchema = z.strictObject({
21
+ skill: z.string().min(1),
22
+ source: z.string().min(1),
23
+ licence: z.string().min(1).nullable(),
24
+ description: z.string().min(1).optional(),
25
+ skillMd: z.string().min(1),
26
+ });
27
+ export function parseSeedListEntry(line) {
28
+ const [repoPart, path] = line.trim().split('#', 2);
29
+ const segments = (repoPart ?? '').split('/').filter((s) => s.length > 0);
30
+ if (segments.length !== 2) {
31
+ throw new Error(`invalid seed list entry "${line}" — expected owner/repo or owner/repo#path`);
32
+ }
33
+ return {
34
+ owner: segments[0],
35
+ repo: segments[1],
36
+ ...(path ? { path } : {}),
37
+ };
38
+ }
39
+ const GITHUB_API = 'https://api.github.com';
40
+ async function githubJson(url, token, fetchImpl) {
41
+ const res = await fetchImpl(url, {
42
+ headers: {
43
+ accept: 'application/vnd.github+json',
44
+ 'user-agent': 'jinn-harness-layer-seed-import',
45
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
46
+ },
47
+ });
48
+ if (!res.ok)
49
+ throw new Error(`GitHub ${res.status} for ${url}`);
50
+ return res.json();
51
+ }
52
+ /**
53
+ * GitHub-backed seed source over a disclosed entry list. Licence comes from
54
+ * the repo's declared licence; SKILL.md from `{path}/SKILL.md` (repo root
55
+ * when no path is given).
56
+ */
57
+ export function createGithubSeedSource(options) {
58
+ const fetchImpl = options.fetchImpl ?? fetch;
59
+ return {
60
+ name: 'github-seed-list',
61
+ async list() {
62
+ const skills = [];
63
+ for (const entry of options.entries) {
64
+ const repoUrl = `${GITHUB_API}/repos/${entry.owner}/${entry.repo}`;
65
+ const repo = (await githubJson(repoUrl, options.token, fetchImpl));
66
+ const skillMdPath = entry.path ? `${entry.path}/SKILL.md` : 'SKILL.md';
67
+ const content = (await githubJson(`${repoUrl}/contents/${skillMdPath}`, options.token, fetchImpl));
68
+ if (!content.content || content.encoding !== 'base64') {
69
+ throw new Error(`no SKILL.md at ${entry.owner}/${entry.repo}/${skillMdPath}`);
70
+ }
71
+ skills.push({
72
+ skill: [entry.owner, entry.repo, ...(entry.path ? [entry.path] : [])].join('/'),
73
+ source: repo.html_url ?? `https://github.com/${entry.owner}/${entry.repo}`,
74
+ licence: repo.license?.spdx_id ?? null,
75
+ ...(repo.description ? { description: repo.description } : {}),
76
+ skillMd: Buffer.from(content.content, 'base64').toString('utf-8'),
77
+ });
78
+ }
79
+ return skills;
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-skill licence gate (plan Task 6, spec §7 / D8).
3
+ *
4
+ * The allowlist is bounded and disclosed: permissive licences import (with
5
+ * attribution carried in the published envelope); anything else — including
6
+ * a missing licence — skips with a stated reason. Copyleft is excluded not
7
+ * as a legal judgement but as a posture: the corpus republishes content, and
8
+ * share-alike terms make that a per-licence analysis we have not done.
9
+ */
10
+ /** SPDX ids that import (with attribution). Bounded, disclosed. */
11
+ export declare const IMPORT_LICENCE_ALLOWLIST: readonly string[];
12
+ export interface LicenceVerdict {
13
+ verdict: 'import' | 'skip';
14
+ reason: string;
15
+ }
16
+ /** Map an SPDX id (or null = no licence found) to an import verdict. */
17
+ export declare function checkLicence(spdx: string | null | undefined): LicenceVerdict;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Per-skill licence gate (plan Task 6, spec §7 / D8).
3
+ *
4
+ * The allowlist is bounded and disclosed: permissive licences import (with
5
+ * attribution carried in the published envelope); anything else — including
6
+ * a missing licence — skips with a stated reason. Copyleft is excluded not
7
+ * as a legal judgement but as a posture: the corpus republishes content, and
8
+ * share-alike terms make that a per-licence analysis we have not done.
9
+ */
10
+ /** SPDX ids that import (with attribution). Bounded, disclosed. */
11
+ export const IMPORT_LICENCE_ALLOWLIST = [
12
+ 'MIT',
13
+ 'Apache-2.0',
14
+ 'BSD-2-Clause',
15
+ 'BSD-3-Clause',
16
+ 'ISC',
17
+ 'CC0-1.0',
18
+ 'Unlicense',
19
+ 'MIT-0',
20
+ ];
21
+ /** Map an SPDX id (or null = no licence found) to an import verdict. */
22
+ export function checkLicence(spdx) {
23
+ if (!spdx || spdx === 'NOASSERTION') {
24
+ return {
25
+ verdict: 'skip',
26
+ reason: 'no licence found — cannot republish without one',
27
+ };
28
+ }
29
+ if (IMPORT_LICENCE_ALLOWLIST.includes(spdx)) {
30
+ return { verdict: 'import', reason: `${spdx} — permissive, import with attribution` };
31
+ }
32
+ return {
33
+ verdict: 'skip',
34
+ reason: `${spdx} is not on the import allowlist (permissive licences only)`,
35
+ };
36
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `plan()` — fetch + licence-check, ZERO writes (plan Task 6).
3
+ *
4
+ * Takes only a SeedSource; it cannot publish, anchor or touch the ledger by
5
+ * construction (no publish deps in the signature). Its output is the
6
+ * ImportReport the human approves before `execute()` may run.
7
+ */
8
+ import type { SeedSource } from './fetch.js';
9
+ import type { ImportReport } from './report.js';
10
+ export declare function plan(source: SeedSource): Promise<ImportReport>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `plan()` — fetch + licence-check, ZERO writes (plan Task 6).
3
+ *
4
+ * Takes only a SeedSource; it cannot publish, anchor or touch the ledger by
5
+ * construction (no publish deps in the signature). Its output is the
6
+ * ImportReport the human approves before `execute()` may run.
7
+ */
8
+ import { checkLicence } from './licence.js';
9
+ export async function plan(source) {
10
+ const skills = await source.list();
11
+ return skills.map((skill) => {
12
+ const { verdict, reason } = checkLicence(skill.licence);
13
+ return {
14
+ skill: skill.skill,
15
+ source: skill.source,
16
+ licence: skill.licence,
17
+ verdict,
18
+ reason,
19
+ };
20
+ });
21
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The ImportReport — the approval-gate artifact (plan Task 6).
3
+ *
4
+ * `seed plan` renders and writes this; Oak reads and approves (editing
5
+ * verdicts if needed) BEFORE `seed execute` runs. Nothing lands on chain
6
+ * that was not on this list.
7
+ */
8
+ import { z } from 'zod';
9
+ export declare const ImportReportRowSchema: z.ZodObject<{
10
+ skill: z.ZodString;
11
+ source: z.ZodString;
12
+ licence: z.ZodNullable<z.ZodString>;
13
+ verdict: z.ZodEnum<{
14
+ import: "import";
15
+ skip: "skip";
16
+ }>;
17
+ reason: z.ZodString;
18
+ }, z.core.$strict>;
19
+ export type ImportReportRow = z.infer<typeof ImportReportRowSchema>;
20
+ export declare const ImportReportSchema: z.ZodArray<z.ZodObject<{
21
+ skill: z.ZodString;
22
+ source: z.ZodString;
23
+ licence: z.ZodNullable<z.ZodString>;
24
+ verdict: z.ZodEnum<{
25
+ import: "import";
26
+ skip: "skip";
27
+ }>;
28
+ reason: z.ZodString;
29
+ }, z.core.$strict>>;
30
+ export type ImportReport = z.infer<typeof ImportReportSchema>;
31
+ /** Parse an untrusted value (e.g. an edited report file); throws ZodError. */
32
+ export declare function parseImportReport(input: unknown): ImportReport;
33
+ /** Human-readable report table for the terminal. */
34
+ export declare function renderImportReport(report: ImportReport): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The ImportReport — the approval-gate artifact (plan Task 6).
3
+ *
4
+ * `seed plan` renders and writes this; Oak reads and approves (editing
5
+ * verdicts if needed) BEFORE `seed execute` runs. Nothing lands on chain
6
+ * that was not on this list.
7
+ */
8
+ import { z } from 'zod';
9
+ export const ImportReportRowSchema = z.strictObject({
10
+ skill: z.string().min(1),
11
+ source: z.string().min(1),
12
+ licence: z.string().min(1).nullable(),
13
+ verdict: z.enum(['import', 'skip']),
14
+ reason: z.string().min(1),
15
+ });
16
+ export const ImportReportSchema = z.array(ImportReportRowSchema);
17
+ /** Parse an untrusted value (e.g. an edited report file); throws ZodError. */
18
+ export function parseImportReport(input) {
19
+ return ImportReportSchema.parse(input);
20
+ }
21
+ /** Human-readable report table for the terminal. */
22
+ export function renderImportReport(report) {
23
+ if (report.length === 0)
24
+ return 'Empty report — no skills listed.';
25
+ const lines = [`${report.length} skill(s)`, ''];
26
+ for (const row of report) {
27
+ lines.push(`${row.verdict === 'import' ? 'IMPORT' : 'skip '} ${row.skill}`, ` licence ${row.licence ?? '- (none found)'}`, ` source ${row.source}`, ` reason ${row.reason}`, '');
28
+ }
29
+ lines.pop();
30
+ return lines.join('\n');
31
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Seed-import idempotency state (issue #1771, folding the idempotency AC
3
+ * originally scoped to #1772/R2 — R4 has no dependency on R1 so it ships
4
+ * here; see the #1771 PR body for the rationale).
5
+ *
6
+ * Both seed lanes (skill seeds in `execute.ts`, evidence-episode seeds in
7
+ * `episode-execute.ts`) publish through this shared store: a small
8
+ * identity -> {contentHash, envelopeRef} map that each `execute*()` consults
9
+ * before publishing. Re-running an import over unchanged content is a no-op
10
+ * (a `skipped` row; no new envelope); re-running over CHANGED content
11
+ * republishes and points the new record's provenance at the prior
12
+ * `envelopeRef` via `supersedes` — skills carry it in
13
+ * `SkillArtifactV1.provenance.supersedes` (client/src/types/skill-artifact.ts,
14
+ * already schema'd for this, issue #1462); episodes carry it in the
15
+ * `seed.attribution` step attribute (episode-fetch.ts's step convention),
16
+ * since the frozen `jinn.trace-envelope.v0` has no top-level field for it.
17
+ *
18
+ * File-backed by default (`~/.jinn-client/harness-layer/seed-import-state.json`,
19
+ * mirroring `ledger.ts`'s convention) so real CLI runs persist across
20
+ * invocations; tests inject `createMemorySeedImportState()`.
21
+ *
22
+ * Deliberately NOT a corpus query: idempotency here answers "did *I* already
23
+ * publish this exact seed identity, unchanged?" — a question the operator's
24
+ * own local state answers without a network round trip. It does not detect a
25
+ * differently-identified duplicate already sitting in the shared corpus
26
+ * (two seeds, two identities, same content) — that is the consumer-side
27
+ * content-key dedup (rescope plan §3.3) and the testnet hygiene sweep
28
+ * (#1776), not a publish-time concern.
29
+ */
30
+ import { z } from 'zod';
31
+ export declare const DEFAULT_SEED_IMPORT_STATE_PATH: string;
32
+ export declare const SeedPublicationRecordSchema: z.ZodObject<{
33
+ contentHash: z.ZodString;
34
+ envelopeRef: z.ZodString;
35
+ publishedAt: z.ZodISODateTime;
36
+ }, z.core.$strict>;
37
+ export type SeedPublicationRecord = z.infer<typeof SeedPublicationRecordSchema>;
38
+ export interface SeedImportStateStore {
39
+ /** The prior publication of this identity, or undefined if never published. */
40
+ get(identity: string): SeedPublicationRecord | undefined;
41
+ /** Record identity's latest publication (overwrites any prior entry). */
42
+ set(identity: string, record: SeedPublicationRecord): void;
43
+ }
44
+ /** In-memory store — tests, and any embedder that does not want on-disk state. */
45
+ export declare function createMemorySeedImportState(initial?: Record<string, SeedPublicationRecord>): SeedImportStateStore;
46
+ export interface FileSeedImportStateOptions {
47
+ /**
48
+ * Optional sink for the fail-closed corrupt-state warning. The read still
49
+ * throws; this hook only lets a CLI surface the same diagnostic separately.
50
+ */
51
+ onWarning?: (message: string) => void;
52
+ }
53
+ /**
54
+ * File-backed store (JSON map, `~/.jinn-client/harness-layer/seed-import-state.json`
55
+ * by default) — read-modify-write on every `set()`. Fine at seed-import's
56
+ * scale (dozens to low hundreds of curated, human-approved entries via the
57
+ * plan/execute gate), not a database.
58
+ *
59
+ * A corrupt/foreign state file is FAIL-CLOSED: reads and writes throw rather
60
+ * than treating it as empty and destroying supersedes lineage. Persistence
61
+ * writes a complete same-directory temp file and atomically renames it over
62
+ * the destination, so interruption cannot leave a partial JSON document.
63
+ */
64
+ export declare function createFileSeedImportState(path?: string, opts?: FileSeedImportStateOptions): SeedImportStateStore;
65
+ /**
66
+ * Deterministic content fingerprint for idempotency: sha256 over the
67
+ * RFC 8785 canonical JSON of the seed's meaningful fields (the caller
68
+ * decides which fields are "meaningful" — e.g. excludes capture-time-only
69
+ * fields like `capturedAt`). Two calls over the same logical content always
70
+ * agree, regardless of key order.
71
+ */
72
+ export declare function hashSeedContent(value: unknown): string;