dna-sdk 0.3.1 → 0.5.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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `sourceFromUrl` — the URL → SourcePort factory, as a PUBLIC SDK surface.
3
+ *
4
+ * 1:1 parity with python `dna/adapters/source_url.py`. Promotes URL→adapter
5
+ * resolution out of ad-hoc host code so `fromConfig()` and any host share ONE
6
+ * factory.
7
+ *
8
+ * Scheme map (TS runtime):
9
+ *
10
+ * file:// <path> → FilesystemSource (read/write on disk)
11
+ * fs:// <path> → alias of file://
12
+ * <plain path> → treated as file://<path>
13
+ * postgresql:// … → PostgresSource (node-postgres)
14
+ * postgres:// … → alias of postgresql://
15
+ * sqlite:// <path> → NOT SUPPORTED in the TS runtime (Python-only; the
16
+ * SqlAlchemy adapter is a Python thing) — fails loud.
17
+ *
18
+ * An unknown scheme fails loud with the supported set.
19
+ */
20
+ import type { SourcePort } from "../kernel/protocols.js";
21
+ export declare class UnsupportedSourceScheme extends Error {
22
+ constructor(message: string);
23
+ }
24
+ interface SourceFromUrlOptions {
25
+ /** Postgres schema namespace (postgres sources only). */
26
+ schema?: string;
27
+ }
28
+ /**
29
+ * Build a source from a scheme URL (see module docstring).
30
+ *
31
+ * Postgres sources run their migrations on first use; this awaits `init()` so
32
+ * the returned source is ready.
33
+ */
34
+ export declare function sourceFromUrl(url: string, opts?: SourceFromUrlOptions): Promise<SourcePort>;
35
+ /**
36
+ * The `file://` URL the SDK falls back to with no explicit config.
37
+ * Priority: explicit override > `DNA_BASE_DIR` env > `./.dna`.
38
+ */
39
+ export declare function resolveDefaultFsUrl(baseDirOverride?: string): string;
40
+ export {};
@@ -0,0 +1,55 @@
1
+ export class UnsupportedSourceScheme extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "UnsupportedSourceScheme";
5
+ Object.setPrototypeOf(this, new.target.prototype);
6
+ }
7
+ }
8
+ function urlToFsPath(url) {
9
+ // Mirror the Python rule (join netloc + path so `fs://./x` stays RELATIVE).
10
+ const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(.*)$/.exec(url);
11
+ if (!m)
12
+ return url; // plain path, no scheme
13
+ return m[2]; // everything after scheme:// (host+path joined)
14
+ }
15
+ function schemeOf(url) {
16
+ const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
17
+ return (m ? m[1] : "file").toLowerCase();
18
+ }
19
+ /**
20
+ * Build a source from a scheme URL (see module docstring).
21
+ *
22
+ * Postgres sources run their migrations on first use; this awaits `init()` so
23
+ * the returned source is ready.
24
+ */
25
+ export async function sourceFromUrl(url, opts = {}) {
26
+ const scheme = schemeOf(url);
27
+ const base = scheme.split("+", 1)[0];
28
+ if (scheme === "file" || scheme === "fs" || scheme === "") {
29
+ const { FilesystemSource } = await import("./filesystem/source.js");
30
+ return new FilesystemSource(urlToFsPath(url) || url);
31
+ }
32
+ if (base === "postgresql" || base === "postgres") {
33
+ const { PostgresSource } = await import("./postgres/source.js");
34
+ const src = new PostgresSource({ connectionString: url, schema: opts.schema });
35
+ await src.init();
36
+ return src;
37
+ }
38
+ if (base === "sqlite") {
39
+ throw new UnsupportedSourceScheme(`sqlite:// sources are Python-only — they ride the Python SqlAlchemy ` +
40
+ `adapter, which the TypeScript runtime does not ship. Use file:// ` +
41
+ `(filesystem) or postgresql:// here. Got: ${url}`);
42
+ }
43
+ throw new UnsupportedSourceScheme(`unsupported source URL scheme '${scheme}://' — the TS runtime ships ` +
44
+ `file:// (filesystem) and postgresql:// adapters. Got: ${url}`);
45
+ }
46
+ /**
47
+ * The `file://` URL the SDK falls back to with no explicit config.
48
+ * Priority: explicit override > `DNA_BASE_DIR` env > `./.dna`.
49
+ */
50
+ export function resolveDefaultFsUrl(baseDirOverride) {
51
+ const base = baseDirOverride ?? process.env.DNA_BASE_DIR;
52
+ if (base)
53
+ return `file://${base}`;
54
+ return "file://.dna";
55
+ }
@@ -43,3 +43,18 @@ export declare function createRuntimeWithBuiltins(): Runtime;
43
43
  * `ManifestInstance` for the given scope.
44
44
  */
45
45
  export declare function quickManifest(scope: string, baseDir?: string): Promise<ManifestInstance>;
46
+ /**
47
+ * Boot a fully-wired Kernel from a `dna.config.yaml` (declarative port wiring —
48
+ * s-dx-kernel-from-config). TS twin of python `Kernel.from_config`.
49
+ *
50
+ * The config selects the `source` (`file://` / `postgresql://`) and,
51
+ * optionally, the `search` + `embedding` providers; every port is resolved to
52
+ * its adapter and wired. With NO config present (and no `path`) the behavior is
53
+ * unchanged — a filesystem `.dna` source, exactly like the bare default.
54
+ *
55
+ * Returns a wired Kernel; call `.instance(scope)` for the ManifestInstance.
56
+ * (Python exposes this as the `Kernel.from_config` static; the TS bootstrap
57
+ * keeps it a free function, mirroring `quickInstance` — same documented
58
+ * asymmetry the SDK already lives with.)
59
+ */
60
+ export declare function fromConfig(path?: string): Promise<Kernel>;
package/dist/bootstrap.js CHANGED
@@ -40,6 +40,8 @@ import { TestkitExtension } from "./extensions/testkit.js";
40
40
  import { ModelRegExtension } from "./extensions/modelreg.js";
41
41
  import { AutomationExtension } from "./extensions/automation.js";
42
42
  import { EvalExtension } from "./extensions/eval.js";
43
+ import { loadConfig } from "./config.js";
44
+ import { resolveDefaultFsUrl, sourceFromUrl } from "./adapters/source-url.js";
43
45
  /**
44
46
  * The canonical built-in extension set. Loaded into BOTH a Kernel and a
45
47
  * Runtime through this SINGLE list so the two bootstraps can never drift
@@ -132,3 +134,84 @@ export async function quickManifest(scope, baseDir = ".dna") {
132
134
  rt.resolver("github", new GitHubResolver());
133
135
  return rt.manifest(scope);
134
136
  }
137
+ /** Minimal CachePort for non-filesystem sources (all docs self-contained).
138
+ * TS twin of python `kernel_bootstrap._NoopCache`. */
139
+ class NoopCache {
140
+ async loadAll() {
141
+ return [];
142
+ }
143
+ async loadKey() {
144
+ return [];
145
+ }
146
+ async store(_scope, _key, _items) {
147
+ // no-op
148
+ }
149
+ async has() {
150
+ return true; // pretend always hit → skip resolver calls
151
+ }
152
+ }
153
+ /**
154
+ * Boot a fully-wired Kernel from a `dna.config.yaml` (declarative port wiring —
155
+ * s-dx-kernel-from-config). TS twin of python `Kernel.from_config`.
156
+ *
157
+ * The config selects the `source` (`file://` / `postgresql://`) and,
158
+ * optionally, the `search` + `embedding` providers; every port is resolved to
159
+ * its adapter and wired. With NO config present (and no `path`) the behavior is
160
+ * unchanged — a filesystem `.dna` source, exactly like the bare default.
161
+ *
162
+ * Returns a wired Kernel; call `.instance(scope)` for the ManifestInstance.
163
+ * (Python exposes this as the `Kernel.from_config` static; the TS bootstrap
164
+ * keeps it a free function, mirroring `quickInstance` — same documented
165
+ * asymmetry the SDK already lives with.)
166
+ */
167
+ export async function fromConfig(path) {
168
+ const cfg = loadConfig(path);
169
+ const sourceUrl = cfg ? cfg.source : resolveDefaultFsUrl();
170
+ const source = await sourceFromUrl(sourceUrl);
171
+ const k = createKernelWithBuiltins();
172
+ k.source(source);
173
+ if (source.supportsReaders) {
174
+ const baseDir = source.baseDir;
175
+ k.cache(new FilesystemCache(baseDir));
176
+ k.resolver("local", new LocalResolver(baseDir));
177
+ k.resolver("http", new HttpResolver());
178
+ k.resolver("https", new HttpResolver());
179
+ k.resolver("github", new GitHubResolver());
180
+ }
181
+ else {
182
+ k.cache(new NoopCache());
183
+ }
184
+ // Uniform writer/reader wiring for any KernelAttachable source.
185
+ const attach = source.attachKernel;
186
+ if (typeof attach === "function")
187
+ attach.call(source, k);
188
+ if (cfg) {
189
+ await wireSearch(k, cfg);
190
+ await wireEmbedding(k, cfg);
191
+ }
192
+ return k;
193
+ }
194
+ async function wireEmbedding(kernel, cfg) {
195
+ if (cfg.embedding === "off" || cfg.embedding === "fake")
196
+ return;
197
+ if (cfg.embedding === "onnx") {
198
+ // Dynamic import: the ONNX adapter (transformers.js) must never enter the
199
+ // default bundle (guard: tests/embedding-import-isolation.test.ts).
200
+ const { OnnxEmbeddingProvider } = await import("./adapters/embedding/onnx.js");
201
+ kernel.embeddingProvider(new OnnxEmbeddingProvider());
202
+ }
203
+ }
204
+ async function wireSearch(kernel, cfg) {
205
+ if (cfg.search === "off")
206
+ return;
207
+ if (cfg.search === "sqlite-vec") {
208
+ // Dynamic import: sqlite-vec must never enter the default bundle
209
+ // (guard: tests/search-import-isolation.test.ts).
210
+ const { SqliteVecRecordSearchProvider } = await import("./adapters/search/sqlite-vec.js");
211
+ kernel.recordSearchProvider(new SqliteVecRecordSearchProvider(kernel, {}));
212
+ }
213
+ else if (cfg.search === "pgvector") {
214
+ throw new Error("search: pgvector is Python-only in this build — the TS runtime ships " +
215
+ "the sqlite-vec record-search provider. Use `search: sqlite-vec` here.");
216
+ }
217
+ }
@@ -0,0 +1,34 @@
1
+ export declare const CONFIG_FILENAME = "dna.config.yaml";
2
+ declare const VALID_SEARCH: readonly ["off", "pgvector", "sqlite-vec"];
3
+ declare const VALID_EMBEDDING: readonly ["off", "fake", "onnx"];
4
+ export type SearchMode = (typeof VALID_SEARCH)[number];
5
+ export type EmbeddingMode = (typeof VALID_EMBEDDING)[number];
6
+ /** Parsed + validated `dna.config.yaml`. */
7
+ export interface DnaConfig {
8
+ /** Scheme URL: `file://` | `sqlite://` | `postgresql://`. */
9
+ source: string;
10
+ /** Record-search provider selector (default `off`). */
11
+ search: SearchMode;
12
+ /** Embedding provider selector (default `off`). */
13
+ embedding: EmbeddingMode;
14
+ /** Where it was loaded from (`null` for a synthesized default). */
15
+ path: string | null;
16
+ }
17
+ /**
18
+ * Path to `dna.config.yaml` in `start` (default: cwd), or `null` if absent.
19
+ * Deliberately NOT a walk-up — a config's meaning is tied to the boot dir.
20
+ */
21
+ export declare function findConfig(start?: string): string | null;
22
+ /**
23
+ * Load + validate `dna.config.yaml`.
24
+ *
25
+ * - `path` given → it MUST exist (a typo'd path is an error, not a silent
26
+ * fallback); parsed and validated.
27
+ * - `path` omitted → look for `dna.config.yaml` in cwd. Found → parsed. Absent
28
+ * → `null` (the caller keeps its default: a filesystem `.dna` source).
29
+ *
30
+ * Throws on: not-a-mapping, missing `source`, unknown keys, or an out-of-enum
31
+ * `search` / `embedding` value.
32
+ */
33
+ export declare function loadConfig(path?: string): DnaConfig | null;
34
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `dna.config.yaml` — declarative port wiring (s-dx-kernel-from-config).
3
+ *
4
+ * 1:1 parity with python `dna/config.py`. The SAME file drives both SDKs — the
5
+ * schema is language-agnostic:
6
+ *
7
+ * ```yaml
8
+ * # dna.config.yaml
9
+ * source: postgresql://user:pass@host/db # or sqlite:///./dev.db, file://.dna
10
+ * search: pgvector # pgvector | sqlite-vec | off (default: off)
11
+ * embedding: onnx # onnx | fake | off (default: off / fake floor)
12
+ * ```
13
+ *
14
+ * Only `source` is required. This module parses + VALIDATES the file (unknown
15
+ * keys and bad enum values fail loud); the wiring lives in `fromConfig()`.
16
+ *
17
+ * Runtime asymmetry (documented, not silent): the TS runtime ships the
18
+ * filesystem + postgres source adapters; `sqlite://` is Python-only (it rides
19
+ * the Python-only SqlAlchemy adapter). A `sqlite://` source — or a `pgvector`
20
+ * search — in a TS host fails loud with that explanation at wire time.
21
+ */
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import yaml from "js-yaml";
25
+ export const CONFIG_FILENAME = "dna.config.yaml";
26
+ const VALID_SEARCH = ["off", "pgvector", "sqlite-vec"];
27
+ const VALID_EMBEDDING = ["off", "fake", "onnx"];
28
+ const KNOWN_KEYS = new Set(["source", "search", "embedding"]);
29
+ /**
30
+ * Path to `dna.config.yaml` in `start` (default: cwd), or `null` if absent.
31
+ * Deliberately NOT a walk-up — a config's meaning is tied to the boot dir.
32
+ */
33
+ export function findConfig(start) {
34
+ const candidate = join(start ?? process.cwd(), CONFIG_FILENAME);
35
+ return existsSync(candidate) ? candidate : null;
36
+ }
37
+ /**
38
+ * Load + validate `dna.config.yaml`.
39
+ *
40
+ * - `path` given → it MUST exist (a typo'd path is an error, not a silent
41
+ * fallback); parsed and validated.
42
+ * - `path` omitted → look for `dna.config.yaml` in cwd. Found → parsed. Absent
43
+ * → `null` (the caller keeps its default: a filesystem `.dna` source).
44
+ *
45
+ * Throws on: not-a-mapping, missing `source`, unknown keys, or an out-of-enum
46
+ * `search` / `embedding` value.
47
+ */
48
+ export function loadConfig(path) {
49
+ let resolved;
50
+ if (path !== undefined) {
51
+ if (!existsSync(path)) {
52
+ throw new Error(`dna config not found at ${path} — pass a path to an existing ` +
53
+ `${CONFIG_FILENAME}, or omit it to auto-discover one in the ` +
54
+ `current directory.`);
55
+ }
56
+ resolved = path;
57
+ }
58
+ else {
59
+ const found = findConfig();
60
+ if (found === null)
61
+ return null;
62
+ resolved = found;
63
+ }
64
+ const raw = yaml.load(readFileSync(resolved, "utf-8"));
65
+ return parse(raw, resolved);
66
+ }
67
+ function parse(raw, path) {
68
+ if (raw === null || raw === undefined) {
69
+ throw new Error(`${path} is empty — it must at least declare a \`source:\` URL ` +
70
+ `(e.g. \`source: file://.dna\`).`);
71
+ }
72
+ if (typeof raw !== "object" || Array.isArray(raw)) {
73
+ throw new Error(`${path} must be a YAML mapping (key: value).`);
74
+ }
75
+ const obj = raw;
76
+ const unknown = Object.keys(obj).filter((k) => !KNOWN_KEYS.has(k)).sort();
77
+ if (unknown.length > 0) {
78
+ throw new Error(`${path}: unknown key(s) ${JSON.stringify(unknown)} — supported keys ` +
79
+ `are ${JSON.stringify([...KNOWN_KEYS].sort())}.`);
80
+ }
81
+ const source = obj.source;
82
+ if (typeof source !== "string" || source.length === 0) {
83
+ throw new Error(`${path}: \`source:\` is required and must be a URL string ` +
84
+ `(file:// | sqlite:// | postgresql://).`);
85
+ }
86
+ const search = String(obj.search ?? "off").trim() || "off";
87
+ if (!VALID_SEARCH.includes(search)) {
88
+ throw new Error(`${path}: \`search: ${search}\` is not valid — choose one of ` +
89
+ `${JSON.stringify(VALID_SEARCH)}.`);
90
+ }
91
+ const embedding = String(obj.embedding ?? "off").trim() || "off";
92
+ if (!VALID_EMBEDDING.includes(embedding)) {
93
+ throw new Error(`${path}: \`embedding: ${embedding}\` is not valid — choose one of ` +
94
+ `${JSON.stringify(VALID_EMBEDDING)}.`);
95
+ }
96
+ return {
97
+ source,
98
+ search: search,
99
+ embedding: embedding,
100
+ path,
101
+ };
102
+ }
@@ -13,6 +13,33 @@ import { readSpecString, readSpecStringArray } from "../kernel/spec-access.js";
13
13
  import { SettingKind, ThemeKind, UserProfileKind, CanvasKind } from "./helix_extras.js";
14
14
  import { registerWriteGuards } from "./helix/write-guards.js";
15
15
  const MOD_URL = import.meta.url;
16
+ // ── Named composition layouts (s-dx-named-layouts) ─────────────────────
17
+ //
18
+ // An author orders persona-vs-instruction by NAME (`layout:` in the Agent
19
+ // spec) instead of hand-writing raw Mustache with internal section names.
20
+ // Each preset resolves to one of these embedded templates via
21
+ // `AgentKind.layoutTemplate()`. 1:1 with Python `dna.extensions.helix`.
22
+ //
23
+ // The guardrails block is shared verbatim — guardrails are hard policy and
24
+ // always land LAST, after both the instruction and the soul, regardless of
25
+ // their relative order. (Aligns TS composition to Python, closing the
26
+ // pre-existing i-213/i-011 divergence where TS omitted the guardrails block.)
27
+ const GUARDRAILS_BLOCK = "{{#guardrails-guardrail}}" +
28
+ "## Guardrail: {{name}} ({{severity}})\n" +
29
+ "{{#description}}_{{description}}_\n\n{{/description}}" +
30
+ "{{#rules}}- {{{.}}}\n{{/rules}}\n" +
31
+ "{{/guardrails-guardrail}}";
32
+ // instruction-first (a.k.a. "default") — historic order: instruction, soul,
33
+ // guardrails. IS the kind default template.
34
+ const LAYOUT_INSTRUCTION_FIRST = "{{{agent.instruction}}}\n\n{{{soul_content}}}\n\n" + GUARDRAILS_BLOCK;
35
+ // persona-first — Soul leads, then instruction, then guardrails.
36
+ const LAYOUT_PERSONA_FIRST = "{{{soul_content}}}\n\n{{{agent.instruction}}}\n\n" + GUARDRAILS_BLOCK;
37
+ const AGENT_LAYOUTS = {
38
+ default: LAYOUT_INSTRUCTION_FIRST,
39
+ "instruction-first": LAYOUT_INSTRUCTION_FIRST,
40
+ "persona-first": LAYOUT_PERSONA_FIRST,
41
+ };
42
+ const AGENT_LAYOUT_NAMES = ["default", "instruction-first", "persona-first"];
16
43
  // GenomeKind — Phase 16 (scope segregation)
17
44
  //
18
45
  // Replaces ModuleKind as the scope-root identity Kind. Carries catalog
@@ -253,6 +280,13 @@ class AgentKind extends KindBase {
253
280
  },
254
281
  objective: { widget: "textarea", label: "Objective", order: 15 },
255
282
  model: { widget: "text", label: "Model", order: 20 },
283
+ layout: {
284
+ widget: "select",
285
+ label: "Layout",
286
+ options: ["default", "instruction-first", "persona-first"],
287
+ help: "Named composition order — 'persona-first' puts the Soul before the instruction. Leave empty for the default. A raw promptTemplate, if set, overrides this.",
288
+ order: 25,
289
+ },
256
290
  soul: { widget: "text", label: "Soul", help: "Name of the Soul doc to flatten into the prompt.", order: 30 },
257
291
  skills: { widget: "tags", label: "Skills", order: 40 },
258
292
  actors: { widget: "tags", label: "Actors this agent serves", order: 50 },
@@ -305,7 +339,17 @@ class AgentKind extends KindBase {
305
339
  return { skills: skills.length, soul: readSpecString(doc, "soul") ?? null };
306
340
  }
307
341
  promptTemplate() {
308
- return "{{{agent.instruction}}}\n\n{{{soul_content}}}";
342
+ // IS the `instruction-first` / `default` named layout — the kind default
343
+ // template and the `default` layout are one string, so an agent with no
344
+ // `layout:` composes identically. (Now includes the guardrails block,
345
+ // matching Python — see the layout-constants comment above.)
346
+ return LAYOUT_INSTRUCTION_FIRST;
347
+ }
348
+ layoutTemplate(name) {
349
+ return AGENT_LAYOUTS[name] ?? null;
350
+ }
351
+ layoutNames() {
352
+ return [...AGENT_LAYOUT_NAMES];
309
353
  }
310
354
  preview(doc) {
311
355
  const spec = (doc.spec ?? {});
@@ -27,6 +27,7 @@
27
27
  import { KindBase } from "../kernel/kind_base.js";
28
28
  import { SD, TenantScope } from "../kernel/protocols.js";
29
29
  import { loadDescriptors } from "../kernel/descriptor-loader.js";
30
+ import { HtmlArtifactSchema } from "../kernel/models.js";
30
31
  const API_VERSION = "github.com/ruinosus/dna/sdlc/v1";
31
32
  // v1.3: MILESTONE_STATUSES → EPIC_STATUSES (Jira/ADO alignment).
32
33
  const EPIC_STATUSES = ["planning", "in-progress", "done", "cancelled", "deprecated"];
@@ -1280,6 +1281,161 @@ const JOURNEY_METHODOLOGIES = [
1280
1281
  // the old class is frozen in sdk-py tests/test_kaizen_descriptor_equivalence.py
1281
1282
  // + tests/sdlc.test.ts.
1282
1283
  // ---------------------------------------------------------------------------
1284
+ // ---------------------------------------------------------------------------
1285
+ // HtmlArtifact — an HTML page as a first-class work-item output (record plane)
1286
+ //
1287
+ // s-dx-html-artifact-kind. 1:1 parity with Python HtmlArtifactKind. Bundle
1288
+ // Kind: ARTIFACT.html holds the raw HTML VERBATIM (byte-faithful round-trip —
1289
+ // no frontmatter injection), plus an optional artifact.json companion with
1290
+ // structured metadata (title, description, source, created_at) — the same
1291
+ // mechanic as a Soul (SOUL.md + soul.json). Custom reader/writer (not the
1292
+ // generic marker-frontmatter path) because the marker is arbitrary HTML.
1293
+ // ---------------------------------------------------------------------------
1294
+ class HtmlArtifactKind extends KindBase {
1295
+ apiVersion = API_VERSION;
1296
+ kind = "HtmlArtifact";
1297
+ // alias GENERATED = "<owner>-<kebab(kind)>" = "sdlc-html-artifact".
1298
+ // s-alias-generated-not-typed: new Kinds must NOT type an explicit alias
1299
+ // (EXPLICIT_ALIAS_ALLOWLIST ratchet is shrink-only). Empty here + aliasOwner
1300
+ // triggers generation at load time (Py twin sets alias=None + alias_owner).
1301
+ alias = "";
1302
+ aliasOwner = "sdlc";
1303
+ origin = "github.com/ruinosus/dna/sdlc";
1304
+ isPromptTarget = false;
1305
+ promptTargetPriority = 0;
1306
+ flattenInContext = false;
1307
+ plane = "record";
1308
+ storage = SD.bundle("html-artifacts", "ARTIFACT.html");
1309
+ graphStyle = { fill: "#EA580C", stroke: "#C2410C", textColor: "#fff" };
1310
+ asciiIcon = "📄";
1311
+ displayLabel = "HTML Artifacts";
1312
+ uiSchema = {
1313
+ html: {
1314
+ widget: "code",
1315
+ language: "html",
1316
+ label: "ARTIFACT.html",
1317
+ help: "The raw HTML page (stored byte-faithful).",
1318
+ height: 520,
1319
+ order: 10,
1320
+ },
1321
+ artifact_json: {
1322
+ widget: "code",
1323
+ language: "json",
1324
+ label: "artifact.json",
1325
+ help: "Structured metadata (title, description, source, created_at).",
1326
+ height: 220,
1327
+ order: 20,
1328
+ },
1329
+ };
1330
+ docs = "An HtmlArtifact stores an HTML page as a first-class, linkable output " +
1331
+ "of a work item (Story/Feature/Epic/Spike). Bundle: ARTIFACT.html holds " +
1332
+ "the raw HTML verbatim (byte-faithful round-trip) plus an optional " +
1333
+ "artifact.json companion with structured metadata (title, description, " +
1334
+ "source, created_at) — the same shape as a Soul's SOUL.md + soul.json. " +
1335
+ "Attach one with `dna sdlc produces add <WiKind>/<wi> HtmlArtifact/<name>`.";
1336
+ schema() {
1337
+ return {
1338
+ type: "object",
1339
+ additionalProperties: false,
1340
+ properties: {
1341
+ html: { type: "string", description: "The raw HTML document (byte-faithful)." },
1342
+ artifact_json: {
1343
+ type: "object",
1344
+ additionalProperties: true,
1345
+ description: "Structured metadata: title, description, source, created_at.",
1346
+ },
1347
+ },
1348
+ };
1349
+ }
1350
+ parse(raw) {
1351
+ return HtmlArtifactSchema.parse(raw);
1352
+ }
1353
+ summary(doc) {
1354
+ const spec = (doc.spec ?? {});
1355
+ const aj = spec.artifact_json ?? {};
1356
+ return {
1357
+ title: aj.title ?? null,
1358
+ source: aj.source ?? null,
1359
+ created_at: aj.created_at ?? null,
1360
+ html_bytes: (spec.html ?? "").length,
1361
+ };
1362
+ }
1363
+ preview(doc) {
1364
+ const spec = (doc.spec ?? {});
1365
+ const blocks = [];
1366
+ if (typeof spec.html === "string" && spec.html) {
1367
+ blocks.push({ kind: "code", title: "ARTIFACT.html", body: spec.html, language: "html" });
1368
+ }
1369
+ if (spec.artifact_json && typeof spec.artifact_json === "object") {
1370
+ blocks.push({
1371
+ kind: "code",
1372
+ title: "artifact.json",
1373
+ body: JSON.stringify(spec.artifact_json, null, 2),
1374
+ language: "json",
1375
+ });
1376
+ }
1377
+ if (blocks.length === 0) {
1378
+ return [{ kind: "empty", title: "HtmlArtifact (empty)" }];
1379
+ }
1380
+ return blocks;
1381
+ }
1382
+ }
1383
+ class HtmlArtifactReader {
1384
+ async detect(bundle) {
1385
+ return bundle.exists("ARTIFACT.html");
1386
+ }
1387
+ async read(bundle) {
1388
+ const spec = {};
1389
+ const metadata = {};
1390
+ // ARTIFACT.html — read verbatim (byte-faithful; no frontmatter parse).
1391
+ if (await bundle.exists("ARTIFACT.html")) {
1392
+ spec.html = await bundle.readText("ARTIFACT.html");
1393
+ }
1394
+ // artifact.json companion — structured metadata.
1395
+ if (await bundle.exists("artifact.json")) {
1396
+ const aj = JSON.parse(await bundle.readText("artifact.json"));
1397
+ if (aj && typeof aj === "object" && !Array.isArray(aj)) {
1398
+ spec.artifact_json = aj;
1399
+ const desc = aj.description;
1400
+ if (typeof desc === "string" && desc && !metadata.description) {
1401
+ metadata.description = desc;
1402
+ }
1403
+ }
1404
+ }
1405
+ if (metadata.name == null)
1406
+ metadata.name = bundle.name;
1407
+ return {
1408
+ apiVersion: API_VERSION,
1409
+ kind: "HtmlArtifact",
1410
+ metadata,
1411
+ spec,
1412
+ };
1413
+ }
1414
+ }
1415
+ class HtmlArtifactWriter {
1416
+ canWrite(raw) {
1417
+ return raw.kind === "HtmlArtifact";
1418
+ }
1419
+ serialize(raw) {
1420
+ const files = [];
1421
+ const spec = raw.spec ?? {};
1422
+ // ARTIFACT.html — verbatim HTML (byte-faithful).
1423
+ files.push({ relativePath: "ARTIFACT.html", content: spec.html ?? "" });
1424
+ // artifact.json — canonical JSON companion. metadata.description is a
1425
+ // DERIVED promotion of artifact_json.description on read, so it is NOT
1426
+ // re-emitted separately (no phantom frontmatter — F3 market-fidelity).
1427
+ const aj = spec.artifact_json;
1428
+ if (aj && typeof aj === "object") {
1429
+ files.push({ relativePath: "artifact.json", content: JSON.stringify(aj, null, 2) });
1430
+ }
1431
+ return files;
1432
+ }
1433
+ async write(bundle, raw) {
1434
+ for (const f of this.serialize(raw)) {
1435
+ await bundle.writeText(f.relativePath, f.content ?? "");
1436
+ }
1437
+ }
1438
+ }
1283
1439
  export class SdlcExtension {
1284
1440
  name = "sdlc";
1285
1441
  // v1.14.0 — s-consolidate-cognitive-policies: the 9 cognitive policy
@@ -1302,6 +1458,12 @@ export class SdlcExtension {
1302
1458
  // v1.10.0 — f-reference-citation-kind (ported from Python for
1303
1459
  // s-alias-generated-not-typed: Spike.depFilters → "sdlc-reference").
1304
1460
  kernel.kind(new ReferenceKind());
1461
+ // s-dx-html-artifact-kind — HTML page as a first-class work-item output.
1462
+ // Bundle Kind with a custom reader/writer for byte-faithful HTML,
1463
+ // mirroring the Soul (SOUL.md + soul.json) mechanic.
1464
+ kernel.kind(new HtmlArtifactKind());
1465
+ kernel.reader(new HtmlArtifactReader());
1466
+ kernel.writer(new HtmlArtifactWriter());
1305
1467
  // expr batch B: PromptTemplate is a descriptor now — registered via the
1306
1468
  // loadDescriptors loop below. s-consolidate-cognitive-policies: the
1307
1469
  // cognitive policy family is ONE descriptor (cognitive-policy.kind.yaml).
package/dist/index.d.ts CHANGED
@@ -21,7 +21,12 @@ export { LockManager } from "./kernel/lock-manager.js";
21
21
  export { ReportBuilder } from "./kernel/reports.js";
22
22
  export { serializeRawToFiles } from "./kernel/serialize-to-files.js";
23
23
  export * from "./viz/index.js";
24
- export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, quickManifest } from "./bootstrap.js";
24
+ export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, quickManifest, fromConfig } from "./bootstrap.js";
25
+ export { AgentNotFound, UnknownLayout } from "./kernel/errors.js";
26
+ export { PromptLibrary, loadPrompts } from "./prompts.js";
27
+ export { loadConfig, findConfig, CONFIG_FILENAME } from "./config.js";
28
+ export type { DnaConfig, SearchMode, EmbeddingMode } from "./config.js";
29
+ export { sourceFromUrl, resolveDefaultFsUrl, UnsupportedSourceScheme } from "./adapters/source-url.js";
25
30
  export { HookRegistry, KNOWN_HOOK_NAMES } from "./kernel/hooks.js";
26
31
  export type { HookContext, HookName, HookNameArg, Middleware, EventHandler, PreSaveContext, VetoHandler, } from "./kernel/hooks.js";
27
32
  export type { SourcePort, CachePort, ResolverPort, ReaderPort, WriterPort, KindPort, KindPresentation, Extension, ExtensionHost, CompositionResult, CacheItem, ResolvedItem, StorageDescriptor, StoragePattern, BodyMode, SerializedFile, SerializedDocument, WritableSourcePort, QueryFilter, QueryOrder, CountResult, SourceQueryOpts, SourceCountOpts, RecordStorePort, RecordSearchProvider, EmbeddingPort, ToolPort, } from "./kernel/protocols.js";
package/dist/index.js CHANGED
@@ -24,7 +24,13 @@ export { serializeRawToFiles } from "./kernel/serialize-to-files.js";
24
24
  // Viz — diagram generators, health, matrix, ASCII tree
25
25
  export * from "./viz/index.js";
26
26
  // Opinionated bootstrap — extension wiring lives here, not in kernel/
27
- export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, quickManifest } from "./bootstrap.js";
27
+ export { createKernelWithBuiltins, quickInstance, createRuntimeWithBuiltins, quickManifest, fromConfig } from "./bootstrap.js";
28
+ // DX consumer surface (s-dx-*): fail-loud prompt building + the collapse-the-
29
+ // shim helper + declarative port wiring.
30
+ export { AgentNotFound, UnknownLayout } from "./kernel/errors.js";
31
+ export { PromptLibrary, loadPrompts } from "./prompts.js";
32
+ export { loadConfig, findConfig, CONFIG_FILENAME } from "./config.js";
33
+ export { sourceFromUrl, resolveDefaultFsUrl, UnsupportedSourceScheme } from "./adapters/source-url.js";
28
34
  export { HookRegistry, KNOWN_HOOK_NAMES } from "./kernel/hooks.js";
29
35
  // i-005 — types REFERENCED by already-exported public API, previously
30
36
  // broken xrefs in TypeDoc. Each is public surface a consumer needs to name:
@@ -13,6 +13,41 @@
13
13
  * These errors surface those problems at boot time instead of as silent
14
14
  * runtime drift.
15
15
  */
16
+ /**
17
+ * `buildPrompt({ agent: X })` was asked for an agent that no prompt-target
18
+ * document in the loaded manifest declares (missing, renamed, or unparseable).
19
+ *
20
+ * Fail-loud contract (s-dx-build-prompt-fail-loud): the builder used to RETURN
21
+ * the string `"Agent 'X' not found"` instead of throwing — which sailed
22
+ * straight through a consumer's `if (!text)` check and became the LITERAL
23
+ * agent instruction. Every consumer therefore wrote the same defensive guard
24
+ * (`mi.findAgent(x) === null`) before every call. Throwing a typed error
25
+ * deletes that guard: the miss is now impossible to ignore.
26
+ *
27
+ * 1:1 parity with python `dna.AgentNotFound` (a `LookupError` subclass).
28
+ * Exported publicly from the package root.
29
+ */
30
+ export declare class AgentNotFound extends Error {
31
+ readonly agent: string | null;
32
+ constructor(agent: string | null);
33
+ }
34
+ /**
35
+ * `buildPrompt` hit an Agent whose `layout:` names a preset the Kind does not
36
+ * offer (s-dx-named-layouts).
37
+ *
38
+ * Fail-loud DX: a typo'd layout (`persona_first` for `persona-first`) must not
39
+ * silently fall through to the Kind default and compose in the wrong order —
40
+ * it throws with the valid names listed.
41
+ *
42
+ * 1:1 parity with python `dna.UnknownLayout` (a `ValueError` subclass).
43
+ * Exported publicly from the package root.
44
+ */
45
+ export declare class UnknownLayout extends Error {
46
+ readonly layout: string;
47
+ readonly available: string[];
48
+ readonly agent: string | null;
49
+ constructor(layout: string, available?: string[], agent?: string | null);
50
+ }
16
51
  /** Base class for kernel registration validation failures. */
17
52
  export declare class KernelRegistrationError extends Error {
18
53
  constructor(message?: string);
@@ -13,6 +13,55 @@
13
13
  * These errors surface those problems at boot time instead of as silent
14
14
  * runtime drift.
15
15
  */
16
+ /**
17
+ * `buildPrompt({ agent: X })` was asked for an agent that no prompt-target
18
+ * document in the loaded manifest declares (missing, renamed, or unparseable).
19
+ *
20
+ * Fail-loud contract (s-dx-build-prompt-fail-loud): the builder used to RETURN
21
+ * the string `"Agent 'X' not found"` instead of throwing — which sailed
22
+ * straight through a consumer's `if (!text)` check and became the LITERAL
23
+ * agent instruction. Every consumer therefore wrote the same defensive guard
24
+ * (`mi.findAgent(x) === null`) before every call. Throwing a typed error
25
+ * deletes that guard: the miss is now impossible to ignore.
26
+ *
27
+ * 1:1 parity with python `dna.AgentNotFound` (a `LookupError` subclass).
28
+ * Exported publicly from the package root.
29
+ */
30
+ export class AgentNotFound extends Error {
31
+ agent;
32
+ constructor(agent) {
33
+ super(`Agent '${agent}' not found`);
34
+ this.name = "AgentNotFound";
35
+ this.agent = agent;
36
+ Object.setPrototypeOf(this, new.target.prototype);
37
+ }
38
+ }
39
+ /**
40
+ * `buildPrompt` hit an Agent whose `layout:` names a preset the Kind does not
41
+ * offer (s-dx-named-layouts).
42
+ *
43
+ * Fail-loud DX: a typo'd layout (`persona_first` for `persona-first`) must not
44
+ * silently fall through to the Kind default and compose in the wrong order —
45
+ * it throws with the valid names listed.
46
+ *
47
+ * 1:1 parity with python `dna.UnknownLayout` (a `ValueError` subclass).
48
+ * Exported publicly from the package root.
49
+ */
50
+ export class UnknownLayout extends Error {
51
+ layout;
52
+ available;
53
+ agent;
54
+ constructor(layout, available = [], agent = null) {
55
+ const where = agent ? ` on agent '${agent}'` : "";
56
+ const hint = available.length ? ` — available: ${available.join(", ")}` : "";
57
+ super(`Unknown layout '${layout}'${where}${hint}`);
58
+ this.name = "UnknownLayout";
59
+ this.layout = layout;
60
+ this.available = available;
61
+ this.agent = agent;
62
+ Object.setPrototypeOf(this, new.target.prototype);
63
+ }
64
+ }
16
65
  /** Base class for kernel registration validation failures. */
17
66
  export class KernelRegistrationError extends Error {
18
67
  constructor(message) {
@@ -94,6 +94,19 @@ export declare abstract class KindBase implements Omit<KindPort, "apiVersion" |
94
94
  describe(_doc: Document): string | null;
95
95
  summary(doc: Document): Record<string, unknown> | null;
96
96
  promptTemplate(): string | null;
97
+ /**
98
+ * Resolve a NAMED composition layout to an embedded template
99
+ * (s-dx-named-layouts). Prompt-target Kinds override this to expose
100
+ * author-friendly presets (persona-first, instruction-first) so the common
101
+ * case never hand-writes Mustache. Returns null for an unknown name (the
102
+ * prompt builder fails loud). Twin of Python ``KindBase.layout_template``.
103
+ */
104
+ layoutTemplate(_name: string): string | null;
105
+ /**
106
+ * Public layout names this Kind offers (s-dx-named-layouts) — powers
107
+ * discovery + fail-loud error messages. Twin of Python ``layout_names``.
108
+ */
109
+ layoutNames(): string[];
97
110
  protected canonicalSpec(spec: Record<string, unknown>): Record<string, unknown>;
98
111
  /** Stable SHA-256 of the doc's authored identity — basis for source
99
112
  * diff/sync. Invariant to key order, formatting, volatile stamps, and
@@ -215,6 +215,23 @@ export class KindBase {
215
215
  promptTemplate() {
216
216
  return null;
217
217
  }
218
+ /**
219
+ * Resolve a NAMED composition layout to an embedded template
220
+ * (s-dx-named-layouts). Prompt-target Kinds override this to expose
221
+ * author-friendly presets (persona-first, instruction-first) so the common
222
+ * case never hand-writes Mustache. Returns null for an unknown name (the
223
+ * prompt builder fails loud). Twin of Python ``KindBase.layout_template``.
224
+ */
225
+ layoutTemplate(_name) {
226
+ return null;
227
+ }
228
+ /**
229
+ * Public layout names this Kind offers (s-dx-named-layouts) — powers
230
+ * discovery + fail-loud error messages. Twin of Python ``layout_names``.
231
+ */
232
+ layoutNames() {
233
+ return [];
234
+ }
218
235
  // ---- Source-sync digest (s-sync-s1; twin of Python canonical_digest) ----
219
236
  canonicalSpec(spec) {
220
237
  const out = {};
@@ -384,6 +384,7 @@ export declare const AgentSpecSchema: z.ZodObject<{
384
384
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
385
385
  guardrails: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
386
386
  promptTemplate: z.ZodOptional<z.ZodString>;
387
+ layout: z.ZodOptional<z.ZodString>;
387
388
  tool_groups: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
388
389
  mcp_servers: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodObject<{
389
390
  ref: z.ZodString;
@@ -481,6 +482,7 @@ export declare const AgentSpecSchema: z.ZodObject<{
481
482
  reads: Record<string, Record<string, any>>;
482
483
  type?: string | undefined;
483
484
  promptTemplate?: string | undefined;
485
+ layout?: string | undefined;
484
486
  model?: string | undefined;
485
487
  instruction_file?: string | undefined;
486
488
  soul?: string | undefined;
@@ -517,6 +519,7 @@ export declare const AgentSpecSchema: z.ZodObject<{
517
519
  guardrails?: string[] | undefined;
518
520
  instruction?: string | undefined;
519
521
  promptTemplate?: string | undefined;
522
+ layout?: string | undefined;
520
523
  model?: string | undefined;
521
524
  instruction_file?: string | undefined;
522
525
  tags?: string[] | undefined;
@@ -601,6 +604,7 @@ export declare const AgentSchema: z.ZodObject<{
601
604
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
602
605
  guardrails: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
603
606
  promptTemplate: z.ZodOptional<z.ZodString>;
607
+ layout: z.ZodOptional<z.ZodString>;
604
608
  tool_groups: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
605
609
  mcp_servers: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodObject<{
606
610
  ref: z.ZodString;
@@ -698,6 +702,7 @@ export declare const AgentSchema: z.ZodObject<{
698
702
  reads: Record<string, Record<string, any>>;
699
703
  type?: string | undefined;
700
704
  promptTemplate?: string | undefined;
705
+ layout?: string | undefined;
701
706
  model?: string | undefined;
702
707
  instruction_file?: string | undefined;
703
708
  soul?: string | undefined;
@@ -734,6 +739,7 @@ export declare const AgentSchema: z.ZodObject<{
734
739
  guardrails?: string[] | undefined;
735
740
  instruction?: string | undefined;
736
741
  promptTemplate?: string | undefined;
742
+ layout?: string | undefined;
737
743
  model?: string | undefined;
738
744
  instruction_file?: string | undefined;
739
745
  tags?: string[] | undefined;
@@ -810,6 +816,7 @@ export declare const AgentSchema: z.ZodObject<{
810
816
  reads: Record<string, Record<string, any>>;
811
817
  type?: string | undefined;
812
818
  promptTemplate?: string | undefined;
819
+ layout?: string | undefined;
813
820
  model?: string | undefined;
814
821
  instruction_file?: string | undefined;
815
822
  soul?: string | undefined;
@@ -860,6 +867,7 @@ export declare const AgentSchema: z.ZodObject<{
860
867
  guardrails?: string[] | undefined;
861
868
  instruction?: string | undefined;
862
869
  promptTemplate?: string | undefined;
870
+ layout?: string | undefined;
863
871
  model?: string | undefined;
864
872
  instruction_file?: string | undefined;
865
873
  tags?: string[] | undefined;
@@ -1551,6 +1559,83 @@ export declare const SoulSchema: z.ZodObject<{
1551
1559
  } | undefined;
1552
1560
  }>;
1553
1561
  export type TypedSoul = z.output<typeof SoulSchema>;
1562
+ export declare const HtmlArtifactSpecSchema: z.ZodObject<{
1563
+ html: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1564
+ artifact_json: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1565
+ }, "strip", z.ZodTypeAny, {
1566
+ html: string;
1567
+ artifact_json?: Record<string, unknown> | undefined;
1568
+ }, {
1569
+ html?: string | undefined;
1570
+ artifact_json?: Record<string, unknown> | undefined;
1571
+ }>;
1572
+ export declare const HtmlArtifactSchema: z.ZodObject<{
1573
+ apiVersion: z.ZodLiteral<"github.com/ruinosus/dna/sdlc/v1">;
1574
+ kind: z.ZodLiteral<"HtmlArtifact">;
1575
+ metadata: z.ZodObject<{
1576
+ name: z.ZodString;
1577
+ description: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1578
+ version: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1579
+ icon: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1580
+ group: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1581
+ labels: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1582
+ }, "strip", z.ZodTypeAny, {
1583
+ name: string;
1584
+ description: string;
1585
+ version: string;
1586
+ icon: string;
1587
+ group: string;
1588
+ labels: Record<string, string>;
1589
+ }, {
1590
+ name: string;
1591
+ description?: string | undefined;
1592
+ version?: string | undefined;
1593
+ icon?: string | undefined;
1594
+ group?: string | undefined;
1595
+ labels?: Record<string, string> | undefined;
1596
+ }>;
1597
+ spec: z.ZodDefault<z.ZodObject<{
1598
+ html: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1599
+ artifact_json: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1600
+ }, "strip", z.ZodTypeAny, {
1601
+ html: string;
1602
+ artifact_json?: Record<string, unknown> | undefined;
1603
+ }, {
1604
+ html?: string | undefined;
1605
+ artifact_json?: Record<string, unknown> | undefined;
1606
+ }>>;
1607
+ }, "strip", z.ZodTypeAny, {
1608
+ metadata: {
1609
+ name: string;
1610
+ description: string;
1611
+ version: string;
1612
+ icon: string;
1613
+ group: string;
1614
+ labels: Record<string, string>;
1615
+ };
1616
+ spec: {
1617
+ html: string;
1618
+ artifact_json?: Record<string, unknown> | undefined;
1619
+ };
1620
+ apiVersion: "github.com/ruinosus/dna/sdlc/v1";
1621
+ kind: "HtmlArtifact";
1622
+ }, {
1623
+ metadata: {
1624
+ name: string;
1625
+ description?: string | undefined;
1626
+ version?: string | undefined;
1627
+ icon?: string | undefined;
1628
+ group?: string | undefined;
1629
+ labels?: Record<string, string> | undefined;
1630
+ };
1631
+ apiVersion: "github.com/ruinosus/dna/sdlc/v1";
1632
+ kind: "HtmlArtifact";
1633
+ spec?: {
1634
+ html?: string | undefined;
1635
+ artifact_json?: Record<string, unknown> | undefined;
1636
+ } | undefined;
1637
+ }>;
1638
+ export type TypedHtmlArtifact = z.output<typeof HtmlArtifactSchema>;
1554
1639
  export declare const AgentDefinitionSpecSchema: z.ZodObject<{
1555
1640
  content: z.ZodDefault<z.ZodOptional<z.ZodString>>;
1556
1641
  }, "strip", z.ZodTypeAny, {
@@ -128,6 +128,12 @@ export const AgentSpecSchema = z.object({
128
128
  tags: z.array(z.string()).default([]),
129
129
  guardrails: z.array(z.string()).default([]),
130
130
  promptTemplate: z.string().optional(),
131
+ // s-dx-named-layouts — pick the composition ORDER by name instead of
132
+ // hand-writing raw Mustache. "persona-first" puts the Soul before the
133
+ // instruction; "instruction-first" (a.k.a. "default") keeps the historic
134
+ // order. Resolved by the Kind's layoutTemplate() into an embedded preset.
135
+ // A raw promptTemplate still wins over layout when both are set.
136
+ layout: z.string().optional(),
131
137
  // Phase 14x — tool-group specialization (TS parity with Python).
132
138
  tool_groups: z.array(z.string()).default([]),
133
139
  // s-mcp-servers-on-agent (2026-07-07, spec
@@ -393,6 +399,19 @@ export const SoulSchema = z.object({
393
399
  spec: SoulSpecSchema.default({}),
394
400
  });
395
401
  // ---------------------------------------------------------------------------
402
+ // HtmlArtifact (github.com/ruinosus/dna/sdlc/v1)
403
+ // ---------------------------------------------------------------------------
404
+ export const HtmlArtifactSpecSchema = z.object({
405
+ html: z.string().optional().default(""),
406
+ artifact_json: z.record(z.unknown()).optional(),
407
+ });
408
+ export const HtmlArtifactSchema = z.object({
409
+ apiVersion: z.literal("github.com/ruinosus/dna/sdlc/v1"),
410
+ kind: z.literal("HtmlArtifact"),
411
+ metadata: MetadataSchema,
412
+ spec: HtmlArtifactSpecSchema.default({}),
413
+ });
414
+ // ---------------------------------------------------------------------------
396
415
  // AgentDefinition (agents.md/v1)
397
416
  // ---------------------------------------------------------------------------
398
417
  export const AgentDefinitionSpecSchema = z.object({
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import Mustache from "mustache";
12
12
  import { stripPromptBlock } from "./_text.js";
13
+ import { AgentNotFound, UnknownLayout } from "./errors.js";
13
14
  // ---------------------------------------------------------------------------
14
15
  // PromptBuilder
15
16
  // ---------------------------------------------------------------------------
@@ -38,7 +39,10 @@ export class PromptBuilder {
38
39
  }
39
40
  const agentDoc = agentName ? this._findAgent(agentName) : null;
40
41
  if (!agentDoc) {
41
- return `Agent '${agentName}' not found`;
42
+ // Fail loud (s-dx-build-prompt-fail-loud): throw a typed error instead
43
+ // of returning a placeholder string that would become the literal
44
+ // instruction.
45
+ throw new AgentNotFound(agentName);
42
46
  }
43
47
  // Build context — merge backwards-compat params into enabledSlots
44
48
  const enabledSlots = { ...(opts?.enabledSlots ?? {}) };
@@ -69,7 +73,10 @@ export class PromptBuilder {
69
73
  data: {},
70
74
  });
71
75
  }
72
- return prompt;
76
+ // Clean output (s-dx-clean-composition-output): template sections can pad
77
+ // the composed prompt with trailing newlines; consumers used to strip them
78
+ // themselves. Return it already clean.
79
+ return prompt.replace(/\n+$/, "");
73
80
  }
74
81
  // -------------------------------------------------------------------------
75
82
  // Private helpers
@@ -199,22 +206,33 @@ export class PromptBuilder {
199
206
  async _renderPrompt(ctx, agentDoc) {
200
207
  const agentSpec = agentDoc.spec;
201
208
  const kinds = this.host._kinds;
202
- // 1. Agent-level template override
209
+ // 1. Agent-level raw template override (poweruser escape hatch).
203
210
  const agentTemplate = agentSpec.promptTemplate ??
204
211
  agentSpec.prompt_template ??
205
212
  null;
206
213
  if (agentTemplate) {
207
214
  return await this._mustacheRender(agentTemplate, ctx);
208
215
  }
209
- // 2. Kind default template
210
216
  const agentKp = kinds.get(`${agentDoc.apiVersion}\0${agentDoc.kind}`);
217
+ // 2. Named layout preset (s-dx-named-layouts) — author picks the
218
+ // composition order by NAME; the kernel resolves it to an embedded
219
+ // template so the common case never hand-writes Mustache.
220
+ const layout = agentSpec.layout;
221
+ if (layout && agentKp) {
222
+ const layoutTmpl = agentKp.layoutTemplate(layout);
223
+ if (layoutTmpl == null) {
224
+ throw new UnknownLayout(layout, agentKp.layoutNames(), agentDoc.name);
225
+ }
226
+ return await this._mustacheRender(layoutTmpl, ctx);
227
+ }
228
+ // 3. Kind default template
211
229
  if (agentKp) {
212
230
  const kindTemplate = agentKp.promptTemplate();
213
231
  if (kindTemplate) {
214
232
  return await this._mustacheRender(kindTemplate, ctx);
215
233
  }
216
234
  }
217
- // 3. Fallback: agent instruction as plain text
235
+ // 4. Fallback: agent instruction as plain text
218
236
  const agent = ctx.agent;
219
237
  return agent?.instruction ?? "";
220
238
  }
@@ -0,0 +1,27 @@
1
+ import type { ManifestInstance } from "./kernel/instance.js";
2
+ /** Lazy, cached view `agent name -> composed prompt` over one scope. */
3
+ export declare class PromptLibrary {
4
+ /** The underlying ManifestInstance (drop down for the full surface). */
5
+ readonly mi: ManifestInstance;
6
+ private readonly cache;
7
+ constructor(
8
+ /** The underlying ManifestInstance (drop down for the full surface). */
9
+ mi: ManifestInstance);
10
+ /**
11
+ * Compose `name`'s prompt (cached). Throws {@link AgentNotFound} on a miss;
12
+ * the returned text is clean (no trailing newlines).
13
+ */
14
+ get(name: string): Promise<string>;
15
+ /** True when `name` is a prompt-target document in the scope (no compose). */
16
+ has(name: string): boolean;
17
+ /** Names of every prompt-target document in the scope, sorted. */
18
+ names(): string[];
19
+ }
20
+ /**
21
+ * Compose the prompts of `scope` behind a {@link PromptLibrary}.
22
+ *
23
+ * `baseDir` follows the {@link quickInstance} convention — the directory that
24
+ * holds `<scope>/` (the `.dna` scopes root). Omitted → the `DNA_BASE_DIR` env
25
+ * var, then `.dna` in the cwd.
26
+ */
27
+ export declare function loadPrompts(scope: string, baseDir?: string): Promise<PromptLibrary>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `loadPrompts` — the one call that collapses the prompt shim (TS twin of
3
+ * python `dna.load_prompts`).
4
+ *
5
+ * With the fail-loud builder (s-dx-build-prompt-fail-loud) and clean output
6
+ * (s-dx-clean-composition-output) in place, a hand-rolled prompt module
7
+ * becomes:
8
+ *
9
+ * ```ts
10
+ * import { loadPrompts } from "@ruinosus/dna";
11
+ *
12
+ * const prompts = await loadPrompts("helpdesk");
13
+ * export const TRIAGE = await prompts.get("triage"); // composed, clean, or throws
14
+ * ```
15
+ *
16
+ * `loadPrompts` boots a filesystem kernel for `scope` and returns a
17
+ * {@link PromptLibrary} — a lazy, cached, read-only view from agent name to
18
+ * composed system prompt. A missing agent throws {@link AgentNotFound} (never a
19
+ * placeholder string); the returned text is already stripped of trailing
20
+ * newlines.
21
+ *
22
+ * TS/Py asymmetry: composition is async in TS, so `get(name)` returns a
23
+ * `Promise<string>` (python's `lib[name]` is sync — same reason `buildPrompt`
24
+ * is `await`ed in TS but not in Python).
25
+ */
26
+ import { quickInstance } from "./bootstrap.js";
27
+ /** Lazy, cached view `agent name -> composed prompt` over one scope. */
28
+ export class PromptLibrary {
29
+ mi;
30
+ cache = new Map();
31
+ constructor(
32
+ /** The underlying ManifestInstance (drop down for the full surface). */
33
+ mi) {
34
+ this.mi = mi;
35
+ }
36
+ /**
37
+ * Compose `name`'s prompt (cached). Throws {@link AgentNotFound} on a miss;
38
+ * the returned text is clean (no trailing newlines).
39
+ */
40
+ async get(name) {
41
+ const cached = this.cache.get(name);
42
+ if (cached !== undefined)
43
+ return cached;
44
+ // buildPrompt throws AgentNotFound on a miss and returns clean text.
45
+ const text = await this.mi.buildPrompt({ agent: name });
46
+ this.cache.set(name, text);
47
+ return text;
48
+ }
49
+ /** True when `name` is a prompt-target document in the scope (no compose). */
50
+ has(name) {
51
+ return this.names().includes(name);
52
+ }
53
+ /** Names of every prompt-target document in the scope, sorted. */
54
+ names() {
55
+ const kinds = this.mi._kinds;
56
+ const seen = new Set();
57
+ for (const doc of this.mi.documents) {
58
+ const kp = kinds.get(`${doc.apiVersion}\0${doc.kind}`);
59
+ if (kp?.isPromptTarget)
60
+ seen.add(doc.name);
61
+ }
62
+ return [...seen].sort();
63
+ }
64
+ }
65
+ /**
66
+ * Compose the prompts of `scope` behind a {@link PromptLibrary}.
67
+ *
68
+ * `baseDir` follows the {@link quickInstance} convention — the directory that
69
+ * holds `<scope>/` (the `.dna` scopes root). Omitted → the `DNA_BASE_DIR` env
70
+ * var, then `.dna` in the cwd.
71
+ */
72
+ export async function loadPrompts(scope, baseDir) {
73
+ const resolved = baseDir ?? process.env.DNA_BASE_DIR ?? ".dna";
74
+ const mi = await quickInstance(scope, resolved);
75
+ return new PromptLibrary(mi);
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dna-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "DNA — Domain Notation of Anything (TypeScript SDK)",
5
5
  "type": "module",
6
6
  "license": "MIT",