@ema.co/machina 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ema Unlimited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # **eMa***china*
2
+
3
+ Agent-skills packages for **eMa***china*. Each skill is a self-contained package under [`skills/<name>/`](skills/) with a `SKILL.md` plus optional `references/`, `assets/`, and `scripts/` — the [Agent Skills](https://agentskills.io/specification) / [anthropics/skills](https://github.com/anthropics/skills) layout. This repo ships one marketplace, **`ema`**, with one plugin, **`emachina`**, that exposes every package under `skills/`.
4
+
5
+ This is **not** an application — there is no app build. The two audiences are covered below: [install as a consumer](#install-as-a-consumer) and [develop on this repo](#develop-on-this-repo).
6
+
7
+ ## Skills
8
+
9
+ Packages live under [`skills/`](skills/) — that dir is the authoritative set; the table below is a convenience index.
10
+
11
+ | Skill | What it does | Docs |
12
+ | ----- | ------------ | ---- |
13
+ | [harvest](skills/harvest/) | Universal context harvester — gather/synthesize context from files, URLs, transcripts, CRM, web, or tools into one `harvest-yield.md`. Invoke with `/harvest`. | [Architecture](skills/harvest/README.md) |
14
+
15
+ ## Install (as a consumer)
16
+
17
+ You want to *use* the skills, not develop them. The plugin is `emachina`, in marketplace `ema`.
18
+
19
+ ### Claude Code
20
+
21
+ In a Claude Code session:
22
+
23
+ ```text
24
+ /plugin marketplace add Ema-Unlimited/eMachina
25
+ /plugin install emachina@ema
26
+ ```
27
+
28
+ `marketplace add` also accepts a URL or a local path (e.g. a clone: `/plugin marketplace add ~/path/to/eMachina`). After install, the skills are discoverable — e.g. type `/harvest` or describe a gathering task and the `harvest` skill triggers on its description. Verified with `claude plugin validate --strict`; consumer `marketplace add` / `install` confirmed locally.
29
+
30
+ ### Cursor
31
+
32
+ Install via Cursor's plugin marketplace when published, or open a clone of this repo directly — Cursor loads project skills from `.agents/skills/` ([Cursor Agent Skills docs](https://cursor.com/docs/skills)), which the `.agents/skills → ../skills` hub exposes. No separate `.cursor/skills` symlink is needed.
33
+
34
+ ## Develop on this repo
35
+
36
+ You want to author or change a skill.
37
+
38
+ **1. Get the repo and open it.**
39
+
40
+ ```bash
41
+ git clone git@github.com:Ema-Unlimited/eMachina.git
42
+ cd eMachina
43
+ ```
44
+
45
+ Open in Claude Code or Cursor. Packages under `skills/` are discovered through the symlink hub (below) — no install step needed for local development; edits are live.
46
+
47
+ **2. Read the guidelines first.** [`AGENTS.md`](AGENTS.md) is the canonical project instruction ([`CLAUDE.md`](CLAUDE.md) just imports it, since Claude Code looks for that filename). Authorship rules for packages under `skills/` are in [`skills/AGENTS.md`](skills/AGENTS.md).
48
+
49
+ **3. Author or change a skill** (full procedure in [`AGENTS.md`](AGENTS.md) → *Author or change a skill*):
50
+ - Put the package at `skills/<name>/` with a `SKILL.md` (name + description frontmatter). It is **auto-discovered** under the plugin `source` — no manifest edit needed.
51
+ - Edit the **canonical tree** `skills/<name>/`, never a nested copy under the discovery dirs (`.claude/skills/`, `.agents/skills/`) — those resolve here via symlink.
52
+ - Only touch [`.agents/plugin/marketplace.json`](.agents/plugin/marketplace.json) to add/rename a **plugin** entry itself. Never edit the `.claude-plugin/` or `.cursor-plugin/` views — they are symlinks into it.
53
+
54
+ **4. Validate before you push.**
55
+
56
+ ```bash
57
+ # Plugin manifest (both symlinked views resolve to the same file)
58
+ claude plugin validate .claude-plugin/marketplace.json --strict
59
+
60
+ # Example: harvest's yield-contract validator (per-skill tooling lives in the package)
61
+ python3 skills/harvest/evals/scripts/validate_yield.py <path/to/harvest-yield.md>
62
+ ```
63
+
64
+ > The yield validator needs `pyyaml` (`pip install pyyaml`).
65
+
66
+ **5. Ship via a PR.** Main is protected — branch, push, open a PR (see [`AGENTS.md`](AGENTS.md)). Package a skill for distribution via a discovered packager (e.g. skill-creator) — no fixed `~/.agents/...` path is assumed.
67
+
68
+ ## Layout
69
+
70
+ ```text
71
+ .agents/plugin/marketplace.json # canonical manifest — marketplace "ema" → plugin "emachina"
72
+ .claude-plugin/marketplace.json → ../.agents/plugin/marketplace.json # Claude Code view (symlink)
73
+ .cursor-plugin/marketplace.json → ../.agents/plugin/marketplace.json # Cursor view (symlink)
74
+ .agents/skills → ../skills # agent-agnostic hub (Cursor reads this directly)
75
+ .claude/skills → ../.agents/skills # Claude Code view (routes through the hub)
76
+ .harvest/ # harvest runtime yields (gitignored; see below)
77
+ skills/<name>/ # Canonical packages
78
+ ```
79
+
80
+ One manifest, two symlinked views — edit `.agents/plugin/marketplace.json` only. Skills auto-resolve through the symlink (`source: "./"` is relative to the repo root, i.e. the symlink's location).
81
+
82
+ `.harvest/` tracking (from [`.gitignore`](.gitignore)) — yields (and any transient captures) stay ignored; the agent-memory files under `.harvest/` stay tracked:
83
+
84
+ ```gitignore
85
+ .harvest/*
86
+ !.harvest/AGENTS.md
87
+ !.harvest/CLAUDE.md
88
+ ```
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Vendored from @ema/knowledge-service `src/atom/backends.ts` (2026-07-13).
3
+ * Reference backends: a filesystem StorageBackend (the local staging tier) and a
4
+ * store-derived RetrievalBackend that needs NO DE (keyword scan + audience filter).
5
+ * Cloud implementations (GCS storage; DE/Vertex retrieval via the ema MCP) satisfy
6
+ * the same interfaces — the seam `corpus.ts` selects at runtime.
7
+ */
8
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ function idToFile(id) {
11
+ return `${id.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
12
+ }
13
+ /** PRIMARY store backed by the local filesystem. One JSON blob per atom, keyed by id. */
14
+ export class LocalStorageBackend {
15
+ dir;
16
+ name = "local-fs";
17
+ constructor(dir) {
18
+ this.dir = dir;
19
+ mkdirSync(dir, { recursive: true });
20
+ }
21
+ async put(atom) {
22
+ writeFileSync(join(this.dir, idToFile(atom.id)), JSON.stringify(atom, null, 2));
23
+ }
24
+ async get(id) {
25
+ const path = join(this.dir, idToFile(id));
26
+ if (!existsSync(path))
27
+ return null;
28
+ return JSON.parse(readFileSync(path, "utf8"));
29
+ }
30
+ async has(id) {
31
+ return existsSync(join(this.dir, idToFile(id)));
32
+ }
33
+ async list() {
34
+ if (!existsSync(this.dir))
35
+ return [];
36
+ return readdirSync(this.dir)
37
+ .filter((f) => f.endsWith(".json"))
38
+ .map((f) => JSON.parse(readFileSync(join(this.dir, f), "utf8")));
39
+ }
40
+ async delete(id) {
41
+ const path = join(this.dir, idToFile(id));
42
+ if (existsSync(path))
43
+ rmSync(path);
44
+ }
45
+ }
46
+ /** Caller audience matches an atom iff the atom is public or its token is in the caller set. */
47
+ export function audienceVisible(atomAudience, caller) {
48
+ // Array guard: a nullish or non-array caller must fail closed for a scoped atom
49
+ // (return false), never throw and never fall through to string `.includes` substring matching.
50
+ return atomAudience === "public" || (Array.isArray(caller) && caller.includes(atomAudience));
51
+ }
52
+ function tokenize(s) {
53
+ return s.toLowerCase().match(/[a-z0-9]+/g) ?? [];
54
+ }
55
+ /**
56
+ * DE-free retrieval: an index derived from the primary store. Keyword-overlap scoring
57
+ * over name+description+body, with the `audience` hard filter applied. This is the
58
+ * "works without DE/Vertex" path; org-wide semantic search composes via the ema MCP.
59
+ */
60
+ export class StoreScanRetrieval {
61
+ name = "store-scan";
62
+ atoms = new Map();
63
+ /** Optionally hydrate the projection from the primary store. */
64
+ async rebuildFrom(storage) {
65
+ this.atoms.clear();
66
+ for (const a of await storage.list())
67
+ this.atoms.set(a.id, a);
68
+ }
69
+ async index(atom) {
70
+ this.atoms.set(atom.id, atom);
71
+ }
72
+ async remove(id) {
73
+ this.atoms.delete(id);
74
+ }
75
+ async search(query, opts) {
76
+ const caller = opts.audience ?? ["public"];
77
+ const qTokens = tokenize(query);
78
+ const hits = [];
79
+ for (const atom of this.atoms.values()) {
80
+ if (atom.status === "retracted")
81
+ continue;
82
+ if (!audienceVisible(atom.audience, caller))
83
+ continue;
84
+ const hay = tokenize(`${atom.name} ${atom.description} ${atom.body}`);
85
+ const haySet = new Set(hay);
86
+ const matched = qTokens.filter((t) => haySet.has(t)).length;
87
+ if (matched === 0)
88
+ continue;
89
+ hits.push({
90
+ id: atom.id,
91
+ name: atom.name,
92
+ description: atom.description,
93
+ audience: atom.audience,
94
+ score: qTokens.length ? matched / qTokens.length : 0,
95
+ });
96
+ }
97
+ hits.sort((a, b) => b.score - a.score);
98
+ return hits.slice(0, opts.limit ?? 10);
99
+ }
100
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Vendored from @ema/knowledge-service `src/atom/core.ts` (2026-07-13).
3
+ * The one common normalized path: buildAtom + submitChanges + search, over pluggable
4
+ * backends. The write-ingress invariant: BLOCKING validate → dedup(has(id)) →
5
+ * primary-write-is-truth → best-effort projection → retract = tombstone-not-delete.
6
+ * Conformance: `test/atom-conformance.test.mjs`.
7
+ */
8
+ import { contentAddressId } from "./id.js";
9
+ import { validateAtomInput, assertValidChange, ValidationError } from "./validate.js";
10
+ /** Validate + normalize caller input into a content-addressed Atom. Throws ValidationError on invalid input. */
11
+ export function buildAtom(input) {
12
+ const errors = validateAtomInput(input);
13
+ if (errors.length > 0)
14
+ throw new ValidationError(errors);
15
+ const audience = input.audience ?? "internal"; // public must be explicit
16
+ const id = contentAddressId({ name: input.name, description: input.description, body: input.body });
17
+ return {
18
+ id,
19
+ audience,
20
+ name: input.name,
21
+ description: input.description,
22
+ body: input.body,
23
+ status: "active",
24
+ produced_by: input.produced_by ?? "unknown",
25
+ produced_at: new Date().toISOString(),
26
+ ...(input.supersedes !== undefined ? { supersedes: input.supersedes } : {}),
27
+ ...(input.derived_from !== undefined ? { derived_from: input.derived_from } : {}),
28
+ };
29
+ }
30
+ /** Best-effort projection — a retrieval-backend failure must NOT fail a primary write. */
31
+ async function project(retrieval, fn) {
32
+ if (!retrieval)
33
+ return undefined;
34
+ try {
35
+ await fn(retrieval);
36
+ return undefined;
37
+ }
38
+ catch (err) {
39
+ return `retrieval projection (${retrieval.name}) failed: ${err.message}`;
40
+ }
41
+ }
42
+ /**
43
+ * The sole write ingress. Writes the PRIMARY store first (success = published), then
44
+ * best-effort projects to the retrieval backend. Ops: publish | retract | set_audience.
45
+ */
46
+ export async function submitChanges(change, backends) {
47
+ assertValidChange(change); // BLOCKING validation — no client can bypass
48
+ const { storage, retrieval } = backends;
49
+ switch (change.op) {
50
+ case "publish": {
51
+ if (!change.atom)
52
+ throw new Error("submitChanges(publish): `atom` is required");
53
+ const atom = buildAtom(change.atom);
54
+ if (await storage.has(atom.id)) {
55
+ return { status: "deduplicated", id: atom.id, atom: (await storage.get(atom.id)) ?? atom };
56
+ }
57
+ await storage.put(atom); // PRIMARY write — the publish is durable once this returns
58
+ const warning = await project(retrieval, (r) => r.index(atom));
59
+ return { status: "published", id: atom.id, atom, ...(warning ? { warning } : {}) };
60
+ }
61
+ case "retract": {
62
+ if (!change.id)
63
+ throw new Error("submitChanges(retract): `id` is required");
64
+ const existing = await storage.get(change.id);
65
+ if (!existing)
66
+ throw new Error(`submitChanges(retract): atom "${change.id}" not found`);
67
+ const tombstoned = { ...existing, status: "retracted" };
68
+ await storage.put(tombstoned); // kept, not purged (append-only tombstone)
69
+ await project(retrieval, (r) => r.remove(change.id));
70
+ return { status: "retracted", id: change.id, atom: tombstoned };
71
+ }
72
+ case "set_audience": {
73
+ const id = change.id; // guaranteed present + valid by assertValidChange
74
+ const audience = change.audience;
75
+ const existing = await storage.get(id);
76
+ if (!existing)
77
+ throw new Error(`submitChanges(set_audience): atom "${id}" not found`);
78
+ const updated = { ...existing, audience }; // id unchanged (audience ∉ identity)
79
+ await storage.put(updated);
80
+ await project(retrieval, (r) => r.index(updated));
81
+ return { status: "audience_set", id, atom: updated };
82
+ }
83
+ default: {
84
+ throw new Error(`submitChanges: unknown op "${change.op}"`);
85
+ }
86
+ }
87
+ }
88
+ /** The sole read path — delegates to the retrieval backend with the audience hard filter. */
89
+ export async function search(query, opts, backends) {
90
+ if (!backends.retrieval)
91
+ throw new Error("search: no retrieval backend configured");
92
+ return backends.retrieval.search(query, opts);
93
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Vendored from @ema/knowledge-service `src/pipeline/hash.ts` (2026-07-13) — the canonical
3
+ * hashing for content-addressed atom ids. Wire-conformance guarded by
4
+ * `test/atom-conformance.test.mjs` (golden vectors frozen from the canonical lib).
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ /** Full-width (64 hex char) SHA-256 of a string. */
8
+ export function sha256Hex(input) {
9
+ return createHash("sha256").update(input).digest("hex");
10
+ }
11
+ /**
12
+ * Deterministic ("canonical") JSON: object keys sorted recursively, arrays kept
13
+ * in order, no insignificant whitespace, `undefined` object values omitted (JSON
14
+ * semantics). The same logical value always serializes identically.
15
+ */
16
+ export function canonicalJson(value) {
17
+ return JSON.stringify(canonicalize(value));
18
+ }
19
+ function canonicalize(value) {
20
+ // Honor toJSON (e.g. Date -> ISO string) before structural recursion, matching
21
+ // JSON.stringify semantics — otherwise a Date collapses to {} and two distinct
22
+ // timestamps would hash identically.
23
+ if (value !== null &&
24
+ typeof value === "object" &&
25
+ typeof value.toJSON === "function") {
26
+ return canonicalize(value.toJSON());
27
+ }
28
+ if (Array.isArray(value))
29
+ return value.map(canonicalize);
30
+ if (value !== null && typeof value === "object") {
31
+ const source = value;
32
+ const out = {};
33
+ for (const key of Object.keys(source).sort()) {
34
+ const v = source[key];
35
+ if (v !== undefined)
36
+ out[key] = canonicalize(v);
37
+ }
38
+ return out;
39
+ }
40
+ return value;
41
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Vendored from @ema/knowledge-service `src/atom/id.ts` (2026-07-13).
3
+ * Content-addressing + audience grammar. Conformance: `test/atom-conformance.test.mjs`.
4
+ */
5
+ import { sha256Hex, canonicalJson } from "./hash.js";
6
+ /**
7
+ * Single-token audience grammar (one token per atom):
8
+ * public | internal | internal:<slug> | tenant:<slug> | customer:<slug>
9
+ */
10
+ export const AUDIENCE_RE = /^(public|internal(:[a-z0-9-]+)?|tenant:[a-z0-9-]+|customer:[a-z0-9-]+)$/;
11
+ export function isValidAudience(token) {
12
+ // typeof guard FIRST: RegExp.test coerces its arg to a string, so an array like
13
+ // ["internal"] would stringify to "internal" and falsely match — then fail-closed
14
+ // to invisible-for-everyone. Require an actual string.
15
+ return typeof token === "string" && AUDIENCE_RE.test(token);
16
+ }
17
+ /**
18
+ * Content-addressed id: `atom:` + 32 hex (128-bit) prefix of the SHA-256 of the
19
+ * canonical content. Identical content → identical id → dedup. Audience/provenance
20
+ * are excluded (they are mutable / not content).
21
+ */
22
+ export function contentAddressId(c) {
23
+ const hash = sha256Hex(canonicalJson({ name: c.name, description: c.description, body: c.body }));
24
+ return `atom:${hash.slice(0, 32)}`;
25
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * eMachina's OWNED atom kernel — the local staging tier of the knowledge substrate.
3
+ *
4
+ * Vendored from @ema/knowledge-service `/atom` (2026-07-13) per the greenfield decision
5
+ * (.harvest/knowledge-substrate-greenfield-goals): own the ~small local kernel; consume
6
+ * everything org-wide (DE search, publish, auth) as MCP runtime resources — never a
7
+ * build-time dependency on the sibling repo. Wire-contract conformance with canonical
8
+ * KS is guarded by frozen golden vectors in `test/atom-conformance.test.mjs`: same
9
+ * input MUST produce the same content-addressed id, or staged atoms stop being
10
+ * portable to the org corpus.
11
+ */
12
+ export * from "./types.js";
13
+ export { contentAddressId, isValidAudience, AUDIENCE_RE } from "./id.js";
14
+ export { sha256Hex, canonicalJson } from "./hash.js";
15
+ export { buildAtom, submitChanges, search } from "./core.js";
16
+ export { LocalStorageBackend, StoreScanRetrieval, audienceVisible } from "./backends.js";
17
+ export { validate, validateChange, validateAtomInput, assertValidChange, ValidationError, LIMITS, } from "./validate.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Vendored from @ema/knowledge-service `src/atom/types.ts` (2026-07-13) — the single Atom
3
+ * type + the two pluggable backends (StorageBackend primary, RetrievalBackend projection).
4
+ * ONE unstructured Atom; `audience` is the single hard ACL filter; immutable + append-only
5
+ * (new content = new content-addressed atom + `supersedes`; retract = tombstone).
6
+ */
7
+ export {};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Vendored from @ema/knowledge-service `src/atom/validate.ts` (2026-07-13).
3
+ * Blocking, typed validation — enforced INSIDE the write path (submitChanges) so no
4
+ * client can bypass it. `validate()` is also exported for pre-flight checks.
5
+ * Conformance: `test/atom-conformance.test.mjs`.
6
+ */
7
+ import { isValidAudience } from "./id.js";
8
+ export const LIMITS = {
9
+ MAX_NAME: 512,
10
+ MAX_DESCRIPTION: 4096,
11
+ MAX_BODY: 1_000_000, // 1M chars — generous for free-form knowledge, catches runaway/garbage
12
+ };
13
+ const ATOM_ID_RE = /^atom:[a-f0-9]{16,64}$/;
14
+ /**
15
+ * typeof guard FIRST, then the regex — RegExp.test coerces its arg via ToString, so a
16
+ * single-element array like ["atom:abc…"] stringifies to "atom:abc…" and would falsely
17
+ * match (then get written raw / crash downstream). Same hardening as isValidAudience.
18
+ */
19
+ function isValidAtomId(v) {
20
+ return typeof v === "string" && ATOM_ID_RE.test(v);
21
+ }
22
+ export class ValidationError extends Error {
23
+ errors;
24
+ constructor(errors) {
25
+ super(`validation failed: ${errors.join("; ")}`);
26
+ this.name = "ValidationError";
27
+ this.errors = errors;
28
+ }
29
+ }
30
+ function reqString(v, field, max, errors) {
31
+ if (typeof v !== "string" || v.trim() === "") {
32
+ errors.push(`${field} is required and must be a non-empty string`);
33
+ }
34
+ else if (v.length > max) {
35
+ errors.push(`${field} exceeds max length ${max} (got ${v.length})`);
36
+ }
37
+ }
38
+ /** Field-level validation for a publish payload. Returns a list of errors (empty = valid). */
39
+ export function validateAtomInput(input) {
40
+ const errors = [];
41
+ if (input === null || typeof input !== "object")
42
+ return ["atom input must be an object"];
43
+ reqString(input.name, "name", LIMITS.MAX_NAME, errors);
44
+ reqString(input.description, "description", LIMITS.MAX_DESCRIPTION, errors);
45
+ reqString(input.body, "body", LIMITS.MAX_BODY, errors);
46
+ if (input.audience !== undefined && !isValidAudience(input.audience)) {
47
+ errors.push(`invalid audience token "${input.audience}" (expect public|internal[:slug]|tenant:slug|customer:slug)`);
48
+ }
49
+ if (input.supersedes !== undefined && !isValidAtomId(input.supersedes)) {
50
+ errors.push(`supersedes must be a valid atom id (atom:<hex>), got "${String(input.supersedes)}"`);
51
+ }
52
+ if (input.produced_by !== undefined && typeof input.produced_by !== "string") {
53
+ errors.push("produced_by must be a string");
54
+ }
55
+ return errors;
56
+ }
57
+ /** Op-level validation for a submitChanges request. Returns a list of errors (empty = valid). */
58
+ export function validateChange(change) {
59
+ if (change === null || typeof change !== "object")
60
+ return ["change must be an object"];
61
+ switch (change.op) {
62
+ case "publish":
63
+ if (change.atom === undefined)
64
+ return ["publish requires `atom`"];
65
+ return validateAtomInput(change.atom);
66
+ case "retract":
67
+ return isValidAtomId(change.id) ? [] : [`retract requires a valid atom id, got "${String(change.id)}"`];
68
+ case "set_audience": {
69
+ const errors = [];
70
+ if (!isValidAtomId(change.id))
71
+ errors.push(`set_audience requires a valid atom id, got "${String(change.id)}"`);
72
+ if (change.audience === undefined || !isValidAudience(change.audience)) {
73
+ errors.push(`set_audience requires a valid audience token, got "${change.audience}"`);
74
+ }
75
+ return errors;
76
+ }
77
+ default:
78
+ return [`unknown op "${String(change.op)}" (expect publish|retract|set_audience)`];
79
+ }
80
+ }
81
+ /** Public pre-flight: `{ valid, errors }`. Clients may call before submitChanges. */
82
+ export function validate(change) {
83
+ const errors = validateChange(change);
84
+ return { valid: errors.length === 0, errors };
85
+ }
86
+ /** Blocking assertion used inside submitChanges. Throws ValidationError on any violation. */
87
+ export function assertValidChange(change) {
88
+ const errors = validateChange(change);
89
+ if (errors.length > 0)
90
+ throw new ValidationError(errors);
91
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Capability registry — the SINGLE SOURCE OF TRUTH for the tool surface.
3
+ *
4
+ * generateTools() (tools.ts), the dispatch-map parity guard (server.ts), and
5
+ * config's wired/pending report all DERIVE from this. Adding a noun/method is one
6
+ * edit here — capabilities are DATA, not metadata re-encoded across files. This is
7
+ * the "stable surface + registry" the mcp-build-principles yield chose; it's also
8
+ * the migration the reviewer flagged to do before the 5th (or the generated
9
+ * `automation`) noun.
10
+ *
11
+ * This module is intentionally DEPENDENCY-FREE (no handler imports) so nothing can
12
+ * import-cycle through it; handlers are wired to these names in server.ts (the
13
+ * composition root), and a load-time parity guard there fails loudly on drift.
14
+ */
15
+ export const CAPABILITIES = [
16
+ {
17
+ name: "config",
18
+ status: "wired",
19
+ description: "Inspect eMachina's readiness (the needs/state check). Run this FIRST, and whenever a downstream " +
20
+ "tool reports a gap. Returns which capabilities are wired vs pending, plus required next steps.",
21
+ methods: [{ name: "status", description: "return the readiness snapshot" }],
22
+ },
23
+ {
24
+ name: "yield",
25
+ status: "wired",
26
+ description: "Manage harvest YIELD artifacts — the deterministic substrate. The agent RUNS the harvest procedure " +
27
+ "(intake/gather/synthesis = thinking); this tool validates/lists/reads/scaffolds the yields it produces. " +
28
+ "`validate` is a blocking gate over the canonical contract.",
29
+ methods: [
30
+ { name: "validate", description: "run the canonical validator (blocking gate)" },
31
+ { name: "list", description: "list yield slugs under .harvest/" },
32
+ { name: "get", description: "read a yield by slug" },
33
+ { name: "scaffold", description: "create a new yield from the canonical skeleton" },
34
+ ],
35
+ extraProperties: {
36
+ slug: { type: "string", description: "Yield slug (directory under .harvest/) — for get/scaffold/validate" },
37
+ path: { type: "string", description: "Explicit path to a yield .md — overrides slug for validate" },
38
+ },
39
+ },
40
+ {
41
+ name: "curate",
42
+ status: "wired",
43
+ description: "Memorialize / keep-memory-healthy over the corpus (publish, supersede, dedup, advise), powered by " +
44
+ "the owned atom kernel's submitChanges — content-addressed dedup + kernel-enforced validation " +
45
+ "(wire-conformant with canonical KS via golden vectors). Local corpus is the staging tier; org-wide " +
46
+ "COMPOSES with the ema toolkit's knowledge() by swapping the StorageBackend.",
47
+ methods: [
48
+ { name: "publish", description: "memorialize a yield (by slug) or explicit content as an atom" },
49
+ { name: "supersede", description: "publish an atom that supersedes a prior one (needs `supersedes`)" },
50
+ { name: "advise", description: "pre-flight the lib's validation on a yield without writing" },
51
+ { name: "status", description: "report curate readiness" },
52
+ ],
53
+ extraProperties: {
54
+ slug: { type: "string", description: "Yield slug to memorialize (for publish/supersede/advise)" },
55
+ name: { type: "string", description: "Atom title (if not using slug)" },
56
+ description: { type: "string", description: "Atom description (if not using slug)" },
57
+ body: { type: "string", description: "Atom body/content (if not using slug)" },
58
+ audience: { type: "string", description: "Audience token: public | internal[:slug] | tenant:slug | customer:slug" },
59
+ supersedes: { type: "string", description: "Atom id this one supersedes (for method=supersede)" },
60
+ },
61
+ },
62
+ {
63
+ name: "retrieve",
64
+ status: "wired",
65
+ description: "Search / rank the LOCAL corpus (curated atoms + .harvest yields) via the owned atom kernel's search — " +
66
+ "works offline, with or without DE. Org-wide search COMPOSES with the ema toolkit's knowledge(), not a " +
67
+ "rebuilt DE client (the scar tissue harvest set out to avoid).",
68
+ methods: [{ name: "search", description: "query the corpus (relevance-ranked, audience-filtered)" }],
69
+ extraProperties: {
70
+ query: { type: "string", description: "Search query (for method=search)" },
71
+ limit: { type: "number", description: "Max results (default 10)" },
72
+ audience: { type: "string", description: "Caller audience tokens to filter by (default public+internal)" },
73
+ },
74
+ },
75
+ ];
76
+ export const WIRED = CAPABILITIES.filter((c) => c.status === "wired").map((c) => c.name);
77
+ export const PENDING = CAPABILITIES.filter((c) => c.status === "pending").map((c) => c.name);
@@ -0,0 +1,7 @@
1
+ import { findRepoRoot } from "../../state.js";
2
+ import { dispatchConfig } from "./index.js";
3
+ /** Adapter: resolve context (repo root). Thin — the only place this noun touches cwd. */
4
+ export async function handleConfig(args) {
5
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
6
+ return dispatchConfig(args, ctx);
7
+ }
@@ -0,0 +1,25 @@
1
+ import { success } from "../../responses.js";
2
+ import { PENDING, WIRED } from "../../capabilities.js";
3
+ /** Shape the readiness snapshot. wired/pending derive from the capability registry
4
+ * (SSOT). config OWNS the state report, so it sets _required_next_steps explicitly
5
+ * (middleware's ??= then leaves it untouched). */
6
+ export function formatStatus(state) {
7
+ return success({
8
+ repoRoot: state.repoRoot,
9
+ // Per-capability runtime readiness (distinct from the surface definition in the registry).
10
+ readiness: {
11
+ config: { ready: true, detail: "always available" },
12
+ yield: state.yieldValidator,
13
+ curate: { ready: state.knowledgeSubstrate.ready, detail: state.knowledgeSubstrate.detail },
14
+ retrieve: { ready: state.knowledgeSubstrate.ready, detail: state.knowledgeSubstrate.detail },
15
+ },
16
+ harvest_dir: state.harvestDir,
17
+ wired: WIRED,
18
+ pending: PENDING,
19
+ }, {
20
+ _next_step: state.requiredNextSteps.length
21
+ ? 'Address _required_next_steps, or call a wired tool — e.g. yield(method="list").'
22
+ : 'All checked capabilities ready — yield(method="list") to see the corpus.',
23
+ ...(state.requiredNextSteps.length ? { _required_next_steps: state.requiredNextSteps } : {}),
24
+ });
25
+ }
@@ -0,0 +1,17 @@
1
+ import { getState } from "../../state.js";
2
+ import { clarificationNeeded, error } from "../../responses.js";
3
+ import { formatStatus } from "./formatters.js";
4
+ /** Dispatch by explicit `method` — never inferred. */
5
+ export async function dispatchConfig(args, ctx) {
6
+ const method = args.method;
7
+ switch (method) {
8
+ case "status":
9
+ return formatStatus(getState(ctx.repoRoot));
10
+ case undefined:
11
+ return clarificationNeeded("Which operation?", ["status"], {
12
+ _tip: 'config(method="status") returns the readiness snapshot.',
13
+ });
14
+ default:
15
+ return error(`Unknown method: ${method}`, { _tip: "config supports: status." });
16
+ }
17
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared local-corpus helpers for curate (write) + retrieve (read).
3
+ *
4
+ * The local atom corpus is an atom-kernel StorageBackend (src/mcp/atom) rooted at
5
+ * `<repoRoot>/.knowledge/atoms` (distinct from `.harvest/{slug}/` yields, which
6
+ * follow the harvest write convention). Curate memorializes atoms here; retrieve
7
+ * searches here PLUS the raw `.harvest` yields. Org-wide storage/retrieval swaps
8
+ * this LocalStorageBackend for GcsStorageBackend + DE (compose, don't rebuild).
9
+ */
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { LocalStorageBackend } from "../atom/index.js";
13
+ export const HARVEST = ".harvest";
14
+ export const CORPUS_REL = ".knowledge/atoms";
15
+ /** The local atom corpus (primary StorageBackend for curate/retrieve; GCS in org-wide mode). */
16
+ export function corpusStore(repoRoot) {
17
+ return new LocalStorageBackend(join(repoRoot, CORPUS_REL));
18
+ }
19
+ /** Read a harvest yield's markdown by slug, or null if absent. */
20
+ export function readYield(repoRoot, slug) {
21
+ const path = join(repoRoot, HARVEST, slug, "harvest-yield.md");
22
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
23
+ }
24
+ /** First meaningful (non-heading, non-quote, non-frontmatter) line → minimal description. */
25
+ export function deriveDescription(content, slug) {
26
+ const body = content.replace(/^---[\s\S]*?---/m, "");
27
+ for (const line of body.split("\n")) {
28
+ const t = line.trim();
29
+ if (t && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("`"))
30
+ return t.slice(0, 200);
31
+ }
32
+ return slug;
33
+ }