dna-sdk 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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/adapters/embedding/onnx.d.ts +32 -0
  4. package/dist/adapters/embedding/onnx.js +51 -0
  5. package/dist/adapters/filesystem/cache.d.ts +16 -0
  6. package/dist/adapters/filesystem/cache.js +113 -0
  7. package/dist/adapters/filesystem/index.d.ts +2 -0
  8. package/dist/adapters/filesystem/index.js +2 -0
  9. package/dist/adapters/filesystem/source.d.ts +82 -0
  10. package/dist/adapters/filesystem/source.js +357 -0
  11. package/dist/adapters/postgres/index.d.ts +2 -0
  12. package/dist/adapters/postgres/index.js +2 -0
  13. package/dist/adapters/postgres/migrations.d.ts +14 -0
  14. package/dist/adapters/postgres/migrations.js +88 -0
  15. package/dist/adapters/postgres/source.d.ts +172 -0
  16. package/dist/adapters/postgres/source.js +569 -0
  17. package/dist/adapters/resolvers/github.d.ts +12 -0
  18. package/dist/adapters/resolvers/github.js +47 -0
  19. package/dist/adapters/resolvers/http.d.ts +22 -0
  20. package/dist/adapters/resolvers/http.js +145 -0
  21. package/dist/adapters/resolvers/index.d.ts +3 -0
  22. package/dist/adapters/resolvers/index.js +3 -0
  23. package/dist/adapters/resolvers/local.d.ts +24 -0
  24. package/dist/adapters/resolvers/local.js +132 -0
  25. package/dist/adapters/search/driver.d.ts +45 -0
  26. package/dist/adapters/search/driver.js +138 -0
  27. package/dist/adapters/search/migrations.d.ts +26 -0
  28. package/dist/adapters/search/migrations.js +58 -0
  29. package/dist/adapters/search/rrf.d.ts +23 -0
  30. package/dist/adapters/search/rrf.js +42 -0
  31. package/dist/adapters/search/sqlite-vec.d.ts +66 -0
  32. package/dist/adapters/search/sqlite-vec.js +294 -0
  33. package/dist/bootstrap.d.ts +45 -0
  34. package/dist/bootstrap.js +134 -0
  35. package/dist/extensions/agentskills/DOCS.md +18 -0
  36. package/dist/extensions/agentskills.d.ts +28 -0
  37. package/dist/extensions/agentskills.js +252 -0
  38. package/dist/extensions/agentsmd/DOCS.md +18 -0
  39. package/dist/extensions/agentsmd.d.ts +22 -0
  40. package/dist/extensions/agentsmd.js +170 -0
  41. package/dist/extensions/audit/kinds/audit-log.kind.yaml +148 -0
  42. package/dist/extensions/audit.d.ts +25 -0
  43. package/dist/extensions/audit.js +62 -0
  44. package/dist/extensions/automation/kinds/automation.kind.yaml +296 -0
  45. package/dist/extensions/automation/query.d.ts +49 -0
  46. package/dist/extensions/automation/query.js +77 -0
  47. package/dist/extensions/automation/write-guards.d.ts +59 -0
  48. package/dist/extensions/automation/write-guards.js +153 -0
  49. package/dist/extensions/automation.d.ts +41 -0
  50. package/dist/extensions/automation.js +54 -0
  51. package/dist/extensions/collab.d.ts +16 -0
  52. package/dist/extensions/collab.js +79 -0
  53. package/dist/extensions/doc/kinds/doc.kind.yaml +118 -0
  54. package/dist/extensions/doc.d.ts +25 -0
  55. package/dist/extensions/doc.js +10 -0
  56. package/dist/extensions/eval/kinds/eval-baseline.kind.yaml +65 -0
  57. package/dist/extensions/eval/kinds/eval-case.kind.yaml +140 -0
  58. package/dist/extensions/eval/kinds/eval-run.kind.yaml +134 -0
  59. package/dist/extensions/eval/kinds/eval-suite.kind.yaml +87 -0
  60. package/dist/extensions/eval.d.ts +24 -0
  61. package/dist/extensions/eval.js +31 -0
  62. package/dist/extensions/evidence/kinds/evidence.kind.yaml +92 -0
  63. package/dist/extensions/evidence.d.ts +17 -0
  64. package/dist/extensions/evidence.js +94 -0
  65. package/dist/extensions/federation.d.ts +22 -0
  66. package/dist/extensions/federation.js +173 -0
  67. package/dist/extensions/guardrails/DOCS.md +20 -0
  68. package/dist/extensions/guardrails.d.ts +31 -0
  69. package/dist/extensions/guardrails.js +228 -0
  70. package/dist/extensions/helix/DOCS-Actor.md +20 -0
  71. package/dist/extensions/helix/DOCS-Agent.md +20 -0
  72. package/dist/extensions/helix/DOCS-UseCase.md +20 -0
  73. package/dist/extensions/helix/layers.d.ts +12 -0
  74. package/dist/extensions/helix/layers.js +18 -0
  75. package/dist/extensions/helix/write-guards.d.ts +70 -0
  76. package/dist/extensions/helix/write-guards.js +172 -0
  77. package/dist/extensions/helix.d.ts +29 -0
  78. package/dist/extensions/helix.js +963 -0
  79. package/dist/extensions/helix_extras.d.ts +124 -0
  80. package/dist/extensions/helix_extras.js +328 -0
  81. package/dist/extensions/hooks.d.ts +31 -0
  82. package/dist/extensions/hooks.js +247 -0
  83. package/dist/extensions/kinddef/DOCS.md +24 -0
  84. package/dist/extensions/kinddef.d.ts +40 -0
  85. package/dist/extensions/kinddef.js +171 -0
  86. package/dist/extensions/lesson.d.ts +6 -0
  87. package/dist/extensions/lesson.js +194 -0
  88. package/dist/extensions/modelreg/kinds/model-profile.kind.yaml +155 -0
  89. package/dist/extensions/modelreg.d.ts +30 -0
  90. package/dist/extensions/modelreg.js +38 -0
  91. package/dist/extensions/recognizer.d.ts +34 -0
  92. package/dist/extensions/recognizer.js +272 -0
  93. package/dist/extensions/research.d.ts +32 -0
  94. package/dist/extensions/research.js +292 -0
  95. package/dist/extensions/safety.d.ts +33 -0
  96. package/dist/extensions/safety.js +230 -0
  97. package/dist/extensions/sdlc/kinds/adr.kind.yaml +136 -0
  98. package/dist/extensions/sdlc/kinds/archive-proposal.kind.yaml +107 -0
  99. package/dist/extensions/sdlc/kinds/changelog.kind.yaml +108 -0
  100. package/dist/extensions/sdlc/kinds/cognitive-policy.kind.yaml +470 -0
  101. package/dist/extensions/sdlc/kinds/forecast.kind.yaml +125 -0
  102. package/dist/extensions/sdlc/kinds/insight.kind.yaml +87 -0
  103. package/dist/extensions/sdlc/kinds/kaizen.kind.yaml +106 -0
  104. package/dist/extensions/sdlc/kinds/lesson-learned.kind.yaml +270 -0
  105. package/dist/extensions/sdlc/kinds/narrative.kind.yaml +189 -0
  106. package/dist/extensions/sdlc/kinds/pattern-insight.kind.yaml +204 -0
  107. package/dist/extensions/sdlc/kinds/postmortem.kind.yaml +168 -0
  108. package/dist/extensions/sdlc/kinds/pre-mortem.kind.yaml +145 -0
  109. package/dist/extensions/sdlc/kinds/prompt-template.kind.yaml +109 -0
  110. package/dist/extensions/sdlc/kinds/retrospective.kind.yaml +186 -0
  111. package/dist/extensions/sdlc/kinds/risk-register.kind.yaml +178 -0
  112. package/dist/extensions/sdlc/kinds/saved-view.kind.yaml +127 -0
  113. package/dist/extensions/sdlc/kinds/status-report.kind.yaml +129 -0
  114. package/dist/extensions/sdlc/kinds/synthesis-run.kind.yaml +279 -0
  115. package/dist/extensions/sdlc/kinds/synthesizer-state.kind.yaml +196 -0
  116. package/dist/extensions/sdlc/kinds/workflow-event.kind.yaml +195 -0
  117. package/dist/extensions/sdlc.d.ts +32 -0
  118. package/dist/extensions/sdlc.js +1318 -0
  119. package/dist/extensions/soulspec/DOCS.md +19 -0
  120. package/dist/extensions/soulspec.d.ts +22 -0
  121. package/dist/extensions/soulspec.js +287 -0
  122. package/dist/extensions/tenant.d.ts +6 -0
  123. package/dist/extensions/tenant.js +291 -0
  124. package/dist/extensions/testkit.d.ts +24 -0
  125. package/dist/extensions/testkit.js +211 -0
  126. package/dist/index.d.ts +80 -0
  127. package/dist/index.js +102 -0
  128. package/dist/kernel/_text.d.ts +28 -0
  129. package/dist/kernel/_text.js +42 -0
  130. package/dist/kernel/bundle-handle.d.ts +83 -0
  131. package/dist/kernel/bundle-handle.js +184 -0
  132. package/dist/kernel/capabilities.d.ts +157 -0
  133. package/dist/kernel/capabilities.js +109 -0
  134. package/dist/kernel/collaborator-ports.d.ts +91 -0
  135. package/dist/kernel/collaborator-ports.js +1 -0
  136. package/dist/kernel/composition-resolver.d.ts +215 -0
  137. package/dist/kernel/composition-resolver.js +558 -0
  138. package/dist/kernel/composition.d.ts +10 -0
  139. package/dist/kernel/composition.js +1 -0
  140. package/dist/kernel/descriptor-loader.d.ts +13 -0
  141. package/dist/kernel/descriptor-loader.js +48 -0
  142. package/dist/kernel/document.d.ts +43 -0
  143. package/dist/kernel/document.js +71 -0
  144. package/dist/kernel/embedding.d.ts +28 -0
  145. package/dist/kernel/embedding.js +78 -0
  146. package/dist/kernel/errors.d.ts +62 -0
  147. package/dist/kernel/errors.js +82 -0
  148. package/dist/kernel/events.d.ts +6 -0
  149. package/dist/kernel/events.js +21 -0
  150. package/dist/kernel/evidence-capture.d.ts +69 -0
  151. package/dist/kernel/evidence-capture.js +138 -0
  152. package/dist/kernel/fs.d.ts +20 -0
  153. package/dist/kernel/fs.js +128 -0
  154. package/dist/kernel/generic-rw.d.ts +33 -0
  155. package/dist/kernel/generic-rw.js +235 -0
  156. package/dist/kernel/hooks.d.ts +172 -0
  157. package/dist/kernel/hooks.js +256 -0
  158. package/dist/kernel/index.d.ts +624 -0
  159. package/dist/kernel/index.js +1638 -0
  160. package/dist/kernel/instance.d.ts +211 -0
  161. package/dist/kernel/instance.js +487 -0
  162. package/dist/kernel/kind-registry.d.ts +183 -0
  163. package/dist/kernel/kind-registry.js +710 -0
  164. package/dist/kernel/kind_base.d.ts +106 -0
  165. package/dist/kernel/kind_base.js +244 -0
  166. package/dist/kernel/layer-resolver.d.ts +48 -0
  167. package/dist/kernel/layer-resolver.js +284 -0
  168. package/dist/kernel/lock-manager.d.ts +21 -0
  169. package/dist/kernel/lock-manager.js +43 -0
  170. package/dist/kernel/lock.d.ts +38 -0
  171. package/dist/kernel/lock.js +115 -0
  172. package/dist/kernel/meta.d.ts +135 -0
  173. package/dist/kernel/meta.js +817 -0
  174. package/dist/kernel/models.d.ts +3089 -0
  175. package/dist/kernel/models.js +764 -0
  176. package/dist/kernel/navigator.d.ts +36 -0
  177. package/dist/kernel/navigator.js +132 -0
  178. package/dist/kernel/port-surface.d.ts +33 -0
  179. package/dist/kernel/port-surface.js +180 -0
  180. package/dist/kernel/preview.d.ts +60 -0
  181. package/dist/kernel/preview.js +54 -0
  182. package/dist/kernel/prompt-budget.d.ts +39 -0
  183. package/dist/kernel/prompt-budget.js +43 -0
  184. package/dist/kernel/prompt-builder.d.ts +25 -0
  185. package/dist/kernel/prompt-builder.js +237 -0
  186. package/dist/kernel/protocols.d.ts +751 -0
  187. package/dist/kernel/protocols.js +493 -0
  188. package/dist/kernel/reports.d.ts +26 -0
  189. package/dist/kernel/reports.js +215 -0
  190. package/dist/kernel/resolver.d.ts +146 -0
  191. package/dist/kernel/resolver.js +232 -0
  192. package/dist/kernel/resource.d.ts +57 -0
  193. package/dist/kernel/resource.js +97 -0
  194. package/dist/kernel/runtime.d.ts +16 -0
  195. package/dist/kernel/runtime.js +18 -0
  196. package/dist/kernel/safety-scanner.d.ts +38 -0
  197. package/dist/kernel/safety-scanner.js +228 -0
  198. package/dist/kernel/serialize-to-files.d.ts +29 -0
  199. package/dist/kernel/serialize-to-files.js +23 -0
  200. package/dist/kernel/spec-access.d.ts +23 -0
  201. package/dist/kernel/spec-access.js +40 -0
  202. package/dist/kernel/studio_ui.d.ts +88 -0
  203. package/dist/kernel/studio_ui.js +135 -0
  204. package/dist/kernel/templates.d.ts +77 -0
  205. package/dist/kernel/templates.js +116 -0
  206. package/dist/kernel/tool-registry.d.ts +52 -0
  207. package/dist/kernel/tool-registry.js +89 -0
  208. package/dist/kernel/write-pipeline.d.ts +65 -0
  209. package/dist/kernel/write-pipeline.js +271 -0
  210. package/dist/kernel/writer-helpers.d.ts +24 -0
  211. package/dist/kernel/writer-helpers.js +42 -0
  212. package/dist/memory/decay.d.ts +31 -0
  213. package/dist/memory/decay.js +97 -0
  214. package/dist/memory/ecphory.d.ts +25 -0
  215. package/dist/memory/ecphory.js +178 -0
  216. package/dist/memory/encodingContext.d.ts +21 -0
  217. package/dist/memory/encodingContext.js +62 -0
  218. package/dist/memory/index.d.ts +23 -0
  219. package/dist/memory/index.js +23 -0
  220. package/dist/memory/memoryType.d.ts +7 -0
  221. package/dist/memory/memoryType.js +24 -0
  222. package/dist/memory/policy.d.ts +35 -0
  223. package/dist/memory/policy.js +35 -0
  224. package/dist/memory/retrieval.d.ts +21 -0
  225. package/dist/memory/retrieval.js +145 -0
  226. package/dist/testing/index.d.ts +8 -0
  227. package/dist/testing/index.js +8 -0
  228. package/dist/testing/recordSearchConformance.d.ts +43 -0
  229. package/dist/testing/recordSearchConformance.js +147 -0
  230. package/dist/viz/ascii.d.ts +7 -0
  231. package/dist/viz/ascii.js +60 -0
  232. package/dist/viz/health.d.ts +8 -0
  233. package/dist/viz/health.js +161 -0
  234. package/dist/viz/index.d.ts +10 -0
  235. package/dist/viz/index.js +10 -0
  236. package/dist/viz/matrix.d.ts +8 -0
  237. package/dist/viz/matrix.js +84 -0
  238. package/dist/viz/mermaid.d.ts +38 -0
  239. package/dist/viz/mermaid.js +706 -0
  240. package/package.json +82 -0
@@ -0,0 +1,963 @@
1
+ /**
2
+ * HelixExtension — Module, Agent, Actor kinds.
3
+ *
4
+ * 1:1 parity with Python dna.v3.extensions.helix.
5
+ */
6
+ import yaml from "js-yaml";
7
+ import { join as pathJoin } from "node:path";
8
+ import { nodeFS, readTextSafe, collectDir } from "../kernel/fs.js";
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";
11
+ import { SD } from "../kernel/protocols.js";
12
+ import { readSpecString, readSpecStringArray } from "../kernel/spec-access.js";
13
+ import { SettingKind, ThemeKind, UserProfileKind, CanvasKind } from "./helix_extras.js";
14
+ import { registerWriteGuards } from "./helix/write-guards.js";
15
+ const MOD_URL = import.meta.url;
16
+ // GenomeKind — Phase 16 (scope segregation)
17
+ //
18
+ // Replaces ModuleKind as the scope-root identity Kind. Carries catalog
19
+ // identity, versioning, runtime defaults, and external dependencies.
20
+ // Tenant overlay is field-level via OVERLAYABLE_FIELDS allowlist.
21
+ //
22
+ // 1:1 parity with Python dna.extensions.helix.GenomeKind.
23
+ // Phase 16 commit 3 — root flag transferred from Module to Genome
24
+ // once examples migrated to ``Genome.yaml``. Module's ``isRoot`` flips
25
+ // to false in lockstep so the "exactly one root Kind" invariant is
26
+ // preserved. Commit 4 removes ``isRoot`` from KindPort entirely.
27
+ // ---------------------------------------------------------------------------
28
+ class GenomeKind extends KindBase {
29
+ apiVersion = "github.com/ruinosus/dna/v1";
30
+ kind = "Genome";
31
+ alias = "helix-genome";
32
+ isSchemaAffecting = true;
33
+ isOverlayable = false;
34
+ scopeInheritable = false;
35
+ // Genome IS the catalog identity (Phase 3b ch1, i-112;
36
+ // s-write-path-despecialize). Py twin: GenomeKind.is_catalog_identity.
37
+ isCatalogIdentity = true;
38
+ origin = "github.com/ruinosus/dna";
39
+ isPromptTarget = false;
40
+ promptTargetPriority = 0;
41
+ flattenInContext = false;
42
+ storage = SD.root("Genome.yaml");
43
+ graphStyle = { fill: "#3B82F6", stroke: "#1D4ED8", textColor: "#fff" };
44
+ asciiIcon = "📦";
45
+ displayLabel = "Genome";
46
+ _sourceUrl = MOD_URL;
47
+ // Tenant-overlayable fields. Identity and versioning are NOT here on
48
+ // purpose: tenant overlays must not change owner, version, etc.
49
+ // Kernel enforces this allowlist (commit 2).
50
+ static OVERLAYABLE_FIELDS = new Set([
51
+ "default_agent",
52
+ "default_llm",
53
+ "budget",
54
+ "tags",
55
+ ]);
56
+ docs = "A Genome is the scope-root identity document (Phase 16). It declares " +
57
+ "catalog identity (owner, owner_tenant, repository, visibility), " +
58
+ "versioning (version, changelog_url, deprecated), runtime defaults " +
59
+ "(default_agent, default_llm, budget, tags), and external dependencies. " +
60
+ "Replaces the legacy Module Kind. Layer policy moved to LayerPolicy " +
61
+ "docs at <scope>/policies/. Custom Kinds moved to KindDefinition docs " +
62
+ "at <scope>/kinds/.";
63
+ uiSchema = {
64
+ owner_tenant: { widget: "readonly", label: "Owner tenant", help: "null = platform-owned (catalog item).", order: 5 },
65
+ visibility: { widget: "select", label: "Visibility", options: ["public", "internal", "private"], help: "Who can discover and install this Genome.", order: 6 },
66
+ version: { widget: "text", label: "Version", help: "Semver. Opt-in. null = unversioned.", order: 7 },
67
+ changelog_url: { widget: "text", label: "Changelog URL", order: 8 },
68
+ deprecated: { widget: "checkbox", label: "Deprecated", order: 9 },
69
+ deprecated_message: { widget: "textarea", label: "Deprecated message", order: 10 },
70
+ default_agent: { widget: "text", label: "Default agent", help: "Tenant-overlayable.", order: 20 },
71
+ default_llm: { widget: "text", label: "Default LLM", help: "Tenant-overlayable.", order: 21 },
72
+ budget: { widget: "readonly", label: "Budget", help: "Tenant-overlayable.", order: 22 },
73
+ tags: { widget: "tags", label: "Tags", help: "Tenant-overlayable.", order: 23 },
74
+ owner: { widget: "text", label: "Owner", order: 30 },
75
+ repository: { widget: "text", label: "Repository", order: 31 },
76
+ dependencies: { widget: "readonly", label: "External dependencies", order: 90 },
77
+ };
78
+ // Genome has no inventory deps; ``dependencies`` is a list of external
79
+ // module refs resolved via ResolverPort. Composition validation walks
80
+ // scanner-discovered docs directly.
81
+ dependencies() { return null; }
82
+ schema() { return zodSpecToJsonSchema(GenomeSpecSchema); }
83
+ getDefaultAgentName(doc) {
84
+ return readSpecString(doc, "default_agent") ?? null;
85
+ }
86
+ parse(raw) {
87
+ return GenomeSchema.parse(raw);
88
+ }
89
+ describe(doc) {
90
+ const lines = [`Name: ${doc.name}`, `Kind: Genome`];
91
+ const ownerTenant = readSpecString(doc, "owner_tenant") ?? "platform";
92
+ lines.push(`Owner: ${ownerTenant}`);
93
+ const version = readSpecString(doc, "version");
94
+ if (version)
95
+ lines.push(`Version: ${version}`);
96
+ const visibility = readSpecString(doc, "visibility");
97
+ if (visibility)
98
+ lines.push(`Visibility: ${visibility}`);
99
+ const defaultAgent = readSpecString(doc, "default_agent");
100
+ if (defaultAgent)
101
+ lines.push(`Default: ${defaultAgent}`);
102
+ const spec = (doc.spec ?? {});
103
+ if (spec.deprecated) {
104
+ const msg = readSpecString(doc, "deprecated_message") ?? "";
105
+ lines.push(`Deprecated: ${msg}`);
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+ summary(doc) {
110
+ const spec = (doc.spec ?? {});
111
+ return {
112
+ owner_tenant: spec.owner_tenant ?? null,
113
+ visibility: spec.visibility ?? null,
114
+ version: spec.version ?? null,
115
+ default_agent: spec.default_agent ?? null,
116
+ deprecated: Boolean(spec.deprecated),
117
+ };
118
+ }
119
+ preview(doc) {
120
+ const spec = (doc.spec ?? {});
121
+ const fields = [];
122
+ for (const label of ["owner_tenant", "visibility", "version", "default_agent", "default_llm"]) {
123
+ const value = spec[label];
124
+ if (value !== null && value !== undefined && value !== "") {
125
+ fields.push({ label, value: String(value) });
126
+ }
127
+ }
128
+ if (spec.deprecated) {
129
+ fields.push({ label: "deprecated", value: String(spec.deprecated_message ?? "true") });
130
+ }
131
+ const deps = spec.dependencies;
132
+ if (Array.isArray(deps) && deps.length > 0) {
133
+ fields.push({ label: "dependencies", value: `${deps.length} entries` });
134
+ }
135
+ if (fields.length === 0) {
136
+ return [{ kind: "empty", title: `Genome ${doc.name}` }];
137
+ }
138
+ return [{ kind: "fields", title: `Genome ${doc.name}`, fields }];
139
+ }
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // LayerPolicyKind — Phase 16
143
+ //
144
+ // Overlay policy per (layer, kind) tuple. One LayerPolicy doc per layer
145
+ // dimension (e.g. tenant, branch). Lives at ``<scope>/policies/<id>.yaml``.
146
+ // Replaces the legacy ``Module.spec.layers`` field.
147
+ //
148
+ // 1:1 parity with Python dna.extensions.helix.LayerPolicyKind.
149
+ // ---------------------------------------------------------------------------
150
+ class LayerPolicyKind extends KindBase {
151
+ apiVersion = "github.com/ruinosus/dna/policy/v1";
152
+ kind = "LayerPolicy";
153
+ alias = "policy-layer-policy"; // s-kind-alias-convention-fix: <owner>-<kebab(kind)>; was "policy-layer"
154
+ isSchemaAffecting = true;
155
+ isOverlayable = false;
156
+ scopeInheritable = false;
157
+ origin = "github.com/ruinosus/dna/policy";
158
+ isPromptTarget = false;
159
+ promptTargetPriority = 0;
160
+ flattenInContext = false;
161
+ storage = SD.yaml("policies");
162
+ graphStyle = { fill: "#A855F7", stroke: "#7E22CE", textColor: "#fff" };
163
+ asciiIcon = "🔒";
164
+ displayLabel = "Layer Policies";
165
+ _sourceUrl = MOD_URL;
166
+ docs = "A LayerPolicy declares overlay rules for one layer dimension " +
167
+ "(tenant, branch, region, etc.). Kernel reads these docs to enforce " +
168
+ "write policy when a layer overlay is applied. Replaces the legacy " +
169
+ "Module.spec.layers field. Some Kinds are structurally non-overlayable " +
170
+ "(Genome, KindDefinition, LayerPolicy itself) — their policy is " +
171
+ "always locked regardless of doc contents.";
172
+ uiSchema = {
173
+ layer_id: { widget: "select", label: "Layer dimension", options: ["tenant", "branch", "region", "user"], help: "Which layer dimension this policy applies to.", order: 10 },
174
+ policies: { widget: "readonly", label: "Per-Kind policies", help: "Map of kind alias → policy (open/restricted/locked).", order: 20 },
175
+ };
176
+ dependencies() { return null; }
177
+ schema() { return zodSpecToJsonSchema(LayerPolicySpecSchema); }
178
+ parse(raw) {
179
+ const parsed = LayerPolicySchema.parse(raw);
180
+ // Mirror Python's LayerPolicySpec.from_raw normalization:
181
+ // lowercase + drop falsy values + drop non-string keys.
182
+ const normalized = {};
183
+ for (const [k, v] of Object.entries(parsed.spec.policies ?? {})) {
184
+ if (typeof k === "string" && k && v) {
185
+ normalized[k] = String(v).toLowerCase();
186
+ }
187
+ }
188
+ parsed.spec.policies = normalized;
189
+ return parsed;
190
+ }
191
+ describe(doc) {
192
+ const spec = (doc.spec ?? {});
193
+ const layerId = spec.layer_id ?? doc.name;
194
+ const policies = (spec.policies ?? {});
195
+ const n = Object.keys(policies).length;
196
+ return `Name: ${doc.name}\nKind: LayerPolicy\nLayer: ${layerId}\nRules: ${n}`;
197
+ }
198
+ summary(doc) {
199
+ const spec = (doc.spec ?? {});
200
+ const policies = (spec.policies ?? {});
201
+ return {
202
+ layer_id: spec.layer_id ?? null,
203
+ rule_count: Object.keys(policies).length,
204
+ };
205
+ }
206
+ preview(doc) {
207
+ const spec = (doc.spec ?? {});
208
+ const fields = [];
209
+ if (spec.layer_id) {
210
+ fields.push({ label: "layer_id", value: String(spec.layer_id) });
211
+ }
212
+ const policies = spec.policies;
213
+ if (policies && typeof policies === "object") {
214
+ for (const alias of Object.keys(policies).sort()) {
215
+ fields.push({ label: alias, value: String(policies[alias]) });
216
+ }
217
+ }
218
+ if (fields.length === 0) {
219
+ return [{ kind: "empty", title: `LayerPolicy ${doc.name}` }];
220
+ }
221
+ return [{ kind: "fields", title: `LayerPolicy ${doc.name}`, fields }];
222
+ }
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // AgentKind
226
+ // ---------------------------------------------------------------------------
227
+ class AgentKind extends KindBase {
228
+ apiVersion = "github.com/ruinosus/dna/v1";
229
+ kind = "Agent";
230
+ alias = "helix-agent";
231
+ isSchemaAffecting = true;
232
+ origin = "github.com/ruinosus/dna";
233
+ isPromptTarget = true;
234
+ promptTargetPriority = 10;
235
+ flattenInContext = false;
236
+ storage = SD.bundle("agents", "AGENT.md");
237
+ graphStyle = { fill: "#F97316", stroke: "#EA580C", textColor: "#fff" };
238
+ asciiIcon = "🤖";
239
+ displayLabel = "Agents";
240
+ _sourceUrl = MOD_URL;
241
+ docs = "A Agent is the primary prompt target in a helix manifest — " +
242
+ "what actually runs when a user (or another agent) talks to the system. " +
243
+ "It carries an instruction, a model, and dep_filters declaring which " +
244
+ "Soul, Skills, Guardrails, and Actors it composes with. Bundle-based " +
245
+ "storage: agents/<name>/AGENT.md.";
246
+ uiSchema = {
247
+ instruction: {
248
+ widget: "markdown-toc",
249
+ label: "Instruction (AGENT.md)",
250
+ help: "The agent's main prompt body. Supports Mustache tags like {{soul_content}}.",
251
+ height: 520,
252
+ order: 10,
253
+ },
254
+ objective: { widget: "textarea", label: "Objective", order: 15 },
255
+ model: { widget: "text", label: "Model", order: 20 },
256
+ soul: { widget: "text", label: "Soul", help: "Name of the Soul doc to flatten into the prompt.", order: 30 },
257
+ skills: { widget: "tags", label: "Skills", order: 40 },
258
+ actors: { widget: "tags", label: "Actors this agent serves", order: 50 },
259
+ guardrails: { widget: "tags", label: "Guardrails", order: 60 },
260
+ tools: { widget: "tags", label: "Tools", order: 70 },
261
+ team_members: { widget: "tags", label: "Team members", order: 80 },
262
+ // s-mcp-servers-on-agent (Py parity) — MCPFederation refs.
263
+ mcp_servers: {
264
+ widget: "tags",
265
+ label: "MCP servers",
266
+ help: "MCPFederation doc names this agent consumes (e.g. 'drawio'). Remote tools load as first-class agent tools tagged mcp:<ref>. Entries may also be objects {ref, allowed_tools, timeout_s} for per-agent overrides.",
267
+ order: 87,
268
+ },
269
+ tags: { widget: "tags", label: "Tags", order: 90 },
270
+ };
271
+ depFilters() {
272
+ return {
273
+ soul: "soulspec-soul",
274
+ skills: "agentskills-skill",
275
+ guardrails: "guardrails-guardrail",
276
+ actors: "helix-actor",
277
+ tools: "helix-tool",
278
+ };
279
+ }
280
+ schema() { return zodSpecToJsonSchema(AgentSpecSchema); }
281
+ parse(raw) {
282
+ return AgentSchema.parse(raw);
283
+ }
284
+ describe(doc) {
285
+ const spec = doc.spec;
286
+ const meta = doc.metadata;
287
+ const name = meta.name ?? "";
288
+ const desc = meta.description ?? "";
289
+ const soul = spec.soul ?? "";
290
+ const skills = spec.skills ?? [];
291
+ const model = spec.model ?? "";
292
+ const lines = [`Name: ${name}`, `Kind: Agent`];
293
+ if (desc)
294
+ lines.push(`Desc: ${desc}`);
295
+ if (soul)
296
+ lines.push(`Soul: ${soul}`);
297
+ if (skills.length > 0)
298
+ lines.push(`Skills: ${skills.join(", ")} (${skills.length})`);
299
+ if (model)
300
+ lines.push(`Model: ${model}`);
301
+ return lines.join("\n");
302
+ }
303
+ summary(doc) {
304
+ const skills = readSpecStringArray(doc, "skills");
305
+ return { skills: skills.length, soul: readSpecString(doc, "soul") ?? null };
306
+ }
307
+ promptTemplate() {
308
+ return "{{{agent.instruction}}}\n\n{{{soul_content}}}";
309
+ }
310
+ preview(doc) {
311
+ const spec = (doc.spec ?? {});
312
+ const blocks = [];
313
+ const instruction = typeof spec.instruction === "string" ? spec.instruction : "";
314
+ if (instruction) {
315
+ // Show the raw template with mustache placeholders intact — Self
316
+ // means "the doc you're editing", not the compiled prompt.
317
+ blocks.push({ kind: "markdown", title: "AGENT.md (template)", body: instruction });
318
+ }
319
+ const meta = [];
320
+ if (typeof spec.model === "string")
321
+ meta.push({ label: "model", value: spec.model });
322
+ if (typeof spec.soul === "string")
323
+ meta.push({ label: "soul", value: spec.soul });
324
+ for (const f of ["skills", "guardrails", "tools"]) {
325
+ const arr = spec[f];
326
+ if (Array.isArray(arr) && arr.length > 0) {
327
+ meta.push({ label: f, value: arr.map(String).join(", ") });
328
+ }
329
+ }
330
+ if (meta.length > 0) {
331
+ blocks.push({ kind: "fields", title: "Metadata", fields: meta });
332
+ }
333
+ if (blocks.length === 0) {
334
+ return [{ kind: "empty", title: `Agent ${doc.name}` }];
335
+ }
336
+ return blocks;
337
+ }
338
+ }
339
+ // ---------------------------------------------------------------------------
340
+ // ActorKind
341
+ // ---------------------------------------------------------------------------
342
+ class ActorKind extends KindBase {
343
+ apiVersion = "github.com/ruinosus/dna/v1";
344
+ kind = "Actor";
345
+ alias = "helix-actor";
346
+ origin = "github.com/ruinosus/dna/actor";
347
+ isPromptTarget = false;
348
+ promptTargetPriority = 0;
349
+ flattenInContext = false;
350
+ storage = SD.yaml("actors");
351
+ graphStyle = { fill: "#EC4899", stroke: "#DB2777", textColor: "#fff" };
352
+ asciiIcon = "👤";
353
+ displayLabel = "Actors";
354
+ _sourceUrl = MOD_URL;
355
+ docs = "An Actor is a UML-canonical participant in the system — a human user, " +
356
+ "an external system, or a time/schedule trigger. The actorType field " +
357
+ "disambiguates human/system/time. Actors are referenced by UseCases and " +
358
+ "Agents via dep_filters.actors but are not prompt targets.";
359
+ uiSchema = {
360
+ role: { widget: "text", label: "Role", help: "Short job title or functional role.", order: 10 },
361
+ actor_type: {
362
+ widget: "select",
363
+ label: "Actor type",
364
+ help: "human = person/role; system = external service or upstream; time = scheduled trigger.",
365
+ order: 20,
366
+ },
367
+ goals: { widget: "list-markdown", label: "Goals", help: "What this actor is trying to achieve.", order: 30 },
368
+ pain_points: { widget: "list-markdown", label: "Pain points", order: 40 },
369
+ preferences: { widget: "readonly", label: "Preferences", help: "Nested object; edit in YAML for now.", order: 90 },
370
+ };
371
+ schema() { return zodSpecToJsonSchema(ActorSpecSchema); }
372
+ parse(raw) {
373
+ return ActorSchema.parse(raw);
374
+ }
375
+ summary() { return null; }
376
+ preview(doc) {
377
+ const spec = (doc.spec ?? {});
378
+ const fields = [];
379
+ if (typeof spec.role === "string")
380
+ fields.push({ label: "role", value: spec.role });
381
+ if (typeof spec.actor_type === "string")
382
+ fields.push({ label: "actor_type", value: spec.actor_type });
383
+ if (Array.isArray(spec.goals) && spec.goals.length > 0) {
384
+ fields.push({
385
+ label: "goals",
386
+ value: spec.goals.map((g) => `• ${String(g)}`).join("\n"),
387
+ });
388
+ }
389
+ if (Array.isArray(spec.pain_points) && spec.pain_points.length > 0) {
390
+ fields.push({
391
+ label: "pain_points",
392
+ value: spec.pain_points.map((g) => `• ${String(g)}`).join("\n"),
393
+ });
394
+ }
395
+ if (fields.length === 0) {
396
+ return [{ kind: "empty", title: `Actor ${doc.name}` }];
397
+ }
398
+ return [{ kind: "fields", title: `Actor ${doc.name}`, fields }];
399
+ }
400
+ }
401
+ // ---------------------------------------------------------------------------
402
+ // UseCaseKind
403
+ // ---------------------------------------------------------------------------
404
+ class UseCaseKind extends KindBase {
405
+ apiVersion = "github.com/ruinosus/dna/v1";
406
+ kind = "UseCase";
407
+ alias = "helix-usecase";
408
+ origin = "github.com/ruinosus/dna/usecase";
409
+ isPromptTarget = false;
410
+ promptTargetPriority = 0;
411
+ flattenInContext = false;
412
+ storage = SD.yaml("use_cases");
413
+ graphStyle = { fill: "#F59E0B", stroke: "#D97706", textColor: "#fff" };
414
+ asciiIcon = "📋";
415
+ displayLabel = "UseCases";
416
+ _sourceUrl = MOD_URL;
417
+ docs = "A UseCase is a UML-canonical use case: a goal-oriented interaction " +
418
+ "between Actors and the system. It composes primary_actor, supporting " +
419
+ "actors, agents, preconditions, main_flow, alternate_flows, " +
420
+ "postconditions, and success_criteria. Purely declarative — not a " +
421
+ "prompt target — consumed by tooling for traceability.";
422
+ uiSchema = {
423
+ primary_actor: { widget: "text", label: "Primary actor", help: "Name of the Actor doc that initiates this use case.", order: 10 },
424
+ supporting_actors: { widget: "tags", label: "Supporting actors", order: 20 },
425
+ agents: { widget: "tags", label: "Agents", help: "Agents that fulfill this use case.", order: 30 },
426
+ soul: { widget: "text", label: "Soul", help: "Name of the Soul that shapes the tone of this flow. Optional — overrides the agent's soul for this use case scope.", order: 40 },
427
+ skills: { widget: "tags", label: "Skills", help: "Skills required by the agents to fulfill this use case.", order: 50 },
428
+ tools: { widget: "tags", label: "Tools", help: "Tools the agents invoke during this use case.", order: 60 },
429
+ guardrails: { widget: "tags", label: "Guardrails", help: "Guardrails that apply specifically to this use case.", order: 70 },
430
+ preconditions: { widget: "list-markdown", label: "Preconditions", order: 80 },
431
+ main_flow: { widget: "list-markdown", label: "Main flow", help: "Ordered steps describing the happy path.", order: 90 },
432
+ alternate_flows: { widget: "readonly", label: "Alternate flows", help: "Named deviations. Nested object; edit in YAML.", order: 100 },
433
+ postconditions: { widget: "list-markdown", label: "Postconditions", order: 110 },
434
+ success_criteria: { widget: "list-markdown", label: "Success criteria", order: 120 },
435
+ };
436
+ depFilters() {
437
+ return {
438
+ primary_actor: "helix-actor",
439
+ supporting_actors: "helix-actor",
440
+ agents: "helix-agent",
441
+ soul: "soulspec-soul",
442
+ skills: "agentskills-skill",
443
+ tools: "helix-tool",
444
+ guardrails: "guardrails-guardrail",
445
+ };
446
+ }
447
+ schema() { return zodSpecToJsonSchema(UseCaseSpecSchema); }
448
+ parse(raw) {
449
+ return UseCaseSchema.parse(raw);
450
+ }
451
+ summary() { return null; }
452
+ preview(doc) {
453
+ const spec = (doc.spec ?? {});
454
+ const fields = [];
455
+ if (typeof spec.primary_actor === "string")
456
+ fields.push({ label: "primary_actor", value: spec.primary_actor });
457
+ for (const f of ["supporting_actors", "agents", "skills", "tools", "guardrails"]) {
458
+ const arr = spec[f];
459
+ if (Array.isArray(arr) && arr.length > 0) {
460
+ fields.push({ label: f, value: arr.map(String).join(", ") });
461
+ }
462
+ }
463
+ if (Array.isArray(spec.preconditions) && spec.preconditions.length > 0) {
464
+ fields.push({
465
+ label: "preconditions",
466
+ value: spec.preconditions.map((s) => `• ${String(s)}`).join("\n"),
467
+ });
468
+ }
469
+ if (Array.isArray(spec.main_flow) && spec.main_flow.length > 0) {
470
+ fields.push({
471
+ label: "main_flow",
472
+ value: spec.main_flow.map((s, i) => `${i + 1}. ${String(s)}`).join("\n"),
473
+ });
474
+ }
475
+ if (Array.isArray(spec.success_criteria) && spec.success_criteria.length > 0) {
476
+ fields.push({
477
+ label: "success_criteria",
478
+ value: spec.success_criteria.map((s) => `• ${String(s)}`).join("\n"),
479
+ });
480
+ }
481
+ if (fields.length === 0) {
482
+ return [{ kind: "empty", title: `UseCase ${doc.name}` }];
483
+ }
484
+ return [{ kind: "fields", title: `UseCase ${doc.name}`, fields }];
485
+ }
486
+ }
487
+ // ---------------------------------------------------------------------------
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
+ // AgentReader / AgentWriter
585
+ // ---------------------------------------------------------------------------
586
+ const KNOWN_DIRS = new Set(["scripts", "references", "assets"]);
587
+ // AGENT.md frontmatter passthrough allowlist.
588
+ //
589
+ // Derived from ``AgentSpecSchema`` so adding a field to the
590
+ // Zod schema automatically opens it in the reader and writer — no
591
+ // separate allowlist to keep in sync. Two recurring bugs (the
592
+ // ``shell_sandbox`` 2026-05-08 drift, and ``codegraph`` /
593
+ // ``tool_groups`` / ``tests`` shortly after) were caused by exactly
594
+ // that drift.
595
+ //
596
+ // ``instruction`` is excluded: the reader fills it from the AGENT.md
597
+ // body (or via ``instruction_file`` resolution), never from a top-
598
+ // level frontmatter key. Allowing it here would let an authoring
599
+ // mistake (frontmatter ``instruction:``) silently shadow the body.
600
+ const SPEC_FIELDS = new Set(Object.keys(AgentSpecSchema.shape).filter((k) => k !== "instruction"));
601
+ // ---------------------------------------------------------------------------
602
+ // instruction_file resolver
603
+ // ---------------------------------------------------------------------------
604
+ function _resolveInstructionFile(fs, bundlePath, rel) {
605
+ if (typeof rel !== "string" || rel.length === 0) {
606
+ throw new Error("instruction_file must be a non-empty string");
607
+ }
608
+ if (rel.startsWith("/") || /^[A-Za-z]:/.test(rel)) {
609
+ throw new Error(`instruction_file must be a relative path, got ${JSON.stringify(rel)}`);
610
+ }
611
+ // Count '..' segments in the raw path to catch escape attempts
612
+ const upCount = rel.split("/").filter((p) => p === "..").length;
613
+ if (upCount > 3) {
614
+ throw new Error(`instruction_file exceeds depth cap (up_count=${upCount}, max=3): ${JSON.stringify(rel)}`);
615
+ }
616
+ // Resolve relative to the bundle directory (path.join handles '..' normalization)
617
+ const resolved = pathJoin(bundlePath, rel);
618
+ return fs.readFile(resolved);
619
+ }
620
+ export class AgentReader {
621
+ fs;
622
+ constructor(fs = nodeFS) {
623
+ this.fs = fs;
624
+ }
625
+ detect(bundle) {
626
+ const path = bundle.path ?? "";
627
+ return this.fs.exists(`${path}/AGENT.md`);
628
+ }
629
+ read(bundle) {
630
+ const path = bundle.path ?? "";
631
+ const agentMd = this.fs.readFile(`${path}/AGENT.md`);
632
+ const fm = this._parseFrontmatter(agentMd);
633
+ const name = fm.name || path.split("/").pop() || "";
634
+ const description = fm.description ?? "";
635
+ const labels = fm.labels ?? null;
636
+ // Extract body (after frontmatter)
637
+ const body = agentMd.replace(/^---\n[\s\S]*?---\n?/, "").trim();
638
+ const instructionFile = fm.instruction_file;
639
+ let spec;
640
+ if (instructionFile !== undefined) {
641
+ if (fm.instruction) {
642
+ throw new Error(`${path}: cannot set both frontmatter 'instruction' and 'instruction_file'`);
643
+ }
644
+ if (body.trim() !== "") {
645
+ throw new Error(`${path}: cannot set both AGENT.md body and instruction_file`);
646
+ }
647
+ const content = _resolveInstructionFile(this.fs, path, instructionFile);
648
+ spec = { instruction: content, instruction_file: instructionFile };
649
+ }
650
+ else {
651
+ spec = { instruction: body };
652
+ }
653
+ for (const field of SPEC_FIELDS) {
654
+ if (field in fm && field !== "instruction_file") {
655
+ spec[field] = fm[field];
656
+ }
657
+ }
658
+ // Collect known subdirectories
659
+ for (const dirName of KNOWN_DIRS) {
660
+ const sub = `${path}/${dirName}`;
661
+ if (this.fs.isDirectory(sub)) {
662
+ const files = collectDir(this.fs, sub, sub);
663
+ if (Object.keys(files).length > 0) {
664
+ spec[dirName] = files;
665
+ }
666
+ }
667
+ }
668
+ // Collect extra subdirectories
669
+ const extras = {};
670
+ const entries = this.fs.readDir(path);
671
+ for (const entry of entries) {
672
+ const full = `${path}/${entry}`;
673
+ if (!this.fs.isDirectory(full) || KNOWN_DIRS.has(entry))
674
+ continue;
675
+ const files = collectDir(this.fs, full, full);
676
+ if (Object.keys(files).length > 0) {
677
+ extras[entry] = files;
678
+ }
679
+ }
680
+ if (Object.keys(extras).length > 0) {
681
+ spec.extras = extras;
682
+ }
683
+ // Collect root-level extra files
684
+ const rootFiles = {};
685
+ for (const entry of entries) {
686
+ const full = `${path}/${entry}`;
687
+ if (!this.fs.isFile(full) || entry === "AGENT.md")
688
+ continue;
689
+ const text = readTextSafe(this.fs, full);
690
+ if (text !== null) {
691
+ rootFiles[entry] = text;
692
+ }
693
+ }
694
+ if (Object.keys(rootFiles).length > 0) {
695
+ spec.root_files = rootFiles;
696
+ }
697
+ const metadata = { name, description };
698
+ if (labels) {
699
+ metadata.labels = labels;
700
+ }
701
+ return {
702
+ apiVersion: "github.com/ruinosus/dna/v1",
703
+ kind: "Agent",
704
+ metadata,
705
+ spec,
706
+ };
707
+ }
708
+ _parseFrontmatter(text) {
709
+ const match = text.match(/^---\n([\s\S]*?)---\n?/);
710
+ if (!match)
711
+ return {};
712
+ return yaml.load(match[1]) ?? {};
713
+ }
714
+ }
715
+ export class AgentWriter {
716
+ fs;
717
+ constructor(fs = nodeFS) {
718
+ this.fs = fs;
719
+ }
720
+ canWrite(raw) {
721
+ return raw.kind === "Agent";
722
+ }
723
+ write(bundle, raw) {
724
+ const path = bundle.path ?? "";
725
+ this.fs.mkdir(path);
726
+ const spec = raw.spec ?? {};
727
+ const meta = raw.metadata ?? {};
728
+ const fm = {};
729
+ fm.name = meta.name || path.split("/").pop() || "";
730
+ if (meta.description)
731
+ fm.description = meta.description;
732
+ if (meta.labels)
733
+ fm.labels = meta.labels;
734
+ for (const field of SPEC_FIELDS) {
735
+ if (field in spec && spec[field]) {
736
+ fm[field] = spec[field];
737
+ }
738
+ }
739
+ const frontmatter = yaml.dump(fm, {
740
+ flowLevel: -1,
741
+ forceQuotes: false,
742
+ sortKeys: false,
743
+ });
744
+ const body = spec.instruction_file ? "" : (spec.instruction ?? "");
745
+ this.fs.writeFile(`${path}/AGENT.md`, `---\n${frontmatter}---\n\n${body}`);
746
+ // s-sync-s3 — emit the instruction_file FRAGMENT so the bundle is self-
747
+ // contained (twin of Py AgentWriter). Source: carried source_files entry,
748
+ // else the resolved inline instruction. Without this, writing to a fresh
749
+ // bundle left no instruction.md → the agent's instruction resolved empty.
750
+ const instructionFile = spec.instruction_file;
751
+ const sourceFiles = spec.source_files ?? {};
752
+ if (instructionFile) {
753
+ const frag = sourceFiles[instructionFile] ?? spec.instruction;
754
+ if (frag != null)
755
+ this.fs.writeFile(`${path}/${instructionFile}`, String(frag));
756
+ }
757
+ // Any remaining carried text source_files (binaries aren't handled by the
758
+ // string-only FS shim here — covered by the Postgres net for the runtime).
759
+ for (const [rel, content] of Object.entries(sourceFiles)) {
760
+ if (rel === "AGENT.md" || rel === instructionFile)
761
+ continue;
762
+ if (typeof content === "string")
763
+ this.fs.writeFile(`${path}/${rel}`, content);
764
+ }
765
+ // Write known subdirectories
766
+ for (const dirName of ["scripts", "references", "assets"]) {
767
+ const files = spec[dirName];
768
+ if (files != null && typeof files === "object") {
769
+ for (const [fname, fcontent] of Object.entries(files)) {
770
+ this.fs.writeFile(`${path}/${dirName}/${fname}`, fcontent);
771
+ }
772
+ }
773
+ }
774
+ // Write extras
775
+ const extras = spec.extras;
776
+ if (extras != null && typeof extras === "object") {
777
+ for (const [dirName, dirFiles] of Object.entries(extras)) {
778
+ if (typeof dirFiles === "object") {
779
+ for (const [fname, fcontent] of Object.entries(dirFiles)) {
780
+ this.fs.writeFile(`${path}/${dirName}/${fname}`, fcontent);
781
+ }
782
+ }
783
+ }
784
+ }
785
+ // Write root files
786
+ const rootFiles = spec.root_files;
787
+ if (rootFiles != null && typeof rootFiles === "object") {
788
+ for (const [fname, fcontent] of Object.entries(rootFiles)) {
789
+ this.fs.writeFile(`${path}/${fname}`, fcontent);
790
+ }
791
+ }
792
+ }
793
+ serialize(raw) {
794
+ const files = [];
795
+ const spec = raw.spec ?? {};
796
+ const meta = raw.metadata ?? {};
797
+ // 1. AGENT.md
798
+ const fm = {};
799
+ fm.name = meta.name || "";
800
+ if (meta.description)
801
+ fm.description = meta.description;
802
+ if (meta.labels)
803
+ fm.labels = meta.labels;
804
+ for (const field of SPEC_FIELDS) {
805
+ if (field in spec && spec[field])
806
+ fm[field] = spec[field];
807
+ }
808
+ const frontmatter = yaml.dump(fm, { flowLevel: -1, sortKeys: false });
809
+ const serializeBody = spec.instruction_file ? "" : (spec.instruction ?? "");
810
+ files.push({ relativePath: "AGENT.md", content: `---\n${frontmatter}---\n\n${serializeBody}` });
811
+ // s-sync-s3 — emit the instruction_file fragment + carried text
812
+ // source_files so the serialized bundle is self-contained (twin of write()).
813
+ const serInstrFile = spec.instruction_file;
814
+ const serSourceFiles = spec.source_files ?? {};
815
+ if (serInstrFile) {
816
+ const frag = serSourceFiles[serInstrFile] ?? spec.instruction;
817
+ if (frag != null)
818
+ files.push({ relativePath: serInstrFile, content: String(frag) });
819
+ }
820
+ for (const [rel, content] of Object.entries(serSourceFiles)) {
821
+ if (rel === "AGENT.md" || rel === serInstrFile)
822
+ continue;
823
+ if (typeof content === "string")
824
+ files.push({ relativePath: rel, content });
825
+ }
826
+ // 2. Known sub-directories
827
+ for (const dirName of ["scripts", "references", "assets"]) {
828
+ const dirFiles = spec[dirName];
829
+ if (dirFiles != null && typeof dirFiles === "object") {
830
+ for (const [fname, fcontent] of Object.entries(dirFiles)) {
831
+ files.push({ relativePath: `${dirName}/${fname}`, content: fcontent });
832
+ }
833
+ }
834
+ }
835
+ // 3. Extras
836
+ const extras = spec.extras;
837
+ if (extras != null && typeof extras === "object") {
838
+ for (const [dirName, dirFiles] of Object.entries(extras)) {
839
+ if (typeof dirFiles === "object") {
840
+ for (const [fname, fcontent] of Object.entries(dirFiles)) {
841
+ files.push({ relativePath: `${dirName}/${fname}`, content: fcontent });
842
+ }
843
+ }
844
+ }
845
+ }
846
+ // 4. Root files
847
+ const rootFiles = spec.root_files;
848
+ if (rootFiles != null && typeof rootFiles === "object") {
849
+ for (const [fname, fcontent] of Object.entries(rootFiles)) {
850
+ files.push({ relativePath: fname, content: fcontent });
851
+ }
852
+ }
853
+ return files;
854
+ }
855
+ }
856
+ // ---------------------------------------------------------------------------
857
+ // Extension
858
+ // ---------------------------------------------------------------------------
859
+ // ---------------------------------------------------------------------------
860
+ // Composition profile — declares how Agent composes with other kinds
861
+ // ---------------------------------------------------------------------------
862
+ const HELIX_PROFILE = {
863
+ orchestratorAlias: "helix-agent",
864
+ label: "Helix Agent",
865
+ slots: [
866
+ {
867
+ name: "soul",
868
+ targetAlias: "soulspec-soul",
869
+ cardinality: "one",
870
+ order: 1,
871
+ filterable: false,
872
+ timeline: { label: "Soul", itemLabel: "personality loaded" },
873
+ healthCheck: {
874
+ rule: "at-least-one",
875
+ severity: "warn",
876
+ issueKey: "agents_without_soul",
877
+ message: "Agent has no soul",
878
+ },
879
+ quadrant: null,
880
+ },
881
+ {
882
+ name: "skills",
883
+ targetAlias: "agentskills-skill",
884
+ cardinality: "many",
885
+ order: 2,
886
+ filterable: true,
887
+ timeline: { label: "Skills", itemLabel: "instruction loaded" },
888
+ healthCheck: null,
889
+ quadrant: { axis: "x", label: "Few Skills --> Many Skills", maxScale: 15 },
890
+ },
891
+ {
892
+ name: "guardrails",
893
+ targetAlias: "guardrails-guardrail",
894
+ cardinality: "many",
895
+ order: 3,
896
+ filterable: true,
897
+ timeline: { label: "Guardrails", itemLabel: "rules applied" },
898
+ healthCheck: {
899
+ rule: "at-least-one",
900
+ severity: "warn",
901
+ issueKey: "agents_without_guardrails",
902
+ message: "Agent has no guardrails",
903
+ },
904
+ quadrant: { axis: "y", label: "Few Guardrails --> Many Guardrails", maxScale: 10 },
905
+ },
906
+ {
907
+ name: "tools",
908
+ targetAlias: "helix-tool",
909
+ cardinality: "many",
910
+ order: 4,
911
+ filterable: false,
912
+ timeline: null,
913
+ healthCheck: null,
914
+ quadrant: null,
915
+ },
916
+ {
917
+ name: "actors",
918
+ targetAlias: "helix-actor",
919
+ cardinality: "many",
920
+ order: 5,
921
+ filterable: false,
922
+ timeline: null,
923
+ healthCheck: null,
924
+ quadrant: null,
925
+ },
926
+ ],
927
+ };
928
+ // ---------------------------------------------------------------------------
929
+ // HelixExtension
930
+ // ---------------------------------------------------------------------------
931
+ export class HelixExtension {
932
+ fs;
933
+ name = "helix";
934
+ version = "1.0.0";
935
+ constructor(fs = nodeFS) {
936
+ this.fs = fs;
937
+ }
938
+ register(kernel) {
939
+ // Phase 16 cleanup — ModuleKind class deleted. GenomeKind is the
940
+ // canonical root identity Kind.
941
+ kernel.kind(new GenomeKind());
942
+ kernel.kind(new LayerPolicyKind());
943
+ kernel.kind(new AgentKind());
944
+ kernel.kind(new ToolKind());
945
+ kernel.kind(new ActorKind());
946
+ kernel.kind(new UseCaseKind());
947
+ // 2026-05-26 — absorbed from claude-code-templates catalog (MIT).
948
+ // Setting rounds out the Claude-Code-customization primitives that
949
+ // live alongside Skill / UA / Soul / Tool.
950
+ kernel.kind(new SettingKind());
951
+ kernel.kind(new ThemeKind());
952
+ kernel.kind(new UserProfileKind());
953
+ // s-jarvis-canvas (2026-05-27) — shared whiteboard JARVIS ↔ user.
954
+ kernel.kind(new CanvasKind());
955
+ kernel.reader(new AgentReader(this.fs));
956
+ kernel.writer(new AgentWriter(this.fs));
957
+ kernel.compositionProfile(HELIX_PROFILE);
958
+ // s-write-path-despecialize — Agent write rules (platform-agent
959
+ // fork guard, Kind-Writer contract) are pre_save VETO hooks owned by
960
+ // this extension, not kernel special-cases.
961
+ registerWriteGuards(kernel);
962
+ }
963
+ }