@eir-labs/coltrane 0.5.1 → 0.7.2
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/README.md +23 -0
- package/agents/bill.json +59 -0
- package/agents/deploy-agent.json +68 -0
- package/agents/deploy-scout.json +40 -0
- package/agents/john.json +42 -0
- package/agents/miles.json +44 -0
- package/charts/software-delivery-v1.json +9 -0
- package/charts/software-delivery-v2.json +39 -0
- package/dist/src/bifrost_invoker.js +5 -3
- package/dist/src/bifrost_invoker.js.map +1 -1
- package/dist/src/canonical_form.d.ts +23 -0
- package/dist/src/canonical_form.js +53 -0
- package/dist/src/canonical_form.js.map +1 -1
- package/dist/src/chart.d.ts +254 -0
- package/dist/src/chart.js +897 -0
- package/dist/src/chart.js.map +1 -0
- package/dist/src/claude_invoker.d.ts +20 -3
- package/dist/src/claude_invoker.js +30 -5
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +19 -4
- package/dist/src/cli.js +132 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/composition.d.ts +24 -0
- package/dist/src/composition.js +50 -5
- package/dist/src/composition.js.map +1 -1
- package/dist/src/conduct_daemon.d.ts +59 -0
- package/dist/src/conduct_daemon.js +178 -0
- package/dist/src/conduct_daemon.js.map +1 -0
- package/dist/src/genome_schema.d.ts +1476 -146
- package/dist/src/genome_schema.js +438 -32
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.d.ts +79 -0
- package/dist/src/genome_store.js +503 -0
- package/dist/src/genome_store.js.map +1 -0
- package/dist/src/gig_tracker.d.ts +11 -1
- package/dist/src/gig_tracker.js +5 -0
- package/dist/src/gig_tracker.js.map +1 -1
- package/dist/src/hosted_tools.d.ts +23 -0
- package/dist/src/hosted_tools.js +170 -0
- package/dist/src/hosted_tools.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/ledger.d.ts +22 -0
- package/dist/src/ledger.js +4 -0
- package/dist/src/ledger.js.map +1 -1
- package/dist/src/loader.d.ts +9 -1
- package/dist/src/loader.js +105 -5
- package/dist/src/loader.js.map +1 -1
- package/dist/src/mcp.js +52 -5
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/output_mirror.d.ts +70 -0
- package/dist/src/output_mirror.js +381 -0
- package/dist/src/output_mirror.js.map +1 -0
- package/dist/src/outputs.d.ts +77 -1
- package/dist/src/outputs.js +179 -37
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/play_worker.d.ts +24 -0
- package/dist/src/play_worker.js +49 -0
- package/dist/src/play_worker.js.map +1 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/registry.js +63 -33
- package/dist/src/registry.js.map +1 -1
- package/dist/src/reuse.d.ts +56 -0
- package/dist/src/reuse.js +0 -0
- package/dist/src/reuse.js.map +1 -1
- package/dist/src/runtime.d.ts +91 -2
- package/dist/src/runtime.js +236 -18
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/seal_drill.d.ts +32 -0
- package/dist/src/seal_drill.js +138 -0
- package/dist/src/seal_drill.js.map +1 -0
- package/dist/src/server.d.ts +39 -1
- package/dist/src/server.js +760 -68
- package/dist/src/server.js.map +1 -1
- package/dist/src/skill_subprocess.js +12 -4
- package/dist/src/skill_subprocess.js.map +1 -1
- package/dist/src/supabase_genome.d.ts +28 -0
- package/dist/src/supabase_genome.js +49 -0
- package/dist/src/supabase_genome.js.map +1 -0
- package/dist/src/tool_surface.d.ts +5 -0
- package/dist/src/tool_surface.js +15 -0
- package/dist/src/tool_surface.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/worker.d.ts +182 -0
- package/dist/src/worker.js +609 -0
- package/dist/src/worker.js.map +1 -0
- package/domain_types/branch-state.json +21 -0
- package/domain_types/change-context.json +39 -0
- package/domain_types/change-decision.json +36 -0
- package/domain_types/change-plan.json +47 -0
- package/domain_types/change-request.json +24 -0
- package/domain_types/change-set.json +46 -0
- package/domain_types/change-verdict.json +26 -0
- package/domain_types/deploy-verdict.json +23 -0
- package/domain_types/design-brief.json +37 -0
- package/domain_types/design-concept.json +36 -0
- package/domain_types/design-definition.json +37 -0
- package/domain_types/design-question.json +23 -0
- package/domain_types/design-verdict.json +27 -0
- package/domain_types/preview-deployment.json +32 -0
- package/institutions/quartet.json +344 -0
- package/package.json +21 -3
- package/skills/vercel-api/fixtures/error.json +10 -0
- package/skills/vercel-api/fixtures/ready.json +10 -0
- package/skills/vercel-api/fixtures/unsettled.json +10 -0
- package/skills/vercel-api/meta.json +10 -0
- package/skills/vercel-api/skill.mjs +63 -0
- package/standards/preview-deploy-v1.json +89 -0
- package/standards/product-design-v1.json +122 -0
- package/standards/promote-v1.json +41 -0
- package/standards/software-change-v1.json +147 -0
- package/venues/ci-deploy-room-v1.json +28 -0
- package/venues/empty-room-v1.json +19 -0
package/dist/src/server.js
CHANGED
|
@@ -9,12 +9,15 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
9
9
|
import { MCP_TOOLS, requiresApproval, AGENT_STATUS_ORDER, STANDARD_STATUS_ORDER, SKILL_STATUS_ORDER, checkPromotion, PromotionError, } from "./mcp.js";
|
|
10
10
|
import { loadRegistry, domainTypeDefect } from "./registry.js";
|
|
11
11
|
import { resolveGenome } from "./loader.js";
|
|
12
|
-
import { SkillSchema, AgentSchema, StandardSchema } from "./genome_schema.js";
|
|
12
|
+
import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, venueDefect } from "./genome_schema.js";
|
|
13
|
+
import { composeChart, runChart, chartHash, chartEntrySeedTypes, dispatchTarget, } from "./chart.js";
|
|
13
14
|
import { runSkillFixtures, executeSkill, loadFixtures } from "./skill_subprocess.js";
|
|
14
15
|
import { evolveSkill } from "./skills.js";
|
|
15
16
|
import { sealAgentDefinition, sealDefinition, sealSkillPackage, recordIdentity } from "./genome_writer.js";
|
|
16
|
-
import { createOutputStore, defaultOutputsPersistDir } from "./outputs.js";
|
|
17
|
+
import { createOutputStore, defaultOutputsPersistDir, } from "./outputs.js";
|
|
18
|
+
import { createOutputMirror, defaultMirrorDir, outputPreview, mirrorStorageRef } from "./output_mirror.js";
|
|
17
19
|
import { FileLedger, LedgerError, LEDGER_SCHEMA_VERSION, defaultLedgerPath, } from "./ledger.js";
|
|
20
|
+
import { sealDrill } from "./seal_drill.js";
|
|
18
21
|
import { standardSimulate } from "./simulate.js";
|
|
19
22
|
import { runGig, BudgetExhausted, GigAborted, ResumeRefused, partialGigUsage, partialBudgetState } from "./runtime.js";
|
|
20
23
|
import { createCheckpointStore, createReuseStore } from "./reuse.js";
|
|
@@ -172,6 +175,46 @@ function syncMap(target, source, before) {
|
|
|
172
175
|
}
|
|
173
176
|
return { added, modified, removed };
|
|
174
177
|
}
|
|
178
|
+
// A queryable output row: the Tier-1 metadata projection (+ preview + storage_ref), with the
|
|
179
|
+
// full `data` payload only when the caller wants it. Keeps output_query's default shape
|
|
180
|
+
// backward-compatible (data present) while making the compact traversal and the deep second
|
|
181
|
+
// pass first-class.
|
|
182
|
+
function outputRow(rec, deps, withData) {
|
|
183
|
+
const row = {
|
|
184
|
+
id: rec.id,
|
|
185
|
+
gig_id: rec.gig_id,
|
|
186
|
+
agent_slug: rec.agent_slug,
|
|
187
|
+
from_role: rec.from_role,
|
|
188
|
+
phase: rec.phase,
|
|
189
|
+
primitive: rec.primitive,
|
|
190
|
+
core_type: rec.core_type,
|
|
191
|
+
domain_type: rec.domain_type,
|
|
192
|
+
domain_type_version: rec.domain_type_version,
|
|
193
|
+
domain: rec.domain,
|
|
194
|
+
content_sha: rec.content_sha,
|
|
195
|
+
input_refs: rec.input_refs,
|
|
196
|
+
input_shas: rec.input_shas,
|
|
197
|
+
cost_usd: rec.cost_usd,
|
|
198
|
+
tokens_used: rec.tokens_used,
|
|
199
|
+
duration_ms: rec.duration_ms,
|
|
200
|
+
model: rec.model,
|
|
201
|
+
model_tier: rec.model_tier,
|
|
202
|
+
created_at: rec.created_at,
|
|
203
|
+
preview: outputPreview(rec.data),
|
|
204
|
+
storage_ref: deps.output_mirror ? mirrorStorageRef(rec.content_sha) : "",
|
|
205
|
+
};
|
|
206
|
+
if (withData)
|
|
207
|
+
row["data"] = rec.data;
|
|
208
|
+
if (rec.skill_provenance !== undefined)
|
|
209
|
+
row["skill_provenance"] = rec.skill_provenance;
|
|
210
|
+
if (rec.reused_from !== undefined)
|
|
211
|
+
row["reused_from"] = rec.reused_from;
|
|
212
|
+
return row;
|
|
213
|
+
}
|
|
214
|
+
// A Tier-1 metadata row (from the mirror) shaped as an output_query row.
|
|
215
|
+
function metaToRow(meta) {
|
|
216
|
+
return { ...meta };
|
|
217
|
+
}
|
|
175
218
|
/**
|
|
176
219
|
* Pure tool dispatcher. Routes a tool call to its implementation. No transport,
|
|
177
220
|
* no I/O beyond the injected deps — fully unit-testable.
|
|
@@ -347,28 +390,70 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
347
390
|
...(std ? { standard: { slug: std.slug, phases: std.phases.map((p) => ({ name: p.name, chairs: p.chairs.length })) } } : {}),
|
|
348
391
|
...(observed.length > 0 ? { observed_costs_usd: observed } : {}),
|
|
349
392
|
});
|
|
350
|
-
|
|
393
|
+
// WU-0008 — the seal drill: before quoting a price, prove every chair contract
|
|
394
|
+
// can seal AT ALL. A cost estimate for a run whose terminal chair is doomed is
|
|
395
|
+
// worse than useless (it prices a failure as if it were work). Failures ride in
|
|
396
|
+
// the SUCCESS payload loudly; dispatch stays unchanged — the operator decides.
|
|
397
|
+
const drill = sealDrill({ phases: std.phases.map((p) => ({ name: p.name, chairs: p.chairs.map((c) => ({ role: c.role, output_contract: c.output_contract })) })) }, deps.registry);
|
|
398
|
+
return { ok: true, requires_approval: approval, data: { ...res, seal_drill: drill } };
|
|
351
399
|
}
|
|
352
400
|
case "output_query": {
|
|
353
|
-
|
|
401
|
+
const mirror = deps.output_mirror;
|
|
402
|
+
// SECOND PASS — a single output's FULL payload by id or content_sha. This is the deeper
|
|
403
|
+
// Tier-2 read: prefer the content-addressed artifact tier (local mirror, or remote when
|
|
404
|
+
// drained), falling back to the store. Returns exactly the one output, with its data.
|
|
405
|
+
const wantId = args["output_id"] ? String(args["output_id"]) : undefined;
|
|
406
|
+
const wantSha = args["content_sha"] ? String(args["content_sha"]) : undefined;
|
|
407
|
+
if (wantId || wantSha) {
|
|
408
|
+
const rec = wantId
|
|
409
|
+
? deps.outputs.get(wantId)
|
|
410
|
+
: deps.outputs.all().find((o) => o.content_sha === wantSha);
|
|
411
|
+
let row = rec ? outputRow(rec, deps, true) : undefined;
|
|
412
|
+
// Store miss (or a mirror-only payload): read the artifact tier directly.
|
|
413
|
+
if ((!row || row["data"] === undefined) && mirror) {
|
|
414
|
+
const sel = {};
|
|
415
|
+
if (wantId)
|
|
416
|
+
sel.id = wantId;
|
|
417
|
+
if (wantSha)
|
|
418
|
+
sel.content_sha = wantSha;
|
|
419
|
+
const hit = mirror.readPayload(sel);
|
|
420
|
+
if (hit?.meta)
|
|
421
|
+
row = { ...metaToRow(hit.meta), data: hit.data };
|
|
422
|
+
else if (row && hit?.data !== undefined)
|
|
423
|
+
row["data"] = hit.data;
|
|
424
|
+
}
|
|
425
|
+
const outputs = row ? [row] : [];
|
|
426
|
+
return { ok: true, requires_approval: approval, data: { outputs, total_count: outputs.length } };
|
|
427
|
+
}
|
|
428
|
+
// LIST — Tier-1 traversal. The store is now cross-process-fresh (the `fullyHydrated`
|
|
429
|
+
// latch is gone), so a gig sealed by a separate CLI process is visible here. `data` is
|
|
430
|
+
// carried by default (backward-compat) and on `data_filter`; pass `include_data:false`
|
|
431
|
+
// for the compact Tier-1 rows (metadata + preview + storage_ref, no payload).
|
|
432
|
+
const dataFilter = args["data_filter"];
|
|
433
|
+
const hasDataFilter = Boolean(dataFilter && typeof dataFilter === "object" && !Array.isArray(dataFilter));
|
|
434
|
+
const includeData = args["include_data"] === false ? false : true;
|
|
435
|
+
let recs = deps.outputs.all();
|
|
354
436
|
if (args["domain_type"])
|
|
355
|
-
|
|
437
|
+
recs = recs.filter((o) => o.domain_type === args["domain_type"]);
|
|
356
438
|
if (args["gig_id"])
|
|
357
|
-
|
|
439
|
+
recs = recs.filter((o) => o.gig_id === args["gig_id"]);
|
|
358
440
|
if (args["agent_slug"])
|
|
359
|
-
|
|
441
|
+
recs = recs.filter((o) => o.agent_slug === args["agent_slug"]);
|
|
360
442
|
// #234 — `data_filter` was advertised and ignored, so a caller narrowing a query by
|
|
361
443
|
// payload got the UNFILTERED set back and a `total_count` describing it. Every key must
|
|
362
444
|
// match (AND), compared structurally so an object or array value filters as written.
|
|
363
|
-
|
|
364
|
-
if (dataFilter && typeof dataFilter === "object" && !Array.isArray(dataFilter)) {
|
|
445
|
+
if (hasDataFilter) {
|
|
365
446
|
const entries = Object.entries(dataFilter);
|
|
366
|
-
|
|
447
|
+
recs = recs.filter((o) => {
|
|
367
448
|
const data = (o.data ?? {});
|
|
368
449
|
return entries.every(([k, v]) => canonJson(data[k]) === canonJson(v));
|
|
369
450
|
});
|
|
370
451
|
}
|
|
371
|
-
|
|
452
|
+
// Payload is dropped only when the caller explicitly asks for the compact rows AND is not
|
|
453
|
+
// filtering by payload (a data_filter needs the payload to have been read).
|
|
454
|
+
const withData = includeData || hasDataFilter;
|
|
455
|
+
const outputs = recs.map((o) => outputRow(o, deps, withData));
|
|
456
|
+
return { ok: true, requires_approval: approval, data: { outputs, total_count: outputs.length } };
|
|
372
457
|
}
|
|
373
458
|
case "output_trace": {
|
|
374
459
|
const id = String(args["output_id"] ?? "");
|
|
@@ -381,39 +466,30 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
381
466
|
if (!["upstream", "downstream", "both"].includes(direction)) {
|
|
382
467
|
return { ok: false, requires_approval: approval, error: `unrecognized direction "${direction}" — use "upstream", "downstream" or "both"` };
|
|
383
468
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
downstream.push(o);
|
|
401
|
-
next.push(o.id);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
frontier = next;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const nodes = direction === "upstream" ? upstream
|
|
408
|
-
: direction === "downstream" ? downstream
|
|
409
|
-
: [...upstream, ...downstream.filter((d) => !upstream.some((u) => u.id === d.id))];
|
|
469
|
+
// The WALK — every direction of it — belongs to the store, which is the one owner of the
|
|
470
|
+
// performance-family crossing rule and of the labels that make a crossing visible. This
|
|
471
|
+
// handler used to hand-roll the forward walk over `all()`, which gave downstream a
|
|
472
|
+
// different scope than upstream (none at all) and no labels either.
|
|
473
|
+
const nodes = deps.outputs.trace(id, {
|
|
474
|
+
...(maxDepth !== undefined ? { max_depth: maxDepth } : {}),
|
|
475
|
+
direction: direction,
|
|
476
|
+
});
|
|
477
|
+
// A hole is reported as itself: named on its own key AND left in the graph, never dropped
|
|
478
|
+
// from either. It is deliberately kept OUT of the two classifications, which read fields
|
|
479
|
+
// an absent record does not have — a hole carries no `input_refs`, so the root-signal test
|
|
480
|
+
// ("nothing upstream of it") would have called every unresolvable reference a root signal,
|
|
481
|
+
// which is the opposite of what is known about it: nothing at all.
|
|
482
|
+
const missing = nodes.filter((n) => n.missing === true);
|
|
483
|
+
const held = nodes.filter((n) => n.missing !== true);
|
|
484
|
+
const all = deps.outputs.all();
|
|
410
485
|
return {
|
|
411
486
|
ok: true, requires_approval: approval,
|
|
412
487
|
data: {
|
|
413
488
|
graph: { nodes }, direction,
|
|
414
|
-
root_signals:
|
|
489
|
+
root_signals: held.filter((o) => o.input_refs.length === 0),
|
|
415
490
|
// The other end of the chain: outputs nothing else was derived from.
|
|
416
|
-
terminal_outputs:
|
|
491
|
+
terminal_outputs: held.filter((o) => !all.some((x) => x.input_refs.includes(o.id))),
|
|
492
|
+
missing,
|
|
417
493
|
},
|
|
418
494
|
};
|
|
419
495
|
}
|
|
@@ -474,10 +550,45 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
474
550
|
if (!deps.standards || !deps.invoke) {
|
|
475
551
|
return { ok: false, not_implemented: true, requires_approval: approval, error: "gig_dispatch needs standards + invoke wired into the server" };
|
|
476
552
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
553
|
+
// ── which performance? ───────────────────────────────────────────────────────────────
|
|
554
|
+
// EXACTLY ONE of standard_slug / chart_slug. A single-standard dispatch IS the degenerate
|
|
555
|
+
// one-movement chart, so naming both names two performances and naming neither names none;
|
|
556
|
+
// the refinement lives in one place (dispatchTarget) and is shared with the CLI.
|
|
557
|
+
const target = dispatchTarget({
|
|
558
|
+
standard_slug: args["standard_slug"] === undefined || args["standard_slug"] === null ? undefined : String(args["standard_slug"]),
|
|
559
|
+
chart_slug: args["chart_slug"] === undefined || args["chart_slug"] === null ? undefined : String(args["chart_slug"]),
|
|
560
|
+
});
|
|
561
|
+
if (!target.ok)
|
|
562
|
+
return { ok: false, requires_approval: approval, error: target.error };
|
|
563
|
+
const chartDef = target.kind === "chart" ? deps.charts?.get(target.slug) : undefined;
|
|
564
|
+
if (target.kind === "chart" && !chartDef) {
|
|
565
|
+
return {
|
|
566
|
+
ok: false, requires_approval: approval,
|
|
567
|
+
error: deps.charts
|
|
568
|
+
? `unknown chart "${target.slug}"`
|
|
569
|
+
: `unknown chart "${target.slug}": this server has no charts map (bootstrap from a genome with charts/)`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const slug2 = target.slug;
|
|
573
|
+
// The standards this dispatch will actually run: one, or one per movement. Every preflight
|
|
574
|
+
// below is stated ONCE over this list, so a chart cannot route around a gate a standard
|
|
575
|
+
// dispatch has to pass.
|
|
576
|
+
const targetStandards = [];
|
|
577
|
+
if (chartDef) {
|
|
578
|
+
for (const m of chartDef.movements) {
|
|
579
|
+
const s = deps.standards.get(m.standard_slug);
|
|
580
|
+
// A dead standard name is composeChart's R2 and is reported with the rule named below;
|
|
581
|
+
// the preflights simply have nothing to check for that movement.
|
|
582
|
+
if (s)
|
|
583
|
+
targetStandards.push(s);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
const standard = deps.standards.get(slug2);
|
|
588
|
+
if (!standard)
|
|
589
|
+
return { ok: false, requires_approval: approval, error: `unknown standard "${slug2}"` };
|
|
590
|
+
targetStandards.push(standard);
|
|
591
|
+
}
|
|
481
592
|
// #203, the READ side. Preserving `status` through the loader was only half of it: the
|
|
482
593
|
// symptom recorded on the issue — "a retired standard stays dispatchable and nothing
|
|
483
594
|
// says otherwise" — survived the field being kept, because nothing consulted it. A
|
|
@@ -490,17 +601,39 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
490
601
|
//
|
|
491
602
|
// deprecated ALLOWS and warns; retired REFUSES. Were both refused, `deprecated` would
|
|
492
603
|
// be a spelling of `retired` and there would be no way to say the softer thing.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
604
|
+
//
|
|
605
|
+
// Swept over EVERY standard this dispatch will run, so a chart cannot smuggle a retired
|
|
606
|
+
// movement past a gate a direct dispatch of the same standard would fail.
|
|
607
|
+
const warnings = [];
|
|
608
|
+
for (const s of targetStandards) {
|
|
609
|
+
const stdStatus = s.status;
|
|
610
|
+
if (stdStatus === "retired") {
|
|
611
|
+
return {
|
|
612
|
+
ok: false, requires_approval: approval,
|
|
613
|
+
error: `standard "${s.slug}" is retired and cannot be dispatched. ` +
|
|
614
|
+
`Promote it back to active (standard_promote) if it should run again.`,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
if (stdStatus === "deprecated") {
|
|
618
|
+
warnings.push(`standard "${s.slug}" is deprecated — it still runs, but should not be built on.`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// WU-0008 preflight: run the same sealDrill used by standard_simulate BEFORE spending
|
|
622
|
+
// on any chair. A structurally-unsealable standard is refused here (pennies) instead of
|
|
623
|
+
// after a chair runs and aborts. Gate is placed once, above the wait/async split, so a
|
|
624
|
+
// single check covers both runGig call-sites below — and over every movement's standard,
|
|
625
|
+
// because a chart that cannot seal at movement three is refused before movement one.
|
|
626
|
+
for (const s of targetStandards) {
|
|
627
|
+
const drill = sealDrill({ phases: s.phases.map((p) => ({ name: p.name, chairs: p.chairs.map((c) => ({ role: c.role, output_contract: c.output_contract })) })) }, deps.registry);
|
|
628
|
+
if (!drill.ok) {
|
|
629
|
+
return {
|
|
630
|
+
ok: false, requires_approval: approval,
|
|
631
|
+
error: `standard "${s.slug}" cannot seal: ` +
|
|
632
|
+
drill.failures.map((f) => `${f.phase}/${f.role} → ${f.domain_type} (${f.errors.join("; ")})`).join(", "),
|
|
633
|
+
data: { seal_drill: drill },
|
|
634
|
+
};
|
|
635
|
+
}
|
|
500
636
|
}
|
|
501
|
-
const warnings = stdStatus === "deprecated"
|
|
502
|
-
? [`standard "${slug2}" is deprecated — it still runs, but should not be built on.`]
|
|
503
|
-
: [];
|
|
504
637
|
// Optional budget arg — when present, runtime enforces per-gig cost-budget
|
|
505
638
|
// and raises BudgetExhausted on depletion (PR for T10 gap, see runtime.ts).
|
|
506
639
|
const budgetArg = args["budget"];
|
|
@@ -545,6 +678,22 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
545
678
|
...(resumeArg !== undefined ? { resume_from: resumeArg } : {}),
|
|
546
679
|
...(reuseOn && deps.reuse ? { reuse: deps.reuse } : {}),
|
|
547
680
|
};
|
|
681
|
+
// ── the human seat's door ────────────────────────────────────────────────────────
|
|
682
|
+
// A chair marked `human: true` parks the run until its incumbent's verdict arrives
|
|
683
|
+
// here, keyed by role. The typical shape is the SECOND call on one gig: dispatch,
|
|
684
|
+
// park, then re-dispatch with `resume_gig_id` + the approval, which restores the
|
|
685
|
+
// chairs already paid for and seals the verdict under `approved_by`.
|
|
686
|
+
const approvalsArg = args["approvals"];
|
|
687
|
+
const approvals = approvalsArg && typeof approvalsArg === "object" && !Array.isArray(approvalsArg)
|
|
688
|
+
? approvalsArg
|
|
689
|
+
: undefined;
|
|
690
|
+
const approvedBy = typeof args["approved_by"] === "string" && args["approved_by"].trim() !== ""
|
|
691
|
+
? args["approved_by"]
|
|
692
|
+
: undefined;
|
|
693
|
+
const humanWiring = {
|
|
694
|
+
...(approvals ? { approvals } : {}),
|
|
695
|
+
...(approvedBy !== undefined ? { approved_by: approvedBy } : {}),
|
|
696
|
+
};
|
|
548
697
|
/** What a run skipped, and why — echoed on every reply so a saving is never silent. */
|
|
549
698
|
const savings = (res) => ({
|
|
550
699
|
...(res.skipped ? { skipped: res.skipped } : {}),
|
|
@@ -552,20 +701,198 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
552
701
|
...(res.reuse ? { reuse: res.reuse } : {}),
|
|
553
702
|
...(res.checkpoint_error ? { checkpoint_error: res.checkpoint_error } : {}),
|
|
554
703
|
});
|
|
704
|
+
const wait = args["wait"] === true;
|
|
705
|
+
// ── the chart path ────────────────────────────────────────────────────────────────────
|
|
706
|
+
// A chart is COMPOSED here, not at load, because the last rule needs a fact the genome does
|
|
707
|
+
// not hold: which types the dispatch payload carries. Everything else composeChart checks it
|
|
708
|
+
// already checked at load; this pass is the one that can hold R7 to the real payload, so a
|
|
709
|
+
// performance whose first movement was never seeded is refused before it spawns anything.
|
|
710
|
+
if (chartDef) {
|
|
711
|
+
const composed = composeChart({
|
|
712
|
+
chart: chartDef,
|
|
713
|
+
standards: deps.standards,
|
|
714
|
+
...(deps.agents ? { agents: deps.agents } : {}),
|
|
715
|
+
...(deps.venues ? { venues: deps.venues } : {}),
|
|
716
|
+
payload_types: Object.keys(gigInput),
|
|
717
|
+
});
|
|
718
|
+
if (!composed.ok) {
|
|
719
|
+
return {
|
|
720
|
+
ok: false, requires_approval: approval,
|
|
721
|
+
error: `chart "${slug2}" cannot be performed: ` + composed.violations.map((v) => `${v.rule}: ${v.detail}`).join(" | "),
|
|
722
|
+
data: { validation_result: { valid: false, violations: composed.violations } },
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const plan = composed;
|
|
726
|
+
const chartDeps = {
|
|
727
|
+
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
728
|
+
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
729
|
+
...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
730
|
+
};
|
|
731
|
+
/** The ARRANGEMENT's manifest. A chart has no single genome_hash or run_fingerprint — it
|
|
732
|
+
* has a chart_hash and one run per movement — so the reply says what a chart run is
|
|
733
|
+
* rather than reshaping it into a standard run's fields. */
|
|
734
|
+
const chartManifest = (res) => ({
|
|
735
|
+
chart_slug: res.chart_slug, chart_hash: res.chart_hash,
|
|
736
|
+
movements: res.movements.map((m) => ({
|
|
737
|
+
movement_id: m.movement_id, standard_slug: m.standard_slug, gig_id: m.gig_id,
|
|
738
|
+
status: m.status, output_count: m.outputs.length, spent_usd: m.spent_usd,
|
|
739
|
+
...(m.result ? { genome_hash: m.result.genome_hash, run_fingerprint: m.result.run_fingerprint } : {}),
|
|
740
|
+
})),
|
|
741
|
+
output_count: res.movements.reduce((n, m) => n + m.outputs.length, 0),
|
|
742
|
+
spent_usd: res.spent_usd,
|
|
743
|
+
...(res.budget ? { budget: res.budget } : {}),
|
|
744
|
+
...(res.gates_approved ? { gates_approved: res.gates_approved } : {}),
|
|
745
|
+
...(res.resumed ? { resumed: res.resumed } : {}),
|
|
746
|
+
});
|
|
747
|
+
if (wait) {
|
|
748
|
+
try {
|
|
749
|
+
const res = await runChart(plan, gigInput, chartDeps);
|
|
750
|
+
return {
|
|
751
|
+
ok: true, requires_approval: approval,
|
|
752
|
+
data: {
|
|
753
|
+
gig_id: res.gig_id, status: res.status,
|
|
754
|
+
...(res.awaiting ? { awaiting: res.awaiting } : {}),
|
|
755
|
+
...(depth ? { depth } : {}),
|
|
756
|
+
warnings, manifest: chartManifest(res),
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
catch (e) {
|
|
761
|
+
if (e instanceof ResumeRefused) {
|
|
762
|
+
return { ok: false, requires_approval: approval, error: e.message,
|
|
763
|
+
data: { resume_refused: true, gig_id: e.gig_id, drift: e.drift } };
|
|
764
|
+
}
|
|
765
|
+
if (e instanceof BudgetExhausted) {
|
|
766
|
+
const partial = partialGigUsage(e);
|
|
767
|
+
return { ok: false, requires_approval: approval, error: e.message,
|
|
768
|
+
data: { budget_exhausted: true, agent_slug: e.agent_slug, balance: e.balance, cost: e.cost, budget_state: e.state,
|
|
769
|
+
...(partial ? { usage: partial } : {}) } };
|
|
770
|
+
}
|
|
771
|
+
throw e;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// Async, the default. Same live-state row an async standard dispatch registers, so
|
|
775
|
+
// gig_monitor and gig_abort reach a performance exactly as they reach a run: the row
|
|
776
|
+
// names the standard the performance OPENS with, and `chart_slug` names the arrangement.
|
|
777
|
+
const chartGigId = resumeArg ?? randomUUID();
|
|
778
|
+
const chartRuns = deps.gig_runs ?? (deps.gig_runs = new Map());
|
|
779
|
+
const priorChartState = chartRuns.get(chartGigId);
|
|
780
|
+
const chartState = newGigRun(chartGigId, plan.movements[0].standard.slug, plan.movements.reduce((n, m) => n + m.standard.phases.length, 0), new Date().toISOString());
|
|
781
|
+
chartState.chart_slug = plan.chart.slug;
|
|
782
|
+
const chartController = new AbortController();
|
|
783
|
+
chartState.controller = chartController;
|
|
784
|
+
chartRuns.set(chartGigId, chartState);
|
|
785
|
+
pruneGigRuns(chartRuns);
|
|
786
|
+
const chartLogDir = deps.gig_log_base ? join(deps.gig_log_base, "gigs", chartGigId) : undefined;
|
|
787
|
+
const onChartProgress = (ev) => {
|
|
788
|
+
applyGigProgress(chartState, ev);
|
|
789
|
+
if (chartLogDir && ev.type === "agent_event") {
|
|
790
|
+
try {
|
|
791
|
+
mkdirSync(chartLogDir, { recursive: true });
|
|
792
|
+
appendFileSync(join(chartLogDir, `${ev.role}.jsonl`), JSON.stringify(ev.event) + "\n");
|
|
793
|
+
}
|
|
794
|
+
catch { /* best-effort */ }
|
|
795
|
+
}
|
|
796
|
+
const line = gigEventLogLine(chartGigId, ev);
|
|
797
|
+
if (line) {
|
|
798
|
+
try {
|
|
799
|
+
process.stderr.write(line + "\n");
|
|
800
|
+
}
|
|
801
|
+
catch { /* best-effort */ }
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
const chartPromise = runChart(plan, gigInput, {
|
|
805
|
+
...chartDeps, gig_id: chartGigId, onProgress: onChartProgress, signal: chartController.signal,
|
|
806
|
+
});
|
|
807
|
+
let chartRefusal;
|
|
808
|
+
if (resumeArg !== undefined) {
|
|
809
|
+
void chartPromise.catch((e) => { if (e instanceof ResumeRefused)
|
|
810
|
+
chartRefusal = e; });
|
|
811
|
+
}
|
|
812
|
+
void chartPromise
|
|
813
|
+
.then((res) => {
|
|
814
|
+
chartState.status = res.status === "complete" ? "complete" : "awaiting_approval";
|
|
815
|
+
// An arrangement-level GATE has no phase — its position is the movement it gates — so
|
|
816
|
+
// the movement_id fills the slot a within-movement park fills with its phase name.
|
|
817
|
+
if (res.awaiting)
|
|
818
|
+
chartState.awaiting = { phase: res.awaiting.phase ?? res.awaiting.movement_id, role: res.awaiting.chair };
|
|
819
|
+
chartState.finished_at = new Date().toISOString();
|
|
820
|
+
// The arrangement's identity in the slot the run's identity occupies — the same
|
|
821
|
+
// substitution `run_fingerprint` makes for a chart (src/chart.ts), so a monitor reading
|
|
822
|
+
// this field gets the hash that actually identifies what ran.
|
|
823
|
+
chartState.genome_hash = res.chart_hash;
|
|
824
|
+
chartState.outputs_count = res.movements.reduce((n, m) => n + m.outputs.length, 0);
|
|
825
|
+
// A budget_exhausted performance settled without completing and without parking. It
|
|
826
|
+
// is not `complete` and there is no person to wait for, so it reads as failed with
|
|
827
|
+
// the boundary it stopped at named — the same posture a depleted run has.
|
|
828
|
+
if (res.status === "budget_exhausted") {
|
|
829
|
+
chartState.status = "failed";
|
|
830
|
+
chartState.error = `budget envelope exhausted at movement "${res.budget?.exhausted_at_movement ?? "?"}" (spent $${res.spent_usd} of $${res.budget?.total_usd ?? "?"})`;
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
.catch((e) => {
|
|
834
|
+
chartState.finished_at = new Date().toISOString();
|
|
835
|
+
if (e instanceof GigAborted) {
|
|
836
|
+
chartState.status = "aborted";
|
|
837
|
+
chartState.abort_reason = e.reason;
|
|
838
|
+
chartState.outputs_count = e.outputs.length;
|
|
839
|
+
if (e.usage)
|
|
840
|
+
chartState.usage = e.usage;
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
chartState.status = "failed";
|
|
844
|
+
chartState.error = e instanceof Error ? e.message : String(e);
|
|
845
|
+
const partial = partialGigUsage(e);
|
|
846
|
+
if (partial)
|
|
847
|
+
chartState.usage = partial;
|
|
848
|
+
const bs = partialBudgetState(e);
|
|
849
|
+
if (bs)
|
|
850
|
+
chartState.budget_state = bs;
|
|
851
|
+
onChartProgress({ type: "gig_failed", error: chartState.error });
|
|
852
|
+
})
|
|
853
|
+
.finally(() => { chartState.controller = undefined; });
|
|
854
|
+
if (resumeArg !== undefined) {
|
|
855
|
+
await Promise.resolve(); // one turn — see the ordering note on the standard path below
|
|
856
|
+
if (chartRefusal) {
|
|
857
|
+
if (priorChartState)
|
|
858
|
+
chartRuns.set(chartGigId, priorChartState);
|
|
859
|
+
else
|
|
860
|
+
chartRuns.delete(chartGigId);
|
|
861
|
+
return { ok: false, requires_approval: approval, error: chartRefusal.message,
|
|
862
|
+
data: { resume_refused: true, gig_id: chartRefusal.gig_id, drift: chartRefusal.drift } };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
ok: true, requires_approval: approval,
|
|
867
|
+
data: {
|
|
868
|
+
gig_id: chartGigId, status: "running", chart_slug: plan.chart.slug, chart_hash: plan.chart_hash,
|
|
869
|
+
...(depth ? { depth } : {}), warnings, ...(chartLogDir ? { log_dir: chartLogDir } : {}),
|
|
870
|
+
...(resumeArg !== undefined ? { resumed_from: resumeArg } : {}),
|
|
871
|
+
...(reuseOn ? { reuse: true } : {}),
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
// ── the single-standard path ──────────────────────────────────────────────────────────
|
|
876
|
+
// Reached only when the target is a standard: the chart branch above always returns, and an
|
|
877
|
+
// unresolvable standard slug returned before the preflights. So there is exactly one here.
|
|
878
|
+
const standard = targetStandards[0];
|
|
555
879
|
// Synchronous mode (opt-in via wait:true) — block, return the manifest. The
|
|
556
880
|
// deterministic test path and any caller that wants the answer in one call.
|
|
557
|
-
const wait = args["wait"] === true;
|
|
558
881
|
if (wait) {
|
|
559
882
|
try {
|
|
560
883
|
const res = await runGig(standard, gigInput, {
|
|
561
884
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
562
885
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
563
|
-
...(depth ? { depth } : {}), ...reuseWiring,
|
|
886
|
+
...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
564
887
|
});
|
|
565
888
|
return {
|
|
566
889
|
ok: true, requires_approval: approval,
|
|
567
890
|
data: {
|
|
568
891
|
gig_id: res.gig_id,
|
|
892
|
+
// The run's own verdict on itself. A parked gig reported as nothing at all read
|
|
893
|
+
// as a completed one to every caller of the synchronous path.
|
|
894
|
+
status: res.status,
|
|
895
|
+
...(res.awaiting ? { awaiting: res.awaiting } : {}),
|
|
569
896
|
...(depth ? { depth } : {}),
|
|
570
897
|
warnings,
|
|
571
898
|
manifest: {
|
|
@@ -641,7 +968,7 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
641
968
|
const runPromise = runGig(standard, gigInput, {
|
|
642
969
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
643
970
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
644
|
-
gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring,
|
|
971
|
+
gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
645
972
|
});
|
|
646
973
|
// A REFUSED resume must be answered in THIS reply, not discovered later by polling. The
|
|
647
974
|
// gate throws in runGig's SYNCHRONOUS phase — before its first `await`, which is exactly
|
|
@@ -657,7 +984,12 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
657
984
|
}
|
|
658
985
|
void runPromise
|
|
659
986
|
.then((res) => {
|
|
660
|
-
|
|
987
|
+
// A run that PARKED at a human chair settled without completing. Recording it as
|
|
988
|
+
// `complete` erased the park from gig_monitor — the only surface an async caller
|
|
989
|
+
// has — so the operator saw a finished gig with a chair that never sealed.
|
|
990
|
+
state.status = res.status === "awaiting_approval" ? "awaiting_approval" : "complete";
|
|
991
|
+
if (res.awaiting)
|
|
992
|
+
state.awaiting = res.awaiting;
|
|
661
993
|
state.finished_at = new Date().toISOString();
|
|
662
994
|
state.run_fingerprint = res.run_fingerprint;
|
|
663
995
|
state.genome_hash = res.genome_hash;
|
|
@@ -744,7 +1076,13 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
744
1076
|
ok: true, requires_approval: approval,
|
|
745
1077
|
data: {
|
|
746
1078
|
status: live.status,
|
|
1079
|
+
// Who the run is waiting ON. `awaiting_approval` with no chair named leaves the
|
|
1080
|
+
// operator to guess which seat is theirs to sit in.
|
|
1081
|
+
...(live.awaiting ? { awaiting: live.awaiting } : {}),
|
|
747
1082
|
standard_slug: live.standard_slug,
|
|
1083
|
+
// Present iff this run is a performance of a chart. Absent means one standard, which
|
|
1084
|
+
// is what every caller has always been looking at.
|
|
1085
|
+
...(live.chart_slug ? { chart_slug: live.chart_slug } : {}),
|
|
748
1086
|
current_phase: live.current_phase ?? null,
|
|
749
1087
|
phases_total: live.phases_total,
|
|
750
1088
|
phases_complete: live.phases_seen.length,
|
|
@@ -873,6 +1211,105 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
873
1211
|
throw e;
|
|
874
1212
|
}
|
|
875
1213
|
}
|
|
1214
|
+
case "chart_define": {
|
|
1215
|
+
// The arrangement, authored through the genome's mouth. Every field is read explicitly from
|
|
1216
|
+
// the schema's own key set — the surface is generated from ChartSchema, so a field added
|
|
1217
|
+
// there must be threaded here or the drift guard reds.
|
|
1218
|
+
if (!deps.standards) {
|
|
1219
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "chart_define needs a standards map (bootstrap from a genome) — a chart's movements name standards, and a chart composed against nothing is a chart of dead names" };
|
|
1220
|
+
}
|
|
1221
|
+
const cSlug = String(args["slug"] ?? "");
|
|
1222
|
+
const chartInputDef = {
|
|
1223
|
+
slug: cSlug,
|
|
1224
|
+
movements: args["movements"] ?? [],
|
|
1225
|
+
...(args["edges"] !== undefined ? { edges: args["edges"] } : {}),
|
|
1226
|
+
...(args["approval_gates"] !== undefined ? { approval_gates: args["approval_gates"] } : {}),
|
|
1227
|
+
...(args["budget_envelope"] !== undefined ? { budget_envelope: args["budget_envelope"] } : {}),
|
|
1228
|
+
...(args["venue"] !== undefined ? { venue: args["venue"] } : {}),
|
|
1229
|
+
};
|
|
1230
|
+
const shape = ChartSchema.safeParse(chartInputDef);
|
|
1231
|
+
const composed = composeChart({
|
|
1232
|
+
chart: chartInputDef,
|
|
1233
|
+
standards: deps.standards,
|
|
1234
|
+
...(deps.agents ? { agents: deps.agents } : {}),
|
|
1235
|
+
...(deps.venues ? { venues: deps.venues } : {}),
|
|
1236
|
+
// Authoring time knows no payload, so a boundary movement's declared gig contract stands
|
|
1237
|
+
// in for it — the same rule the loader applies, and dispatch re-checks against the real
|
|
1238
|
+
// payload. (An unparseable chart has no movements to read; composeChart reports R0.)
|
|
1239
|
+
payload_types: shape.success ? chartEntrySeedTypes(shape.data, deps.standards) : [],
|
|
1240
|
+
});
|
|
1241
|
+
if (!composed.ok) {
|
|
1242
|
+
const why = composed.violations.map((v) => `${v.rule}: ${v.detail}`).join(" | ");
|
|
1243
|
+
return {
|
|
1244
|
+
ok: false, requires_approval: approval, error: `chart "${cSlug}" was refused: ${why}`,
|
|
1245
|
+
data: { validation_result: { valid: false, violations: composed.violations } },
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
// Write-through to the LIVE map so gig_dispatch can perform it in the same session.
|
|
1249
|
+
deps.charts?.set(cSlug, composed.chart);
|
|
1250
|
+
const chartFile = { ...composed.chart };
|
|
1251
|
+
const sealedChart = sealDefinition("chart_define", cSlug, chartFile, deps.ledger, deps.genome_dir, "charts");
|
|
1252
|
+
return {
|
|
1253
|
+
ok: true, requires_approval: approval,
|
|
1254
|
+
data: {
|
|
1255
|
+
chart_id: composed.chart.slug, chart_hash: composed.chart_hash,
|
|
1256
|
+
movements: composed.order, edges_classified: composed.edges_classified,
|
|
1257
|
+
content_hash: sealedChart.content_hash, dependency_hash: sealedChart.dependency_hash,
|
|
1258
|
+
effective_hash: sealedChart.effective_hash,
|
|
1259
|
+
validation_result: { valid: true },
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
case "venue_define": {
|
|
1264
|
+
// The room. BOTH gates, in the loader's order: the single Zod source for the shape, then
|
|
1265
|
+
// venueDefect for the cross-field rules — so a venue authored here cannot slip past a check
|
|
1266
|
+
// a venue read off disk would hit.
|
|
1267
|
+
const vSlug = String(args["slug"] ?? "");
|
|
1268
|
+
const venueInputDef = {
|
|
1269
|
+
slug: vSlug,
|
|
1270
|
+
institution_slug: args["institution_slug"],
|
|
1271
|
+
...(args["description"] !== undefined ? { description: args["description"] } : {}),
|
|
1272
|
+
...(args["flavor"] !== undefined ? { flavor: args["flavor"] } : {}),
|
|
1273
|
+
// Pass through only what was stated. The schema owns the defaults — including that an
|
|
1274
|
+
// unstated `equipment` is the EMPTY room — so this handler cannot disagree with the loader
|
|
1275
|
+
// about what a bare venue means.
|
|
1276
|
+
...(args["equipment"] !== undefined ? { equipment: args["equipment"] } : {}),
|
|
1277
|
+
...(args["doors"] !== undefined ? { doors: args["doors"] } : {}),
|
|
1278
|
+
...(args["installs"] !== undefined ? { installs: args["installs"] } : {}),
|
|
1279
|
+
...(args["credential_surface"] !== undefined ? { credential_surface: args["credential_surface"] } : {}),
|
|
1280
|
+
...(args["lifecycle"] !== undefined ? { lifecycle: args["lifecycle"] } : {}),
|
|
1281
|
+
...(args["responsible_chair"] !== undefined ? { responsible_chair: args["responsible_chair"] } : {}),
|
|
1282
|
+
};
|
|
1283
|
+
const parsedVenue = VenueSchema.safeParse(venueInputDef);
|
|
1284
|
+
if (!parsedVenue.success) {
|
|
1285
|
+
const why = parsedVenue.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1286
|
+
return {
|
|
1287
|
+
ok: false, requires_approval: approval, error: `venue "${vSlug}" was refused: ${why}`,
|
|
1288
|
+
data: { validation_result: { valid: false, error: why } },
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const vDefect = venueDefect(parsedVenue.data);
|
|
1292
|
+
if (vDefect) {
|
|
1293
|
+
return {
|
|
1294
|
+
ok: false, requires_approval: approval, error: vDefect,
|
|
1295
|
+
data: { validation_result: { valid: false, error: vDefect } },
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
deps.venues?.set(vSlug, parsedVenue.data);
|
|
1299
|
+
const sealedVenue = sealDefinition("venue_define", vSlug, parsedVenue.data, deps.ledger, deps.genome_dir, "venues");
|
|
1300
|
+
return {
|
|
1301
|
+
ok: true, requires_approval: approval,
|
|
1302
|
+
data: {
|
|
1303
|
+
venue_id: parsedVenue.data.slug,
|
|
1304
|
+
// What a room actually permits, echoed back: an author who meant "read-only tools" and
|
|
1305
|
+
// typed nothing has built the EMPTY room, and the count is where they see it.
|
|
1306
|
+
tool_count: parsedVenue.data.equipment.tools.length,
|
|
1307
|
+
content_hash: sealedVenue.content_hash, dependency_hash: sealedVenue.dependency_hash,
|
|
1308
|
+
effective_hash: sealedVenue.effective_hash,
|
|
1309
|
+
validation_result: { valid: true },
|
|
1310
|
+
},
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
876
1313
|
case "agent_validate_pipeline": {
|
|
877
1314
|
if (Array.isArray(args["primitives"])) {
|
|
878
1315
|
try {
|
|
@@ -935,6 +1372,9 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
935
1372
|
slug: baseDef.slug,
|
|
936
1373
|
extends: baseDef.extends,
|
|
937
1374
|
schema: { properties: nextProps },
|
|
1375
|
+
// 2026-08-08 — the undeclared-required check needs the extension's required
|
|
1376
|
+
// set, or an extend could version-in a requirement no producer can see.
|
|
1377
|
+
required_fields: nextRequired,
|
|
938
1378
|
});
|
|
939
1379
|
if (extendDefect) {
|
|
940
1380
|
return { ok: false, requires_approval: approval, error: `type_extend rejected: ${extendDefect}` };
|
|
@@ -1112,6 +1552,13 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1112
1552
|
// mutate deps.agents in place so the next reload sees the new baseline.
|
|
1113
1553
|
const agentsBefore = new Map(deps.agents ?? []);
|
|
1114
1554
|
const agentsDiff = syncMap(deps.agents, fresh.agents, agentsBefore);
|
|
1555
|
+
// charts + venues — same in-place sync. A reload that refreshed standards and left the
|
|
1556
|
+
// arrangements over them stale would leave gig_dispatch performing a chart the genome no
|
|
1557
|
+
// longer describes, which is the class of drift genome_reload exists to close.
|
|
1558
|
+
const chartsBefore = new Map(deps.charts ?? []);
|
|
1559
|
+
const chartsDiff = syncMap(deps.charts, fresh.charts, chartsBefore);
|
|
1560
|
+
const venuesBefore = new Map(deps.venues ?? []);
|
|
1561
|
+
const venuesDiff = syncMap(deps.venues, fresh.venues, venuesBefore);
|
|
1115
1562
|
// typesBefore is captured for symmetry; not currently surfaced beyond typeDiff.
|
|
1116
1563
|
void typesBefore;
|
|
1117
1564
|
return {
|
|
@@ -1125,6 +1572,8 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1125
1572
|
skills: skillsDiff.added,
|
|
1126
1573
|
evals: evalsDiff.added,
|
|
1127
1574
|
agents: agentsDiff.added,
|
|
1575
|
+
charts: chartsDiff.added,
|
|
1576
|
+
venues: venuesDiff.added,
|
|
1128
1577
|
},
|
|
1129
1578
|
modified: {
|
|
1130
1579
|
domain_types: typeDiff.modified,
|
|
@@ -1132,6 +1581,8 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1132
1581
|
skills: skillsDiff.modified,
|
|
1133
1582
|
evals: evalsDiff.modified,
|
|
1134
1583
|
agents: agentsDiff.modified,
|
|
1584
|
+
charts: chartsDiff.modified,
|
|
1585
|
+
venues: venuesDiff.modified,
|
|
1135
1586
|
},
|
|
1136
1587
|
removed: {
|
|
1137
1588
|
domain_types: typeDiff.removed,
|
|
@@ -1139,6 +1590,8 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1139
1590
|
skills: skillsDiff.removed,
|
|
1140
1591
|
evals: evalsDiff.removed,
|
|
1141
1592
|
agents: agentsDiff.removed,
|
|
1593
|
+
charts: chartsDiff.removed,
|
|
1594
|
+
venues: venuesDiff.removed,
|
|
1142
1595
|
},
|
|
1143
1596
|
},
|
|
1144
1597
|
load_errors: deps.load_errors,
|
|
@@ -1494,12 +1947,20 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1494
1947
|
const evolveSlug = typeof args["slug"] === "string" ? args["slug"] : undefined;
|
|
1495
1948
|
const changes = (args["changes"] && typeof args["changes"] === "object")
|
|
1496
1949
|
? args["changes"] : undefined;
|
|
1497
|
-
if (evolveSlug && changes && deps.genome_dir) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1950
|
+
if (evolveSlug && changes && (deps.genome_dir || deps.agents?.has(evolveSlug))) {
|
|
1951
|
+
// The base definition: the genome file when a working tree exists, else the loaded
|
|
1952
|
+
// agents map (a hosted surface has no filesystem — the STORE genome is the base,
|
|
1953
|
+
// and the seam above persists the merged definition back through the store).
|
|
1954
|
+
let currentDef;
|
|
1955
|
+
if (deps.genome_dir && existsSync(join(deps.genome_dir, "agents", `${evolveSlug}.json`))) {
|
|
1956
|
+
currentDef = JSON.parse(readFileSync(join(deps.genome_dir, "agents", `${evolveSlug}.json`), "utf-8"));
|
|
1957
|
+
}
|
|
1958
|
+
else if (deps.agents?.has(evolveSlug)) {
|
|
1959
|
+
currentDef = deps.agents.get(evolveSlug);
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1500
1962
|
return { ok: false, requires_approval: approval, error: `agent_evolve: unknown agent "${evolveSlug}" (no agents/${evolveSlug}.json)` };
|
|
1501
1963
|
}
|
|
1502
|
-
const currentDef = JSON.parse(readFileSync(agentPath, "utf-8"));
|
|
1503
1964
|
const nextDef = { ...currentDef, ...changes };
|
|
1504
1965
|
// The agent must still be a legal composition on its own…
|
|
1505
1966
|
try {
|
|
@@ -1556,9 +2017,12 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1556
2017
|
agentsArr[i] = sealed.agent;
|
|
1557
2018
|
}
|
|
1558
2019
|
}
|
|
2020
|
+
// next_def is the seam's persistence source on a hosted surface (the store
|
|
2021
|
+
// upsert writes the MERGED definition, not the raw evolve args).
|
|
2022
|
+
deps.agents?.set(evolveSlug, sealed.agent);
|
|
1559
2023
|
return {
|
|
1560
2024
|
ok: true, requires_approval: approval,
|
|
1561
|
-
data: { new_version, evolved: sealed.agent, content_hash: sealed.content_hash, effective_hash: sealed.effective_hash, cascade_check: { agents_affected: [], standards_affected } },
|
|
2025
|
+
data: { new_version, evolved: sealed.agent, next_def: nextDef, content_hash: sealed.content_hash, effective_hash: sealed.effective_hash, cascade_check: { agents_affected: [], standards_affected } },
|
|
1562
2026
|
};
|
|
1563
2027
|
}
|
|
1564
2028
|
return { ok: true, requires_approval: approval, data: { new_version, cascade_check: { agents_affected: [], standards_affected: [] } } };
|
|
@@ -1620,6 +2084,130 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1620
2084
|
// production status, and never RUN, TESTED, LISTED or REVISED through the engine. The
|
|
1621
2085
|
// fixture gate on promotion made that gap sharper — you could be refused for failing
|
|
1622
2086
|
// fixtures with no way to run them and see why.
|
|
2087
|
+
// ── the org context switch — set once, inherited by every member write ─────────────
|
|
2088
|
+
case "org_use": {
|
|
2089
|
+
const orgSlug = String(args["org_slug"] ?? "");
|
|
2090
|
+
if (!orgSlug)
|
|
2091
|
+
return { ok: false, requires_approval: approval, error: "org_use requires org_slug" };
|
|
2092
|
+
if (!deps.orgUse) {
|
|
2093
|
+
return {
|
|
2094
|
+
ok: false, requires_approval: approval,
|
|
2095
|
+
error: "org context is a store concept — a file genome has one implicit org (this working tree). On a hosted surface the host wires deps.orgUse to the store's coltrane_org_use RPC.",
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
try {
|
|
2099
|
+
const set = await deps.orgUse(orgSlug);
|
|
2100
|
+
return { ok: true, requires_approval: approval, data: { org_slug: set, set: true } };
|
|
2101
|
+
}
|
|
2102
|
+
catch (e) {
|
|
2103
|
+
return { ok: false, requires_approval: approval, error: e instanceof Error ? e.message : String(e) };
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
// ── discoverability parity — a dispatcher must be able to FIND a slug over MCP ──────
|
|
2107
|
+
// (tests/genome_browse_parity.test.ts). Backed by the deps maps, so the same handler
|
|
2108
|
+
// serves a working-tree load and a hosted store load identically; no filesystem.
|
|
2109
|
+
case "standard_browse": {
|
|
2110
|
+
if (!deps.standards)
|
|
2111
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "standard_browse needs a standards map (bootstrap from a genome)" };
|
|
2112
|
+
let list = [...deps.standards.values()];
|
|
2113
|
+
if (args["domain"])
|
|
2114
|
+
list = list.filter((s) => s.domain === args["domain"]);
|
|
2115
|
+
if (args["status"])
|
|
2116
|
+
list = list.filter((s) => (s.status ?? "active") === args["status"]);
|
|
2117
|
+
const standards = list
|
|
2118
|
+
.map((s) => ({
|
|
2119
|
+
slug: s.slug, domain: s.domain, status: s.status ?? "active",
|
|
2120
|
+
phases: s.phases.map((p) => p.name), phase_count: s.phases.length,
|
|
2121
|
+
chair_count: s.phases.reduce((n, p) => n + p.chairs.length, 0),
|
|
2122
|
+
input_types: s.input_types ?? [], output_types: s.output_types ?? [],
|
|
2123
|
+
eval_slugs: s.eval_slugs ?? [], description: s.description ?? null,
|
|
2124
|
+
}))
|
|
2125
|
+
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
2126
|
+
return { ok: true, requires_approval: approval, data: { standards, count: standards.length } };
|
|
2127
|
+
}
|
|
2128
|
+
case "chart_browse": {
|
|
2129
|
+
if (!deps.charts)
|
|
2130
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "chart_browse needs a charts map (bootstrap from a genome)" };
|
|
2131
|
+
let clist = [...deps.charts.values()];
|
|
2132
|
+
if (args["venue"])
|
|
2133
|
+
clist = clist.filter((c) => c.venue === args["venue"]);
|
|
2134
|
+
if (args["standard_slug"])
|
|
2135
|
+
clist = clist.filter((c) => c.movements.some((m) => m.standard_slug === args["standard_slug"]));
|
|
2136
|
+
const charts = clist
|
|
2137
|
+
.map((c) => {
|
|
2138
|
+
// The arrangement's identity, when it is computable. chartHash folds each movement's
|
|
2139
|
+
// standard PROJECTION, so a chart naming a standard this server does not hold has no
|
|
2140
|
+
// hash to report — and reporting null is the honest answer, not a fabricated prefix.
|
|
2141
|
+
const resolvedMovements = [];
|
|
2142
|
+
for (const m of c.movements) {
|
|
2143
|
+
const s = deps.standards?.get(m.standard_slug);
|
|
2144
|
+
if (s)
|
|
2145
|
+
resolvedMovements.push({ movement_id: m.movement_id, standard: s, runtime_fills: m.runtime_fills, seatings: m.seatings });
|
|
2146
|
+
}
|
|
2147
|
+
const complete = resolvedMovements.length === c.movements.length;
|
|
2148
|
+
return {
|
|
2149
|
+
slug: c.slug,
|
|
2150
|
+
standard_slugs: c.movements.map((m) => m.standard_slug),
|
|
2151
|
+
movement_ids: c.movements.map((m) => m.movement_id),
|
|
2152
|
+
movement_count: c.movements.length,
|
|
2153
|
+
edge_count: c.edges.length,
|
|
2154
|
+
gate_count: c.approval_gates.length,
|
|
2155
|
+
venue: c.venue ?? null,
|
|
2156
|
+
budget_usd: c.budget_envelope?.total_usd ?? null,
|
|
2157
|
+
// A PREFIX: enough to tell two arrangements apart in a listing, not the identity
|
|
2158
|
+
// itself (which a caller reads off the define call or the ledger).
|
|
2159
|
+
chart_hash: complete ? chartHash({ movements: resolvedMovements, chart: c }).slice(0, 12) : null,
|
|
2160
|
+
};
|
|
2161
|
+
})
|
|
2162
|
+
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
2163
|
+
return { ok: true, requires_approval: approval, data: { charts, count: charts.length } };
|
|
2164
|
+
}
|
|
2165
|
+
case "venue_browse": {
|
|
2166
|
+
if (!deps.venues)
|
|
2167
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "venue_browse needs a venues map (bootstrap from a genome)" };
|
|
2168
|
+
let vlist = [...deps.venues.values()];
|
|
2169
|
+
if (args["institution_slug"])
|
|
2170
|
+
vlist = vlist.filter((v) => v.institution_slug === args["institution_slug"]);
|
|
2171
|
+
if (args["flavor"])
|
|
2172
|
+
vlist = vlist.filter((v) => v.flavor === args["flavor"]);
|
|
2173
|
+
const venues = vlist
|
|
2174
|
+
.map((v) => ({
|
|
2175
|
+
slug: v.slug, institution_slug: v.institution_slug, flavor: v.flavor ?? null,
|
|
2176
|
+
// COUNTS, not contents: the numbers are what a seating decision turns on ("does this
|
|
2177
|
+
// room hold anything at all", "can anything leave"), and the full lists are one
|
|
2178
|
+
// venue_define / file read away.
|
|
2179
|
+
tool_count: v.equipment.tools.length,
|
|
2180
|
+
tools: v.equipment.tools,
|
|
2181
|
+
ingress_count: v.doors?.ingress.length ?? 0,
|
|
2182
|
+
egress_count: v.doors?.egress.length ?? 0,
|
|
2183
|
+
install_count: v.installs.length,
|
|
2184
|
+
credential_surface: v.credential_surface,
|
|
2185
|
+
lifecycle: v.lifecycle.policy,
|
|
2186
|
+
rebuild_cadence: v.lifecycle.rebuild_cadence ?? null,
|
|
2187
|
+
responsible_chair: v.responsible_chair ?? null,
|
|
2188
|
+
description: v.description ?? null,
|
|
2189
|
+
}))
|
|
2190
|
+
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
2191
|
+
return { ok: true, requires_approval: approval, data: { venues, count: venues.length } };
|
|
2192
|
+
}
|
|
2193
|
+
case "agent_browse": {
|
|
2194
|
+
if (!deps.agents)
|
|
2195
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "agent_browse needs an agents map (bootstrap from a genome)" };
|
|
2196
|
+
let list = [...deps.agents.values()];
|
|
2197
|
+
if (args["domain"])
|
|
2198
|
+
list = list.filter((a) => a.domain === args["domain"]);
|
|
2199
|
+
if (args["primitive"])
|
|
2200
|
+
list = list.filter((a) => a.primitives.includes(args["primitive"]));
|
|
2201
|
+
const agents = list
|
|
2202
|
+
.map((a) => ({
|
|
2203
|
+
slug: a.slug, primitives: a.primitives, domain: a.domain,
|
|
2204
|
+
input_types: a.input_types, output_types: a.output_types,
|
|
2205
|
+
behavioral_primitives: a.behavioral_primitives,
|
|
2206
|
+
skill_slugs: a.skill_slugs ?? [], model_tier: a.model_tier ?? null,
|
|
2207
|
+
}))
|
|
2208
|
+
.sort((x, y) => (x.slug < y.slug ? -1 : 1));
|
|
2209
|
+
return { ok: true, requires_approval: approval, data: { agents, count: agents.length } };
|
|
2210
|
+
}
|
|
1623
2211
|
case "skill_browse": {
|
|
1624
2212
|
if (!deps.skills)
|
|
1625
2213
|
return { ok: false, not_implemented: true, requires_approval: approval, error: "skill_browse needs a skills map (bootstrap from a genome)" };
|
|
@@ -2153,18 +2741,114 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
2153
2741
|
return { ok: false, requires_approval: approval, error: e instanceof Error ? e.message : String(e) };
|
|
2154
2742
|
}
|
|
2155
2743
|
}
|
|
2156
|
-
|
|
2744
|
+
// Tools whose implementation is inherently local-process. Each entry names WHY, so the
|
|
2745
|
+
// hosted refusal teaches instead of stonewalling.
|
|
2746
|
+
const HOSTED_BLOCKED = {
|
|
2747
|
+
server_restart: "server_restart is a local-process concern (the stdio relay restarts its child); a hosted, per-request surface has no server process to restart",
|
|
2748
|
+
skill_execute: "skill_execute runs a skill's code half in a local subprocess against its on-disk package; hosted skills carry no local package — run code skills where the genome files live",
|
|
2749
|
+
skill_evolve: "skill_evolve runs candidate code against local fixtures in a subprocess; hosted skills carry no local package to evolve",
|
|
2750
|
+
charter_read: "charter_read reads a charter file from a local path; a hosted surface has no filesystem",
|
|
2751
|
+
gig_logs: "gig_logs tails per-chair log files under the local outputs dir; hosted runs are executed by the drain worker and their record lives in the store",
|
|
2752
|
+
genome_reload: "genome_reload re-reads genome files from disk; a hosted surface loads its genome from the store per-request, so there is nothing to reload",
|
|
2753
|
+
};
|
|
2754
|
+
// The genome-mutation tools whose success must ALSO land in the hosted store, and the class
|
|
2755
|
+
// each persists as. Payload is filtered by the class's own Zod schema key list (the same
|
|
2756
|
+
// single source the handlers copy from), so the store payload can't drift from the schema.
|
|
2757
|
+
const HOSTED_UPSERT = {
|
|
2758
|
+
agent_define: { cls: "agent", keys: Object.keys(AgentSchema.shape) },
|
|
2759
|
+
agent_evolve: { cls: "agent", keys: Object.keys(AgentSchema.shape) },
|
|
2760
|
+
standard_compose: { cls: "standard", keys: Object.keys(StandardSchema.shape) },
|
|
2761
|
+
type_register: { cls: "domain_type", keys: Object.keys(DomainTypeSchema.shape) },
|
|
2762
|
+
skill_define: { cls: "skill", keys: Object.keys(SkillSchema.shape) },
|
|
2763
|
+
// The chart and the venue ride the same port. The store side is NOT built — coltrane_genome_upsert
|
|
2764
|
+
// has no branch for either class — so a hosted chart_define reaches the RPC and is refused there,
|
|
2765
|
+
// with the store's own message, and the mutation fails loudly. That is the right failure: the
|
|
2766
|
+
// class travels the port it is supposed to travel, and the missing half announces itself instead
|
|
2767
|
+
// of the engine quietly declining to try. (Store-side work: two tables + two upsert branches.)
|
|
2768
|
+
chart_define: { cls: "chart", keys: Object.keys(ChartSchema.shape) },
|
|
2769
|
+
venue_define: { cls: "venue", keys: Object.keys(VenueSchema.shape) },
|
|
2770
|
+
};
|
|
2771
|
+
async function callSurfaceTool(slug, args, deps) {
|
|
2772
|
+
if (deps.hosted) {
|
|
2773
|
+
const blocked = HOSTED_BLOCKED[slug];
|
|
2774
|
+
if (blocked)
|
|
2775
|
+
return { ok: false, hosted_unsupported: true, error: blocked };
|
|
2776
|
+
if (slug === "gig_dispatch") {
|
|
2777
|
+
// Hosted dispatch NEVER spawns. With a queue seam it queues (the gig table is the
|
|
2778
|
+
// queue; a drain worker claims and runs); without one it says so, typed.
|
|
2779
|
+
if (deps.queueGig) {
|
|
2780
|
+
try {
|
|
2781
|
+
const data = await deps.queueGig(args);
|
|
2782
|
+
return { ok: true, data };
|
|
2783
|
+
}
|
|
2784
|
+
catch (e) {
|
|
2785
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
return {
|
|
2789
|
+
ok: false,
|
|
2790
|
+
hosted_unsupported: true,
|
|
2791
|
+
error: "hosted dispatch goes through the queue RPC (coltrane_gig_dispatch) — nothing spawns in a hosted " +
|
|
2792
|
+
"surface. Wire deps.queueGig (e.g. postgrestQueueGig(ctx) from ./genome_store) to queue the gig " +
|
|
2793
|
+
"for the drain worker.",
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const result = await dispatchTool(slug, args, deps);
|
|
2798
|
+
// Hosted persistence: without a genome_dir the handlers compute + validate + seal identity
|
|
2799
|
+
// but write no file (the validation path). The store upsert is the hosted write half; a
|
|
2800
|
+
// refused upsert fails the mutation loudly — a definition that only ever lived in this
|
|
2801
|
+
// request's memory must not report success.
|
|
2802
|
+
const up = HOSTED_UPSERT[slug];
|
|
2803
|
+
if (deps.hosted && deps.store && result.ok && up) {
|
|
2804
|
+
// No org rides the call: the caller set a working org ONCE (org_use) and the store's
|
|
2805
|
+
// resolver supplies it — explicit-per-call disambiguators are exactly the bookkeeping
|
|
2806
|
+
// the surface must not push onto agents.
|
|
2807
|
+
// agent_evolve persists the MERGED definition the handler computed, not the raw args.
|
|
2808
|
+
const source = slug === "agent_evolve" && result.data && typeof result.data === "object" && result.data["next_def"]
|
|
2809
|
+
? result.data["next_def"]
|
|
2810
|
+
: args;
|
|
2811
|
+
const payload = {};
|
|
2812
|
+
for (const k of up.keys)
|
|
2813
|
+
if (source[k] !== undefined)
|
|
2814
|
+
payload[k] = source[k];
|
|
2815
|
+
try {
|
|
2816
|
+
await deps.store.upsert(up.cls, payload);
|
|
2817
|
+
}
|
|
2818
|
+
catch (e) {
|
|
2819
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return result;
|
|
2823
|
+
}
|
|
2824
|
+
/** The engine's FULL MCP tool surface as a transport-agnostic registry. The stdio server
|
|
2825
|
+
* below consumes this internally; a hosted host mounts it per-request with hosted deps. */
|
|
2826
|
+
export function createToolSurface(deps) {
|
|
2827
|
+
return MCP_TOOLS.map((t) => ({
|
|
2828
|
+
name: t.slug,
|
|
2829
|
+
description: `${t.category} tool`,
|
|
2830
|
+
input_schema: t.input_schema,
|
|
2831
|
+
call: (args) => callSurfaceTool(t.slug, args, deps),
|
|
2832
|
+
}));
|
|
2833
|
+
}
|
|
2834
|
+
/** Build the low-level MCP Server with ListTools + CallTool wired to the tool surface. */
|
|
2157
2835
|
export function createColtraneServer(deps, recorder) {
|
|
2158
2836
|
const server = new Server({ name: "coltrane", version: COLTRANE_VERSION }, { capabilities: { tools: {} } });
|
|
2837
|
+
const surface = createToolSurface(deps);
|
|
2838
|
+
const byName = new Map(surface.map((t) => [t.name, t]));
|
|
2159
2839
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2160
|
-
tools:
|
|
2161
|
-
name: t.
|
|
2162
|
-
description:
|
|
2840
|
+
tools: surface.map((t) => ({
|
|
2841
|
+
name: t.name,
|
|
2842
|
+
description: t.description,
|
|
2163
2843
|
inputSchema: t.input_schema,
|
|
2164
2844
|
})),
|
|
2165
2845
|
}));
|
|
2166
2846
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2167
|
-
const
|
|
2847
|
+
const args = (req.params.arguments ?? {});
|
|
2848
|
+
const tool = byName.get(req.params.name);
|
|
2849
|
+
// A name outside the surface falls through to the dispatcher's own unknown-tool answer,
|
|
2850
|
+
// keeping the reply byte-identical to the pre-surface path.
|
|
2851
|
+
const result = tool ? await tool.call(args) : await dispatchTool(req.params.name, args, deps);
|
|
2168
2852
|
if (recorder) {
|
|
2169
2853
|
recorder.recordToolCall(req.params.name);
|
|
2170
2854
|
recorder.recordObservability(`call:${req.params.name}`, { ok: result.ok });
|
|
@@ -2234,13 +2918,19 @@ export function bootstrapServerDeps(genomeRoot) {
|
|
|
2234
2918
|
// the tool as mcp__coltrane__<slug> — the name the server exposes — so a bare-slug grant (the form
|
|
2235
2919
|
// agent_define accepts) is actually callable instead of a silent dead name that seals nothing.
|
|
2236
2920
|
const toolProviders = new Map([...REGISTERED_TOOL_SLUGS].map((slug) => [slug, { tool: slug, kind: "in_house", server: ENGINE_MCP_SERVER }]));
|
|
2921
|
+
// The two-tier local mirror, rooted at `<root>/.coltrane` (gitignored, beside the ledger).
|
|
2922
|
+
// The SAME instance is handed to the OutputStore and exposed as `output_mirror`, so every
|
|
2923
|
+
// sealed output — whether this process ran the gig or a separate CLI process did — lands in
|
|
2924
|
+
// one content-addressed store MCP retrieval reads. COLTRANE_MIRROR_DIR overrides (tests).
|
|
2925
|
+
const output_mirror = createOutputMirror(defaultMirrorDir(root));
|
|
2237
2926
|
return {
|
|
2238
2927
|
registry,
|
|
2239
2928
|
toolProviders,
|
|
2929
|
+
output_mirror,
|
|
2240
2930
|
// PR #78 follow-up: persist outputs to disk so the audit chain survives an
|
|
2241
2931
|
// MCP session close (Rob cold-trial requirement). COLTRANE_OUTPUTS_DIR
|
|
2242
2932
|
// overrides the default ~/.eir/coltrane_outputs path (tests + sandboxes).
|
|
2243
|
-
outputs: createOutputStore(registry, { persistDir: defaultOutputsPersistDir() }),
|
|
2933
|
+
outputs: createOutputStore(registry, { persistDir: defaultOutputsPersistDir(), mirror: output_mirror }),
|
|
2244
2934
|
// #209 — the audit spine is durable by default. The line above gives OUTPUTS a persistDir
|
|
2245
2935
|
// under an explicit "the audit chain must survive an MCP session close" requirement
|
|
2246
2936
|
// (PR #78); the ledger sat in RAM directly beneath it, which made absence-of-row mean
|
|
@@ -2250,6 +2940,8 @@ export function bootstrapServerDeps(genomeRoot) {
|
|
|
2250
2940
|
// — as tests/dispatch_tool_resolution.test.ts does with no root — leaves no trace.
|
|
2251
2941
|
ledger: new FileLedger(defaultLedgerPath(root)),
|
|
2252
2942
|
standards: genome.standards, // ← gig_dispatch can now resolve file-defined standards
|
|
2943
|
+
charts: genome.charts, // ← gig_dispatch resolves a chart_slug; chart_browse lists them
|
|
2944
|
+
venues: genome.venues, // ← the ceiling a chart's venue imposes has to resolve to something
|
|
2253
2945
|
invoke: makeClaudeInvoker({
|
|
2254
2946
|
registry,
|
|
2255
2947
|
model: process.env["COLTRANE_MODEL"],
|