@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.
@@ -0,0 +1,10 @@
1
+ import { findRepoRoot } from "../../state.js";
2
+ import { dispatchCurate } from "./index.js";
3
+ /**
4
+ * Curate — memorialize / dedup / supersede / advise over the corpus. Adapter: resolve
5
+ * context (repo root), delegate to the atom-kernel-backed dispatcher. Thin.
6
+ */
7
+ export async function handleCurate(args) {
8
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
9
+ return dispatchCurate(args, ctx);
10
+ }
@@ -0,0 +1,104 @@
1
+ import { submitChanges, validate, ValidationError } from "../../atom/index.js";
2
+ import { clarificationNeeded, error, success } from "../../responses.js";
3
+ import { corpusStore, deriveDescription, readYield } from "../corpus.js";
4
+ /** Build an AtomInput from a yield slug or explicit fields. */
5
+ function atomInputFromArgs(repoRoot, args) {
6
+ const audience = typeof args.audience === "string" ? args.audience : undefined;
7
+ const supersedes = typeof args.supersedes === "string" ? args.supersedes : undefined;
8
+ const slug = typeof args.slug === "string" ? args.slug : undefined;
9
+ if (slug) {
10
+ const content = readYield(repoRoot, slug);
11
+ if (content === null)
12
+ return { err: `Yield not found for slug: ${slug}` };
13
+ return {
14
+ input: {
15
+ name: slug,
16
+ description: deriveDescription(content, slug),
17
+ body: content,
18
+ produced_by: "curate",
19
+ ...(audience ? { audience } : {}),
20
+ ...(supersedes ? { supersedes } : {}),
21
+ },
22
+ };
23
+ }
24
+ const name = args.name;
25
+ const description = args.description;
26
+ const body = args.body;
27
+ if (!name || !description || !body) {
28
+ return { err: "Provide either a `slug` (of a .harvest yield) or explicit name + description + body." };
29
+ }
30
+ return {
31
+ input: {
32
+ name, description, body, produced_by: "curate",
33
+ ...(audience ? { audience } : {}),
34
+ ...(supersedes ? { supersedes } : {}),
35
+ },
36
+ };
37
+ }
38
+ export async function dispatchCurate(args, ctx) {
39
+ const method = args.method;
40
+ const storage = corpusStore(ctx.repoRoot);
41
+ switch (method) {
42
+ case "advise": {
43
+ // Pre-flight the lib's validation WITHOUT writing — surface the contract to the agent.
44
+ const { input, err } = atomInputFromArgs(ctx.repoRoot, args);
45
+ if (err || !input)
46
+ return error(err ?? "invalid input", { _tip: 'curate(method="advise", slug="…").' });
47
+ const v = validate({ op: "publish", atom: input });
48
+ if (v.valid) {
49
+ return success({ advice: "valid", name: input.name }, {
50
+ _next_step: `curate(method="publish", slug="${args.slug ?? input.name}") to memorialize it.`,
51
+ });
52
+ }
53
+ return {
54
+ status: "analysis",
55
+ summary: "Yield fails atom-kernel validation — fix before publishing.",
56
+ issues: v.errors,
57
+ next_steps: ["Fix the listed issues, then re-run curate(advise)."],
58
+ _warning: "Validation is enforced by the atom kernel; publish would be rejected.",
59
+ };
60
+ }
61
+ case "publish":
62
+ case "supersede": {
63
+ const { input, err } = atomInputFromArgs(ctx.repoRoot, args);
64
+ if (err || !input) {
65
+ return error(err ?? "invalid input", {
66
+ _tip: 'curate(method="publish", slug="…"), or pass name/description/body. supersede also needs `supersedes`.',
67
+ });
68
+ }
69
+ if (method === "supersede" && !input.supersedes) {
70
+ return error("supersede requires `supersedes` (the atom id being superseded).", {
71
+ _tip: 'curate(method="supersede", slug="…", supersedes="atom:…").',
72
+ });
73
+ }
74
+ try {
75
+ const res = await submitChanges({ op: "publish", atom: input }, { storage });
76
+ return success({ id: res.id, curate_status: res.status, name: input.name, audience: res.atom?.audience }, {
77
+ _next_step: 'retrieve(method="search", query="…") — the atom is now in the local corpus.',
78
+ _tip: res.status === "deduplicated"
79
+ ? "Identical content already memorialized — content-addressed dedup (no-op)."
80
+ : "Memorialized to the local corpus (swap StorageBackend for GCS org-wide).",
81
+ });
82
+ }
83
+ catch (e) {
84
+ if (e instanceof ValidationError) {
85
+ return {
86
+ status: "analysis",
87
+ summary: "Validation failed (enforced by the atom kernel).",
88
+ issues: e.errors,
89
+ next_steps: ["Fix the issues, then re-run curate."],
90
+ };
91
+ }
92
+ return error(e instanceof Error ? e.message : String(e));
93
+ }
94
+ }
95
+ case "status":
96
+ return success({ wired: true }, { _tip: "curate → atom-kernel submitChanges (local corpus StorageBackend)." });
97
+ case undefined:
98
+ return clarificationNeeded("Which operation?", ["publish", "supersede", "advise", "status"], {
99
+ _tip: 'e.g. curate(method="publish", slug="my-slug").',
100
+ });
101
+ default:
102
+ return error(`Unknown method: ${method}`, { _tip: "curate supports: publish, supersede, advise, status." });
103
+ }
104
+ }
@@ -0,0 +1,12 @@
1
+ import { findRepoRoot } from "../../state.js";
2
+ import { dispatchRetrieve } from "./index.js";
3
+ /**
4
+ * Retrieve — search/rank the corpus. Adapter: resolve context (repo root), delegate.
5
+ * LOCAL .harvest retrieval (via the owned atom kernel) works offline, with or
6
+ * without DE; org-wide search COMPOSES with the ema toolkit's knowledge() — not a
7
+ * rebuilt DE client (the scar tissue the harvest set out to avoid).
8
+ */
9
+ export async function handleRetrieve(args) {
10
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
11
+ return dispatchRetrieve(args, ctx);
12
+ }
@@ -0,0 +1,63 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { buildAtom, search, StoreScanRetrieval } from "../../atom/index.js";
4
+ import { clarificationNeeded, error, list } from "../../responses.js";
5
+ import { HARVEST, corpusStore, deriveDescription, readYield } from "../corpus.js";
6
+ /** Load curated atoms + raw yields into a DE-free retrieval index (content-addressed dedup). */
7
+ async function loadCorpus(repoRoot) {
8
+ const retrieval = new StoreScanRetrieval();
9
+ const ids = new Set();
10
+ // 1) curated atoms (curate output; GcsStorageBackend in org-wide mode)
11
+ for (const atom of await corpusStore(repoRoot).list()) {
12
+ await retrieval.index(atom);
13
+ ids.add(atom.id);
14
+ }
15
+ // 2) raw .harvest yields as atoms (always-available local corpus)
16
+ const root = join(repoRoot, HARVEST);
17
+ if (existsSync(root)) {
18
+ for (const d of readdirSync(root, { withFileTypes: true })) {
19
+ if (!d.isDirectory() || d.name.startsWith("."))
20
+ continue;
21
+ const content = readYield(repoRoot, d.name);
22
+ if (content === null)
23
+ continue;
24
+ const atom = buildAtom({
25
+ name: d.name,
26
+ description: deriveDescription(content, d.name),
27
+ body: content,
28
+ audience: "internal",
29
+ produced_by: "harvest",
30
+ });
31
+ await retrieval.index(atom);
32
+ ids.add(atom.id);
33
+ }
34
+ }
35
+ return { retrieval, count: ids.size };
36
+ }
37
+ export async function dispatchRetrieve(args, ctx) {
38
+ const method = args.method ?? "search";
39
+ if (method !== "search") {
40
+ return error(`Unknown method: ${method}`, { _tip: "retrieve supports: search." });
41
+ }
42
+ const query = args.query;
43
+ if (!query || query.trim() === "") {
44
+ return clarificationNeeded("What should I search the corpus for?", [], {
45
+ _tip: 'retrieve(method="search", query="…").',
46
+ });
47
+ }
48
+ const limit = typeof args.limit === "number" ? args.limit : 10;
49
+ const audience = Array.isArray(args.audience) ? args.audience : ["public", "internal"];
50
+ const { retrieval, count } = await loadCorpus(ctx.repoRoot);
51
+ const hits = await search(query, { audience, limit }, { retrieval });
52
+ const results = hits.map((h) => ({ slug: h.name, score: Number(h.score.toFixed(3)), description: h.description }));
53
+ const top = results[0];
54
+ return {
55
+ ...list("results", results, {
56
+ _next_step: top
57
+ ? `yield(method="get", slug="${top.slug}") to read the top hit.`
58
+ : "No local hits — broaden the query, or compose with the ema toolkit's knowledge() for org-wide search.",
59
+ _tip: `Searched ${count} local atoms (curated + .harvest yields; audience: ${audience.join(", ")}). Org-wide composes with ema-toolkit knowledge() (DE).`,
60
+ }),
61
+ composes_with: ["ema-toolkit:knowledge()"],
62
+ };
63
+ }
@@ -0,0 +1,7 @@
1
+ import { findRepoRoot } from "../../state.js";
2
+ import { dispatchYield } from "./index.js";
3
+ /** Adapter: resolve context (repo root). Thin. */
4
+ export async function handleYield(args) {
5
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
6
+ return dispatchYield(args, ctx);
7
+ }
@@ -0,0 +1,11 @@
1
+ import { analysis, success } from "../../responses.js";
2
+ /** Validation result — RETURNED, never thrown, so the agent's guidance loop stays steerable. */
3
+ export function formatValidate(ok, path, detail) {
4
+ if (ok) {
5
+ return success({ valid: true, path, validator_output: detail }, { _tip: "Yield conforms to the contract." });
6
+ }
7
+ return analysis(`Yield failed validation: ${path}`, [{ severity: "critical", message: detail }], ["Fix the reported issues", 'Re-run yield(method="validate", slug="…")'], {
8
+ _warning: "The yield does NOT conform to the contract — treat it as not accepted.",
9
+ _fix: detail,
10
+ });
11
+ }
@@ -0,0 +1,98 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { clarificationNeeded, error, list, singleItem, success } from "../../responses.js";
5
+ import { VALIDATOR_REL } from "../../state.js";
6
+ import { formatValidate } from "./formatters.js";
7
+ const HARVEST = ".harvest";
8
+ const ASSET_REL = "skills/harvest/assets/harvest-yield.example.md";
9
+ function yieldPath(repoRoot, slug) {
10
+ return join(repoRoot, HARVEST, slug, "harvest-yield.md");
11
+ }
12
+ /** Dispatch by explicit `method`. The agent runs the harvest PROCEDURE; this tool
13
+ * manages the yield ARTIFACTS it produces (deterministic execution). */
14
+ export async function dispatchYield(args, ctx) {
15
+ const method = args.method;
16
+ const { repoRoot } = ctx;
17
+ const slug = args.slug;
18
+ switch (method) {
19
+ case "validate": {
20
+ const path = args.path ?? (slug ? yieldPath(repoRoot, slug) : undefined);
21
+ if (!path)
22
+ return error("validate needs a slug or path", { _tip: 'yield(method="validate", slug="my-slug").' });
23
+ if (!existsSync(path))
24
+ return error(`Yield not found: ${path}`, { _tip: 'Check the slug, or yield(method="list").' });
25
+ // ── Blocking validator gate: shell to the CANONICAL validator (single source of
26
+ // truth). No "PASS" without validator output; the contract is not re-encoded here.
27
+ try {
28
+ const out = execFileSync("python3", [join(repoRoot, VALIDATOR_REL), path], { encoding: "utf8" });
29
+ return formatValidate(true, path, out.trim());
30
+ }
31
+ catch (e) {
32
+ const err = e;
33
+ const detail = ((err.stdout ?? "") + (err.stderr ?? "")).trim() || err.message || String(e);
34
+ return formatValidate(false, path, detail);
35
+ }
36
+ }
37
+ case "list": {
38
+ const root = join(repoRoot, HARVEST);
39
+ if (!existsSync(root))
40
+ return error(`.harvest/ not found under ${repoRoot}`, { _tip: "Run from the eMachina repo root." });
41
+ const slugs = readdirSync(root, { withFileTypes: true })
42
+ .filter((d) => d.isDirectory() && existsSync(yieldPath(repoRoot, d.name)))
43
+ .map((d) => d.name);
44
+ return list("slugs", slugs, { _next_step: 'yield(method="get", slug="…") to read one.' });
45
+ }
46
+ case "get": {
47
+ if (!slug)
48
+ return error("get needs a slug", { _tip: 'yield(method="get", slug="my-slug").' });
49
+ const path = yieldPath(repoRoot, slug);
50
+ if (!existsSync(path))
51
+ return error(`Yield not found for slug: ${slug}`, { _tip: 'yield(method="list") to see available slugs.' });
52
+ return singleItem("yield", { slug, path, content: readFileSync(path, "utf8") });
53
+ }
54
+ case "scaffold": {
55
+ if (!slug)
56
+ return error("scaffold needs a slug", { _tip: 'yield(method="scaffold", slug="my-new-slug").' });
57
+ const path = yieldPath(repoRoot, slug);
58
+ if (existsSync(path))
59
+ return error(`Yield already exists for slug: ${slug}`, { _tip: "Pick a new slug, or get the existing one." });
60
+ let content;
61
+ try {
62
+ content = scaffoldFromAsset(repoRoot, slug);
63
+ }
64
+ catch (e) {
65
+ return error(e instanceof Error ? e.message : String(e), {
66
+ _tip: "Run from the eMachina repo root so the canonical skeleton is reachable.",
67
+ });
68
+ }
69
+ mkdirSync(join(repoRoot, HARVEST, slug), { recursive: true });
70
+ writeFileSync(path, content);
71
+ return success({ slug, path }, {
72
+ _next_step: `Fill the placeholders (Ask/Why/Goals/Findings/Sources), then yield(method="validate", slug="${slug}").`,
73
+ _warning: "Scaffold copies the canonical skeleton — the agent authors the content (the MCP never invents findings).",
74
+ });
75
+ }
76
+ case undefined:
77
+ return clarificationNeeded("Which operation?", ["validate", "list", "get", "scaffold"], {
78
+ _tip: 'e.g. yield(method="list").',
79
+ });
80
+ default:
81
+ return error(`Unknown method: ${method}`, { _tip: "yield supports: validate, list, get, scaffold." });
82
+ }
83
+ }
84
+ /**
85
+ * Build a new yield from the CANONICAL skeleton asset — don't re-encode the contract.
86
+ * Substitute the derived `name` (= slug) and drop the asset's illustrative (future-dated)
87
+ * `updated` line (optional per the validator; the write hook / author stamps the real clock).
88
+ * No hardcoded fallback: if the asset is missing we error rather than drift.
89
+ */
90
+ function scaffoldFromAsset(repoRoot, slug) {
91
+ const assetPath = join(repoRoot, ASSET_REL);
92
+ if (!existsSync(assetPath)) {
93
+ throw new Error(`Canonical yield skeleton not found at ${ASSET_REL} — cannot scaffold without it (no hardcoded fallback, to avoid contract drift).`);
94
+ }
95
+ return readFileSync(assetPath, "utf8")
96
+ .replace(/^name:.*$/m, `name: ${slug}`)
97
+ .replace(/^[ \t]*updated:.*\r?\n/m, "");
98
+ }
@@ -0,0 +1,14 @@
1
+ import { getState } from "./state.js";
2
+ export function enrichWithState(result, _ctx) {
3
+ try {
4
+ const state = getState();
5
+ // ??= — never clobber a hint the handler already set (e.g. config's own list).
6
+ if (state.requiredNextSteps.length > 0) {
7
+ result._required_next_steps ??= state.requiredNextSteps;
8
+ }
9
+ }
10
+ catch {
11
+ // Best-effort guidance. Swallow — never break dispatch.
12
+ }
13
+ return result;
14
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Response standards — fixed categories + advisory hints.
3
+ *
4
+ * The MCP returns DATA in these shapes; the agent orchestrates. Two hard rules
5
+ * from the ema-mcp-toolkit disciplines (see .harvest/mcp-build-principles):
6
+ * 1. Errors are RETURNED, never thrown — throwing breaks the agent's guidance loop.
7
+ * 2. `_`-prefixed hints steer the agent through multi-step flows and are NEVER
8
+ * part of the primary result data.
9
+ */
10
+ export function success(data = {}, hints = {}) {
11
+ return { status: "success", ...data, ...hints };
12
+ }
13
+ /** Returned, not thrown. */
14
+ export function error(message, hints = {}) {
15
+ return { status: "error", error: message, ...hints };
16
+ }
17
+ export function clarificationNeeded(message, options, hints = {}) {
18
+ return { status: "clarification_needed", message, options, ...hints };
19
+ }
20
+ export function analysis(summary, issues, next_steps, hints = {}) {
21
+ return { status: "analysis", summary, issues, next_steps, ...hints };
22
+ }
23
+ export function list(key, items, hints = {}) {
24
+ return { status: "list", count: items.length, [key]: items, ...hints };
25
+ }
26
+ export function singleItem(key, item, hints = {}) {
27
+ return { status: "single_item", [key]: item, ...hints };
28
+ }
29
+ /**
30
+ * A capability that isn't wired yet — honest, categorized, state-aware.
31
+ * Used for capabilities declared but not yet wired.
32
+ */
33
+ export function pending(capability, reason, hints = {}) {
34
+ return {
35
+ status: "pending",
36
+ capability,
37
+ reason,
38
+ _warning: `${capability} is not wired yet: ${reason}`,
39
+ ...hints,
40
+ };
41
+ }