@eir-labs/coltrane 0.6.2 → 0.7.3
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/lineage-scout-external.json +25 -0
- package/agents/lineage-scout-internal.json +32 -0
- package/agents/lineage-scribe.json +21 -0
- package/agents/lineage-weaver.json +23 -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/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/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/genome_schema.d.ts +1203 -166
- package/dist/src/genome_schema.js +344 -34
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.d.ts +53 -3
- package/dist/src/genome_store.js +316 -139
- package/dist/src/genome_store.js.map +1 -1
- 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/index.d.ts +1 -0
- package/dist/src/index.js +1 -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 +54 -4
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/output_mirror.d.ts +1 -1
- package/dist/src/outputs.d.ts +75 -1
- package/dist/src/outputs.js +142 -28
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/reuse.d.ts +71 -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/server.d.ts +16 -0
- package/dist/src/server.js +600 -64
- package/dist/src/server.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/worker.d.ts +228 -0
- package/dist/src/worker.js +737 -0
- package/dist/src/worker.js.map +1 -0
- package/domain_types/alignment-plan.json +16 -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/internal-inventory.json +26 -0
- package/domain_types/lineage-hit.json +19 -0
- package/domain_types/lineage-map.json +32 -0
- package/domain_types/lineage-question.json +17 -0
- package/domain_types/lineage-record.json +19 -0
- package/domain_types/lineage-verdict.json +16 -0
- package/domain_types/preview-deployment.json +32 -0
- package/institutions/quartet.json +344 -0
- package/package.json +4 -1
- 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/lineage-pass-v1.json +96 -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/conduct_daemon.d.ts +0 -59
- package/dist/src/conduct_daemon.js +0 -178
- package/dist/src/conduct_daemon.js.map +0 -1
- package/dist/src/play_worker.d.ts +0 -24
- package/dist/src/play_worker.js +0 -49
- package/dist/src/play_worker.js.map +0 -1
- package/dist/src/supabase_genome.d.ts +0 -28
- package/dist/src/supabase_genome.js +0 -49
- package/dist/src/supabase_genome.js.map +0 -1
package/dist/src/server.js
CHANGED
|
@@ -9,11 +9,12 @@ 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, DomainTypeSchema } 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";
|
|
17
18
|
import { createOutputMirror, defaultMirrorDir, outputPreview, mirrorStorageRef } from "./output_mirror.js";
|
|
18
19
|
import { FileLedger, LedgerError, LEDGER_SCHEMA_VERSION, defaultLedgerPath, } from "./ledger.js";
|
|
19
20
|
import { sealDrill } from "./seal_drill.js";
|
|
@@ -465,39 +466,30 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
465
466
|
if (!["upstream", "downstream", "both"].includes(direction)) {
|
|
466
467
|
return { ok: false, requires_approval: approval, error: `unrecognized direction "${direction}" — use "upstream", "downstream" or "both"` };
|
|
467
468
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
downstream.push(o);
|
|
485
|
-
next.push(o.id);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
frontier = next;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const nodes = direction === "upstream" ? upstream
|
|
492
|
-
: direction === "downstream" ? downstream
|
|
493
|
-
: [...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();
|
|
494
485
|
return {
|
|
495
486
|
ok: true, requires_approval: approval,
|
|
496
487
|
data: {
|
|
497
488
|
graph: { nodes }, direction,
|
|
498
|
-
root_signals:
|
|
489
|
+
root_signals: held.filter((o) => o.input_refs.length === 0),
|
|
499
490
|
// The other end of the chain: outputs nothing else was derived from.
|
|
500
|
-
terminal_outputs:
|
|
491
|
+
terminal_outputs: held.filter((o) => !all.some((x) => x.input_refs.includes(o.id))),
|
|
492
|
+
missing,
|
|
501
493
|
},
|
|
502
494
|
};
|
|
503
495
|
}
|
|
@@ -558,10 +550,45 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
558
550
|
if (!deps.standards || !deps.invoke) {
|
|
559
551
|
return { ok: false, not_implemented: true, requires_approval: approval, error: "gig_dispatch needs standards + invoke wired into the server" };
|
|
560
552
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
}
|
|
565
592
|
// #203, the READ side. Preserving `status` through the loader was only half of it: the
|
|
566
593
|
// symptom recorded on the issue — "a retired standard stays dispatchable and nothing
|
|
567
594
|
// says otherwise" — survived the field being kept, because nothing consulted it. A
|
|
@@ -574,29 +601,38 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
574
601
|
//
|
|
575
602
|
// deprecated ALLOWS and warns; retired REFUSES. Were both refused, `deprecated` would
|
|
576
603
|
// be a spelling of `retired` and there would be no way to say the softer thing.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}
|
|
584
620
|
}
|
|
585
|
-
const warnings = stdStatus === "deprecated"
|
|
586
|
-
? [`standard "${slug2}" is deprecated — it still runs, but should not be built on.`]
|
|
587
|
-
: [];
|
|
588
621
|
// WU-0008 preflight: run the same sealDrill used by standard_simulate BEFORE spending
|
|
589
622
|
// on any chair. A structurally-unsealable standard is refused here (pennies) instead of
|
|
590
623
|
// after a chair runs and aborts. Gate is placed once, above the wait/async split, so a
|
|
591
|
-
// single check covers both runGig call-sites below
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
+
}
|
|
600
636
|
}
|
|
601
637
|
// Optional budget arg — when present, runtime enforces per-gig cost-budget
|
|
602
638
|
// and raises BudgetExhausted on depletion (PR for T10 gap, see runtime.ts).
|
|
@@ -642,6 +678,22 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
642
678
|
...(resumeArg !== undefined ? { resume_from: resumeArg } : {}),
|
|
643
679
|
...(reuseOn && deps.reuse ? { reuse: deps.reuse } : {}),
|
|
644
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
|
+
};
|
|
645
697
|
/** What a run skipped, and why — echoed on every reply so a saving is never silent. */
|
|
646
698
|
const savings = (res) => ({
|
|
647
699
|
...(res.skipped ? { skipped: res.skipped } : {}),
|
|
@@ -649,20 +701,198 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
649
701
|
...(res.reuse ? { reuse: res.reuse } : {}),
|
|
650
702
|
...(res.checkpoint_error ? { checkpoint_error: res.checkpoint_error } : {}),
|
|
651
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];
|
|
652
879
|
// Synchronous mode (opt-in via wait:true) — block, return the manifest. The
|
|
653
880
|
// deterministic test path and any caller that wants the answer in one call.
|
|
654
|
-
const wait = args["wait"] === true;
|
|
655
881
|
if (wait) {
|
|
656
882
|
try {
|
|
657
883
|
const res = await runGig(standard, gigInput, {
|
|
658
884
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
659
885
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
660
|
-
...(depth ? { depth } : {}), ...reuseWiring,
|
|
886
|
+
...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
661
887
|
});
|
|
662
888
|
return {
|
|
663
889
|
ok: true, requires_approval: approval,
|
|
664
890
|
data: {
|
|
665
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 } : {}),
|
|
666
896
|
...(depth ? { depth } : {}),
|
|
667
897
|
warnings,
|
|
668
898
|
manifest: {
|
|
@@ -738,7 +968,7 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
738
968
|
const runPromise = runGig(standard, gigInput, {
|
|
739
969
|
outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
|
|
740
970
|
model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
|
|
741
|
-
gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring,
|
|
971
|
+
gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
|
|
742
972
|
});
|
|
743
973
|
// A REFUSED resume must be answered in THIS reply, not discovered later by polling. The
|
|
744
974
|
// gate throws in runGig's SYNCHRONOUS phase — before its first `await`, which is exactly
|
|
@@ -754,7 +984,12 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
754
984
|
}
|
|
755
985
|
void runPromise
|
|
756
986
|
.then((res) => {
|
|
757
|
-
|
|
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;
|
|
758
993
|
state.finished_at = new Date().toISOString();
|
|
759
994
|
state.run_fingerprint = res.run_fingerprint;
|
|
760
995
|
state.genome_hash = res.genome_hash;
|
|
@@ -841,7 +1076,13 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
841
1076
|
ok: true, requires_approval: approval,
|
|
842
1077
|
data: {
|
|
843
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 } : {}),
|
|
844
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 } : {}),
|
|
845
1086
|
current_phase: live.current_phase ?? null,
|
|
846
1087
|
phases_total: live.phases_total,
|
|
847
1088
|
phases_complete: live.phases_seen.length,
|
|
@@ -970,6 +1211,105 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
970
1211
|
throw e;
|
|
971
1212
|
}
|
|
972
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
|
+
}
|
|
973
1313
|
case "agent_validate_pipeline": {
|
|
974
1314
|
if (Array.isArray(args["primitives"])) {
|
|
975
1315
|
try {
|
|
@@ -1212,6 +1552,13 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1212
1552
|
// mutate deps.agents in place so the next reload sees the new baseline.
|
|
1213
1553
|
const agentsBefore = new Map(deps.agents ?? []);
|
|
1214
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);
|
|
1215
1562
|
// typesBefore is captured for symmetry; not currently surfaced beyond typeDiff.
|
|
1216
1563
|
void typesBefore;
|
|
1217
1564
|
return {
|
|
@@ -1225,6 +1572,8 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1225
1572
|
skills: skillsDiff.added,
|
|
1226
1573
|
evals: evalsDiff.added,
|
|
1227
1574
|
agents: agentsDiff.added,
|
|
1575
|
+
charts: chartsDiff.added,
|
|
1576
|
+
venues: venuesDiff.added,
|
|
1228
1577
|
},
|
|
1229
1578
|
modified: {
|
|
1230
1579
|
domain_types: typeDiff.modified,
|
|
@@ -1232,6 +1581,8 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1232
1581
|
skills: skillsDiff.modified,
|
|
1233
1582
|
evals: evalsDiff.modified,
|
|
1234
1583
|
agents: agentsDiff.modified,
|
|
1584
|
+
charts: chartsDiff.modified,
|
|
1585
|
+
venues: venuesDiff.modified,
|
|
1235
1586
|
},
|
|
1236
1587
|
removed: {
|
|
1237
1588
|
domain_types: typeDiff.removed,
|
|
@@ -1239,6 +1590,8 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1239
1590
|
skills: skillsDiff.removed,
|
|
1240
1591
|
evals: evalsDiff.removed,
|
|
1241
1592
|
agents: agentsDiff.removed,
|
|
1593
|
+
charts: chartsDiff.removed,
|
|
1594
|
+
venues: venuesDiff.removed,
|
|
1242
1595
|
},
|
|
1243
1596
|
},
|
|
1244
1597
|
load_errors: deps.load_errors,
|
|
@@ -1486,6 +1839,24 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1486
1839
|
data: { status, aborted, cancellable: aborted, cleanup_result: { reason } },
|
|
1487
1840
|
};
|
|
1488
1841
|
}
|
|
1842
|
+
case "gig_approve": {
|
|
1843
|
+
// Approval is a MEMBER act whose authority lives in the STORE (the coltrane_gig_approve RPC
|
|
1844
|
+
// is member-JWT-only; an agent token is refused there — the enforcement belongs store-side,
|
|
1845
|
+
// not here). The HOSTED surface routes this to deps.approveGig (see callSurfaceTool);
|
|
1846
|
+
// reaching this switch is a NON-hosted call, which has no store-backed run to approve — a
|
|
1847
|
+
// local run takes its human verdicts through gig_dispatch's `approvals` argument instead.
|
|
1848
|
+
const gigId = String(args["gig_id"] ?? "");
|
|
1849
|
+
const role = String(args["role"] ?? "");
|
|
1850
|
+
const verdict = args["verdict"];
|
|
1851
|
+
if (!gigId || !role || verdict === undefined) {
|
|
1852
|
+
return { ok: false, requires_approval: approval, error: "gig_approve requires gig_id, role, and verdict (the Judgment the human seat seals)" };
|
|
1853
|
+
}
|
|
1854
|
+
return {
|
|
1855
|
+
ok: false, not_implemented: true, requires_approval: approval,
|
|
1856
|
+
error: `gig_approve is the hosted member seam — approve gig "${gigId}" role "${role}" over a store-backed ` +
|
|
1857
|
+
"surface (wire deps.approveGig). A local run takes its human verdicts through gig_dispatch's `approvals` argument.",
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1489
1860
|
case "agent_define": {
|
|
1490
1861
|
// Build the def by the SCHEMA's own field list (genome_schema.ts AgentSchema): copy exactly
|
|
1491
1862
|
// the fields the schema declares from args. This handler was one of the restatements that
|
|
@@ -1594,12 +1965,20 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1594
1965
|
const evolveSlug = typeof args["slug"] === "string" ? args["slug"] : undefined;
|
|
1595
1966
|
const changes = (args["changes"] && typeof args["changes"] === "object")
|
|
1596
1967
|
? args["changes"] : undefined;
|
|
1597
|
-
if (evolveSlug && changes && deps.genome_dir) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1968
|
+
if (evolveSlug && changes && (deps.genome_dir || deps.agents?.has(evolveSlug))) {
|
|
1969
|
+
// The base definition: the genome file when a working tree exists, else the loaded
|
|
1970
|
+
// agents map (a hosted surface has no filesystem — the STORE genome is the base,
|
|
1971
|
+
// and the seam above persists the merged definition back through the store).
|
|
1972
|
+
let currentDef;
|
|
1973
|
+
if (deps.genome_dir && existsSync(join(deps.genome_dir, "agents", `${evolveSlug}.json`))) {
|
|
1974
|
+
currentDef = JSON.parse(readFileSync(join(deps.genome_dir, "agents", `${evolveSlug}.json`), "utf-8"));
|
|
1975
|
+
}
|
|
1976
|
+
else if (deps.agents?.has(evolveSlug)) {
|
|
1977
|
+
currentDef = deps.agents.get(evolveSlug);
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1600
1980
|
return { ok: false, requires_approval: approval, error: `agent_evolve: unknown agent "${evolveSlug}" (no agents/${evolveSlug}.json)` };
|
|
1601
1981
|
}
|
|
1602
|
-
const currentDef = JSON.parse(readFileSync(agentPath, "utf-8"));
|
|
1603
1982
|
const nextDef = { ...currentDef, ...changes };
|
|
1604
1983
|
// The agent must still be a legal composition on its own…
|
|
1605
1984
|
try {
|
|
@@ -1656,9 +2035,12 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1656
2035
|
agentsArr[i] = sealed.agent;
|
|
1657
2036
|
}
|
|
1658
2037
|
}
|
|
2038
|
+
// next_def is the seam's persistence source on a hosted surface (the store
|
|
2039
|
+
// upsert writes the MERGED definition, not the raw evolve args).
|
|
2040
|
+
deps.agents?.set(evolveSlug, sealed.agent);
|
|
1659
2041
|
return {
|
|
1660
2042
|
ok: true, requires_approval: approval,
|
|
1661
|
-
data: { new_version, evolved: sealed.agent, content_hash: sealed.content_hash, effective_hash: sealed.effective_hash, cascade_check: { agents_affected: [], standards_affected } },
|
|
2043
|
+
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 } },
|
|
1662
2044
|
};
|
|
1663
2045
|
}
|
|
1664
2046
|
return { ok: true, requires_approval: approval, data: { new_version, cascade_check: { agents_affected: [], standards_affected: [] } } };
|
|
@@ -1720,6 +2102,25 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1720
2102
|
// production status, and never RUN, TESTED, LISTED or REVISED through the engine. The
|
|
1721
2103
|
// fixture gate on promotion made that gap sharper — you could be refused for failing
|
|
1722
2104
|
// fixtures with no way to run them and see why.
|
|
2105
|
+
// ── the org context switch — set once, inherited by every member write ─────────────
|
|
2106
|
+
case "org_use": {
|
|
2107
|
+
const orgSlug = String(args["org_slug"] ?? "");
|
|
2108
|
+
if (!orgSlug)
|
|
2109
|
+
return { ok: false, requires_approval: approval, error: "org_use requires org_slug" };
|
|
2110
|
+
if (!deps.orgUse) {
|
|
2111
|
+
return {
|
|
2112
|
+
ok: false, requires_approval: approval,
|
|
2113
|
+
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.",
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
const set = await deps.orgUse(orgSlug);
|
|
2118
|
+
return { ok: true, requires_approval: approval, data: { org_slug: set, set: true } };
|
|
2119
|
+
}
|
|
2120
|
+
catch (e) {
|
|
2121
|
+
return { ok: false, requires_approval: approval, error: e instanceof Error ? e.message : String(e) };
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
1723
2124
|
// ── discoverability parity — a dispatcher must be able to FIND a slug over MCP ──────
|
|
1724
2125
|
// (tests/genome_browse_parity.test.ts). Backed by the deps maps, so the same handler
|
|
1725
2126
|
// serves a working-tree load and a hosted store load identically; no filesystem.
|
|
@@ -1742,6 +2143,103 @@ async function runImpl(slug, args, deps, approval) {
|
|
|
1742
2143
|
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
1743
2144
|
return { ok: true, requires_approval: approval, data: { standards, count: standards.length } };
|
|
1744
2145
|
}
|
|
2146
|
+
case "standard_inspect": {
|
|
2147
|
+
// The single-record read, mirroring skill_inspect: browse lists shallow rows; this returns
|
|
2148
|
+
// ONE standard's full shape. Same failure postures as the browse handlers — no standards
|
|
2149
|
+
// map is "needs a standards map", an unknown slug is an honest error, never a silent null.
|
|
2150
|
+
if (!deps.standards)
|
|
2151
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "standard_inspect needs a standards map (bootstrap from a genome)" };
|
|
2152
|
+
const target = String(args["slug"] ?? "");
|
|
2153
|
+
if (!target)
|
|
2154
|
+
return { ok: false, requires_approval: approval, error: "standard_inspect requires slug" };
|
|
2155
|
+
const std = deps.standards.get(target);
|
|
2156
|
+
if (!std)
|
|
2157
|
+
return { ok: false, requires_approval: approval, error: `unknown standard "${target}"` };
|
|
2158
|
+
return {
|
|
2159
|
+
ok: true, requires_approval: approval,
|
|
2160
|
+
data: {
|
|
2161
|
+
slug: std.slug, domain: std.domain, status: std.status ?? "active",
|
|
2162
|
+
phases: std.phases.map((p) => ({
|
|
2163
|
+
name: p.name,
|
|
2164
|
+
chairs: p.chairs.map((c) => ({
|
|
2165
|
+
role: c.role,
|
|
2166
|
+
agent_slug: c.agent_slug ?? null,
|
|
2167
|
+
skill_slug: c.skill_slug ?? null,
|
|
2168
|
+
human: c.human ?? false,
|
|
2169
|
+
input_contract: c.input_contract ?? [],
|
|
2170
|
+
output_contract: c.output_contract ?? [],
|
|
2171
|
+
})),
|
|
2172
|
+
})),
|
|
2173
|
+
input_types: std.input_types ?? [], output_types: std.output_types ?? [],
|
|
2174
|
+
eval_slugs: std.eval_slugs ?? [], description: std.description ?? null,
|
|
2175
|
+
},
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
case "chart_browse": {
|
|
2179
|
+
if (!deps.charts)
|
|
2180
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "chart_browse needs a charts map (bootstrap from a genome)" };
|
|
2181
|
+
let clist = [...deps.charts.values()];
|
|
2182
|
+
if (args["venue"])
|
|
2183
|
+
clist = clist.filter((c) => c.venue === args["venue"]);
|
|
2184
|
+
if (args["standard_slug"])
|
|
2185
|
+
clist = clist.filter((c) => c.movements.some((m) => m.standard_slug === args["standard_slug"]));
|
|
2186
|
+
const charts = clist
|
|
2187
|
+
.map((c) => {
|
|
2188
|
+
// The arrangement's identity, when it is computable. chartHash folds each movement's
|
|
2189
|
+
// standard PROJECTION, so a chart naming a standard this server does not hold has no
|
|
2190
|
+
// hash to report — and reporting null is the honest answer, not a fabricated prefix.
|
|
2191
|
+
const resolvedMovements = [];
|
|
2192
|
+
for (const m of c.movements) {
|
|
2193
|
+
const s = deps.standards?.get(m.standard_slug);
|
|
2194
|
+
if (s)
|
|
2195
|
+
resolvedMovements.push({ movement_id: m.movement_id, standard: s, runtime_fills: m.runtime_fills, seatings: m.seatings });
|
|
2196
|
+
}
|
|
2197
|
+
const complete = resolvedMovements.length === c.movements.length;
|
|
2198
|
+
return {
|
|
2199
|
+
slug: c.slug,
|
|
2200
|
+
standard_slugs: c.movements.map((m) => m.standard_slug),
|
|
2201
|
+
movement_ids: c.movements.map((m) => m.movement_id),
|
|
2202
|
+
movement_count: c.movements.length,
|
|
2203
|
+
edge_count: c.edges.length,
|
|
2204
|
+
gate_count: c.approval_gates.length,
|
|
2205
|
+
venue: c.venue ?? null,
|
|
2206
|
+
budget_usd: c.budget_envelope?.total_usd ?? null,
|
|
2207
|
+
// A PREFIX: enough to tell two arrangements apart in a listing, not the identity
|
|
2208
|
+
// itself (which a caller reads off the define call or the ledger).
|
|
2209
|
+
chart_hash: complete ? chartHash({ movements: resolvedMovements, chart: c }).slice(0, 12) : null,
|
|
2210
|
+
};
|
|
2211
|
+
})
|
|
2212
|
+
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
2213
|
+
return { ok: true, requires_approval: approval, data: { charts, count: charts.length } };
|
|
2214
|
+
}
|
|
2215
|
+
case "venue_browse": {
|
|
2216
|
+
if (!deps.venues)
|
|
2217
|
+
return { ok: false, not_implemented: true, requires_approval: approval, error: "venue_browse needs a venues map (bootstrap from a genome)" };
|
|
2218
|
+
let vlist = [...deps.venues.values()];
|
|
2219
|
+
if (args["institution_slug"])
|
|
2220
|
+
vlist = vlist.filter((v) => v.institution_slug === args["institution_slug"]);
|
|
2221
|
+
if (args["flavor"])
|
|
2222
|
+
vlist = vlist.filter((v) => v.flavor === args["flavor"]);
|
|
2223
|
+
const venues = vlist
|
|
2224
|
+
.map((v) => ({
|
|
2225
|
+
slug: v.slug, institution_slug: v.institution_slug, flavor: v.flavor ?? null,
|
|
2226
|
+
// COUNTS, not contents: the numbers are what a seating decision turns on ("does this
|
|
2227
|
+
// room hold anything at all", "can anything leave"), and the full lists are one
|
|
2228
|
+
// venue_define / file read away.
|
|
2229
|
+
tool_count: v.equipment.tools.length,
|
|
2230
|
+
tools: v.equipment.tools,
|
|
2231
|
+
ingress_count: v.doors?.ingress.length ?? 0,
|
|
2232
|
+
egress_count: v.doors?.egress.length ?? 0,
|
|
2233
|
+
install_count: v.installs.length,
|
|
2234
|
+
credential_surface: v.credential_surface,
|
|
2235
|
+
lifecycle: v.lifecycle.policy,
|
|
2236
|
+
rebuild_cadence: v.lifecycle.rebuild_cadence ?? null,
|
|
2237
|
+
responsible_chair: v.responsible_chair ?? null,
|
|
2238
|
+
description: v.description ?? null,
|
|
2239
|
+
}))
|
|
2240
|
+
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
2241
|
+
return { ok: true, requires_approval: approval, data: { venues, count: venues.length } };
|
|
2242
|
+
}
|
|
1745
2243
|
case "agent_browse": {
|
|
1746
2244
|
if (!deps.agents)
|
|
1747
2245
|
return { ok: false, not_implemented: true, requires_approval: approval, error: "agent_browse needs an agents map (bootstrap from a genome)" };
|
|
@@ -2308,9 +2806,17 @@ const HOSTED_BLOCKED = {
|
|
|
2308
2806
|
// single source the handlers copy from), so the store payload can't drift from the schema.
|
|
2309
2807
|
const HOSTED_UPSERT = {
|
|
2310
2808
|
agent_define: { cls: "agent", keys: Object.keys(AgentSchema.shape) },
|
|
2809
|
+
agent_evolve: { cls: "agent", keys: Object.keys(AgentSchema.shape) },
|
|
2311
2810
|
standard_compose: { cls: "standard", keys: Object.keys(StandardSchema.shape) },
|
|
2312
2811
|
type_register: { cls: "domain_type", keys: Object.keys(DomainTypeSchema.shape) },
|
|
2313
2812
|
skill_define: { cls: "skill", keys: Object.keys(SkillSchema.shape) },
|
|
2813
|
+
// The chart and the venue ride the same port. The store side is NOT built — coltrane_genome_upsert
|
|
2814
|
+
// has no branch for either class — so a hosted chart_define reaches the RPC and is refused there,
|
|
2815
|
+
// with the store's own message, and the mutation fails loudly. That is the right failure: the
|
|
2816
|
+
// class travels the port it is supposed to travel, and the missing half announces itself instead
|
|
2817
|
+
// of the engine quietly declining to try. (Store-side work: two tables + two upsert branches.)
|
|
2818
|
+
chart_define: { cls: "chart", keys: Object.keys(ChartSchema.shape) },
|
|
2819
|
+
venue_define: { cls: "venue", keys: Object.keys(VenueSchema.shape) },
|
|
2314
2820
|
};
|
|
2315
2821
|
async function callSurfaceTool(slug, args, deps) {
|
|
2316
2822
|
if (deps.hosted) {
|
|
@@ -2337,6 +2843,27 @@ async function callSurfaceTool(slug, args, deps) {
|
|
|
2337
2843
|
"for the drain worker.",
|
|
2338
2844
|
};
|
|
2339
2845
|
}
|
|
2846
|
+
if (slug === "gig_approve") {
|
|
2847
|
+
// Approval is a MEMBER act. The tool is a pure pass-through to the store's member-JWT-only
|
|
2848
|
+
// coltrane_gig_approve RPC — an AGENT token is refused THERE, which is where that
|
|
2849
|
+
// enforcement belongs, not in the engine. With the seam wired it approves; without it, typed.
|
|
2850
|
+
if (deps.approveGig) {
|
|
2851
|
+
try {
|
|
2852
|
+
const data = await deps.approveGig(args);
|
|
2853
|
+
return { ok: true, data };
|
|
2854
|
+
}
|
|
2855
|
+
catch (e) {
|
|
2856
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
return {
|
|
2860
|
+
ok: false,
|
|
2861
|
+
hosted_unsupported: true,
|
|
2862
|
+
error: "hosted approval goes through the member RPC (coltrane_gig_approve) — a member act the store " +
|
|
2863
|
+
"authorizes, never the engine. Wire deps.approveGig (parallel to deps.queueGig) to approve the " +
|
|
2864
|
+
"parked gig over the wire.",
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2340
2867
|
}
|
|
2341
2868
|
const result = await dispatchTool(slug, args, deps);
|
|
2342
2869
|
// Hosted persistence: without a genome_dir the handlers compute + validate + seal identity
|
|
@@ -2345,10 +2872,17 @@ async function callSurfaceTool(slug, args, deps) {
|
|
|
2345
2872
|
// request's memory must not report success.
|
|
2346
2873
|
const up = HOSTED_UPSERT[slug];
|
|
2347
2874
|
if (deps.hosted && deps.store && result.ok && up) {
|
|
2875
|
+
// No org rides the call: the caller set a working org ONCE (org_use) and the store's
|
|
2876
|
+
// resolver supplies it — explicit-per-call disambiguators are exactly the bookkeeping
|
|
2877
|
+
// the surface must not push onto agents.
|
|
2878
|
+
// agent_evolve persists the MERGED definition the handler computed, not the raw args.
|
|
2879
|
+
const source = slug === "agent_evolve" && result.data && typeof result.data === "object" && result.data["next_def"]
|
|
2880
|
+
? result.data["next_def"]
|
|
2881
|
+
: args;
|
|
2348
2882
|
const payload = {};
|
|
2349
2883
|
for (const k of up.keys)
|
|
2350
|
-
if (
|
|
2351
|
-
payload[k] =
|
|
2884
|
+
if (source[k] !== undefined)
|
|
2885
|
+
payload[k] = source[k];
|
|
2352
2886
|
try {
|
|
2353
2887
|
await deps.store.upsert(up.cls, payload);
|
|
2354
2888
|
}
|
|
@@ -2477,6 +3011,8 @@ export function bootstrapServerDeps(genomeRoot) {
|
|
|
2477
3011
|
// — as tests/dispatch_tool_resolution.test.ts does with no root — leaves no trace.
|
|
2478
3012
|
ledger: new FileLedger(defaultLedgerPath(root)),
|
|
2479
3013
|
standards: genome.standards, // ← gig_dispatch can now resolve file-defined standards
|
|
3014
|
+
charts: genome.charts, // ← gig_dispatch resolves a chart_slug; chart_browse lists them
|
|
3015
|
+
venues: genome.venues, // ← the ceiling a chart's venue imposes has to resolve to something
|
|
2480
3016
|
invoke: makeClaudeInvoker({
|
|
2481
3017
|
registry,
|
|
2482
3018
|
model: process.env["COLTRANE_MODEL"],
|