dna-sdk 0.4.0 → 0.6.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.
@@ -10,6 +10,9 @@
10
10
  * file:// <path> → FilesystemSource (read/write on disk)
11
11
  * fs:// <path> → alias of file://
12
12
  * <plain path> → treated as file://<path>
13
+ * pkg://<pkg>[/sub] → FilesystemSource (READ-ONLY) over a scope embedded
14
+ * as PACKAGE DATA of <pkg> (sub defaults to .dna);
15
+ * travels with the app (tarball / Docker)
13
16
  * postgresql:// … → PostgresSource (node-postgres)
14
17
  * postgres:// … → alias of postgresql://
15
18
  * sqlite:// <path> → NOT SUPPORTED in the TS runtime (Python-only; the
@@ -16,6 +16,22 @@ function schemeOf(url) {
16
16
  const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
17
17
  return (m ? m[1] : "file").toLowerCase();
18
18
  }
19
+ /**
20
+ * Split `pkg://<package>[/<subpath>]` into `{ pkg, subpath }`. Parity with the
21
+ * python `_parse_pkg_url`: `pkg://app` → `{pkg:"app", subpath:""}`;
22
+ * `pkg://app/.dna` → `{pkg:"app", subpath:".dna"}`. The package is the netloc
23
+ * (a dotted `pkg://my.app` stays `my.app`).
24
+ */
25
+ function parsePkgUrl(url) {
26
+ const m = /^pkg:\/\/([^/]*)(?:\/(.*))?$/.exec(url);
27
+ const pkg = m?.[1] ?? "";
28
+ if (!pkg) {
29
+ throw new UnsupportedSourceScheme(`pkg:// source URL is missing a package name — use ` +
30
+ `pkg://<package>[/<subpath>] (e.g. pkg://app or pkg://app/.dna). ` +
31
+ `Got: ${url}`);
32
+ }
33
+ return { pkg, subpath: m?.[2] ?? "" };
34
+ }
19
35
  /**
20
36
  * Build a source from a scheme URL (see module docstring).
21
37
  *
@@ -29,6 +45,16 @@ export async function sourceFromUrl(url, opts = {}) {
29
45
  const { FilesystemSource } = await import("./filesystem/source.js");
30
46
  return new FilesystemSource(urlToFsPath(url) || url);
31
47
  }
48
+ if (scheme === "pkg") {
49
+ // A scope embedded as PACKAGE DATA — resolve it from inside the installed
50
+ // package so it travels with the app (tarball / Docker), no path
51
+ // navigation and no manual copy. READ-ONLY: FilesystemSource has no write
52
+ // surface here (to write, use file:// or postgresql://).
53
+ const { FilesystemSource } = await import("./filesystem/source.js");
54
+ const { anchorScopesRoot, DEFAULT_SUBPATH } = await import("../package-scope.js");
55
+ const { pkg, subpath } = parsePkgUrl(url);
56
+ return new FilesystemSource(anchorScopesRoot(pkg, subpath || DEFAULT_SUBPATH));
57
+ }
32
58
  if (base === "postgresql" || base === "postgres") {
33
59
  const { PostgresSource } = await import("./postgres/source.js");
34
60
  const src = new PostgresSource({ connectionString: url, schema: opts.schema });
@@ -41,7 +67,8 @@ export async function sourceFromUrl(url, opts = {}) {
41
67
  `(filesystem) or postgresql:// here. Got: ${url}`);
42
68
  }
43
69
  throw new UnsupportedSourceScheme(`unsupported source URL scheme '${scheme}://' — the TS runtime ships ` +
44
- `file:// (filesystem) and postgresql:// adapters. Got: ${url}`);
70
+ `file:// (filesystem), pkg:// (read-only package-data scope) and ` +
71
+ `postgresql:// adapters. Got: ${url}`);
45
72
  }
46
73
  /**
47
74
  * The `file://` URL the SDK falls back to with no explicit config.
@@ -0,0 +1,240 @@
1
+ # Tool — a declarative, invocable capability an agent can call (record plane).
2
+ #
3
+ # "Tools as data" (f-dna-tools-as-data). The DNA already governs persona,
4
+ # instruction and guardrails declaratively; a Tool moves the AGENT-FACING
5
+ # surface of a tool into the same declarative plane — the ``description`` the
6
+ # model reads to decide whether to call it (``metadata.description``) and the
7
+ # ``parameters`` JSON Schema of its arguments (``spec.input_schema``). One
8
+ # source of truth, versioned, testable, and overridable per tenant, served
9
+ # identically to a Python backend (`@tool`) and a TypeScript frontend
10
+ # (CopilotKit `useCopilotAction`) via ``dna.load_tools`` / ``loadTools``.
11
+ #
12
+ # F3 migration (s-tool-kind-descriptor): this WAS a hand-written ``ToolKind``
13
+ # class (helix/__init__.py + helix.ts) with a ``TypedTool``/``ToolSpec``
14
+ # model. It is now a ``kinds/tool.kind.yaml`` descriptor — a record Kind
15
+ # expressed as data, per the repo's own ratchet (record Kinds are
16
+ # descriptors, never classes). The invocation surface (type/endpoint/mcp/
17
+ # python/shell + input/output schema + auth + read_only/requires_confirmation)
18
+ # is preserved verbatim; only the IMPLEMENTATION moved class → descriptor and
19
+ # the plane moved composition → record (a Tool is not a prompt target and
20
+ # never composes into an agent prompt, so it carries no composition signal —
21
+ # writing a Tool no longer invalidates the composition schema cache).
22
+ #
23
+ # Agents reference Tools by name via ``dep_filters.tools`` (unchanged — the
24
+ # alias ``helix-tool`` is stable). Stored as ``tools/<name>.yaml`` —
25
+ # marketplace-shareable as standalone bundles.
26
+ #
27
+ # tenant_scope intentionally NOT declared — permissive (base Tool + optional
28
+ # per-tenant overlay). A tenant may legitimately override a tool's
29
+ # description/parameters (the SaaS value hook); the base stays intact and the
30
+ # máxima "inheritable ⇒ never tenanted" holds.
31
+ #
32
+ # PARITY-CRITICAL package data: byte-identical mirror under both runtimes'
33
+ # helix/kinds/ (tests/test_descriptor_hash_parity.py enforces).
34
+ apiVersion: github.com/ruinosus/dna/core/v1
35
+ kind: KindDefinition
36
+ metadata:
37
+ name: tool
38
+ spec:
39
+ target_api_version: github.com/ruinosus/dna/v1
40
+ target_kind: Tool
41
+ alias: helix-tool
42
+ origin: github.com/ruinosus/dna/tool
43
+ plane: record
44
+ prompt_target: false
45
+ flatten_in_context: false
46
+ prompt_target_priority: 0
47
+ storage:
48
+ type: yaml
49
+ container: tools
50
+ schema:
51
+ type: object
52
+ properties:
53
+ type:
54
+ type: string
55
+ enum:
56
+ - http
57
+ - mcp
58
+ - python
59
+ - shell
60
+ - builtin
61
+ description: How the tool is executed. builtin | http | mcp | python
62
+ | shell.
63
+ endpoint:
64
+ type: string
65
+ description: URL called when type=http. Supports {placeholder}
66
+ templating.
67
+ method:
68
+ type: string
69
+ description: HTTP method when type=http (default POST).
70
+ mcp_server:
71
+ type: string
72
+ description: MCP server name when type=mcp.
73
+ mcp_tool:
74
+ type: string
75
+ description: Tool name on the MCP server when type=mcp.
76
+ python_module:
77
+ type: string
78
+ description: Dotted import path when type=python.
79
+ python_callable:
80
+ type: string
81
+ description: Attribute on the module (function or class) when
82
+ type=python.
83
+ shell_command:
84
+ type: string
85
+ description: Command template when type=shell. Never executed without
86
+ confirmation.
87
+ input_schema:
88
+ type: object
89
+ description: JSON Schema of the arguments the agent passes when
90
+ invoking the tool — the "parameters" the model fills in. Surfaced
91
+ as ``parameters`` by ``dna.load_tools`` / ``loadTools``.
92
+ output_schema:
93
+ type: object
94
+ description: JSON Schema describing the shape of the tool's response.
95
+ auth_type:
96
+ type: string
97
+ enum:
98
+ - none
99
+ - api_key
100
+ - bearer
101
+ - oauth2
102
+ description: Credential strategy for the invocation.
103
+ auth_env_var:
104
+ type: string
105
+ description: Environment variable holding the credential (e.g.
106
+ GITHUB_TOKEN).
107
+ read_only:
108
+ type: boolean
109
+ description: False = the tool may mutate state (DB writes, file
110
+ changes, external side effects).
111
+ requires_confirmation:
112
+ type: boolean
113
+ description: Force user approval before each invocation.
114
+ tags:
115
+ type: array
116
+ items:
117
+ type: string
118
+ description: Free-form labels for filtering and search.
119
+ examples:
120
+ type: array
121
+ items:
122
+ type: object
123
+ description: Usage examples ([{input, output}]).
124
+ ui:
125
+ mode: build
126
+ label:
127
+ en: Tools
128
+ pt-BR: Ferramentas
129
+ description:
130
+ en: '@tool functions available to agents.'
131
+ pt-BR: Ferramentas (@tool) disponíveis aos agentes.
132
+ routes:
133
+ list: docs/Tool
134
+ detail: docs/Tool/:name
135
+ permissions:
136
+ list: any
137
+ detail: any
138
+ in_sidebar: true
139
+ display_order: 53
140
+ ui_schema:
141
+ type:
142
+ widget: select
143
+ label: Invocation type
144
+ help: 'How the tool is executed: http | mcp | python | shell | builtin.'
145
+ order: 10
146
+ endpoint:
147
+ widget: text
148
+ label: HTTP endpoint
149
+ help: URL called when type=http. Supports {placeholder} templating.
150
+ order: 20
151
+ method:
152
+ widget: select
153
+ label: HTTP method
154
+ order: 25
155
+ mcp_server:
156
+ widget: text
157
+ label: MCP server
158
+ help: Server name when type=mcp.
159
+ order: 30
160
+ mcp_tool:
161
+ widget: text
162
+ label: MCP tool name
163
+ order: 35
164
+ python_module:
165
+ widget: text
166
+ label: Python module
167
+ help: Dotted import path when type=python.
168
+ order: 40
169
+ python_callable:
170
+ widget: text
171
+ label: Python callable
172
+ help: Attribute on the module (function or class).
173
+ order: 45
174
+ shell_command:
175
+ widget: textarea
176
+ label: Shell command
177
+ help: Command template when type=shell. Never executed without
178
+ confirmation.
179
+ order: 50
180
+ input_schema:
181
+ widget: code
182
+ language: yaml
183
+ label: Input schema (JSON Schema)
184
+ help: Validates the arguments the agent passes when invoking the tool.
185
+ height: 260
186
+ order: 60
187
+ output_schema:
188
+ widget: code
189
+ language: yaml
190
+ label: Output schema (JSON Schema)
191
+ help: Describes the shape of the tool's response.
192
+ height: 220
193
+ order: 70
194
+ auth_type:
195
+ widget: select
196
+ label: Auth type
197
+ help: none | api_key | bearer | oauth2.
198
+ order: 80
199
+ auth_env_var:
200
+ widget: text
201
+ label: Auth env var
202
+ help: Environment variable holding the credential.
203
+ order: 85
204
+ read_only:
205
+ widget: checkbox
206
+ label: Read-only
207
+ help: Uncheck if the tool mutates state (database writes, file changes,
208
+ external side effects).
209
+ order: 90
210
+ requires_confirmation:
211
+ widget: checkbox
212
+ label: Requires confirmation
213
+ help: Force user approval before each invocation.
214
+ order: 95
215
+ tags:
216
+ widget: tags
217
+ label: Tags
218
+ order: 100
219
+ examples:
220
+ widget: readonly
221
+ label: Examples
222
+ help: Usage examples. Nested list — edit via YAML for now.
223
+ order: 110
224
+ graph_style:
225
+ fill: '#14B8A6'
226
+ stroke: '#0D9488'
227
+ text_color: '#fff'
228
+ ascii_icon: 🔧
229
+ display_label: Tools
230
+ docs: A Tool is a declarative, invocable capability an agent can call — an
231
+ HTTP endpoint, an MCP server tool, a Python callable, a shell command, or
232
+ a builtin. It bridges DNA with OpenAI/Anthropic tool-calling conventions.
233
+ The agent-facing surface is its ``metadata.description`` (the text the
234
+ model reads to decide to call it) and its ``spec.input_schema`` (the
235
+ "parameters" JSON Schema of the arguments); ``dna.load_tools`` /
236
+ ``loadTools`` serve exactly that surface, identically to Python and
237
+ TypeScript consumers from this one source. It also declares an auth
238
+ strategy and read_only / requires_confirmation flags the host honors at
239
+ runtime. Agents reference Tools via ``dep_filters.tools``. Stored as
240
+ ``tools/<name>.yaml`` — marketplace-shareable as standalone bundles.
@@ -7,12 +7,40 @@ import yaml from "js-yaml";
7
7
  import { join as pathJoin } from "node:path";
8
8
  import { nodeFS, readTextSafe, collectDir } from "../kernel/fs.js";
9
9
  import { KindBase } from "../kernel/kind_base.js";
10
- import { AgentSchema, ActorSchema, UseCaseSchema, ToolSchema, AgentSpecSchema, ActorSpecSchema, UseCaseSpecSchema, ToolSpecSchema, GenomeSchema, GenomeSpecSchema, LayerPolicySchema, LayerPolicySpecSchema, zodSpecToJsonSchema } from "../kernel/models.js";
10
+ import { AgentSchema, ActorSchema, UseCaseSchema, AgentSpecSchema, ActorSpecSchema, UseCaseSpecSchema, GenomeSchema, GenomeSpecSchema, LayerPolicySchema, LayerPolicySpecSchema, zodSpecToJsonSchema } from "../kernel/models.js";
11
+ import { loadDescriptors } from "../kernel/descriptor-loader.js";
11
12
  import { SD } from "../kernel/protocols.js";
12
13
  import { readSpecString, readSpecStringArray } from "../kernel/spec-access.js";
13
14
  import { SettingKind, ThemeKind, UserProfileKind, CanvasKind } from "./helix_extras.js";
14
15
  import { registerWriteGuards } from "./helix/write-guards.js";
15
16
  const MOD_URL = import.meta.url;
17
+ // ── Named composition layouts (s-dx-named-layouts) ─────────────────────
18
+ //
19
+ // An author orders persona-vs-instruction by NAME (`layout:` in the Agent
20
+ // spec) instead of hand-writing raw Mustache with internal section names.
21
+ // Each preset resolves to one of these embedded templates via
22
+ // `AgentKind.layoutTemplate()`. 1:1 with Python `dna.extensions.helix`.
23
+ //
24
+ // The guardrails block is shared verbatim — guardrails are hard policy and
25
+ // always land LAST, after both the instruction and the soul, regardless of
26
+ // their relative order. (Aligns TS composition to Python, closing the
27
+ // pre-existing i-213/i-011 divergence where TS omitted the guardrails block.)
28
+ const GUARDRAILS_BLOCK = "{{#guardrails-guardrail}}" +
29
+ "## Guardrail: {{name}} ({{severity}})\n" +
30
+ "{{#description}}_{{description}}_\n\n{{/description}}" +
31
+ "{{#rules}}- {{{.}}}\n{{/rules}}\n" +
32
+ "{{/guardrails-guardrail}}";
33
+ // instruction-first (a.k.a. "default") — historic order: instruction, soul,
34
+ // guardrails. IS the kind default template.
35
+ const LAYOUT_INSTRUCTION_FIRST = "{{{agent.instruction}}}\n\n{{{soul_content}}}\n\n" + GUARDRAILS_BLOCK;
36
+ // persona-first — Soul leads, then instruction, then guardrails.
37
+ const LAYOUT_PERSONA_FIRST = "{{{soul_content}}}\n\n{{{agent.instruction}}}\n\n" + GUARDRAILS_BLOCK;
38
+ const AGENT_LAYOUTS = {
39
+ default: LAYOUT_INSTRUCTION_FIRST,
40
+ "instruction-first": LAYOUT_INSTRUCTION_FIRST,
41
+ "persona-first": LAYOUT_PERSONA_FIRST,
42
+ };
43
+ const AGENT_LAYOUT_NAMES = ["default", "instruction-first", "persona-first"];
16
44
  // GenomeKind — Phase 16 (scope segregation)
17
45
  //
18
46
  // Replaces ModuleKind as the scope-root identity Kind. Carries catalog
@@ -253,6 +281,13 @@ class AgentKind extends KindBase {
253
281
  },
254
282
  objective: { widget: "textarea", label: "Objective", order: 15 },
255
283
  model: { widget: "text", label: "Model", order: 20 },
284
+ layout: {
285
+ widget: "select",
286
+ label: "Layout",
287
+ options: ["default", "instruction-first", "persona-first"],
288
+ help: "Named composition order — 'persona-first' puts the Soul before the instruction. Leave empty for the default. A raw promptTemplate, if set, overrides this.",
289
+ order: 25,
290
+ },
256
291
  soul: { widget: "text", label: "Soul", help: "Name of the Soul doc to flatten into the prompt.", order: 30 },
257
292
  skills: { widget: "tags", label: "Skills", order: 40 },
258
293
  actors: { widget: "tags", label: "Actors this agent serves", order: 50 },
@@ -305,7 +340,17 @@ class AgentKind extends KindBase {
305
340
  return { skills: skills.length, soul: readSpecString(doc, "soul") ?? null };
306
341
  }
307
342
  promptTemplate() {
308
- return "{{{agent.instruction}}}\n\n{{{soul_content}}}";
343
+ // IS the `instruction-first` / `default` named layout — the kind default
344
+ // template and the `default` layout are one string, so an agent with no
345
+ // `layout:` composes identically. (Now includes the guardrails block,
346
+ // matching Python — see the layout-constants comment above.)
347
+ return LAYOUT_INSTRUCTION_FIRST;
348
+ }
349
+ layoutTemplate(name) {
350
+ return AGENT_LAYOUTS[name] ?? null;
351
+ }
352
+ layoutNames() {
353
+ return [...AGENT_LAYOUT_NAMES];
309
354
  }
310
355
  preview(doc) {
311
356
  const spec = (doc.spec ?? {});
@@ -485,102 +530,6 @@ class UseCaseKind extends KindBase {
485
530
  }
486
531
  }
487
532
  // ---------------------------------------------------------------------------
488
- // ToolKind
489
- // ---------------------------------------------------------------------------
490
- class ToolKind extends KindBase {
491
- apiVersion = "github.com/ruinosus/dna/v1";
492
- kind = "Tool";
493
- alias = "helix-tool";
494
- isSchemaAffecting = true;
495
- origin = "github.com/ruinosus/dna/tool";
496
- isPromptTarget = false;
497
- promptTargetPriority = 0;
498
- flattenInContext = false;
499
- storage = SD.yaml("tools");
500
- graphStyle = { fill: "#14B8A6", stroke: "#0D9488", textColor: "#fff" };
501
- asciiIcon = "🔧";
502
- displayLabel = "Tools";
503
- _sourceUrl = MOD_URL;
504
- docs = "A Tool is a declarative, invocable capability an agent can call: an " +
505
- "HTTP endpoint, an MCP server tool, a Python callable, a shell command, " +
506
- "or a builtin. Bridges helix with OpenAI/Anthropic tool-calling " +
507
- "conventions. Each Tool declares an input/output JSON Schema, an auth " +
508
- "strategy, and read_only / requires_confirmation flags that the harness " +
509
- "honors at runtime. Agents reference Tools via dep_filters.tools. " +
510
- "Stored as tools/<name>.yaml — marketplace-shareable as standalone bundles.";
511
- uiSchema = {
512
- type: { widget: "select", label: "Invocation type", help: "How the tool is executed: http | mcp | python | shell | builtin.", order: 10 },
513
- endpoint: { widget: "text", label: "HTTP endpoint", help: "URL called when type=http. Supports {placeholder} templating.", order: 20 },
514
- method: { widget: "select", label: "HTTP method", order: 25 },
515
- mcp_server: { widget: "text", label: "MCP server", help: "Server name when type=mcp.", order: 30 },
516
- mcp_tool: { widget: "text", label: "MCP tool name", order: 35 },
517
- python_module: { widget: "text", label: "Python module", help: "Dotted import path when type=python.", order: 40 },
518
- python_callable: { widget: "text", label: "Python callable", help: "Attribute on the module (function or class).", order: 45 },
519
- shell_command: { widget: "textarea", label: "Shell command", help: "Command template when type=shell. Never executed without confirmation.", order: 50 },
520
- input_schema: {
521
- widget: "code",
522
- language: "yaml",
523
- label: "Input schema (JSON Schema)",
524
- help: "Validates the arguments the agent passes when invoking the tool.",
525
- height: 260,
526
- order: 60,
527
- },
528
- output_schema: {
529
- widget: "code",
530
- language: "yaml",
531
- label: "Output schema (JSON Schema)",
532
- help: "Describes the shape of the tool's response.",
533
- height: 220,
534
- order: 70,
535
- },
536
- auth_type: { widget: "select", label: "Auth type", help: "none | api_key | bearer | oauth2.", order: 80 },
537
- auth_env_var: { widget: "text", label: "Auth env var", help: "Environment variable holding the credential.", order: 85 },
538
- read_only: { widget: "checkbox", label: "Read-only", help: "Uncheck if the tool mutates state.", order: 90 },
539
- requires_confirmation: { widget: "checkbox", label: "Requires confirmation", help: "Force user approval before each invocation.", order: 95 },
540
- tags: { widget: "tags", label: "Tags", order: 100 },
541
- examples: { widget: "readonly", label: "Examples", help: "Usage examples. Edit via YAML for now.", order: 110 },
542
- };
543
- schema() { return zodSpecToJsonSchema(ToolSpecSchema); }
544
- parse(raw) {
545
- return ToolSchema.parse(raw);
546
- }
547
- summary() { return null; }
548
- preview(doc) {
549
- const spec = (doc.spec ?? {});
550
- const fields = [];
551
- if (typeof spec.type === "string")
552
- fields.push({ label: "type", value: spec.type });
553
- if (typeof spec.endpoint === "string")
554
- fields.push({ label: "endpoint", value: spec.endpoint });
555
- if (typeof spec.method === "string")
556
- fields.push({ label: "method", value: spec.method });
557
- if (typeof spec.mcp_server === "string")
558
- fields.push({ label: "mcp_server", value: spec.mcp_server });
559
- if (typeof spec.mcp_tool === "string")
560
- fields.push({ label: "mcp_tool", value: spec.mcp_tool });
561
- if (typeof spec.python_module === "string")
562
- fields.push({ label: "python_module", value: spec.python_module });
563
- if (typeof spec.python_callable === "string")
564
- fields.push({ label: "python_callable", value: spec.python_callable });
565
- if (typeof spec.shell_command === "string")
566
- fields.push({ label: "shell_command", value: spec.shell_command });
567
- if (spec.input_schema)
568
- fields.push({ label: "input_schema", value: JSON.stringify(spec.input_schema, null, 2) });
569
- if (spec.output_schema)
570
- fields.push({ label: "output_schema", value: JSON.stringify(spec.output_schema, null, 2) });
571
- if (typeof spec.auth_type === "string")
572
- fields.push({ label: "auth", value: spec.auth_type });
573
- if (spec.read_only != null)
574
- fields.push({ label: "read_only", value: String(spec.read_only) });
575
- if (spec.requires_confirmation != null)
576
- fields.push({ label: "requires_confirmation", value: String(spec.requires_confirmation) });
577
- if (fields.length === 0) {
578
- return [{ kind: "empty", title: `Tool ${doc.name}` }];
579
- }
580
- return [{ kind: "fields", title: `Tool ${doc.name}`, fields }];
581
- }
582
- }
583
- // ---------------------------------------------------------------------------
584
533
  // AgentReader / AgentWriter
585
534
  // ---------------------------------------------------------------------------
586
535
  const KNOWN_DIRS = new Set(["scripts", "references", "assets"]);
@@ -941,9 +890,15 @@ export class HelixExtension {
941
890
  kernel.kind(new GenomeKind());
942
891
  kernel.kind(new LayerPolicyKind());
943
892
  kernel.kind(new AgentKind());
944
- kernel.kind(new ToolKind());
945
893
  kernel.kind(new ActorKind());
946
894
  kernel.kind(new UseCaseKind());
895
+ // Tool (helix-tool) ships as a descriptor — helix/kinds/tool.kind.yaml
896
+ // (f-dna-tools-as-data / s-tool-kind-descriptor). It WAS a hand-written
897
+ // ToolKind class; migrated to a record-plane descriptor per the repo's
898
+ // own ratchet (record Kinds are data, not classes).
899
+ for (const raw of loadDescriptors(import.meta.url, "helix/kinds")) {
900
+ kernel.kindFromDescriptor(raw);
901
+ }
947
902
  // 2026-05-26 — absorbed from claude-code-templates catalog (MIT).
948
903
  // Setting rounds out the Claude-Code-customization primitives that
949
904
  // live alongside Skill / UA / Soul / Tool.
@@ -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).