@jmanuelcorral/openteam 0.17.5 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.es.md +4 -4
  2. package/README.md +4 -4
  3. package/dist/certificates/graph-release-certificate.json +2 -2
  4. package/dist/certificates/graph-shadow-certificate.json +2 -2
  5. package/dist/cli.js +819 -2448
  6. package/dist/commands/dispatch.d.ts +2 -135
  7. package/dist/commands/dispatch.d.ts.map +1 -1
  8. package/dist/commands/doctor.d.ts.map +1 -1
  9. package/dist/commands/setup.d.ts +19 -1
  10. package/dist/commands/setup.d.ts.map +1 -1
  11. package/dist/config/schema.d.ts +2 -0
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6104 -6392
  16. package/dist/opencodeArtifacts/orchestratorAgent.d.ts +11 -6
  17. package/dist/opencodeArtifacts/orchestratorAgent.d.ts.map +1 -1
  18. package/dist/orchestrator/coordinator.d.ts +26 -0
  19. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  20. package/dist/orchestrator/nodeIteration.d.ts +36 -0
  21. package/dist/orchestrator/nodeIteration.d.ts.map +1 -0
  22. package/dist/orchestrator/outputContract.d.ts +91 -0
  23. package/dist/orchestrator/outputContract.d.ts.map +1 -0
  24. package/dist/orchestrator/reportArtifactProbe.d.ts +40 -0
  25. package/dist/orchestrator/reportArtifactProbe.d.ts.map +1 -0
  26. package/dist/orchestrator/reportArtifactReader.d.ts +36 -0
  27. package/dist/orchestrator/reportArtifactReader.d.ts.map +1 -0
  28. package/dist/orchestrator/reportDurability.d.ts +70 -0
  29. package/dist/orchestrator/reportDurability.d.ts.map +1 -0
  30. package/dist/orchestrator/subsessions.d.ts +1 -1
  31. package/dist/orchestrator/subsessions.d.ts.map +1 -1
  32. package/dist/plugin/orchestrateTool.d.ts +41 -1
  33. package/dist/plugin/orchestrateTool.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/orchestrator/ralphLoop.d.ts +0 -57
  36. package/dist/orchestrator/ralphLoop.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -12,7 +12,6 @@ import {
12
12
  confirm as clackConfirm,
13
13
  isCancel as clackIsCancel
14
14
  } from "@clack/prompts";
15
- import { createOpencodeClient } from "@opencode-ai/sdk";
16
15
 
17
16
  // src/cli/consoleServe.ts
18
17
  import { randomBytes } from "node:crypto";
@@ -48,65 +47,6 @@ import { z as z2 } from "zod";
48
47
 
49
48
  // src/orchestrator/roles.ts
50
49
  import { z } from "zod";
51
-
52
- // src/capabilities/select.ts
53
- var BLENDED_COST_INPUT_WEIGHT = 0.75;
54
- var BLENDED_COST_OUTPUT_WEIGHT = 1 - BLENDED_COST_INPUT_WEIGHT;
55
- var availabilityRank = {
56
- available: 0,
57
- degraded: 1,
58
- unavailable: 2
59
- };
60
- var tierRequirements = {
61
- trivial: { reasoning: 0, code: 1, context: 8000 },
62
- simple: { reasoning: 1, code: 2, context: 16000 },
63
- moderate: { reasoning: 3, code: 3, context: 32000 },
64
- hard: { reasoning: 4, code: 5, context: 64000 }
65
- };
66
- function deriveRequirement(tier, task) {
67
- const base = tierRequirements[tier];
68
- const estimatedContext = (task.estimatedInputTokens ?? 0) + (task.estimatedOutputTokens ?? 0);
69
- const contextFromPrompt = Math.ceil(task.promptChars / 4);
70
- const minContextWindow = Math.max(base.context, estimatedContext, contextFromPrompt);
71
- const codeBump = task.hasCode ? 1 : 0;
72
- return {
73
- minReasoningTier: base.reasoning,
74
- minCodeQualityTier: Math.min(5, base.code + codeBump),
75
- minContextWindow,
76
- needsTools: task.requiresTools === true,
77
- needsVision: task.requiresVision === true
78
- };
79
- }
80
- function blendedCostPer1M(profile) {
81
- return profile.costPer1M.inputUSD * BLENDED_COST_INPUT_WEIGHT + profile.costPer1M.outputUSD * BLENDED_COST_OUTPUT_WEIGHT;
82
- }
83
- function modelKey(profile) {
84
- return `${profile.ref.providerID}/${profile.ref.modelID}`;
85
- }
86
- function compareCapableFrontier(req, left, right) {
87
- const costDelta = blendedCostPer1M(left) - blendedCostPer1M(right);
88
- if (costDelta !== 0) {
89
- return costDelta;
90
- }
91
- const availabilityDelta = availabilityRank[left.availability] - availabilityRank[right.availability];
92
- if (availabilityDelta !== 0) {
93
- return availabilityDelta;
94
- }
95
- const reasoningWasteDelta = left.reasoningTier - req.minReasoningTier - (right.reasoningTier - req.minReasoningTier);
96
- if (reasoningWasteDelta !== 0) {
97
- return reasoningWasteDelta;
98
- }
99
- return modelKey(left).localeCompare(modelKey(right));
100
- }
101
- function satisfiesRequirement(req, profile) {
102
- return profile.kind === "frontier" && profile.availability !== "unavailable" && profile.contextWindow >= req.minContextWindow && profile.reasoningTier >= req.minReasoningTier && profile.codeQualityTier >= req.minCodeQualityTier && (!req.needsTools || profile.supportsToolCalling) && (!req.needsVision || profile.supportsVision);
103
- }
104
- function selectCheapestCapableFrontier(req, profiles) {
105
- const selected = profiles.filter((profile) => satisfiesRequirement(req, profile)).sort((left, right) => compareCapableFrontier(req, left, right))[0];
106
- return selected?.ref ?? null;
107
- }
108
-
109
- // src/orchestrator/roles.ts
110
50
  var modelSelectionSchema = z.object({
111
51
  providerID: z.string().min(1),
112
52
  modelID: z.string().min(1)
@@ -276,15 +216,6 @@ function synthesiseFallbackProfile(roleID) {
276
216
  localFirst: true
277
217
  };
278
218
  }
279
- function roleToRequirement(role, task, tier) {
280
- const baseRequirement = deriveRequirement(tier ?? role.defaultTier, task);
281
- return {
282
- ...baseRequirement,
283
- minReasoningTier: Math.max(baseRequirement.minReasoningTier, role.minReasoningTier),
284
- minCodeQualityTier: Math.max(baseRequirement.minCodeQualityTier, role.minCodeQualityTier),
285
- needsTools: baseRequirement.needsTools || role.needsToolCallingByDefault
286
- };
287
- }
288
219
 
289
220
  // src/config/migrations.ts
290
221
  var CURRENT_CONFIG_VERSION = 1;
@@ -440,7 +371,8 @@ var OrchestratorRolesSchema = z2.record(z2.string().min(1), z2.unknown()).defaul
440
371
  return parsedRoles;
441
372
  });
442
373
  var OrchestratorConfigSchema = z2.object({
443
- roles: OrchestratorRolesSchema
374
+ roles: OrchestratorRolesSchema,
375
+ maxRetries: z2.number().int().min(0).optional()
444
376
  }).strict();
445
377
  var defaultFrontierModel = {
446
378
  providerID: "anthropic",
@@ -609,456 +541,6 @@ import { z as z7 } from "zod";
609
541
  // src/orchestrator/coordinator.ts
610
542
  import { z as z6 } from "zod";
611
543
 
612
- // src/capabilities/classify.ts
613
- var hardKeywords = [
614
- "security",
615
- "auth",
616
- "authentication",
617
- "authorization",
618
- "concurrency",
619
- "race condition",
620
- "architecture",
621
- "refactor",
622
- "migration",
623
- "debug",
624
- "root cause"
625
- ];
626
- function scoreOverride(override) {
627
- if (override === "alwaysFrontier") {
628
- return 30;
629
- }
630
- if (override === "alwaysLocal") {
631
- return -20;
632
- }
633
- return 0;
634
- }
635
- function tierFromScore(score) {
636
- if (score <= 15) {
637
- return "trivial";
638
- }
639
- if (score <= 35) {
640
- return "simple";
641
- }
642
- if (score <= 65) {
643
- return "moderate";
644
- }
645
- return "hard";
646
- }
647
- function confidenceFromScore(score) {
648
- if (score >= 75 || score <= 10) {
649
- return 0.9;
650
- }
651
- if (score >= 30 && score <= 70) {
652
- return 0.58;
653
- }
654
- return 0.72;
655
- }
656
- function addScore(condition, amount, reason, state) {
657
- if (condition) {
658
- state.score += amount;
659
- state.reasons.push(reason);
660
- }
661
- }
662
- function classifyHeuristic(task) {
663
- const state = { score: scoreOverride(task.explicitOverride), reasons: [] };
664
- const prompt = task.prompt?.toLowerCase() ?? "";
665
- const estimatedTokens = (task.estimatedInputTokens ?? 0) + (task.estimatedOutputTokens ?? 0);
666
- addScore(task.promptChars <= 80 && !task.hasCode && task.requiresTools !== true, -5, "short-text-prompt", state);
667
- addScore(task.promptChars > 1500 || estimatedTokens > 700, 10, "large-prompt", state);
668
- addScore(task.promptChars > 5000 || estimatedTokens > 3000, 15, "very-large-context", state);
669
- addScore(task.hasCode, 15, "code-present", state);
670
- addScore(task.requiresTools === true, 15, "tools-required", state);
671
- addScore(task.requiresVision === true, 15, "vision-required", state);
672
- addScore((task.fileCount ?? 0) > 1, 20, "multi-file-task", state);
673
- addScore((task.diffHunks ?? 0) > 2, 15, "multi-hunk-diff", state);
674
- addScore(hardKeywords.some((keyword) => prompt.includes(keyword)), 25, "hard-keyword", state);
675
- addScore(task.userAskedForQuality === true, 20, "quality-requested", state);
676
- addScore(task.privacySensitive === true, 10, "privacy-sensitive", state);
677
- const normalizedScore = Math.max(0, Math.min(100, state.score));
678
- return {
679
- tier: tierFromScore(normalizedScore),
680
- confidence: confidenceFromScore(normalizedScore),
681
- reasons: state.reasons.length > 0 ? state.reasons : ["no-hard-signals"],
682
- ambiguous: normalizedScore >= 30 && normalizedScore <= 70
683
- };
684
- }
685
-
686
- // src/local/slotPool.ts
687
- var POOL_SEPARATOR = "::";
688
- function slotPoolKey(runtime, modelID) {
689
- if (modelID === undefined) {
690
- return runtime.id;
691
- }
692
- const declared = runtime.modelConcurrency?.[modelID];
693
- return declared === undefined ? runtime.id : `${runtime.id}${POOL_SEPARATOR}${modelID}`;
694
- }
695
- function withReportedModelSlots(runtime, reported) {
696
- if (reported === undefined || Object.keys(reported).length === 0) {
697
- return runtime;
698
- }
699
- return {
700
- ...runtime,
701
- modelConcurrency: { ...reported, ...runtime.modelConcurrency }
702
- };
703
- }
704
-
705
- // src/capabilities/budget.ts
706
- var WARNING_UTILIZATION = 0.8;
707
- function projectedSpend(current, estimatedCostUSD) {
708
- return Math.max(0, current) + Math.max(0, estimatedCostUSD);
709
- }
710
- function budgetExceeded(projected, limit) {
711
- return limit !== undefined && projected >= limit;
712
- }
713
- function budgetNearLimit(projected, limit) {
714
- return limit !== undefined && projected >= limit * WARNING_UTILIZATION && projected < limit;
715
- }
716
- function exhaustedDecision(budgets, reason) {
717
- return {
718
- action: budgets.hardStopOnBudgetExhaustion ? "blockFrontier" : "forceLocal",
719
- reason
720
- };
721
- }
722
- function evaluateBudget(state, estimatedCostUSD, budgets) {
723
- const projectedSessionUSD = projectedSpend(state.sessionUSD, estimatedCostUSD);
724
- const projectedMonthlyUSD = budgets.monthlyUSD === undefined ? undefined : projectedSpend(state.monthlyUSD ?? 0, estimatedCostUSD);
725
- if (budgetExceeded(projectedSessionUSD, budgets.sessionUSD)) {
726
- return exhaustedDecision(budgets, "session-budget-exhausted");
727
- }
728
- if (budgetExceeded(projectedMonthlyUSD ?? 0, budgets.monthlyUSD)) {
729
- return exhaustedDecision(budgets, "monthly-budget-exhausted");
730
- }
731
- if (state.frontierTokens !== undefined && budgets.frontierTokensPerSession !== undefined && state.frontierTokens >= budgets.frontierTokensPerSession) {
732
- return exhaustedDecision(budgets, "frontier-token-budget-exhausted");
733
- }
734
- if (budgetNearLimit(projectedSessionUSD, budgets.sessionUSD)) {
735
- return { action: "warn", reason: "session-budget-near-limit" };
736
- }
737
- if (budgetNearLimit(projectedMonthlyUSD ?? 0, budgets.monthlyUSD)) {
738
- return { action: "warn", reason: "monthly-budget-near-limit" };
739
- }
740
- return { action: "ok" };
741
- }
742
-
743
- // src/router/modelSelection.ts
744
- function modelKey2(model) {
745
- return `${model.providerID}/${model.modelID}`;
746
- }
747
- function sameModel(left, right) {
748
- return left.providerID === right.providerID && left.modelID === right.modelID;
749
- }
750
- function compareModels(left, right) {
751
- return modelKey2(left).localeCompare(modelKey2(right));
752
- }
753
- function uniqueModelSelections(models) {
754
- const byKey = new Map;
755
- for (const model of models) {
756
- byKey.set(modelKey2(model), model);
757
- }
758
- return [...byKey.values()].sort(compareModels);
759
- }
760
- function localDefault(config) {
761
- return config.local.runtimes.find((runtime) => runtime.enabled)?.defaultModel ?? config.router.localDefault;
762
- }
763
- function resolveConfiguredFrontierBaseline(config) {
764
- if (config.baseline.mode === "pinned" && config.baseline.pinnedModel !== null) {
765
- return config.baseline.pinnedModel;
766
- }
767
- return config.baseline.hardDefault;
768
- }
769
- function compareAvailableModels(left, right) {
770
- return compareModels(left, right);
771
- }
772
- function defaultAvailableModels(config) {
773
- const local = localDefault(config);
774
- const models = [];
775
- if (local !== null) {
776
- models.push({ ...local, kind: "local", available: true });
777
- }
778
- models.push({
779
- ...resolveConfiguredFrontierBaseline(config),
780
- kind: "frontier",
781
- available: true
782
- });
783
- return models.sort(compareAvailableModels);
784
- }
785
- function availableModels(input) {
786
- return (input.availableModels ?? defaultAvailableModels(input.config)).slice().sort(compareAvailableModels);
787
- }
788
- function listedModel(input, model) {
789
- return availableModels(input).find((candidate) => sameModel(candidate, model));
790
- }
791
- function frontierAvailable(input, model) {
792
- return listedModel(input, model)?.available !== false;
793
- }
794
- function localCandidateIsUsable(input, model) {
795
- return model.kind === "local" && model.available && !(input.task.requiresTools === true && model.supportsTools === false);
796
- }
797
- function availableLocalFallbacks(input, selected) {
798
- return uniqueModelSelections(availableModels(input).filter((model) => localCandidateIsUsable(input, model)).filter((model) => !sameModel(model, selected)).map((model) => ({
799
- providerID: model.providerID,
800
- modelID: model.modelID
801
- })));
802
- }
803
-
804
- // src/router/cost.ts
805
- function modelProfile(input, model) {
806
- return input.profiles?.find((profile) => sameModel(profile.ref, model));
807
- }
808
- function estimatedCostUSD(input, model) {
809
- const profile = modelProfile(input, model);
810
- if (profile === undefined) {
811
- return 0;
812
- }
813
- const inputTokens = input.task.estimatedInputTokens ?? 0;
814
- const outputTokens = input.task.estimatedOutputTokens ?? 0;
815
- return inputTokens / 1e6 * profile.costPer1M.inputUSD + outputTokens / 1e6 * profile.costPer1M.outputUSD;
816
- }
817
- function roundUSD(value) {
818
- return Math.round(value * 1000000000000) / 1000000000000;
819
- }
820
-
821
- // src/router/frontierBaseline.ts
822
- function resolveFrontierBaseline(input, tier) {
823
- if (input.config.baseline.mode === "pinned" && input.config.baseline.pinnedModel !== null) {
824
- return { model: input.config.baseline.pinnedModel, rationale: [] };
825
- }
826
- if (input.profiles !== undefined) {
827
- const selected = selectCheapestCapableFrontier(deriveRequirement(tier, input.task), input.profiles);
828
- if (selected !== null) {
829
- return {
830
- model: selected,
831
- rationale: ["auto-cheapest-capable-frontier"]
832
- };
833
- }
834
- return {
835
- model: input.config.baseline.hardDefault,
836
- rationale: ["auto-cheapest-capable-unavailable-hard-default"]
837
- };
838
- }
839
- return { model: input.config.baseline.hardDefault, rationale: [] };
840
- }
841
-
842
- // src/router/chooseModel.ts
843
- function localStatus(input, model) {
844
- const listed = listedModel(input, model);
845
- return {
846
- available: input.availableModels === undefined ? listed?.available !== false : listed?.available === true,
847
- missingTools: input.task.requiresTools === true && listed?.supportsTools === false
848
- };
849
- }
850
- function buildFallbackChain(input, selected, frontier, allowFrontier) {
851
- const chain = availableLocalFallbacks(input, selected);
852
- if (allowFrontier && !sameModel(frontier, selected) && frontierAvailable(input, frontier)) {
853
- chain.push(frontier);
854
- }
855
- return chain;
856
- }
857
- function inferRouteKind(config, selected, listed) {
858
- if (listed !== undefined) {
859
- return listed.kind;
860
- }
861
- return config.local.runtimes.some((runtime) => sameModel(runtime.defaultModel, selected)) ? "local" : "frontier";
862
- }
863
- function localFallbackReason(status) {
864
- return status.missingTools ? "local-missing-tools-fallback" : "local-unavailable-fallback";
865
- }
866
- function decision(selected, routeKind, rationale, fallbackChain) {
867
- return {
868
- selected,
869
- routeKind,
870
- rationale,
871
- fallbackChain
872
- };
873
- }
874
- function blockedDecision(frontier, rationale, blockReason) {
875
- return {
876
- selected: frontier,
877
- routeKind: "blocked",
878
- rationale,
879
- fallbackChain: [],
880
- blockReason
881
- };
882
- }
883
- function resolveLocalPrimary(input, primary, frontier, options) {
884
- const status = localStatus(input, primary);
885
- if (status.available && !status.missingTools) {
886
- return decision(primary, "local", options.baseRationale, buildFallbackChain(input, primary, frontier, options.allowFrontier));
887
- }
888
- const fallbackReason = localFallbackReason(status);
889
- const nextLocal = availableLocalFallbacks(input, primary)[0];
890
- if (nextLocal !== undefined) {
891
- return decision(nextLocal, "local", [...options.baseRationale, fallbackReason], buildFallbackChain(input, nextLocal, frontier, options.allowFrontier));
892
- }
893
- if (!options.allowFrontier) {
894
- return decision(primary, "local", [...options.baseRationale, fallbackReason, options.noLocalRationale], []);
895
- }
896
- if (frontierAvailable(input, frontier)) {
897
- return decision(frontier, "frontier", [...options.baseRationale, fallbackReason, "no-local-available-frontier"], []);
898
- }
899
- return decision(primary, "local", [...options.baseRationale, fallbackReason, "frontier-unavailable"], []);
900
- }
901
- function resolveFrontierPrimary(input, frontier, rationale) {
902
- if (frontierAvailable(input, frontier)) {
903
- return decision(frontier, "frontier", rationale, []);
904
- }
905
- const nextLocal = availableLocalFallbacks(input, frontier)[0];
906
- if (nextLocal !== undefined) {
907
- return decision(nextLocal, "local", [...rationale, "frontier-unavailable-fallback"], buildFallbackChain(input, nextLocal, frontier, false));
908
- }
909
- return decision(frontier, "frontier", [...rationale, "frontier-unavailable"], []);
910
- }
911
- function tierForInput(input) {
912
- return input.tier ?? classifyHeuristic(input.task).tier;
913
- }
914
- function finalizeDecision(input, tier, frontier, budgetAction, core) {
915
- const selectedCostUSD = estimatedCostUSD(input, core.selected);
916
- const baselineCostUSD = estimatedCostUSD(input, frontier.model);
917
- const savingsUSD = core.routeKind === "frontier" && sameModel(core.selected, frontier.model) ? 0 : baselineCostUSD - selectedCostUSD;
918
- const result = {
919
- ...core,
920
- tier,
921
- rationale: [...frontier.rationale, ...core.rationale],
922
- estimatedCostUSD: roundUSD(selectedCostUSD),
923
- baselineCostUSD: roundUSD(baselineCostUSD),
924
- estimatedSavingsUSD: roundUSD(savingsUSD),
925
- budgetAction
926
- };
927
- if (core.blockReason !== undefined) {
928
- result.blockReason = core.blockReason;
929
- }
930
- return result;
931
- }
932
- function chooseModel(input) {
933
- const tier = tierForInput(input);
934
- const local = localDefault(input.config);
935
- const frontier = resolveFrontierBaseline(input, tier);
936
- const frontierCostUSD = estimatedCostUSD(input, frontier.model);
937
- const budgetAction = input.budgetState === undefined ? { action: "ok" } : evaluateBudget(input.budgetState, frontierCostUSD, input.config.budgets);
938
- const override = input.task.explicitOverride;
939
- if (input.task.privacySensitive === true && input.config.privacyMode === "forceLocalOnSensitive") {
940
- if (local === null) {
941
- if (input.config.router.localOnly) {
942
- return finalizeDecision(input, tier, frontier, budgetAction, blockedDecision(frontier.model, [
943
- "privacy-force-local",
944
- "privacy-force-local-none-configured",
945
- "router-local-only"
946
- ], "privacy-local-only-none-configured"));
947
- }
948
- return finalizeDecision(input, tier, frontier, budgetAction, resolveFrontierPrimary(input, frontier.model, [
949
- "privacy-force-local",
950
- "privacy-force-local-none-configured"
951
- ]));
952
- }
953
- return finalizeDecision(input, tier, frontier, budgetAction, resolveLocalPrimary(input, local, frontier.model, {
954
- baseRationale: ["privacy-force-local"],
955
- allowFrontier: false,
956
- noLocalRationale: "privacy-force-local-unavailable"
957
- }));
958
- }
959
- if (budgetAction.action === "forceLocal" || budgetAction.action === "blockFrontier") {
960
- if (local === null) {
961
- if (budgetAction.action === "blockFrontier") {
962
- return finalizeDecision(input, tier, frontier, budgetAction, blockedDecision(frontier.model, ["budget-blockFrontier", "budget-local-none-configured"], "budget-blockFrontier-none-configured"));
963
- }
964
- return finalizeDecision(input, tier, frontier, budgetAction, resolveFrontierPrimary(input, frontier.model, [
965
- "budget-forceLocal",
966
- "budget-local-none-configured"
967
- ]));
968
- }
969
- return finalizeDecision(input, tier, frontier, budgetAction, resolveLocalPrimary(input, local, frontier.model, {
970
- baseRationale: [`budget-${budgetAction.action}`],
971
- allowFrontier: false,
972
- noLocalRationale: "budget-local-unavailable"
973
- }));
974
- }
975
- if (typeof override === "object") {
976
- return finalizeDecision(input, tier, frontier, budgetAction, decision(override, inferRouteKind(input.config, override, listedModel(input, override)), ["explicit-model-override"], buildFallbackChain(input, override, frontier.model, true)));
977
- }
978
- if (override === "alwaysLocal") {
979
- if (local === null) {
980
- return finalizeDecision(input, tier, frontier, budgetAction, blockedDecision(frontier.model, ["explicit-always-local", "forced-local-none-configured"], "forced-local-none-configured"));
981
- }
982
- return finalizeDecision(input, tier, frontier, budgetAction, resolveLocalPrimary(input, local, frontier.model, {
983
- baseRationale: ["explicit-always-local"],
984
- allowFrontier: false,
985
- noLocalRationale: "forced-local-unavailable"
986
- }));
987
- }
988
- if (input.config.router.localOnly) {
989
- if (local === null) {
990
- return finalizeDecision(input, tier, frontier, budgetAction, blockedDecision(frontier.model, ["router-local-only", "forced-local-none-configured"], "forced-local-none-configured"));
991
- }
992
- return finalizeDecision(input, tier, frontier, budgetAction, resolveLocalPrimary(input, local, frontier.model, {
993
- baseRationale: ["router-local-only"],
994
- allowFrontier: false,
995
- noLocalRationale: "forced-local-unavailable"
996
- }));
997
- }
998
- if (override === "alwaysFrontier") {
999
- return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["explicit-always-frontier"], []));
1000
- }
1001
- if (input.config.router.frontierOnly) {
1002
- return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["router-frontier-only"], []));
1003
- }
1004
- const shortEnoughForLocal = input.task.promptChars <= input.config.router.trivialPromptMaxChars;
1005
- const largeEnoughForFrontier = input.task.promptChars >= input.config.router.frontierPromptMinChars;
1006
- if (!input.task.hasCode && shortEnoughForLocal && !largeEnoughForFrontier) {
1007
- if (local === null) {
1008
- return finalizeDecision(input, tier, frontier, budgetAction, resolveFrontierPrimary(input, frontier.model, [
1009
- "trivial-short-no-code",
1010
- "no-local-runtime-configured"
1011
- ]));
1012
- }
1013
- return finalizeDecision(input, tier, frontier, budgetAction, resolveLocalPrimary(input, local, frontier.model, {
1014
- baseRationale: ["trivial-short-no-code"],
1015
- allowFrontier: true,
1016
- noLocalRationale: "forced-local-unavailable"
1017
- }));
1018
- }
1019
- return finalizeDecision(input, tier, frontier, budgetAction, resolveFrontierPrimary(input, frontier.model, [
1020
- input.task.hasCode ? "code-detected" : "large-or-nontrivial-prompt"
1021
- ]));
1022
- }
1023
-
1024
- // src/telemetry/cost.ts
1025
- function routeKind(decision2) {
1026
- if (decision2.routeKind === "local")
1027
- return "local";
1028
- if (decision2.routeKind === "blocked")
1029
- return "blocked";
1030
- return "frontier";
1031
- }
1032
- function budgetAction(decision2) {
1033
- const reason = decision2.budgetAction.reason;
1034
- return reason === undefined ? decision2.budgetAction.action : `${decision2.budgetAction.action}:${reason}`;
1035
- }
1036
- function toCostRecord(decision2, context) {
1037
- const record = {
1038
- ts: context.ts,
1039
- promptHash: context.promptHash,
1040
- promptChars: context.promptChars,
1041
- tier: decision2.tier,
1042
- routeKind: routeKind(decision2),
1043
- selected: decision2.selected,
1044
- rationale: decision2.rationale.join(","),
1045
- estimatedCostUSD: decision2.estimatedCostUSD,
1046
- baselineCostUSD: decision2.baselineCostUSD,
1047
- estimatedSavingsUSD: decision2.estimatedSavingsUSD,
1048
- budgetAction: budgetAction(decision2)
1049
- };
1050
- if (context.sessionID !== undefined) {
1051
- record.sessionID = context.sessionID;
1052
- }
1053
- if (context.tokensIn !== undefined) {
1054
- record.tokensIn = context.tokensIn;
1055
- }
1056
- if (context.tokensOut !== undefined) {
1057
- record.tokensOut = context.tokensOut;
1058
- }
1059
- return record;
1060
- }
1061
-
1062
544
  // src/storage/path.ts
1063
545
  class StoragePathError extends Error {
1064
546
  constructor(message) {
@@ -1457,9 +939,6 @@ function warnCostRecordRejections(result, source) {
1457
939
  // src/telemetry/eventLog.ts
1458
940
  var DEFAULT_SESSIONS_DIR = ".opencode/openteam-local/sessions";
1459
941
  var LEGACY_SESSION_ID = "legacy";
1460
- function createNullEventSink() {
1461
- return { emit: () => {} };
1462
- }
1463
942
  function parseEventsJsonl(text) {
1464
943
  const events = [];
1465
944
  const rejections = [];
@@ -1600,939 +1079,319 @@ function hashPrompt(prompt) {
1600
1079
  return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
1601
1080
  }
1602
1081
 
1603
- // src/orchestrator/permissions.ts
1604
- var ROLE_PERMISSIONS = {
1605
- architect: [
1606
- "read",
1607
- "edit",
1608
- "multiFileEdit",
1609
- "destructive",
1610
- "network",
1611
- "shell"
1082
+ // src/orchestrator/coordinator.ts
1083
+ var TelemetryFailureClassSchema = z6.enum([
1084
+ "create-rejected",
1085
+ "create-no-id",
1086
+ "prompt-rejected",
1087
+ "prompt-timeout",
1088
+ "prompt-rate-limited",
1089
+ "prompt-auth",
1090
+ "prompt-content-filter",
1091
+ "prompt-context-length",
1092
+ "prompt-validation",
1093
+ "prompt-connection",
1094
+ "prompt-server",
1095
+ "unknown"
1096
+ ]);
1097
+ var FAILURE_CAUSE_RULES = [
1098
+ [
1099
+ [
1100
+ "rate limit",
1101
+ "rate-limit",
1102
+ "ratelimit",
1103
+ /(?<!\d)429(?!\d)/,
1104
+ "too many requests",
1105
+ "quota exceeded"
1106
+ ],
1107
+ "prompt-rate-limited"
1612
1108
  ],
1613
- integration: ["read", "edit", "multiFileEdit", "network", "shell"],
1614
- "local-runtime": ["read", "edit", "network", "shell"],
1615
- "routing-cost": ["read", "edit"],
1616
- scribe: ["read", "edit"],
1617
- ralph: ["read", "edit", "shell"],
1618
- guardian: ["read"],
1619
- tester: ["read", "edit", "shell"],
1620
- reviewer: ["read"]
1621
- };
1622
- var elevatedTier = {
1623
- trivial: "simple",
1624
- simple: "moderate",
1625
- moderate: "hard",
1626
- hard: "hard"
1627
- };
1628
- function elevateTierForPermissions(baseTier, perms) {
1629
- const needsElevation = perms.includes("destructive") || perms.includes("multiFileEdit");
1630
- return needsElevation ? elevatedTier[baseTier] : baseTier;
1631
- }
1632
-
1633
- // src/orchestrator/runtimeSelection.ts
1634
- function configuredRuntime(runtimeId, config) {
1635
- return config.local.runtimes.find((runtime) => runtime.id === runtimeId);
1636
- }
1637
- function runtimeIdOfModel(model, config) {
1638
- return config.local.runtimes.find((runtime) => sameModel(runtime.defaultModel, model))?.id;
1639
- }
1640
- function roleLocalModel(role, runtimeId, fallback, runtime) {
1641
- const pinned = role.localModels?.[runtimeId];
1642
- if (pinned !== undefined) {
1643
- return { ...fallback, modelID: pinned };
1644
- }
1645
- const classModel = runtimeClassModel(role.localModelClass, runtime);
1646
- return classModel === undefined ? fallback : { ...fallback, modelID: classModel };
1647
- }
1648
- function runtimeClassModel(modelClass, runtime) {
1649
- if (modelClass === undefined || runtime === undefined) {
1650
- return;
1109
+ [
1110
+ [
1111
+ "unauthorized",
1112
+ "authentication",
1113
+ /(?<!\d)(?:401|403)(?!\d)/,
1114
+ "forbidden",
1115
+ "api key",
1116
+ "invalid key",
1117
+ "permission denied"
1118
+ ],
1119
+ "prompt-auth"
1120
+ ],
1121
+ [
1122
+ [
1123
+ "content policy",
1124
+ "content filter",
1125
+ "content_filter",
1126
+ "moderation",
1127
+ "safety system",
1128
+ "flagged by"
1129
+ ],
1130
+ "prompt-content-filter"
1131
+ ],
1132
+ [
1133
+ [
1134
+ "context length",
1135
+ "context window",
1136
+ "maximum context",
1137
+ "too many tokens",
1138
+ "token limit",
1139
+ "context_length_exceeded"
1140
+ ],
1141
+ "prompt-context-length"
1142
+ ],
1143
+ [
1144
+ [
1145
+ "invalid request",
1146
+ "invalid_request",
1147
+ "validation",
1148
+ "bad request",
1149
+ /(?<!\d)400(?!\d)/,
1150
+ "unprocessable",
1151
+ "malformed"
1152
+ ],
1153
+ "prompt-validation"
1154
+ ],
1155
+ [
1156
+ [
1157
+ "econnrefused",
1158
+ "econnreset",
1159
+ "enotfound",
1160
+ "network error",
1161
+ "connection refused",
1162
+ "connection reset",
1163
+ "fetch failed",
1164
+ "socket hang up"
1165
+ ],
1166
+ "prompt-connection"
1167
+ ],
1168
+ [
1169
+ [
1170
+ "internal server error",
1171
+ /(?<!\d)(?:500|502|503|504)(?!\d)/,
1172
+ "service unavailable",
1173
+ "bad gateway",
1174
+ "overloaded"
1175
+ ],
1176
+ "prompt-server"
1177
+ ]
1178
+ ];
1179
+ var TIMEOUT_NEEDLES = [
1180
+ "timed out",
1181
+ "timeout",
1182
+ "timedout",
1183
+ "etimedout",
1184
+ "deadline exceeded"
1185
+ ];
1186
+ function matchFailureCause(lowerReason) {
1187
+ for (const [needles, cls] of FAILURE_CAUSE_RULES) {
1188
+ if (needles.some((needle) => typeof needle === "string" ? lowerReason.includes(needle) : needle.test(lowerReason))) {
1189
+ return cls;
1190
+ }
1651
1191
  }
1652
- return modelClass === "thinking" ? runtime.thinkingModel : runtime.coderModel;
1192
+ return;
1653
1193
  }
1654
- function selectDispatchRuntime(role, decision2, reachable, config) {
1655
- if (decision2.routeKind !== "local") {
1656
- return { runtimeId: undefined, decision: decision2 };
1194
+ function classifyFailureForTelemetry(failureReason, failureStage) {
1195
+ const lower = failureReason.toLowerCase();
1196
+ if (TIMEOUT_NEEDLES.some((needle) => lower.includes(needle))) {
1197
+ return "prompt-timeout";
1657
1198
  }
1658
- const listed = role.localRuntimes;
1659
- if (listed !== undefined && listed.length > 0 && reachable !== undefined) {
1660
- for (const runtimeId of listed) {
1661
- if (!reachable.has(runtimeId)) {
1662
- continue;
1663
- }
1664
- const runtime = configuredRuntime(runtimeId, config);
1665
- if (runtime === undefined) {
1666
- continue;
1667
- }
1668
- const model = roleLocalModel(role, runtimeId, runtime.defaultModel, runtime);
1669
- return {
1670
- runtimeId,
1671
- decision: { ...decision2, selected: model }
1672
- };
1673
- }
1199
+ if (lower.includes("did not include a session id") || lower.includes("missing session")) {
1200
+ return "create-no-id";
1674
1201
  }
1675
- const resolvedRuntimeId = runtimeIdOfModel(decision2.selected, config);
1676
- if (resolvedRuntimeId !== undefined) {
1677
- const pinned = roleLocalModel(role, resolvedRuntimeId, decision2.selected, configuredRuntime(resolvedRuntimeId, config));
1678
- if (pinned !== decision2.selected) {
1679
- return {
1680
- runtimeId: resolvedRuntimeId,
1681
- decision: { ...decision2, selected: pinned }
1682
- };
1683
- }
1202
+ if (failureStage === "create") {
1203
+ return "create-rejected";
1684
1204
  }
1685
- return {
1686
- runtimeId: resolvedRuntimeId,
1687
- decision: decision2
1688
- };
1689
- }
1690
-
1691
- // src/orchestrator/subsessions.ts
1692
- var UNKNOWN_ERROR_REASON_MAX_CHARS = 500;
1693
- var TRUNCATED_REASON_SUFFIX = "…[truncated]";
1694
- var MAX_SERIALIZED_ARRAY_ITEMS = 20;
1695
- var MAX_SERIALIZED_OBJECT_PROPERTIES = 25;
1696
- var MAX_SERIALIZED_DEPTH = 4;
1697
- function isObject(value) {
1698
- return typeof value === "object" && value !== null;
1699
- }
1700
- function isError(value) {
1701
- try {
1702
- return value instanceof Error;
1703
- } catch {
1704
- return false;
1205
+ const cause = matchFailureCause(lower);
1206
+ if (cause !== undefined) {
1207
+ return cause;
1705
1208
  }
1706
- }
1707
- function safeToString(value) {
1708
- try {
1709
- return String(value);
1710
- } catch {
1711
- return "unserializable error value";
1209
+ if (failureStage === "prompt") {
1210
+ return "prompt-rejected";
1712
1211
  }
1212
+ return "unknown";
1713
1213
  }
1714
- function safeGet(value, key) {
1715
- try {
1716
- return Reflect.get(value, key);
1717
- } catch {
1718
- return;
1214
+ function classifyOpencodeSessionError(error) {
1215
+ if (error === undefined) {
1216
+ return "unknown";
1217
+ }
1218
+ const text = `${error.name ?? ""} ${error.message ?? ""}`.trim();
1219
+ if (text.length === 0) {
1220
+ return "unknown";
1719
1221
  }
1222
+ return classifyFailureForTelemetry(text, undefined);
1720
1223
  }
1721
- function stringProperty(value, key) {
1722
- const candidate = safeGet(value, key);
1723
- return typeof candidate === "string" ? candidate : undefined;
1224
+
1225
+ // src/console/protocol.ts
1226
+ function redactFrameText(text) {
1227
+ return `[redacted · ${text.length} chars · ${hashPrompt(text)}]`;
1724
1228
  }
1725
- function nestedMessage(value, key) {
1726
- const container = safeGet(value, key);
1727
- if (!isObject(container)) {
1728
- return;
1729
- }
1730
- const message = stringProperty(container, "message");
1731
- return message !== undefined && message.trim().length > 0 ? message : undefined;
1229
+ var MAX_INPUT_CHARS = 1e5;
1230
+ var SessionInputSchema = z7.object({
1231
+ text: z7.string().min(1).max(MAX_INPUT_CHARS)
1232
+ }).strict();
1233
+ function parseSessionInput(raw) {
1234
+ const result = SessionInputSchema.safeParse(raw);
1235
+ return result.success ? result.data : undefined;
1236
+ }
1237
+ function asRecord(value) {
1238
+ return typeof value === "object" && value !== null ? value : undefined;
1732
1239
  }
1733
- function functionName(value) {
1734
- return value.name.length > 0 ? `:${value.name}` : "";
1240
+ function str(value) {
1241
+ return typeof value === "string" && value.length > 0 ? value : undefined;
1735
1242
  }
1736
- function jsonCompatible(value, seen, depth) {
1737
- if (value === null) {
1738
- return null;
1243
+ function eventSessionID(event) {
1244
+ const props = asRecord(event.properties);
1245
+ if (props === undefined) {
1246
+ return;
1739
1247
  }
1740
- switch (typeof value) {
1741
- case "string":
1742
- case "number":
1743
- case "boolean":
1744
- return value;
1745
- case "bigint":
1746
- return `${value.toString()}n`;
1747
- case "symbol":
1748
- return safeToString(value);
1749
- case "undefined":
1750
- return "[undefined]";
1751
- case "function":
1752
- return `[Function${functionName(value)}]`;
1753
- case "object":
1754
- break;
1248
+ return str(props.sessionID) ?? str(asRecord(props.part)?.sessionID) ?? str(asRecord(props.info)?.sessionID);
1249
+ }
1250
+ function partToFrame(part, options) {
1251
+ if (part.type === "text") {
1252
+ const text = str(part.text);
1253
+ if (text === undefined) {
1254
+ return;
1255
+ }
1256
+ return options.diagnostic === true ? { kind: "text", text } : { kind: "text", text: redactFrameText(text), redacted: true };
1755
1257
  }
1756
- if (seen.has(value)) {
1757
- return "[Circular]";
1258
+ if (part.type === "tool") {
1259
+ const tool = str(part.tool) ?? "tool";
1260
+ const status = str(asRecord(part.state)?.status) ?? "run";
1261
+ return { kind: "tool", text: `⚙ ${tool} · ${status}` };
1758
1262
  }
1759
- if (depth >= MAX_SERIALIZED_DEPTH) {
1760
- return "[MaxDepth]";
1263
+ return;
1264
+ }
1265
+ function messagesToFrames(raw, sessionID, options = {}) {
1266
+ if (!Array.isArray(raw)) {
1267
+ return [];
1761
1268
  }
1762
- seen.add(value);
1763
- try {
1764
- if (Array.isArray(value)) {
1765
- const items = value.slice(0, MAX_SERIALIZED_ARRAY_ITEMS).map((item) => jsonCompatible(item, seen, depth + 1));
1766
- if (value.length > MAX_SERIALIZED_ARRAY_ITEMS) {
1767
- items.push(`...${value.length - MAX_SERIALIZED_ARRAY_ITEMS} more items`);
1269
+ const frames = [];
1270
+ for (const entry of raw) {
1271
+ const record = asRecord(entry);
1272
+ if (record === undefined) {
1273
+ continue;
1274
+ }
1275
+ const info = asRecord(record.info);
1276
+ const entrySession = str(info?.sessionID) ?? str(record.sessionID);
1277
+ if (entrySession !== undefined && entrySession !== sessionID) {
1278
+ continue;
1279
+ }
1280
+ const parts = record.parts;
1281
+ if (!Array.isArray(parts)) {
1282
+ continue;
1283
+ }
1284
+ for (const rawPart of parts) {
1285
+ const part = asRecord(rawPart);
1286
+ if (part === undefined) {
1287
+ continue;
1768
1288
  }
1769
- return items;
1770
- }
1771
- const descriptors = Object.getOwnPropertyDescriptors(value);
1772
- const entries = Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable);
1773
- const result = {};
1774
- for (const [index, [key, descriptor]] of entries.entries()) {
1775
- if (index >= MAX_SERIALIZED_OBJECT_PROPERTIES) {
1776
- result["..."] = `${entries.length - MAX_SERIALIZED_OBJECT_PROPERTIES} more properties`;
1777
- break;
1289
+ const frame = partToFrame(part, options);
1290
+ if (frame !== undefined) {
1291
+ frames.push(frame);
1778
1292
  }
1779
- result[key] = "value" in descriptor ? jsonCompatible(descriptor.value, seen, depth + 1) : "[Getter]";
1780
1293
  }
1781
- return result;
1782
- } catch (serializationError) {
1783
- return `[Unserializable object: ${safeToString(serializationError)}]`;
1784
- } finally {
1785
- seen.delete(value);
1786
- }
1787
- }
1788
- function truncateReason(reason) {
1789
- if (reason.length <= UNKNOWN_ERROR_REASON_MAX_CHARS) {
1790
- return reason;
1791
1294
  }
1792
- return `${reason.slice(0, UNKNOWN_ERROR_REASON_MAX_CHARS - TRUNCATED_REASON_SUFFIX.length)}${TRUNCATED_REASON_SUFFIX}`;
1793
- }
1794
- function objectReason(error) {
1795
- try {
1796
- const serialized = JSON.stringify(jsonCompatible(error, new WeakSet, 0));
1797
- if (serialized !== undefined && serialized !== "[object Object]") {
1798
- return truncateReason(serialized);
1799
- }
1800
- } catch {}
1801
- const fallback = safeToString(error);
1802
- return truncateReason(fallback === "[object Object]" ? "unserializable error object" : fallback);
1295
+ return frames;
1803
1296
  }
1804
- function errorReason(error) {
1805
- if (isError(error)) {
1806
- const message = stringProperty(error, "message");
1807
- if (message !== undefined) {
1808
- return message;
1809
- }
1297
+ function eventToSessionFrame(event, sessionID, options = {}) {
1298
+ if (eventSessionID(event) !== sessionID) {
1299
+ return;
1810
1300
  }
1811
- if (isObject(error)) {
1812
- const message = stringProperty(error, "message");
1813
- if (message !== undefined) {
1814
- return message;
1301
+ const props = asRecord(event.properties) ?? {};
1302
+ switch (event.type) {
1303
+ case "message.part.updated": {
1304
+ const part = asRecord(props.part);
1305
+ return part === undefined ? undefined : partToFrame(part, options);
1815
1306
  }
1816
- const dataMessage = nestedMessage(error, "data");
1817
- if (dataMessage !== undefined) {
1818
- return dataMessage;
1307
+ case "session.idle":
1308
+ return { kind: "status", text: "● session ready" };
1309
+ case "session.error": {
1310
+ const err = asRecord(props.error);
1311
+ const cls = classifyOpencodeSessionError(err === undefined ? undefined : { name: str(err.name), message: str(err.message) });
1312
+ return { kind: "error", text: `✖ ${cls}` };
1819
1313
  }
1820
- const nestedErrorMessage = nestedMessage(error, "error");
1821
- if (nestedErrorMessage !== undefined) {
1822
- return nestedErrorMessage;
1314
+ case "permission.updated": {
1315
+ const title = str(props.title) ?? "permission requested";
1316
+ const id = str(props.id);
1317
+ const frame = { kind: "permission", text: `⚠ ${title}` };
1318
+ if (id !== undefined) {
1319
+ frame.permissionID = id;
1320
+ }
1321
+ return frame;
1823
1322
  }
1824
- return objectReason(error);
1825
- }
1826
- return safeToString(error);
1827
- }
1828
- function createArgs(req) {
1829
- const body = {};
1830
- const args = { body };
1831
- if (req.parentSessionID !== undefined) {
1832
- body.parentID = req.parentSessionID;
1833
- }
1834
- if (req.title !== undefined) {
1835
- body.title = req.title;
1836
- }
1837
- if (req.directory !== undefined) {
1838
- args.query = { directory: req.directory };
1839
- }
1840
- return args;
1841
- }
1842
- function promptArgs(sessionID, req) {
1843
- const body = {
1844
- model: req.model,
1845
- parts: [{ type: "text", text: req.prompt }]
1846
- };
1847
- if (req.agent !== undefined) {
1848
- body.agent = req.agent;
1849
- }
1850
- if (req.systemPrompt !== undefined) {
1851
- body.system = req.systemPrompt;
1323
+ default:
1324
+ return;
1852
1325
  }
1853
- return {
1854
- path: { id: sessionID },
1855
- body
1856
- };
1857
1326
  }
1858
- async function withTimeout(promise, timeoutMs) {
1859
- if (timeoutMs === undefined) {
1860
- return promise;
1861
- }
1862
- let timeoutID;
1863
- const timeout = new Promise((_, reject) => {
1864
- timeoutID = setTimeout(() => {
1865
- reject(new Error(`prompt timed out after ${timeoutMs}ms`));
1866
- }, timeoutMs);
1867
- });
1868
- try {
1869
- return await Promise.race([promise, timeout]);
1870
- } finally {
1871
- clearTimeout(timeoutID);
1872
- }
1327
+ function frameToSse(frame) {
1328
+ return `event: frame
1329
+ data: ${JSON.stringify(frame)}
1330
+
1331
+ `;
1873
1332
  }
1874
- async function runSubsession(client, req, deps = {}) {
1875
- let sessionID;
1876
- try {
1877
- const created = await client.session.create(createArgs(req));
1878
- if (created.error !== undefined) {
1879
- return {
1880
- success: false,
1881
- roleID: req.roleID,
1882
- model: req.model,
1883
- failureStage: "create",
1884
- failureReason: errorReason(created.error)
1885
- };
1333
+
1334
+ // src/console/sse.ts
1335
+ function pushSse(buffer, chunk) {
1336
+ const combined = (buffer + chunk).replace(/\r\n/g, `
1337
+ `);
1338
+ const parts = combined.split(`
1339
+
1340
+ `);
1341
+ const rest = parts.pop() ?? "";
1342
+ const messages = [];
1343
+ for (const block of parts) {
1344
+ if (block.length === 0) {
1345
+ continue;
1886
1346
  }
1887
- sessionID = created.data?.id;
1888
- if (sessionID === undefined || sessionID.length === 0) {
1889
- return {
1890
- success: false,
1891
- roleID: req.roleID,
1892
- model: req.model,
1893
- failureStage: "create",
1894
- failureReason: "create response did not include a session id"
1895
- };
1347
+ let event;
1348
+ const dataLines = [];
1349
+ for (const line of block.split(`
1350
+ `)) {
1351
+ if (line.startsWith(":") || line.length === 0) {
1352
+ continue;
1353
+ }
1354
+ if (line.startsWith("event:")) {
1355
+ event = line.slice("event:".length).trimStart();
1356
+ } else if (line.startsWith("data:")) {
1357
+ dataLines.push(line.slice("data:".length).replace(/^ /, ""));
1358
+ }
1896
1359
  }
1897
- } catch (error) {
1898
- return {
1899
- success: false,
1900
- roleID: req.roleID,
1901
- model: req.model,
1902
- failureStage: "create",
1903
- failureReason: errorReason(error)
1904
- };
1905
- }
1906
- try {
1907
- const prompted = await withTimeout(client.session.prompt(promptArgs(sessionID, req)), deps.timeoutMs);
1908
- if (prompted.error !== undefined) {
1909
- return {
1910
- success: false,
1911
- sessionID,
1912
- roleID: req.roleID,
1913
- model: req.model,
1914
- failureStage: "prompt",
1915
- failureReason: errorReason(prompted.error)
1916
- };
1360
+ if (dataLines.length === 0 && event === undefined) {
1361
+ continue;
1917
1362
  }
1918
- return {
1919
- success: true,
1920
- sessionID,
1921
- roleID: req.roleID,
1922
- model: req.model,
1923
- response: prompted.data
1924
- };
1925
- } catch (error) {
1926
- return {
1927
- success: false,
1928
- sessionID,
1929
- roleID: req.roleID,
1930
- model: req.model,
1931
- failureStage: "prompt",
1932
- failureReason: errorReason(error)
1933
- };
1363
+ const message = { data: dataLines.join(`
1364
+ `) };
1365
+ if (event !== undefined) {
1366
+ message.event = event;
1367
+ }
1368
+ messages.push(message);
1934
1369
  }
1370
+ return { messages, rest };
1935
1371
  }
1936
-
1937
- // src/orchestrator/coordinator.ts
1938
- function warnUnknownRole(roleID, warn, warnedRoles) {
1939
- if (isKnownNonWorkerRole(roleID))
1372
+ function parseEventData(data) {
1373
+ if (data.length === 0) {
1940
1374
  return;
1941
- if (warnedRoles !== undefined) {
1942
- if (warnedRoles.has(roleID))
1943
- return;
1944
- warnedRoles.add(roleID);
1945
1375
  }
1946
- (warn ?? console.warn)(`[openteam] unknown roleID "${roleID}" — no matching profile in config orchestrator.roles or TEAM_ROLES. ` + `A neutral frontier-eligible profile will be used and the request will be ` + `dispatched to agent "${roleID}". If this is a custom agent in your workspace, ` + `this message is informational. To silence it, add "${roleID}" to orchestrator.roles in .opencode/openteam.json.`);
1947
- }
1948
- function roleFor(input) {
1949
- return getRoleProfile(input.roleID, input.config.orchestrator?.roles) ?? synthesiseFallbackProfile(input.roleID);
1950
- }
1951
- function priorityForRole(role) {
1952
- return role.roleID === "scribe" ? "background" : "worker";
1953
- }
1954
- function mandatoryLocalRuntimeMessage(roleID) {
1955
- return `openteam: role "${roleID}" requires a local runtime, but no local runtime is reachable. ` + `This role must run locally and will not fall back to a frontier provider. ` + `Start one of the supported local runtimes (Ollama, LM Studio, or Lemonade) and try again. ` + "Run `openteam doctor` to diagnose your local runtime configuration.";
1956
- }
1957
- function assertLocalRuntimeForRole(role, roleID, reachable) {
1958
- if (role.requiresLocalRuntime !== true) {
1376
+ try {
1377
+ return JSON.parse(data);
1378
+ } catch {
1959
1379
  return;
1960
1380
  }
1961
- if (reachable !== true) {
1962
- throw new Error(mandatoryLocalRuntimeMessage(roleID));
1963
- }
1964
- }
1965
- function roleHasRuntimeList(role) {
1966
- return role.localRuntimes !== undefined && role.localRuntimes.length > 0;
1967
- }
1968
- async function resolveBatchReachability(inputs, deps) {
1969
- const roles = inputs.map(roleFor);
1970
- const anyMandatory = roles.some((role) => role.requiresLocalRuntime === true);
1971
- const anyList = roles.some(roleHasRuntimeList);
1972
- if (!anyMandatory && !anyList) {
1973
- return { any: undefined, set: undefined };
1974
- }
1975
- if (anyList && deps.reachableRuntimeIds !== undefined) {
1976
- const set = await deps.reachableRuntimeIds();
1977
- return { any: set.size > 0, set };
1978
- }
1979
- const any = await deps.localRuntimeReachable();
1980
- return { any, set: undefined };
1981
- }
1982
- function roleLocalReachable(role, batch) {
1983
- const listed = role.localRuntimes;
1984
- if (listed !== undefined && listed.length > 0 && batch.set !== undefined) {
1985
- return listed.some((id) => batch.set?.has(id) === true);
1986
- }
1987
- return batch.any;
1988
- }
1989
- function permissionsFor(roleID) {
1990
- return ROLE_PERMISSIONS[roleID] ?? [];
1991
- }
1992
- function roleRequiresFrontier(role, task, config) {
1993
- return task.explicitOverride === undefined && config.router.localOnly !== true && role.localFirst === false && role.defaultTier === "hard" && role.preferredFrontierModels.length > 0;
1994
- }
1995
- function roleAdjustedTask(role, input, requirement) {
1996
- const task = {
1997
- ...input.task,
1998
- prompt: input.task.prompt ?? input.prompt
1999
- };
2000
- if (requirement.needsTools) {
2001
- task.requiresTools = true;
2002
- }
2003
- if (roleRequiresFrontier(role, task, input.config)) {
2004
- task.explicitOverride = "alwaysFrontier";
2005
- }
2006
- return task;
2007
- }
2008
- function roleAdjustedTaskForRole(input) {
2009
- const role = roleFor(input);
2010
- const tier = elevateTierForPermissions(classifyHeuristic({
2011
- ...input.task,
2012
- prompt: input.task.prompt ?? input.prompt
2013
- }).tier, permissionsFor(role.roleID));
2014
- return roleAdjustedTask(role, input, roleToRequirement(role, input.task, tier));
2015
1381
  }
2016
- function satisfiesRoleRequirement(requirement, profile) {
2017
- return profile.kind === "frontier" && profile.availability !== "unavailable" && profile.contextWindow >= requirement.minContextWindow && profile.reasoningTier >= requirement.minReasoningTier && profile.codeQualityTier >= requirement.minCodeQualityTier && (!requirement.needsTools || profile.supportsToolCalling) && (!requirement.needsVision || profile.supportsVision);
2018
- }
2019
- function roleCapableProfiles(profiles, requirement) {
2020
- if (profiles === undefined) {
1382
+
1383
+ // src/console/opencodeClient.ts
1384
+ function unwrapEvent(value) {
1385
+ if (typeof value !== "object" || value === null) {
2021
1386
  return;
2022
1387
  }
2023
- return profiles.filter((profile) => satisfiesRoleRequirement(requirement, profile));
2024
- }
2025
- function routingInput(input, role) {
2026
- const classifiedTier = classifyHeuristic({
2027
- ...input.task,
2028
- prompt: input.task.prompt ?? input.prompt
2029
- }).tier;
2030
- const permissionTier = elevateTierForPermissions(classifiedTier, permissionsFor(role.roleID));
2031
- const tier = permissionTier;
2032
- const requirement = roleToRequirement(role, input.task, tier);
2033
- const task = roleAdjustedTaskForRole(input);
2034
- const baseInput = {
2035
- config: input.config,
2036
- task,
2037
- tier
2038
- };
2039
- const profiles = roleCapableProfiles(input.profiles, requirement);
2040
- if (profiles !== undefined) {
2041
- baseInput.profiles = profiles;
2042
- }
2043
- if (input.availableModels !== undefined) {
2044
- baseInput.availableModels = input.availableModels;
2045
- }
2046
- if (input.budgetState !== undefined) {
2047
- baseInput.budgetState = input.budgetState;
2048
- }
2049
- return baseInput;
1388
+ const record = value;
1389
+ const payload = typeof record.payload === "object" && record.payload !== null ? record.payload : record;
1390
+ return typeof payload.type === "string" ? payload : undefined;
2050
1391
  }
2051
- function recordSessionID(input, subsession) {
2052
- return subsession.sessionID ?? input.parentSessionID;
2053
- }
2054
- function buildRecord(input, decision2, subsession, deps, decisionID, batchID) {
2055
- const context = {
2056
- ts: deps.now(),
2057
- promptHash: hashPrompt(input.prompt),
2058
- promptChars: input.task.promptChars
2059
- };
2060
- const sessionID = recordSessionID(input, subsession);
2061
- if (sessionID !== undefined) {
2062
- context.sessionID = sessionID;
2063
- }
2064
- if (input.task.estimatedInputTokens !== undefined) {
2065
- context.tokensIn = input.task.estimatedInputTokens;
2066
- }
2067
- if (input.task.estimatedOutputTokens !== undefined) {
2068
- context.tokensOut = input.task.estimatedOutputTokens;
2069
- }
2070
- const base = toCostRecord(decision2, context);
2071
- const record = {
2072
- ...base,
2073
- decisionID,
2074
- agent: input.roleID,
2075
- success: subsession.success
2076
- };
2077
- if (batchID !== undefined) {
2078
- record.batchID = batchID;
2079
- }
2080
- if (input.taskID !== undefined) {
2081
- record.taskID = input.taskID;
2082
- }
2083
- if (subsession.failureReason !== undefined) {
2084
- record.failureReason = subsession.failureReason;
2085
- }
2086
- if (subsession.failureStage !== undefined) {
2087
- record.failureStage = subsession.failureStage;
2088
- }
2089
- return record;
2090
- }
2091
- var TelemetryFailureClassSchema = z6.enum([
2092
- "create-rejected",
2093
- "create-no-id",
2094
- "prompt-rejected",
2095
- "prompt-timeout",
2096
- "prompt-rate-limited",
2097
- "prompt-auth",
2098
- "prompt-content-filter",
2099
- "prompt-context-length",
2100
- "prompt-validation",
2101
- "prompt-connection",
2102
- "prompt-server",
2103
- "unknown"
2104
- ]);
2105
- var FAILURE_CAUSE_RULES = [
2106
- [
2107
- [
2108
- "rate limit",
2109
- "rate-limit",
2110
- "ratelimit",
2111
- /(?<!\d)429(?!\d)/,
2112
- "too many requests",
2113
- "quota exceeded"
2114
- ],
2115
- "prompt-rate-limited"
2116
- ],
2117
- [
2118
- [
2119
- "unauthorized",
2120
- "authentication",
2121
- /(?<!\d)(?:401|403)(?!\d)/,
2122
- "forbidden",
2123
- "api key",
2124
- "invalid key",
2125
- "permission denied"
2126
- ],
2127
- "prompt-auth"
2128
- ],
2129
- [
2130
- [
2131
- "content policy",
2132
- "content filter",
2133
- "content_filter",
2134
- "moderation",
2135
- "safety system",
2136
- "flagged by"
2137
- ],
2138
- "prompt-content-filter"
2139
- ],
2140
- [
2141
- [
2142
- "context length",
2143
- "context window",
2144
- "maximum context",
2145
- "too many tokens",
2146
- "token limit",
2147
- "context_length_exceeded"
2148
- ],
2149
- "prompt-context-length"
2150
- ],
2151
- [
2152
- [
2153
- "invalid request",
2154
- "invalid_request",
2155
- "validation",
2156
- "bad request",
2157
- /(?<!\d)400(?!\d)/,
2158
- "unprocessable",
2159
- "malformed"
2160
- ],
2161
- "prompt-validation"
2162
- ],
2163
- [
2164
- [
2165
- "econnrefused",
2166
- "econnreset",
2167
- "enotfound",
2168
- "network error",
2169
- "connection refused",
2170
- "connection reset",
2171
- "fetch failed",
2172
- "socket hang up"
2173
- ],
2174
- "prompt-connection"
2175
- ],
2176
- [
2177
- [
2178
- "internal server error",
2179
- /(?<!\d)(?:500|502|503|504)(?!\d)/,
2180
- "service unavailable",
2181
- "bad gateway",
2182
- "overloaded"
2183
- ],
2184
- "prompt-server"
2185
- ]
2186
- ];
2187
- var TIMEOUT_NEEDLES = [
2188
- "timed out",
2189
- "timeout",
2190
- "timedout",
2191
- "etimedout",
2192
- "deadline exceeded"
2193
- ];
2194
- function matchFailureCause(lowerReason) {
2195
- for (const [needles, cls] of FAILURE_CAUSE_RULES) {
2196
- if (needles.some((needle) => typeof needle === "string" ? lowerReason.includes(needle) : needle.test(lowerReason))) {
2197
- return cls;
2198
- }
2199
- }
2200
- return;
2201
- }
2202
- function classifyFailureForTelemetry(failureReason, failureStage) {
2203
- const lower = failureReason.toLowerCase();
2204
- if (TIMEOUT_NEEDLES.some((needle) => lower.includes(needle))) {
2205
- return "prompt-timeout";
2206
- }
2207
- if (lower.includes("did not include a session id") || lower.includes("missing session")) {
2208
- return "create-no-id";
2209
- }
2210
- if (failureStage === "create") {
2211
- return "create-rejected";
2212
- }
2213
- const cause = matchFailureCause(lower);
2214
- if (cause !== undefined) {
2215
- return cause;
2216
- }
2217
- if (failureStage === "prompt") {
2218
- return "prompt-rejected";
2219
- }
2220
- return "unknown";
2221
- }
2222
- function classifyOpencodeSessionError(error) {
2223
- if (error === undefined) {
2224
- return "unknown";
2225
- }
2226
- const text = `${error.name ?? ""} ${error.message ?? ""}`.trim();
2227
- if (text.length === 0) {
2228
- return "unknown";
2229
- }
2230
- return classifyFailureForTelemetry(text, undefined);
2231
- }
2232
- function correlatedCostRecordToRouteEvent(record) {
2233
- const event = {
2234
- ...costRecordToRouteEvent(record),
2235
- decisionID: record.decisionID,
2236
- agent: record.agent,
2237
- success: record.success
2238
- };
2239
- if (record.batchID !== undefined) {
2240
- event.batchID = record.batchID;
2241
- }
2242
- if (record.taskID !== undefined) {
2243
- event.taskID = record.taskID;
2244
- }
2245
- if (record.failureReason !== undefined) {
2246
- event.failureReason = classifyFailureForTelemetry(record.failureReason, record.failureStage);
2247
- }
2248
- if (record.failureStage !== undefined) {
2249
- event.failureStage = record.failureStage;
2250
- }
2251
- return event;
2252
- }
2253
- async function safeEmit(sink, event) {
2254
- try {
2255
- await sink.emit(event);
2256
- } catch {}
2257
- }
2258
- function buildQueueEvent(observation, sessionID, ts, correlation) {
2259
- const event = {
2260
- v: EVENT_SCHEMA_VERSION,
2261
- type: "queue",
2262
- ts,
2263
- sessionID,
2264
- runtimeId: observation.runtimeId,
2265
- waitedMs: observation.waitedMs,
2266
- activeCount: observation.activeCount,
2267
- queuedCount: observation.queuedCount
2268
- };
2269
- if (correlation.decisionID !== undefined) {
2270
- event.decisionID = correlation.decisionID;
2271
- }
2272
- if (correlation.agent !== undefined) {
2273
- event.agent = correlation.agent;
2274
- }
2275
- if (correlation.batchID !== undefined) {
2276
- event.batchID = correlation.batchID;
2277
- }
2278
- return event;
2279
- }
2280
- async function runRoleTaskWithCorrelation(input, deps, correlation, batch) {
2281
- if (getRoleProfile(input.roleID, input.config.orchestrator?.roles) === undefined) {
2282
- warnUnknownRole(input.roleID, deps.warn, deps.warnedRoles);
2283
- }
2284
- const role = roleFor(input);
2285
- assertLocalRuntimeForRole(role, input.roleID, roleLocalReachable(role, batch));
2286
- const routed = chooseModel(routingInput(input, role));
2287
- if (routed.routeKind === "blocked") {
2288
- throw new Error(`openteam coordinator: request for role "${input.roleID}" blocked — ${routed.blockReason ?? "no-local-runtime-configured"}. Configure a local runtime or disable the constraint.`);
2289
- }
2290
- const selection = selectDispatchRuntime(role, routed, batch.set, input.config);
2291
- const decision2 = selection.decision;
2292
- const request = {
2293
- roleID: input.roleID,
2294
- model: decision2.selected,
2295
- prompt: input.prompt
2296
- };
2297
- if (input.parentSessionID !== undefined) {
2298
- request.parentSessionID = input.parentSessionID;
2299
- }
2300
- if (input.title !== undefined) {
2301
- request.title = input.title;
2302
- }
2303
- if (input.systemPrompt !== undefined) {
2304
- request.systemPrompt = input.systemPrompt;
2305
- }
2306
- if (input.agentOverride !== undefined) {
2307
- request.agent = input.agentOverride;
2308
- } else if (role.opencodeAgent !== undefined) {
2309
- request.agent = role.opencodeAgent;
2310
- }
2311
- if (input.directory !== undefined) {
2312
- request.directory = input.directory;
2313
- }
2314
- const subsessionDeps = deps.timeoutMs === undefined ? {} : { timeoutMs: deps.timeoutMs };
2315
- const runtimeId = selection.runtimeId;
2316
- let release;
2317
- let queueObservation;
2318
- if (runtimeId !== undefined && deps.runtimeLimiter !== undefined) {
2319
- const limiter = deps.runtimeLimiter;
2320
- const priority = priorityForRole(role);
2321
- const dispatchRuntime = configuredRuntime(runtimeId, input.config) ?? { id: runtimeId };
2322
- const poolKey = slotPoolKey(withReportedModelSlots(dispatchRuntime, deps.reportedModelSlots?.(runtimeId)), decision2.selected.modelID);
2323
- release = limiter.tryAcquire(poolKey, priority);
2324
- let waitedMs = 0;
2325
- if (release === undefined) {
2326
- const acquisition = await limiter.acquire(poolKey, priority);
2327
- release = acquisition.release;
2328
- waitedMs = acquisition.waitedMs;
2329
- }
2330
- queueObservation = {
2331
- runtimeId,
2332
- waitedMs,
2333
- activeCount: limiter.activeCount(poolKey),
2334
- queuedCount: limiter.queuedCount(poolKey)
2335
- };
2336
- }
2337
- try {
2338
- const subsession = await runSubsession(deps.client, request, subsessionDeps);
2339
- const record = buildRecord(input, decision2, subsession, deps, correlation.decisionID, correlation.batchID);
2340
- await safeEmit(deps.sink, correlatedCostRecordToRouteEvent(record));
2341
- if (queueObservation !== undefined) {
2342
- const sessionID = recordSessionID(input, subsession);
2343
- if (sessionID !== undefined) {
2344
- const queueCorrelation = {
2345
- decisionID: correlation.decisionID,
2346
- agent: input.roleID
2347
- };
2348
- if (correlation.batchID !== undefined) {
2349
- queueCorrelation.batchID = correlation.batchID;
2350
- }
2351
- await safeEmit(deps.sink, buildQueueEvent(queueObservation, sessionID, deps.now(), queueCorrelation));
2352
- }
2353
- }
2354
- return { decision: decision2, subsession, record };
2355
- } finally {
2356
- release?.();
2357
- }
2358
- }
2359
- async function runRoleTask(input, deps) {
2360
- const batch = await resolveBatchReachability([input], deps);
2361
- return runRoleTaskWithCorrelation(input, deps, {
2362
- decisionID: deps.newDecisionID()
2363
- }, batch);
2364
- }
2365
-
2366
- // src/console/protocol.ts
2367
- function redactFrameText(text) {
2368
- return `[redacted · ${text.length} chars · ${hashPrompt(text)}]`;
2369
- }
2370
- var MAX_INPUT_CHARS = 1e5;
2371
- var SessionInputSchema = z7.object({
2372
- text: z7.string().min(1).max(MAX_INPUT_CHARS)
2373
- }).strict();
2374
- function parseSessionInput(raw) {
2375
- const result = SessionInputSchema.safeParse(raw);
2376
- return result.success ? result.data : undefined;
2377
- }
2378
- function asRecord(value) {
2379
- return typeof value === "object" && value !== null ? value : undefined;
2380
- }
2381
- function str(value) {
2382
- return typeof value === "string" && value.length > 0 ? value : undefined;
2383
- }
2384
- function eventSessionID(event) {
2385
- const props = asRecord(event.properties);
2386
- if (props === undefined) {
2387
- return;
2388
- }
2389
- return str(props.sessionID) ?? str(asRecord(props.part)?.sessionID) ?? str(asRecord(props.info)?.sessionID);
2390
- }
2391
- function partToFrame(part, options) {
2392
- if (part.type === "text") {
2393
- const text = str(part.text);
2394
- if (text === undefined) {
2395
- return;
2396
- }
2397
- return options.diagnostic === true ? { kind: "text", text } : { kind: "text", text: redactFrameText(text), redacted: true };
2398
- }
2399
- if (part.type === "tool") {
2400
- const tool = str(part.tool) ?? "tool";
2401
- const status = str(asRecord(part.state)?.status) ?? "run";
2402
- return { kind: "tool", text: `⚙ ${tool} · ${status}` };
2403
- }
2404
- return;
2405
- }
2406
- function messagesToFrames(raw, sessionID, options = {}) {
2407
- if (!Array.isArray(raw)) {
2408
- return [];
2409
- }
2410
- const frames = [];
2411
- for (const entry of raw) {
2412
- const record = asRecord(entry);
2413
- if (record === undefined) {
2414
- continue;
2415
- }
2416
- const info = asRecord(record.info);
2417
- const entrySession = str(info?.sessionID) ?? str(record.sessionID);
2418
- if (entrySession !== undefined && entrySession !== sessionID) {
2419
- continue;
2420
- }
2421
- const parts = record.parts;
2422
- if (!Array.isArray(parts)) {
2423
- continue;
2424
- }
2425
- for (const rawPart of parts) {
2426
- const part = asRecord(rawPart);
2427
- if (part === undefined) {
2428
- continue;
2429
- }
2430
- const frame = partToFrame(part, options);
2431
- if (frame !== undefined) {
2432
- frames.push(frame);
2433
- }
2434
- }
2435
- }
2436
- return frames;
2437
- }
2438
- function eventToSessionFrame(event, sessionID, options = {}) {
2439
- if (eventSessionID(event) !== sessionID) {
2440
- return;
2441
- }
2442
- const props = asRecord(event.properties) ?? {};
2443
- switch (event.type) {
2444
- case "message.part.updated": {
2445
- const part = asRecord(props.part);
2446
- return part === undefined ? undefined : partToFrame(part, options);
2447
- }
2448
- case "session.idle":
2449
- return { kind: "status", text: "● session ready" };
2450
- case "session.error": {
2451
- const err = asRecord(props.error);
2452
- const cls = classifyOpencodeSessionError(err === undefined ? undefined : { name: str(err.name), message: str(err.message) });
2453
- return { kind: "error", text: `✖ ${cls}` };
2454
- }
2455
- case "permission.updated": {
2456
- const title = str(props.title) ?? "permission requested";
2457
- const id = str(props.id);
2458
- const frame = { kind: "permission", text: `⚠ ${title}` };
2459
- if (id !== undefined) {
2460
- frame.permissionID = id;
2461
- }
2462
- return frame;
2463
- }
2464
- default:
2465
- return;
2466
- }
2467
- }
2468
- function frameToSse(frame) {
2469
- return `event: frame
2470
- data: ${JSON.stringify(frame)}
2471
-
2472
- `;
2473
- }
2474
-
2475
- // src/console/sse.ts
2476
- function pushSse(buffer, chunk) {
2477
- const combined = (buffer + chunk).replace(/\r\n/g, `
2478
- `);
2479
- const parts = combined.split(`
2480
-
2481
- `);
2482
- const rest = parts.pop() ?? "";
2483
- const messages = [];
2484
- for (const block of parts) {
2485
- if (block.length === 0) {
2486
- continue;
2487
- }
2488
- let event;
2489
- const dataLines = [];
2490
- for (const line of block.split(`
2491
- `)) {
2492
- if (line.startsWith(":") || line.length === 0) {
2493
- continue;
2494
- }
2495
- if (line.startsWith("event:")) {
2496
- event = line.slice("event:".length).trimStart();
2497
- } else if (line.startsWith("data:")) {
2498
- dataLines.push(line.slice("data:".length).replace(/^ /, ""));
2499
- }
2500
- }
2501
- if (dataLines.length === 0 && event === undefined) {
2502
- continue;
2503
- }
2504
- const message = { data: dataLines.join(`
2505
- `) };
2506
- if (event !== undefined) {
2507
- message.event = event;
2508
- }
2509
- messages.push(message);
2510
- }
2511
- return { messages, rest };
2512
- }
2513
- function parseEventData(data) {
2514
- if (data.length === 0) {
2515
- return;
2516
- }
2517
- try {
2518
- return JSON.parse(data);
2519
- } catch {
2520
- return;
2521
- }
2522
- }
2523
-
2524
- // src/console/opencodeClient.ts
2525
- function unwrapEvent(value) {
2526
- if (typeof value !== "object" || value === null) {
2527
- return;
2528
- }
2529
- const record = value;
2530
- const payload = typeof record.payload === "object" && record.payload !== null ? record.payload : record;
2531
- return typeof payload.type === "string" ? payload : undefined;
2532
- }
2533
- function messageToFrame(message, sessionID, options = {}) {
2534
- const event = unwrapEvent(parseEventData(message.data));
2535
- return event === undefined ? undefined : eventToSessionFrame(event, sessionID, options);
1392
+ function messageToFrame(message, sessionID, options = {}) {
1393
+ const event = unwrapEvent(parseEventData(message.data));
1394
+ return event === undefined ? undefined : eventToSessionFrame(event, sessionID, options);
2536
1395
  }
2537
1396
  function createConsoleClient(deps) {
2538
1397
  const doFetch = deps.fetch ?? fetch;
@@ -3199,7 +2058,7 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
3199
2058
  ]
3200
2059
  ],
3201
2060
  ["graph.worktrees", ["enabled"]],
3202
- ["orchestrator", ["roles"]]
2061
+ ["orchestrator", ["roles", "maxRetries"]]
3203
2062
  ]);
3204
2063
  function isUnrecognizedKeysIssue(issue) {
3205
2064
  return issue.code === "unrecognized_keys" && Array.isArray(issue.keys);
@@ -4322,10 +3181,10 @@ function sessionsPanel(sessions) {
4322
3181
  "</section>"
4323
3182
  ].join("");
4324
3183
  }
4325
- function decisionRow(decision2) {
4326
- const who = decision2.agent === undefined ? "" : `<span class="who">${escapeHtml(decision2.agent)}</span> `;
4327
- const tags = decision2.tags === undefined || decision2.tags.length === 0 ? "" : ` ${decision2.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
4328
- return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision2.summary)}${tags}</li>`;
3184
+ function decisionRow(decision) {
3185
+ const who = decision.agent === undefined ? "" : `<span class="who">${escapeHtml(decision.agent)}</span> `;
3186
+ const tags = decision.tags === undefined || decision.tags.length === 0 ? "" : ` ${decision.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
3187
+ return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision.summary)}${tags}</li>`;
4329
3188
  }
4330
3189
  function decisionsPanel(decisions, idSuffix) {
4331
3190
  const id = panelId("panel-decisions", idSuffix);
@@ -5198,14 +4057,14 @@ function isRoute(event) {
5198
4057
  function recentRoutes(records, limit) {
5199
4058
  return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
5200
4059
  }
5201
- function decisionToActivity(decision2) {
4060
+ function decisionToActivity(decision) {
5202
4061
  const entry = {
5203
- ts: decision2.ts,
4062
+ ts: decision.ts,
5204
4063
  kind: "decision",
5205
- summary: decision2.summary
4064
+ summary: decision.summary
5206
4065
  };
5207
- if (decision2.agent !== undefined) {
5208
- entry.agent = decision2.agent;
4066
+ if (decision.agent !== undefined) {
4067
+ entry.agent = decision.agent;
5209
4068
  }
5210
4069
  return entry;
5211
4070
  }
@@ -5243,9 +4102,9 @@ function buildSessionTabs(aggregate, costRecords, nowMs, limits, options) {
5243
4102
  return aggregate.sessions.map((summary) => {
5244
4103
  const id = summary.sessionID;
5245
4104
  const routes = costRecords.filter((record) => record.sessionID === id);
5246
- const decisions = aggregate.decisions.filter((decision2) => decision2.sessionID === id).slice(0, Math.max(0, limits.decisionsLimit));
4105
+ const decisions = aggregate.decisions.filter((decision) => decision.sessionID === id).slice(0, Math.max(0, limits.decisionsLimit));
5247
4106
  const meetings = aggregate.meetings.filter((meeting) => meeting.sessionID === id).slice(0, Math.max(0, limits.meetingsLimit));
5248
- const activity = composeActivity(aggregate.activity.filter((view) => view.sessionID === id), aggregate.decisions.filter((decision2) => decision2.sessionID === id), aggregate.meetings.filter((meeting) => meeting.sessionID === id), limits.activityLimit);
4107
+ const activity = composeActivity(aggregate.activity.filter((view) => view.sessionID === id), aggregate.decisions.filter((decision) => decision.sessionID === id), aggregate.meetings.filter((meeting) => meeting.sessionID === id), limits.activityLimit);
5249
4108
  const tab = {
5250
4109
  sessionID: id,
5251
4110
  status: deriveSessionStatus(summary.lastTs, nowMs, {
@@ -8299,11 +7158,11 @@ function toErrorOutcome(error) {
8299
7158
  function createGraphIngress(deps) {
8300
7159
  const { policy, journal, lease, effects, holder } = deps;
8301
7160
  function taskGate(taskID) {
8302
- const decision2 = decideIngress(policy, taskID);
8303
- return decision2.route === "legacy" ? {
7161
+ const decision = decideIngress(policy, taskID);
7162
+ return decision.route === "legacy" ? {
8304
7163
  kind: "legacy",
8305
- reason: decision2.reason,
8306
- ...decision2.gateViolations !== undefined ? { gateViolations: decision2.gateViolations } : {}
7164
+ reason: decision.reason,
7165
+ ...decision.gateViolations !== undefined ? { gateViolations: decision.gateViolations } : {}
8307
7166
  } : undefined;
8308
7167
  }
8309
7168
  function runGate() {
@@ -9049,8 +7908,8 @@ import { promisify } from "node:util";
9049
7908
  var ORCHESTRATOR_AGENT_PATH = ".opencode/agent/openteam.md";
9050
7909
  var OPENTEAM_ROSTER_PATH = ".opencode/openteam/roster.md";
9051
7910
  var OPENTEAM_ROSTER_FILENAME = OPENTEAM_ROSTER_PATH.split("/").pop() ?? OPENTEAM_ROSTER_PATH;
9052
- function buildOrchestratorAgent(frontier, options = {}) {
9053
- const model = `${frontier.providerID}/${frontier.modelID}`;
7911
+ function buildOrchestratorAgent(model, options = {}) {
7912
+ const modelSpec = `${model.providerID}/${model.modelID}`;
9054
7913
  const permissionLines = options.yolo ? ["permission:", ' "*": allow'] : [
9055
7914
  "permission:",
9056
7915
  " read: allow",
@@ -9069,7 +7928,7 @@ function buildOrchestratorAgent(frontier, options = {}) {
9069
7928
  "---",
9070
7929
  'description: "openteam multi-agent orchestrator: on every request it checks whether a team exists; if not, it understands the tasks and creates the team of subagents on demand, plans dependencies, parallelizes independent work and keeps the todo list attributed to the owner of each task, with local-first routing."',
9071
7930
  "mode: primary",
9072
- `model: ${model}`,
7931
+ `model: ${modelSpec}`,
9073
7932
  "temperature: 0.2",
9074
7933
  ...permissionLines,
9075
7934
  "---"
@@ -9277,7 +8136,34 @@ function buildOrchestratorAgent(frontier, options = {}) {
9277
8136
  "- `directory` (optional): working directory if it differs from the current one.",
9278
8137
  "",
9279
8138
  "The tool returns a per-role summary: selected model, success or",
9280
- "failure, and created session. If one role fails, the rest keep running.",
8139
+ "failure, the created session, and — for each role — a `report` file path",
8140
+ "the role was required to write its findings to. The summary carries **no**",
8141
+ "findings text itself (privacy boundary): the actual work lives only in those",
8142
+ "report files. A durability gate verifies in code that each report was",
8143
+ "actually written: a role that runs but leaves no usable report is reported",
8144
+ "as failed with reason `report-missing-or-empty`, never as a plain success.",
8145
+ "If one role fails, the rest keep running.",
8146
+ "",
8147
+ "### After `openteam-orchestrate` returns — READ the reports, then report",
8148
+ "",
8149
+ "When the tool returns, the orchestrated work is **already finished** — the",
8150
+ "call is synchronous and every subsession has completed (or failed) by then.",
8151
+ "You MUST:",
8152
+ "",
8153
+ "1. **READ each `report` file path** the tool returned (use the `read` tool).",
8154
+ " That file is the only place a role's findings exist; the tool result",
8155
+ " deliberately omits them.",
8156
+ "2. **Synthesise the real findings** from those files for the user: what was",
8157
+ " found, what changed, conclusions, risks and follow-ups.",
8158
+ "3. **Never report the task as `in_progress`, `pending`, or “the agent is",
8159
+ " working on it” after the tool has returned** — it has not; it is done.",
8160
+ "4. **Never describe, summarise, or invent work you have not read** from a",
8161
+ " report file. If a role is reported as failed with reason",
8162
+ " `report-missing-or-empty`, the durability gate ran it but its findings",
8163
+ " were **not preserved to disk**: tell the user plainly that this role's",
8164
+ " work was done but not saved and must be re-run — never fabricate what it",
8165
+ " might have found. If a role failed for any other reason, report the",
8166
+ " failure and its reason.",
9281
8167
  "",
9282
8168
  "**Use `task` directly** only for ad-hoc subagents that are not in the",
9283
8169
  "roster (e.g. an agent created on demand for a one-off job).",
@@ -9851,7 +8737,7 @@ async function listOpenAICompatibleModels(baseURL, fetch2) {
9851
8737
  return parseOpenAIModels(payload);
9852
8738
  }
9853
8739
  function parseOpenAIModels(payload) {
9854
- if (!isObject2(payload) || !Array.isArray(payload.data)) {
8740
+ if (!isObject(payload) || !Array.isArray(payload.data)) {
9855
8741
  throw new Error("Malformed /models response: expected data array");
9856
8742
  }
9857
8743
  return payload.data.map(parseOpenAIModel);
@@ -9886,7 +8772,7 @@ async function probeOpenAICompatibleRuntime(input) {
9886
8772
  }
9887
8773
  }
9888
8774
  function parseOpenAIModel(value) {
9889
- if (!isObject2(value) || typeof value.id !== "string" || value.id.length === 0) {
8775
+ if (!isObject(value) || typeof value.id !== "string" || value.id.length === 0) {
9890
8776
  throw new Error("Malformed /models response: model id must be a string");
9891
8777
  }
9892
8778
  const contextWindow = readContextWindow(value);
@@ -9932,7 +8818,7 @@ function readCapabilityBoolean(value) {
9932
8818
  }
9933
8819
  return;
9934
8820
  }
9935
- if (!isObject2(value)) {
8821
+ if (!isObject(value)) {
9936
8822
  return;
9937
8823
  }
9938
8824
  return readBoolean(value, [
@@ -9956,7 +8842,7 @@ function readContextWindow(model) {
9956
8842
  if (direct !== undefined) {
9957
8843
  return direct;
9958
8844
  }
9959
- if (isObject2(model.limit)) {
8845
+ if (isObject(model.limit)) {
9960
8846
  return readPositiveInteger(model.limit, ["context"]);
9961
8847
  }
9962
8848
  return;
@@ -9979,7 +8865,7 @@ function readPositiveInteger(object, keys) {
9979
8865
  }
9980
8866
  return;
9981
8867
  }
9982
- function isObject2(value) {
8868
+ function isObject(value) {
9983
8869
  return typeof value === "object" && value !== null && !Array.isArray(value);
9984
8870
  }
9985
8871
 
@@ -10139,11 +9025,11 @@ async function listLemonadeModels(baseURL, fetch2) {
10139
9025
  }
10140
9026
  function readNonTextModelIDs(payload) {
10141
9027
  const ids = new Set;
10142
- if (!isObject3(payload) || !Array.isArray(payload.data)) {
9028
+ if (!isObject2(payload) || !Array.isArray(payload.data)) {
10143
9029
  return ids;
10144
9030
  }
10145
9031
  for (const entry of payload.data) {
10146
- if (!isObject3(entry) || typeof entry.id !== "string") {
9032
+ if (!isObject2(entry) || typeof entry.id !== "string") {
10147
9033
  continue;
10148
9034
  }
10149
9035
  if (isNonTextModel(entry)) {
@@ -10172,12 +9058,12 @@ async function readLoadedModels(baseURL, fetch2) {
10172
9058
  } catch {
10173
9059
  return;
10174
9060
  }
10175
- if (!isObject3(payload) || !Array.isArray(payload.all_models_loaded)) {
9061
+ if (!isObject2(payload) || !Array.isArray(payload.all_models_loaded)) {
10176
9062
  return;
10177
9063
  }
10178
9064
  const loaded = new Map;
10179
9065
  for (const entry of payload.all_models_loaded) {
10180
- if (!isObject3(entry) || typeof entry.model_name !== "string") {
9066
+ if (!isObject2(entry) || typeof entry.model_name !== "string") {
10181
9067
  continue;
10182
9068
  }
10183
9069
  if (entry.loaded === false) {
@@ -10192,7 +9078,7 @@ function readSlotCount(entry) {
10192
9078
  }
10193
9079
  function slotsFromLlamacppArgs(entry) {
10194
9080
  const options = entry.recipe_options;
10195
- if (!isObject3(options) || typeof options.llamacpp_args !== "string") {
9081
+ if (!isObject2(options) || typeof options.llamacpp_args !== "string") {
10196
9082
  return;
10197
9083
  }
10198
9084
  const match = PARALLEL_ARG.exec(options.llamacpp_args);
@@ -10245,7 +9131,7 @@ function enrichWithLoadState(models, loaded) {
10245
9131
  return hit.slots === undefined ? { ...model, loaded: true } : { ...model, loaded: true, slots: hit.slots };
10246
9132
  });
10247
9133
  }
10248
- function isObject3(value) {
9134
+ function isObject2(value) {
10249
9135
  return typeof value === "object" && value !== null && !Array.isArray(value);
10250
9136
  }
10251
9137
 
@@ -10344,9 +9230,6 @@ class RuntimeRegistry {
10344
9230
  return options;
10345
9231
  }
10346
9232
  }
10347
- function anyRuntimeReachable(snapshots) {
10348
- return snapshots.some((snapshot) => snapshot.reachable);
10349
- }
10350
9233
 
10351
9234
  // src/cli/setupAdapters.ts
10352
9235
  class PromptCancelledError extends Error {
@@ -10441,484 +9324,253 @@ var DETECTION_RUNTIMES = [
10441
9324
  baseURL: LEMONADE_DEFAULT_BASE_URL,
10442
9325
  defaultModel: { providerID: "lemonade", modelID: "probe" }
10443
9326
  },
10444
- {
10445
- id: "foundry-local",
10446
- enabled: true,
10447
- discovery: "cli",
10448
- defaultModel: { providerID: "foundry-local", modelID: "probe" }
10449
- }
10450
- ];
10451
- function createLoadFrontierProfiles() {
10452
- return async () => {
10453
- try {
10454
- const result = await loadCapabilityProfiles({
10455
- fetch: globalThis.fetch,
10456
- now: () => Date.now()
10457
- });
10458
- return result.profiles.length > 0 ? result.profiles : [...CURATED_FRONTIER_PROFILES];
10459
- } catch {
10460
- return [...CURATED_FRONTIER_PROFILES];
10461
- }
10462
- };
10463
- }
10464
- function createDetect(exec) {
10465
- return async () => {
10466
- const registry = new RuntimeRegistry({
10467
- fetch: globalThis.fetch,
10468
- clock: () => new Date().toISOString(),
10469
- foundryDiscovery: foundryDiscoveryFromExec(exec)
10470
- });
10471
- const snapshots = await registry.probe(DETECTION_RUNTIMES);
10472
- return snapshots.map((snapshot) => {
10473
- const detected = {
10474
- id: snapshot.id,
10475
- baseURL: snapshot.baseURL,
10476
- reachable: snapshot.reachable,
10477
- models: snapshot.models.map((model) => model.modelID)
10478
- };
10479
- if (snapshot.models.some((model) => model.loaded !== undefined)) {
10480
- detected.modelInfo = snapshot.models;
10481
- }
10482
- if (snapshot.error !== undefined) {
10483
- detected.error = snapshot.error;
10484
- }
10485
- return detected;
10486
- });
10487
- };
10488
- }
10489
- function createProbeRuntime(exec) {
10490
- return async (id, baseURL) => {
10491
- const registry = new RuntimeRegistry({
10492
- fetch: globalThis.fetch,
10493
- clock: () => new Date().toISOString(),
10494
- foundryDiscovery: foundryDiscoveryFromExec(exec)
10495
- });
10496
- const runtime = {
10497
- id,
10498
- enabled: true,
10499
- baseURL,
10500
- defaultModel: { providerID: id, modelID: "probe" }
10501
- };
10502
- const [snapshot] = await registry.probe([runtime]);
10503
- const detected = {
10504
- id,
10505
- baseURL: snapshot?.baseURL ?? baseURL,
10506
- reachable: snapshot?.reachable ?? false,
10507
- models: snapshot?.models.map((model) => model.modelID) ?? []
10508
- };
10509
- if (snapshot?.models.some((model) => model.loaded !== undefined) === true) {
10510
- detected.modelInfo = snapshot.models;
10511
- }
10512
- if (snapshot?.error !== undefined) {
10513
- detected.error = snapshot.error;
10514
- }
10515
- return detected;
10516
- };
10517
- }
10518
-
10519
- // src/config/graphFeatureGate.ts
10520
- function checkCertificate(status, name) {
10521
- if (status.status === "absent") {
10522
- return {
10523
- code: `${name}-certificate-absent`,
10524
- detail: `${name} certificate not found — run the ${name} certification suite to produce it`
10525
- };
10526
- }
10527
- if (status.status === "invalid") {
10528
- return {
10529
- code: `${name}-certificate-invalid`,
10530
- detail: `${name} certificate rejected by recomputing parser: ${status.code} — ${status.detail}`
10531
- };
10532
- }
10533
- return;
10534
- }
10535
- function evaluateGraphGate(input) {
10536
- const violations = [];
10537
- const advisories = [];
10538
- const shadowV = checkCertificate(input.shadow, "shadow");
10539
- if (shadowV !== undefined)
10540
- violations.push(shadowV);
10541
- const releaseV = checkCertificate(input.release, "release");
10542
- if (releaseV !== undefined)
10543
- violations.push(releaseV);
10544
- const soakV = checkCertificate(input.soak, "soak");
10545
- if (soakV !== undefined)
10546
- advisories.push(soakV);
10547
- if (input.migration === "pending") {
10548
- violations.push({
10549
- code: "migration-pending",
10550
- detail: "legacy migration has pending work — run the migration tool to completion before enabling active mode"
10551
- });
10552
- } else if (input.migration === "unknown") {
10553
- violations.push({
10554
- code: "migration-unknown",
10555
- detail: "could not determine migration status — check ledger readability and storage access"
10556
- });
10557
- }
10558
- if (!input.operatorApproval) {
10559
- violations.push({
10560
- code: "operator-approval-missing",
10561
- detail: "set graph.operatorApproval to true in .opencode/openteam.json after reviewing all certificates and completing migration — this is a declaration of intent, not a proof"
10562
- });
10563
- }
10564
- return {
10565
- allowed: violations.length === 0,
10566
- violations,
10567
- advisories
10568
- };
10569
- }
10570
- function isGateApplicable(mode) {
10571
- return mode === "active";
10572
- }
10573
-
10574
- // src/messages/commands.ts
10575
- var clearCacheMessages = {
10576
- header: "openteam clear-cache — frozen plugin cache entries:",
10577
- columns: {
10578
- specDir: "spec dir",
10579
- pinned: "spec-pinned",
10580
- installed: "installed",
10581
- mtime: "mtime"
10582
- },
10583
- reparseSkipSuffix: " [SKIP — reparse point]",
10584
- deletedLabel: "deleted.",
10585
- lockedLabel: (message) => `[LOCKED] ${message}`,
10586
- pathOutsideWarning: (specDir, absolutePath) => ` ⚠ ${specDir}: path outside cacheRoot (${absolutePath}), skipped.`,
10587
- processed: (count) => `${count} entry(ies) processed.`,
10588
- found: (count) => `${count} entry(ies) found. Use --delete to remove them.`
10589
- };
10590
- var baselineMessages = {
10591
- effectiveAuto: "cheapest-capable (auto)",
10592
- pinnedSuffix: (ref) => `${ref} (pinned)`,
10593
- summary: (params) => [
10594
- "openteam baseline:",
10595
- ` mode: ${params.mode}`,
10596
- ` pinned: ${params.pinned}`,
10597
- ` hardDefault: ${params.hardDefault}`,
10598
- ` effective: ${params.effective}`
10599
- ],
10600
- invalidModel: (input) => `Invalid model "${input}". Use the provider/model format, e.g. anthropic/claude-sonnet-4-5.`,
10601
- pinnedTo: (ref) => `Baseline pinned to ${ref} (pinned mode).`,
10602
- autoMode: "Baseline set to auto mode (cheapest-capable)."
10603
- };
10604
- var localMessages = {
10605
- help: {
10606
- status: " openteam local status Show the routing mode (local-first / local-only / frontier-only)",
10607
- off: " openteam local off Use frontier models only (no local runtime)",
10608
- only: " openteam local only Use local models only (never frontier)",
10609
- on: " openteam local on Re-enable local-first routing"
10610
- },
10611
- noRuntimes: "none",
10612
- modeFrontierOnly: "frontier-only (frontierOnly)",
10613
- modeLocalOnly: "local-only (localOnly)",
10614
- modeLocalFirst: "local-first",
10615
- summary: (params) => [
10616
- "openteam local:",
10617
- ` mode: ${params.mode}`,
10618
- ` frontierOnly: ${params.frontierOnly}`,
10619
- ` localOnly: ${params.localOnly}`,
10620
- ` privacy: ${params.privacy}`,
10621
- ` runtimes: ${params.runtimes}`
10622
- ],
10623
- frontierOnlyNoChange: "No change: already in frontier-only mode.",
10624
- privacyAdjustedNote: " Privacy adjusted to 'consentBeforeFrontier' (forceLocalOnSensitive requires a local runtime).",
10625
- frontierOnlyEnabled: "Frontier-only mode enabled (frontierOnly=true, localOnly=false).",
10626
- localOnlyNoRuntimes: "Cannot enable local-only mode: no enabled local runtime is configured. Run 'openteam setup' or enable a local runtime in .opencode/openteam.json first.",
10627
- localOnlyNoChange: "No change: already in local-only mode.",
10628
- localOnlyEnabled: "Local-only mode enabled (localOnly=true, frontierOnly=false).",
10629
- localFirstNoChange: "No change: local-first routing is already active.",
10630
- localFirstReEnabled: "Local-first routing re-enabled (frontierOnly=false, localOnly=false). Make sure a local runtime is reachable.",
10631
- unknownSubcommand: (subcommand, help) => `Unknown local subcommand: ${subcommand}
10632
-
10633
- ${help}`
10634
- };
10635
- var frontierCatalogMessages = {
10636
- free: "free",
10637
- costPerMillion: (inputUSD, outputUSD) => `$${inputUSD}/$${outputUSD} per 1M`
10638
- };
10639
- var migrateMessages = {
10640
- header: "openteam migrate:",
10641
- nothingToMigrate: " nothing to migrate — the P15b layout is already in place.",
10642
- counts: (params) => [
10643
- ` moved: ${params.moved} file(s)`,
10644
- ` deduped: ${params.deduped} file(s) (destination already identical)`,
10645
- ` conflicts: ${params.conflicts} file(s)`
10646
- ],
10647
- relocatedHeader: " relocated:",
10648
- relocatedEntry: (from, to, deduped) => ` · ${from} → ${to}${deduped ? " (deduped)" : ""}`,
10649
- conflictsHeader: " ⚠ left in place (destination exists with different content — no data lost):",
10650
- conflictEntry: (from, to) => ` · ${from} → ${to}`,
10651
- manualWorktreesHeader: " ⚠ legacy git worktree(s) need a manual move (run `git worktree move`):",
10652
- manualWorktreeEntry: (label, from, to) => ` · ${label}: ${from} → ${to}`,
10653
- success: " ✓ machine-local runtime output relocated under .opencode/openteam-local/."
10654
- };
10655
- var rolesInitMessages = {
10656
- header: "openteam roles init:",
10657
- title: "openteam roles init",
10658
- noRoster: (path) => ` ✗ no roster at ${path}.`,
10659
- noRosterRemedy: " Cast the team first: run `openteam setup`, then ask the orchestrator to register the cast.",
10660
- unparseableRoster: (path, error) => ` ✗ ${path} is present but unparseable: ${error}`,
10661
- unparseableRosterRemedy: " Run `openteam doctor` for the remedy, then re-run this command.",
10662
- allProfiled: (count) => ` ✓ ${count} roster role(s) checked; no unprofiled roles — every role resolves a routing profile.`,
10663
- whyTitle: "Why you are being asked",
10664
- why: (count) => [
10665
- `${count} roster role(s) resolve no routing profile.`,
10666
- "Until a profile exists they are frontier-eligible: their work can escalate",
10667
- "to a frontier model. A 'local only' note in the roster prose does not",
10668
- "prevent this — prose outside the JSON fence is never read."
10669
- ].join(`
10670
- `),
10671
- policyQuestion: (roleID, agentName) => `Routing policy for "${roleID}" (${agentName})`,
10672
- localOnlyLabel: (roleID, agentName) => `Local only — ${roleID} (${agentName}) never leaves this machine`,
10673
- localOnlyHint: "sets requiresLocalRuntime; the role fails closed when no local runtime is reachable",
10674
- frontierOkLabel: "Local first, frontier allowed",
10675
- frontierOkHint: "records today's implicit behaviour: cheapest-capable frontier when the task warrants it",
10676
- skipLabel: "Skip for now",
10677
- skipHint: "leaves the role unprofiled, so it stays frontier-eligible and doctor keeps reporting it",
10678
- runtimeQuestion: (roleID) => `Preferred local runtime(s) for "${roleID}" (in failover order)`,
10679
- runtimeHint: "machine-local — written to the git-ignored overlay",
10680
- cancelled: (reason) => ` ✗ cancelled (${reason}); nothing was written.`,
10681
- invalidResult: (reason) => ` ✗ refusing to write: the resulting config is invalid — ${reason}`,
10682
- nothingWritten: (skipped) => ` · skipped ${skipped}; nothing written.`,
10683
- nothingToWriteOutro: "Nothing to write.",
10684
- pinnedLocal: (roleIDs, path) => ` ✓ pinned local-only: ${roleIDs} → ${path} (tracked, travels with the roster)`,
10685
- frontierAllowed: (roleIDs, path) => ` ✓ frontier allowed: ${roleIDs} → ${path} (model choice stays "auto": cheapest-capable per task)`,
10686
- runtimeBinding: (path) => ` ✓ runtime binding: ${path} (git-ignored, machine-local)`,
10687
- skippedSummary: (roleIDs) => ` · skipped: ${roleIDs} — still frontier-eligible; re-run this command to revisit.`,
10688
- outro: "Role policy updated."
10689
- };
10690
-
10691
- // src/orchestrator/ralphLoop.ts
10692
- var RALPH_ROLE_ID = "ralph";
10693
- var DEFAULT_RALPH_MAX_ITERATIONS = 10;
10694
- function ralphTaskInput(input) {
10695
- const task = {
10696
- roleID: RALPH_ROLE_ID,
10697
- task: input.task,
10698
- prompt: input.prompt,
10699
- config: input.config
9327
+ {
9328
+ id: "foundry-local",
9329
+ enabled: true,
9330
+ discovery: "cli",
9331
+ defaultModel: { providerID: "foundry-local", modelID: "probe" }
9332
+ }
9333
+ ];
9334
+ function createLoadFrontierProfiles() {
9335
+ return async () => {
9336
+ try {
9337
+ const result = await loadCapabilityProfiles({
9338
+ fetch: globalThis.fetch,
9339
+ now: () => Date.now()
9340
+ });
9341
+ return result.profiles.length > 0 ? result.profiles : [...CURATED_FRONTIER_PROFILES];
9342
+ } catch {
9343
+ return [...CURATED_FRONTIER_PROFILES];
9344
+ }
10700
9345
  };
10701
- if (input.parentSessionID !== undefined) {
10702
- task.parentSessionID = input.parentSessionID;
10703
- }
10704
- return task;
10705
- }
10706
- async function runRalphLoop(input, deps) {
10707
- const cap = input.maxIterations ?? DEFAULT_RALPH_MAX_ITERATIONS;
10708
- if (cap < 1) {
10709
- throw new Error(`openteam ralph loop: maxIterations must be >= 1, got ${cap}`);
10710
- }
10711
- const results = [];
10712
- const taskInput = ralphTaskInput(input);
10713
- while (results.length < cap) {
10714
- const result = await runRoleTask(taskInput, deps);
10715
- results.push(result);
10716
- if (deps.isDone(result)) {
10717
- return {
10718
- iterations: results.length,
10719
- termination: "done",
10720
- decision: result.decision,
10721
- results
9346
+ }
9347
+ function createDetect(exec) {
9348
+ return async () => {
9349
+ const registry = new RuntimeRegistry({
9350
+ fetch: globalThis.fetch,
9351
+ clock: () => new Date().toISOString(),
9352
+ foundryDiscovery: foundryDiscoveryFromExec(exec)
9353
+ });
9354
+ const snapshots = await registry.probe(DETECTION_RUNTIMES);
9355
+ return snapshots.map((snapshot) => {
9356
+ const detected = {
9357
+ id: snapshot.id,
9358
+ baseURL: snapshot.baseURL,
9359
+ reachable: snapshot.reachable,
9360
+ models: snapshot.models.map((model) => model.modelID)
10722
9361
  };
9362
+ if (snapshot.models.some((model) => model.loaded !== undefined)) {
9363
+ detected.modelInfo = snapshot.models;
9364
+ }
9365
+ if (snapshot.error !== undefined) {
9366
+ detected.error = snapshot.error;
9367
+ }
9368
+ return detected;
9369
+ });
9370
+ };
9371
+ }
9372
+ function createProbeRuntime(exec) {
9373
+ return async (id, baseURL) => {
9374
+ const registry = new RuntimeRegistry({
9375
+ fetch: globalThis.fetch,
9376
+ clock: () => new Date().toISOString(),
9377
+ foundryDiscovery: foundryDiscoveryFromExec(exec)
9378
+ });
9379
+ const runtime = {
9380
+ id,
9381
+ enabled: true,
9382
+ baseURL,
9383
+ defaultModel: { providerID: id, modelID: "probe" }
9384
+ };
9385
+ const [snapshot] = await registry.probe([runtime]);
9386
+ const detected = {
9387
+ id,
9388
+ baseURL: snapshot?.baseURL ?? baseURL,
9389
+ reachable: snapshot?.reachable ?? false,
9390
+ models: snapshot?.models.map((model) => model.modelID) ?? []
9391
+ };
9392
+ if (snapshot?.models.some((model) => model.loaded !== undefined) === true) {
9393
+ detected.modelInfo = snapshot.models;
10723
9394
  }
10724
- }
10725
- return {
10726
- iterations: results.length,
10727
- termination: "cap",
10728
- decision: results.at(-1)?.decision,
10729
- results
9395
+ if (snapshot?.error !== undefined) {
9396
+ detected.error = snapshot.error;
9397
+ }
9398
+ return detected;
10730
9399
  };
10731
9400
  }
10732
9401
 
10733
- // src/orchestrator/roster.ts
10734
- import { z as z17 } from "zod";
10735
- var RosterEntrySchema = z17.object({
10736
- roleID: z17.string().min(1),
10737
- agentName: z17.string().min(1)
10738
- }).strict();
10739
- var RosterSchema = z17.object({
10740
- universe: z17.string().min(1),
10741
- entries: z17.array(RosterEntrySchema)
10742
- }).strict();
10743
- var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
10744
-
10745
- class RosterParseError extends Error {
10746
- code;
10747
- constructor(code, message) {
10748
- super(message);
10749
- this.name = "RosterParseError";
10750
- this.code = code;
10751
- }
10752
- }
10753
- function parseRoster(text2) {
10754
- const result = parseRosterResult(text2);
10755
- if (!result.ok) {
10756
- throw result.error;
10757
- }
10758
- return result.roster;
10759
- }
10760
- function parseRosterResult(text2) {
10761
- const match = ROSTER_JSON_FENCE.exec(text2);
10762
- if (match === null || match[1] === undefined) {
10763
- return {
10764
- ok: false,
10765
- error: new RosterParseError("missing-json-block", "openteam roster: no JSON block found in roster file")
10766
- };
10767
- }
10768
- let data;
10769
- try {
10770
- data = JSON.parse(match[1]);
10771
- } catch (error) {
9402
+ // src/config/graphFeatureGate.ts
9403
+ function checkCertificate(status, name) {
9404
+ if (status.status === "absent") {
10772
9405
  return {
10773
- ok: false,
10774
- error: new RosterParseError("malformed-json", `openteam roster: malformed JSON in roster file: ${String(error)}`)
9406
+ code: `${name}-certificate-absent`,
9407
+ detail: `${name} certificate not found — run the ${name} certification suite to produce it`
10775
9408
  };
10776
9409
  }
10777
- const parsed = RosterSchema.safeParse(data);
10778
- if (!parsed.success) {
9410
+ if (status.status === "invalid") {
10779
9411
  return {
10780
- ok: false,
10781
- error: new RosterParseError("invalid-roster-schema", `openteam roster: invalid roster schema: ${parsed.error.message}`)
9412
+ code: `${name}-certificate-invalid`,
9413
+ detail: `${name} certificate rejected by recomputing parser: ${status.code} — ${status.detail}`
10782
9414
  };
10783
9415
  }
10784
- return { ok: true, roster: parsed.data };
10785
- }
10786
- var GUARANTEED_ROLE_IDS = [
10787
- "orchestrator",
10788
- "guardian",
10789
- "scribe",
10790
- "ralph"
10791
- ];
10792
- var GUARANTEED_ROLE_DEFAULT_AGENT_NAMES = {
10793
- orchestrator: "openteam",
10794
- guardian: "guardian",
10795
- scribe: "scribe",
10796
- ralph: "ralph"
10797
- };
10798
- function defaultAgentNameForGuaranteedRole(roleID) {
10799
- return GUARANTEED_ROLE_DEFAULT_AGENT_NAMES[roleID];
10800
- }
10801
- function isGuaranteedRoleID(roleID) {
10802
- return GUARANTEED_ROLE_IDS.includes(roleID);
10803
- }
10804
- function missingGuaranteedRoles(roster) {
10805
- const present = new Set(roster.entries.map((entry) => entry.roleID));
10806
- return GUARANTEED_ROLE_IDS.filter((roleID) => !present.has(roleID));
10807
- }
10808
- function isRoleProfileLike(value) {
10809
- return typeof value === "object" && value !== null && "roleID" in value && "defaultTier" in value;
10810
- }
10811
- function configuredRolesFromAuditInput(input) {
10812
- if ("configuredRoles" in input && !isRoleProfileLike(input.configuredRoles)) {
10813
- return input.configuredRoles;
10814
- }
10815
- return input;
9416
+ return;
10816
9417
  }
10817
- function auditRoster(rosterOrText, configuredRolesOrOptions = {}) {
10818
- const configuredRoles = configuredRolesFromAuditInput(configuredRolesOrOptions);
10819
- if (typeof rosterOrText === "string") {
10820
- const parsed = parseRosterResult(rosterOrText);
10821
- if (!parsed.ok) {
10822
- return {
10823
- findings: [
10824
- {
10825
- kind: "unparseable-roster",
10826
- roleID: "roster",
10827
- code: parsed.error.code,
10828
- message: parsed.error.message
10829
- }
10830
- ],
10831
- hasAllGuaranteedRoles: false,
10832
- missingGuaranteedRoleIDs: [],
10833
- unprofiledRoleEntries: [],
10834
- misassignedGuaranteedRoleEntries: []
10835
- };
10836
- }
10837
- return auditParsedRoster(parsed.roster, configuredRoles);
9418
+ function evaluateGraphGate(input) {
9419
+ const violations = [];
9420
+ const advisories = [];
9421
+ const shadowV = checkCertificate(input.shadow, "shadow");
9422
+ if (shadowV !== undefined)
9423
+ violations.push(shadowV);
9424
+ const releaseV = checkCertificate(input.release, "release");
9425
+ if (releaseV !== undefined)
9426
+ violations.push(releaseV);
9427
+ const soakV = checkCertificate(input.soak, "soak");
9428
+ if (soakV !== undefined)
9429
+ advisories.push(soakV);
9430
+ if (input.migration === "pending") {
9431
+ violations.push({
9432
+ code: "migration-pending",
9433
+ detail: "legacy migration has pending work — run the migration tool to completion before enabling active mode"
9434
+ });
9435
+ } else if (input.migration === "unknown") {
9436
+ violations.push({
9437
+ code: "migration-unknown",
9438
+ detail: "could not determine migration status — check ledger readability and storage access"
9439
+ });
10838
9440
  }
10839
- return auditParsedRoster(rosterOrText, configuredRoles);
10840
- }
10841
- function auditParsedRoster(roster, configuredRoles) {
10842
- const missingGuaranteedRoleIDs = missingGuaranteedRoles(roster);
10843
- const missing = new Set(missingGuaranteedRoleIDs);
10844
- const findings = missingGuaranteedRoleIDs.map((roleID) => ({
10845
- kind: "missing-guaranteed-role",
10846
- roleID
10847
- }));
10848
- const unprofiledRoleEntries = [];
10849
- const misassignedGuaranteedRoleEntries = [];
10850
- roster.entries.forEach((entry) => {
10851
- const profile = getRoleProfile(entry.roleID, configuredRoles);
10852
- if (profile === undefined && !isKnownNonWorkerRole(entry.roleID)) {
10853
- unprofiledRoleEntries.push({
10854
- roleID: entry.roleID,
10855
- agentName: entry.agentName,
10856
- severity: "info"
10857
- });
10858
- findings.push({
10859
- kind: "unresolved-role-profile",
10860
- roleID: entry.roleID,
10861
- agentName: entry.agentName
10862
- });
10863
- }
10864
- if (isGuaranteedRoleID(entry.agentName) && entry.roleID !== entry.agentName && missing.has(entry.agentName)) {
10865
- misassignedGuaranteedRoleEntries.push({
10866
- roleID: entry.roleID,
10867
- agentName: entry.agentName,
10868
- missingRoleID: entry.agentName,
10869
- reason: "agentNameMatchesMissingGuaranteedRole"
10870
- });
10871
- findings.push({
10872
- kind: "non-canonical-role-id",
10873
- roleID: entry.roleID,
10874
- agentName: entry.agentName,
10875
- canonicalRoleID: entry.agentName
10876
- });
10877
- return;
10878
- }
10879
- if (profile !== undefined && isGuaranteedRoleID(profile.roleID) && profile.roleID !== entry.roleID && missing.has(profile.roleID)) {
10880
- misassignedGuaranteedRoleEntries.push({
10881
- roleID: entry.roleID,
10882
- agentName: entry.agentName,
10883
- missingRoleID: profile.roleID,
10884
- reason: "roleIDResolvesToMissingGuaranteedRole"
10885
- });
10886
- findings.push({
10887
- kind: "non-canonical-role-id",
10888
- roleID: entry.roleID,
10889
- agentName: entry.agentName,
10890
- canonicalRoleID: profile.roleID
10891
- });
10892
- }
10893
- });
10894
- return {
10895
- findings,
10896
- hasAllGuaranteedRoles: missingGuaranteedRoleIDs.length === 0,
10897
- missingGuaranteedRoleIDs,
10898
- unprofiledRoleEntries,
10899
- misassignedGuaranteedRoleEntries
10900
- };
10901
- }
10902
- function enforceGuaranteedRoles(roster) {
10903
- const missing = missingGuaranteedRoles(roster);
10904
- if (missing.length === 0) {
10905
- return roster;
9441
+ if (!input.operatorApproval) {
9442
+ violations.push({
9443
+ code: "operator-approval-missing",
9444
+ detail: "set graph.operatorApproval to true in .opencode/openteam.json after reviewing all certificates and completing migration — this is a declaration of intent, not a proof"
9445
+ });
10906
9446
  }
10907
- const added = missing.map((roleID) => ({
10908
- roleID,
10909
- agentName: defaultAgentNameForGuaranteedRole(roleID)
10910
- }));
10911
- return { universe: roster.universe, entries: [...roster.entries, ...added] };
10912
- }
10913
- async function generateRoster(prompt, source) {
10914
- const draft = await source(prompt);
10915
- const validated = RosterSchema.parse(draft);
10916
- return enforceGuaranteedRoles(validated);
9447
+ return {
9448
+ allowed: violations.length === 0,
9449
+ violations,
9450
+ advisories
9451
+ };
10917
9452
  }
10918
- function missingAgentFileMessage(agentName) {
10919
- return `openteam: guaranteed role agent "${agentName}" has no agent file in this workspace, ` + `so it cannot be dispatched. Run \`openteam setup\` to generate the required agent ` + `files before dispatching (see #153).`;
9453
+ function isGateApplicable(mode) {
9454
+ return mode === "active";
10920
9455
  }
10921
9456
 
9457
+ // src/messages/commands.ts
9458
+ var clearCacheMessages = {
9459
+ header: "openteam clear-cache — frozen plugin cache entries:",
9460
+ columns: {
9461
+ specDir: "spec dir",
9462
+ pinned: "spec-pinned",
9463
+ installed: "installed",
9464
+ mtime: "mtime"
9465
+ },
9466
+ reparseSkipSuffix: " [SKIP — reparse point]",
9467
+ deletedLabel: "deleted.",
9468
+ lockedLabel: (message) => `[LOCKED] ${message}`,
9469
+ pathOutsideWarning: (specDir, absolutePath) => ` ⚠ ${specDir}: path outside cacheRoot (${absolutePath}), skipped.`,
9470
+ processed: (count) => `${count} entry(ies) processed.`,
9471
+ found: (count) => `${count} entry(ies) found. Use --delete to remove them.`
9472
+ };
9473
+ var baselineMessages = {
9474
+ effectiveAuto: "cheapest-capable (auto)",
9475
+ pinnedSuffix: (ref) => `${ref} (pinned)`,
9476
+ summary: (params) => [
9477
+ "openteam baseline:",
9478
+ ` mode: ${params.mode}`,
9479
+ ` pinned: ${params.pinned}`,
9480
+ ` hardDefault: ${params.hardDefault}`,
9481
+ ` effective: ${params.effective}`
9482
+ ],
9483
+ invalidModel: (input) => `Invalid model "${input}". Use the provider/model format, e.g. anthropic/claude-sonnet-4-5.`,
9484
+ pinnedTo: (ref) => `Baseline pinned to ${ref} (pinned mode).`,
9485
+ autoMode: "Baseline set to auto mode (cheapest-capable)."
9486
+ };
9487
+ var localMessages = {
9488
+ help: {
9489
+ status: " openteam local status Show the routing mode (local-first / local-only / frontier-only)",
9490
+ off: " openteam local off Use frontier models only (no local runtime)",
9491
+ only: " openteam local only Use local models only (never frontier)",
9492
+ on: " openteam local on Re-enable local-first routing"
9493
+ },
9494
+ noRuntimes: "none",
9495
+ modeFrontierOnly: "frontier-only (frontierOnly)",
9496
+ modeLocalOnly: "local-only (localOnly)",
9497
+ modeLocalFirst: "local-first",
9498
+ summary: (params) => [
9499
+ "openteam local:",
9500
+ ` mode: ${params.mode}`,
9501
+ ` frontierOnly: ${params.frontierOnly}`,
9502
+ ` localOnly: ${params.localOnly}`,
9503
+ ` privacy: ${params.privacy}`,
9504
+ ` runtimes: ${params.runtimes}`
9505
+ ],
9506
+ frontierOnlyNoChange: "No change: already in frontier-only mode.",
9507
+ privacyAdjustedNote: " Privacy adjusted to 'consentBeforeFrontier' (forceLocalOnSensitive requires a local runtime).",
9508
+ frontierOnlyEnabled: "Frontier-only mode enabled (frontierOnly=true, localOnly=false).",
9509
+ localOnlyNoRuntimes: "Cannot enable local-only mode: no enabled local runtime is configured. Run 'openteam setup' or enable a local runtime in .opencode/openteam.json first.",
9510
+ localOnlyNoChange: "No change: already in local-only mode.",
9511
+ localOnlyEnabled: "Local-only mode enabled (localOnly=true, frontierOnly=false).",
9512
+ localFirstNoChange: "No change: local-first routing is already active.",
9513
+ localFirstReEnabled: "Local-first routing re-enabled (frontierOnly=false, localOnly=false). Make sure a local runtime is reachable.",
9514
+ unknownSubcommand: (subcommand, help) => `Unknown local subcommand: ${subcommand}
9515
+
9516
+ ${help}`
9517
+ };
9518
+ var frontierCatalogMessages = {
9519
+ free: "free",
9520
+ costPerMillion: (inputUSD, outputUSD) => `$${inputUSD}/$${outputUSD} per 1M`
9521
+ };
9522
+ var migrateMessages = {
9523
+ header: "openteam migrate:",
9524
+ nothingToMigrate: " nothing to migrate — the P15b layout is already in place.",
9525
+ counts: (params) => [
9526
+ ` moved: ${params.moved} file(s)`,
9527
+ ` deduped: ${params.deduped} file(s) (destination already identical)`,
9528
+ ` conflicts: ${params.conflicts} file(s)`
9529
+ ],
9530
+ relocatedHeader: " relocated:",
9531
+ relocatedEntry: (from, to, deduped) => ` · ${from} → ${to}${deduped ? " (deduped)" : ""}`,
9532
+ conflictsHeader: " ⚠ left in place (destination exists with different content — no data lost):",
9533
+ conflictEntry: (from, to) => ` · ${from} → ${to}`,
9534
+ manualWorktreesHeader: " ⚠ legacy git worktree(s) need a manual move (run `git worktree move`):",
9535
+ manualWorktreeEntry: (label, from, to) => ` · ${label}: ${from} → ${to}`,
9536
+ success: " ✓ machine-local runtime output relocated under .opencode/openteam-local/."
9537
+ };
9538
+ var rolesInitMessages = {
9539
+ header: "openteam roles init:",
9540
+ title: "openteam roles init",
9541
+ noRoster: (path) => ` ✗ no roster at ${path}.`,
9542
+ noRosterRemedy: " Cast the team first: run `openteam setup`, then ask the orchestrator to register the cast.",
9543
+ unparseableRoster: (path, error) => ` ✗ ${path} is present but unparseable: ${error}`,
9544
+ unparseableRosterRemedy: " Run `openteam doctor` for the remedy, then re-run this command.",
9545
+ allProfiled: (count) => ` ✓ ${count} roster role(s) checked; no unprofiled roles — every role resolves a routing profile.`,
9546
+ whyTitle: "Why you are being asked",
9547
+ why: (count) => [
9548
+ `${count} roster role(s) resolve no routing profile.`,
9549
+ "Until a profile exists they are frontier-eligible: their work can escalate",
9550
+ "to a frontier model. A 'local only' note in the roster prose does not",
9551
+ "prevent this — prose outside the JSON fence is never read."
9552
+ ].join(`
9553
+ `),
9554
+ policyQuestion: (roleID, agentName) => `Routing policy for "${roleID}" (${agentName})`,
9555
+ localOnlyLabel: (roleID, agentName) => `Local only — ${roleID} (${agentName}) never leaves this machine`,
9556
+ localOnlyHint: "sets requiresLocalRuntime; the role fails closed when no local runtime is reachable",
9557
+ frontierOkLabel: "Local first, frontier allowed",
9558
+ frontierOkHint: "records today's implicit behaviour: cheapest-capable frontier when the task warrants it",
9559
+ skipLabel: "Skip for now",
9560
+ skipHint: "leaves the role unprofiled, so it stays frontier-eligible and doctor keeps reporting it",
9561
+ runtimeQuestion: (roleID) => `Preferred local runtime(s) for "${roleID}" (in failover order)`,
9562
+ runtimeHint: "machine-local — written to the git-ignored overlay",
9563
+ cancelled: (reason) => ` ✗ cancelled (${reason}); nothing was written.`,
9564
+ invalidResult: (reason) => ` ✗ refusing to write: the resulting config is invalid — ${reason}`,
9565
+ nothingWritten: (skipped) => ` · skipped ${skipped}; nothing written.`,
9566
+ nothingToWriteOutro: "Nothing to write.",
9567
+ pinnedLocal: (roleIDs, path) => ` ✓ pinned local-only: ${roleIDs} → ${path} (tracked, travels with the roster)`,
9568
+ frontierAllowed: (roleIDs, path) => ` ✓ frontier allowed: ${roleIDs} → ${path} (model choice stays "auto": cheapest-capable per task)`,
9569
+ runtimeBinding: (path) => ` ✓ runtime binding: ${path} (git-ignored, machine-local)`,
9570
+ skippedSummary: (roleIDs) => ` · skipped: ${roleIDs} — still frontier-eligible; re-run this command to revisit.`,
9571
+ outro: "Role policy updated."
9572
+ };
9573
+
10922
9574
  // src/orchestrator/worktreeReconciler.ts
10923
9575
  function planOne(wt) {
10924
9576
  const base = { runID: wt.runID, path: wt.path };
@@ -10973,11 +9625,11 @@ function planWorktreeReconciliation(input) {
10973
9625
  }
10974
9626
 
10975
9627
  // src/telemetry/otelConfig.ts
10976
- import { z as z18 } from "zod";
10977
- var OtelBackendConfigSchema = z18.object({
10978
- backend: z18.literal("opentelemetry"),
10979
- connectionEnv: z18.string().min(1),
10980
- serviceName: z18.string().min(1).optional()
9628
+ import { z as z17 } from "zod";
9629
+ var OtelBackendConfigSchema = z17.object({
9630
+ backend: z17.literal("opentelemetry"),
9631
+ connectionEnv: z17.string().min(1),
9632
+ serviceName: z17.string().min(1).optional()
10981
9633
  }).strict();
10982
9634
  function parseConnectionString(raw) {
10983
9635
  const pairs = new Map;
@@ -11501,7 +10153,7 @@ function gitignoreSection(gitignore) {
11501
10153
  lines.push(` remedy: add ${quoted} to ${gitignore.path} (or re-run \`openteam setup\`), then re-run \`openteam doctor\`.`);
11502
10154
  return lines;
11503
10155
  }
11504
- function agentModelsSection(diagnostics, searchedPaths) {
10156
+ function agentModelsSection(diagnostics, searchedPaths, localOnly) {
11505
10157
  const lines = [" agent models:"];
11506
10158
  if (diagnostics.length === 0) {
11507
10159
  lines.push(" (no agent files in .opencode/agent/)");
@@ -11515,6 +10167,13 @@ function agentModelsSection(diagnostics, searchedPaths) {
11515
10167
  if (broken > 0) {
11516
10168
  lines.push(` ⚠ ${broken} agent model(s) will not resolve. This is an agent 'model:' declaration error, not the openteam router.`);
11517
10169
  }
10170
+ const localOnlyGap = localOnly ? diagnostics.find((d) => d.mode === "primary" && d.resolution?.status === "unresolvable" && d.resolution.reason === "provider-not-configured") : undefined;
10171
+ if (localOnlyGap?.model !== undefined) {
10172
+ lines.push(` ✗ local-only mode, but the primary agent '${localOnlyGap.name}' points at provider '${localOnlyGap.model.providerID}', which opencode.json does not configure.`);
10173
+ lines.push(" cause: `openteam setup` recorded the frontier baseline placeholder as this agent's `model:`. That value is correctly inert as baseline.hardDefault while router.localOnly is true, but an agent's own `model:` is load-bearing, so opencode cannot resolve the coordinator against your local-only providers.");
10174
+ lines.push(" consequence: opencode cannot start the openteam coordinator, so it reads no roster and splits no work — even though every subagent resolves on your local runtime.");
10175
+ lines.push(` remedy: re-run \`openteam setup\` and keep local-only mode to regenerate ${localOnlyGap.sourceFile} with your local thinking model, then re-run \`openteam doctor\`.`);
10176
+ }
11518
10177
  lines.push(" note: 'declared' = model set in .opencode/agent/<name>.md frontmatter; 'inherited' = opencode.json default.");
11519
10178
  lines.push(" note: static checks are offline and deterministic; live checks need a reachable local runtime and can be inconclusive.");
11520
10179
  return lines;
@@ -11603,7 +10262,7 @@ function renderDoctor(input) {
11603
10262
  lines.push(" Run `openteam clear-cache --delete` to clean up.");
11604
10263
  }
11605
10264
  if (input.agentModels !== undefined) {
11606
- lines.push(...agentModelsSection(input.agentModels, input.opencodeConfigPaths ?? []));
10265
+ lines.push(...agentModelsSection(input.agentModels, input.opencodeConfigPaths ?? [], input.config.router.localOnly));
11607
10266
  }
11608
10267
  if (input.opencodeConfigError !== undefined) {
11609
10268
  lines.push(` ⚠ opencode config parse error: ${input.opencodeConfigError}`);
@@ -11914,31 +10573,31 @@ function mergeOpencodeConfigs(base, override) {
11914
10573
  }
11915
10574
 
11916
10575
  // src/graph/soakLedger.ts
11917
- import { z as z19 } from "zod";
11918
- var Sha256Schema2 = z19.string().regex(/^[0-9a-f]{64}$/);
11919
- var SoakPlatformSchema = z19.enum(["linux", "win32", "darwin"]);
11920
- var PrivacyPairSchema = z19.object({
11921
- surfacesScanned: z19.number().int().nonnegative(),
11922
- rawFindings: z19.number().int().nonnegative()
10576
+ import { z as z18 } from "zod";
10577
+ var Sha256Schema2 = z18.string().regex(/^[0-9a-f]{64}$/);
10578
+ var SoakPlatformSchema = z18.enum(["linux", "win32", "darwin"]);
10579
+ var PrivacyPairSchema = z18.object({
10580
+ surfacesScanned: z18.number().int().nonnegative(),
10581
+ rawFindings: z18.number().int().nonnegative()
11923
10582
  }).strict();
11924
- var DuplicateEffectPairSchema = z19.object({
11925
- effectsExamined: z19.number().int().nonnegative(),
11926
- duplicatesFound: z19.number().int().nonnegative()
10583
+ var DuplicateEffectPairSchema = z18.object({
10584
+ effectsExamined: z18.number().int().nonnegative(),
10585
+ duplicatesFound: z18.number().int().nonnegative()
11927
10586
  }).strict();
11928
- var ModelVerificationPairSchema = z19.object({
11929
- nodesChecked: z19.number().int().nonnegative(),
11930
- unverified: z19.number().int().nonnegative(),
11931
- mismatched: z19.number().int().nonnegative()
10587
+ var ModelVerificationPairSchema = z18.object({
10588
+ nodesChecked: z18.number().int().nonnegative(),
10589
+ unverified: z18.number().int().nonnegative(),
10590
+ mismatched: z18.number().int().nonnegative()
11932
10591
  }).strict();
11933
- var SoakObservationSchema = z19.object({
11934
- version: z19.literal(1),
11935
- seq: z19.number().int().nonnegative(),
11936
- timestamp: z19.string().datetime(),
10592
+ var SoakObservationSchema = z18.object({
10593
+ version: z18.literal(1),
10594
+ seq: z18.number().int().nonnegative(),
10595
+ timestamp: z18.string().datetime(),
11937
10596
  platform: SoakPlatformSchema,
11938
- opencodeVersion: z19.string().min(1),
11939
- provenance: z19.enum(["genuine-usage", "ci-synthetic"]),
10597
+ opencodeVersion: z18.string().min(1),
10598
+ provenance: z18.enum(["genuine-usage", "ci-synthetic"]),
11940
10599
  traceDigest: Sha256Schema2,
11941
- criticalDivergences: z19.number().int().nonnegative().nullable(),
10600
+ criticalDivergences: z18.number().int().nonnegative().nullable(),
11942
10601
  privacy: PrivacyPairSchema.nullable(),
11943
10602
  duplicateEffects: DuplicateEffectPairSchema.nullable(),
11944
10603
  modelVerification: ModelVerificationPairSchema.nullable(),
@@ -12297,7 +10956,7 @@ function buildOpenTeamConfig(answers) {
12297
10956
  return runtime;
12298
10957
  });
12299
10958
  const primaryLocal = firstEnabled(answers.runtimes);
12300
- const localDefault2 = primaryLocal !== undefined ? {
10959
+ const localDefault = primaryLocal !== undefined ? {
12301
10960
  providerID: choiceProviderID(primaryLocal),
12302
10961
  modelID: primaryLocal.defaultModelID
12303
10962
  } : null;
@@ -12310,7 +10969,7 @@ function buildOpenTeamConfig(answers) {
12310
10969
  },
12311
10970
  router: {
12312
10971
  mode: answers.routerMode,
12313
- localDefault: localDefault2,
10972
+ localDefault,
12314
10973
  frontierOnly: primaryLocal === undefined,
12315
10974
  localOnly
12316
10975
  },
@@ -12319,6 +10978,19 @@ function buildOpenTeamConfig(answers) {
12319
10978
  graph: { mode: "active" }
12320
10979
  });
12321
10980
  }
10981
+ function orchestratorAgentModel(answers) {
10982
+ const primaryLocal = firstEnabled(answers.runtimes);
10983
+ if (answers.localOnly === true && primaryLocal !== undefined) {
10984
+ return {
10985
+ providerID: choiceProviderID(primaryLocal),
10986
+ modelID: primaryLocal.thinkingModelID ?? primaryLocal.defaultModelID
10987
+ };
10988
+ }
10989
+ return {
10990
+ providerID: answers.frontier.providerID,
10991
+ modelID: answers.frontier.modelID
10992
+ };
10993
+ }
12322
10994
  function buildOpencodeConfig(answers) {
12323
10995
  const provider = {};
12324
10996
  const providerOwner = new Map;
@@ -12849,7 +11521,7 @@ async function runSetup(deps) {
12849
11521
  };
12850
11522
  const openTeamConfig = buildOpenTeamConfig(answers);
12851
11523
  const opencodeConfig = buildOpencodeConfig(answers);
12852
- const orchestratorAgent = buildOrchestratorAgent(frontier, { yolo });
11524
+ const orchestratorAgent = buildOrchestratorAgent(orchestratorAgentModel(answers), { yolo });
12853
11525
  const slashCommands = buildOpenteamCommands();
12854
11526
  const openTeamPath = DEFAULT_CONFIG_PATH;
12855
11527
  const existingOpencodePath = await deps.resolveOpencodeWriteTarget(OPENCODE_CONFIG_CANDIDATES);
@@ -13627,8 +12299,6 @@ var HELP = [
13627
12299
  " openteam graph cancel <run> Cancel an active Graph run",
13628
12300
  " openteam graph recover Recover a partial tail of the Graph journal",
13629
12301
  " openteam graph worktrees Reconcile orphaned Graph worktrees",
13630
- " openteam loop Unattended Ralph-pattern loop over the backlog (see #152)",
13631
- " openteam loop --dry-run Preview the loop plan without dispatching anything",
13632
12302
  " openteam mcp Read-only MCP server (team memory) over stdio",
13633
12303
  " openteam report Cost/savings summary (telemetry)",
13634
12304
  " openteam clear-cache List the plugin's frozen cache entries",
@@ -13670,29 +12340,6 @@ var PURGE_HELP = [
13670
12340
  "A non-interactive terminal without --yes fails instead of hanging."
13671
12341
  ].join(`
13672
12342
  `);
13673
- var LOOP_HELP = [
13674
- "openteam loop — unattended Ralph-pattern loop over the shared backlog (#152)",
13675
- "",
13676
- "Usage:",
13677
- " openteam loop --dry-run Preview the plan (iterations, prompt, stops); dispatch nothing",
13678
- " openteam loop --yes Run unattended until the backlog is empty or a stop fires",
13679
- "",
13680
- "Options:",
13681
- " --max-iterations <N> Maximum iterations (default 20)",
13682
- " --budget-usd <N> Spend ceiling",
13683
- " --sleep <seconds> Pause between iterations (default 2)",
13684
- " --no-progress-limit <N> Stop after N iterations with no progress (default 2)",
13685
- " --fresh-context New context per iteration (default on — the pure Ralph pattern)",
13686
- " --no-fresh-context Keep context across iterations",
13687
- " --stop-on-red-tests Stop when the gates go red",
13688
- " --backlog <path> Backlog Markdown file (default .opencode/openteam/backlog.md)",
13689
- " --dry-run Print the plan and invoke nothing",
13690
- " --yes Run unattended",
13691
- "",
13692
- "Stops are evaluated BEFORE each iteration spends: empty backlog, max-iterations,",
13693
- "budget, red tests, no-progress."
13694
- ].join(`
13695
- `);
13696
12343
  var GRAPH_OPERATOR_TASK_ID = "graph-operator";
13697
12344
  var PURGE_FLAG_TO_CATEGORY = new Map(Object.entries(PURGE_CATEGORY_FLAG).map(([id, flag]) => [flag, id]));
13698
12345
  function parseArgs(argv) {
@@ -13875,8 +12522,8 @@ async function regenerateOrchestratorAgent(deps, config, enable) {
13875
12522
  Warning: could not regenerate ${deps.orchestratorAgentPath} (missing 'model' in opencode.json); adjust its permission by hand.`;
13876
12523
  }
13877
12524
  try {
13878
- const frontier = parseModelRef(model);
13879
- const contents = buildOrchestratorAgent(frontier, { yolo: enable });
12525
+ const agentModel2 = parseModelRef(model);
12526
+ const contents = buildOrchestratorAgent(agentModel2, { yolo: enable });
13880
12527
  await deps.writeOrchestratorAgent(contents, deps.orchestratorAgentPath);
13881
12528
  return `
13882
12529
  ${deps.orchestratorAgentPath} regenerated (permission aligned with YOLO).`;
@@ -13998,13 +12645,13 @@ async function resolveOperatorGraphPolicy(config, operator) {
13998
12645
  };
13999
12646
  }
14000
12647
  function checkOperatorGate(policy) {
14001
- const decision2 = decideIngress(policy, GRAPH_OPERATOR_TASK_ID);
14002
- if (decision2.route === "graph") {
12648
+ const decision = decideIngress(policy, GRAPH_OPERATOR_TASK_ID);
12649
+ if (decision.route === "graph") {
14003
12650
  return;
14004
12651
  }
14005
12652
  return {
14006
12653
  exitCode: 1,
14007
- stdout: renderLegacyGraphReason(decision2)
12654
+ stdout: renderLegacyGraphReason(decision)
14008
12655
  };
14009
12656
  }
14010
12657
  async function runGraphStatus(deps, configPath, configResolution) {
@@ -14072,327 +12719,62 @@ async function runGraphRecover(deps, configPath, configResolution) {
14072
12719
  const operator = requireGraphOperator(deps);
14073
12720
  const snapshot = await deps.readGraphSnapshot(configPath, configResolution);
14074
12721
  const plan2 = planGraphRecover(snapshot);
14075
- if (plan2.kind === "none") {
14076
- return {
14077
- exitCode: 0,
14078
- stdout: `openteam graph recover: ${plan2.reason}`
14079
- };
14080
- }
14081
- if (plan2.kind === "blocked") {
14082
- return {
14083
- exitCode: 1,
14084
- stdout: `openteam graph recover: ${plan2.reason}`
14085
- };
14086
- }
14087
- const config = await deps.loadConfig(configPath, configResolution);
14088
- const policy = await resolveOperatorGraphPolicy(config, operator);
14089
- const blocked = checkOperatorGate(policy);
14090
- if (blocked !== undefined) {
14091
- return blocked;
14092
- }
14093
- const recovered = await operator.recovery.recover(config);
14094
- return {
14095
- exitCode: 0,
14096
- stdout: `openteam graph recover: ${recovered.reason} (${recovered.tail})`
14097
- };
14098
- }
14099
- async function runGraphWorktrees(deps, configPath, configResolution) {
14100
- const operator = requireGraphOperator(deps);
14101
- const config = await deps.loadConfig(configPath, configResolution);
14102
- const enabled = config.graph?.worktrees?.enabled ?? false;
14103
- if (!enabled) {
14104
- return {
14105
- exitCode: 0,
14106
- stdout: renderWorktreeReconciliation(planWorktreeReconciliation({ enabled: false, worktrees: [] }))
14107
- };
14108
- }
14109
- const policy = await resolveOperatorGraphPolicy(config, operator);
14110
- const blocked = checkOperatorGate(policy);
14111
- if (blocked !== undefined) {
14112
- return blocked;
14113
- }
14114
- const observations = await operator.worktrees.listObservations(config);
14115
- const report = planWorktreeReconciliation({
14116
- enabled,
14117
- worktrees: observations
14118
- });
14119
- if (report.plans.length === 0) {
14120
- return { exitCode: 0, stdout: renderWorktreeReconciliation(report) };
14121
- }
14122
- for (const plan2 of report.plans) {
14123
- if (!plan2.safe || plan2.action === "retain" || plan2.action === "block") {
14124
- continue;
14125
- }
14126
- await operator.worktrees.apply(plan2.action, plan2.runID);
14127
- }
14128
- return {
14129
- exitCode: report.blocked > 0 ? 1 : 0,
14130
- stdout: renderWorktreeReconciliation(report)
14131
- };
14132
- }
14133
- var DEFAULT_LOOP_MAX_ITERATIONS = 20;
14134
- var DEFAULT_LOOP_SLEEP_SECONDS = 2;
14135
- var DEFAULT_LOOP_NO_PROGRESS_LIMIT = 2;
14136
- var DEFAULT_LOOP_BACKLOG_PATH = ".opencode/openteam/backlog.md";
14137
- var BACKLOG_LINE = /^(\s*)- \[( |x|X)\]\s+(.*)$/;
14138
- var BACKLOG_CAST = /^\[@([^\]]+)\]\s*(.*)$/;
14139
- function parseBacklog2(text2) {
14140
- const items = [];
14141
- const lines = text2.split(/\r?\n/);
14142
- for (let i = 0;i < lines.length; i += 1) {
14143
- const match = BACKLOG_LINE.exec(lines[i] ?? "");
14144
- if (match === null) {
14145
- continue;
14146
- }
14147
- const checked = (match[2] ?? " ").toLowerCase() === "x";
14148
- const rest = match[3] ?? "";
14149
- const cast = BACKLOG_CAST.exec(rest);
14150
- const item = cast ? { line: i, text: (cast[2] ?? "").trim(), checked, cast: cast[1] ?? "" } : { line: i, text: rest.trim(), checked };
14151
- items.push(item);
14152
- }
14153
- return items;
14154
- }
14155
- function uncheckedBacklogItems(items) {
14156
- return items.filter((item) => !item.checked);
14157
- }
14158
- function markBacklogLineDone(text2, line) {
14159
- const lines = text2.split(`
14160
- `);
14161
- const target = lines[line];
14162
- if (target === undefined) {
14163
- return text2;
14164
- }
14165
- lines[line] = target.replace(/- \[ \]/, "- [x]");
14166
- return lines.join(`
14167
- `);
14168
- }
14169
- function evaluateLoopStop(state) {
14170
- if (state.uncheckedCount <= 0) {
14171
- return { stop: true, reason: "empty-backlog" };
14172
- }
14173
- if (state.iteration >= state.maxIterations) {
14174
- return { stop: true, reason: "max-iterations" };
14175
- }
14176
- if (state.budgetUsd !== undefined && state.spentUsd >= state.budgetUsd) {
14177
- return { stop: true, reason: "budget-exhausted" };
14178
- }
14179
- if (state.stopOnRedTests && state.testsRed) {
14180
- return { stop: true, reason: "red-tests" };
14181
- }
14182
- if (state.noProgressCount >= state.noProgressLimit) {
14183
- return { stop: true, reason: "no-progress" };
14184
- }
14185
- return { stop: false };
14186
- }
14187
- function parsePositiveNumber(raw, flag) {
14188
- if (raw === undefined) {
14189
- return { error: `Missing value for ${flag}` };
14190
- }
14191
- const value = Number(raw);
14192
- if (!Number.isFinite(value) || value <= 0) {
14193
- return { error: `${flag} expects a positive number, got "${raw}"` };
14194
- }
14195
- return { value };
14196
- }
14197
- function parseLoopFlags(argv) {
14198
- const flags = {
14199
- maxIterations: DEFAULT_LOOP_MAX_ITERATIONS,
14200
- sleepSeconds: DEFAULT_LOOP_SLEEP_SECONDS,
14201
- noProgressLimit: DEFAULT_LOOP_NO_PROGRESS_LIMIT,
14202
- freshContext: true,
14203
- stopOnRedTests: false,
14204
- dryRun: false,
14205
- yes: false,
14206
- backlogPath: DEFAULT_LOOP_BACKLOG_PATH
14207
- };
14208
- for (let i = 0;i < argv.length; i += 1) {
14209
- const arg = argv[i];
14210
- if (arg === "--config" || arg === "--telemetry") {
14211
- i += 1;
14212
- continue;
14213
- }
14214
- if (arg === "--dry-run") {
14215
- flags.dryRun = true;
14216
- } else if (arg === "--yes" || arg === "-y") {
14217
- flags.yes = true;
14218
- } else if (arg === "--fresh-context") {
14219
- flags.freshContext = true;
14220
- } else if (arg === "--no-fresh-context") {
14221
- flags.freshContext = false;
14222
- } else if (arg === "--stop-on-red-tests") {
14223
- flags.stopOnRedTests = true;
14224
- } else if (arg === "--max-iterations") {
14225
- const parsed = parsePositiveNumber(argv[i + 1], arg);
14226
- if ("error" in parsed)
14227
- return { ok: false, error: parsed.error };
14228
- flags.maxIterations = Math.floor(parsed.value);
14229
- i += 1;
14230
- } else if (arg === "--budget-usd") {
14231
- const parsed = parsePositiveNumber(argv[i + 1], arg);
14232
- if ("error" in parsed)
14233
- return { ok: false, error: parsed.error };
14234
- flags.budgetUsd = parsed.value;
14235
- i += 1;
14236
- } else if (arg === "--sleep") {
14237
- const parsed = parsePositiveNumber(argv[i + 1], arg);
14238
- if ("error" in parsed)
14239
- return { ok: false, error: parsed.error };
14240
- flags.sleepSeconds = parsed.value;
14241
- i += 1;
14242
- } else if (arg === "--no-progress-limit") {
14243
- const parsed = parsePositiveNumber(argv[i + 1], arg);
14244
- if ("error" in parsed)
14245
- return { ok: false, error: parsed.error };
14246
- flags.noProgressLimit = Math.floor(parsed.value);
14247
- i += 1;
14248
- } else if (arg === "--backlog") {
14249
- const value = argv[i + 1];
14250
- if (value === undefined) {
14251
- return { ok: false, error: "Missing value for --backlog" };
14252
- }
14253
- flags.backlogPath = value;
14254
- i += 1;
14255
- }
14256
- }
14257
- return { ok: true, flags };
14258
- }
14259
- var LOOP_GUARANTEED_ROLE_IDS = [RALPH_ROLE_ID, "guardian", "scribe"];
14260
- function guaranteedAgentName(roleID) {
14261
- return TEAM_ROLES[roleID]?.opencodeAgent ?? roleID;
14262
- }
14263
- async function guaranteedAgentStatuses(runtime) {
14264
- const statuses = [];
14265
- for (const roleID of LOOP_GUARANTEED_ROLE_IDS) {
14266
- const agentName = guaranteedAgentName(roleID);
14267
- statuses.push({
14268
- roleID,
14269
- agentName,
14270
- exists: await runtime.agentFileExists(agentName)
14271
- });
14272
- }
14273
- return statuses;
14274
- }
14275
- function rosterPlanSummary(rosterFile) {
14276
- switch (rosterFile.status) {
14277
- case "loaded":
14278
- return `${rosterFile.roster.universe} (${rosterFile.roster.entries.length} roles)`;
14279
- case "absent":
14280
- return "none yet — one would be generated on the first real run";
14281
- case "invalid":
14282
- return `${rosterFile.path} is present but unparseable — run 'openteam doctor'`;
14283
- }
14284
- }
14285
- function renderLoopPlan(plan2) {
14286
- const { flags } = plan2;
14287
- const budget = flags.budgetUsd !== undefined ? `$${flags.budgetUsd}` : "unbounded";
14288
- const freshContext = flags.freshContext ? "on (pure Ralph pattern)" : "off";
14289
- const budgetStop = flags.budgetUsd !== undefined ? ", budget" : "";
14290
- const redTestsStop = flags.stopOnRedTests ? ", red tests" : "";
14291
- const rosterSummary = rosterPlanSummary(plan2.rosterFile);
14292
- const lines = [
14293
- "openteam loop — plan (dry run, nothing dispatched):",
14294
- ` backlog: ${flags.backlogPath} (${plan2.uncheckedCount} unchecked item(s))`,
14295
- ` iterations: up to ${plan2.plannedIterations} (max-iterations ${flags.maxIterations})`,
14296
- ` budget: ${budget}`,
14297
- ` sleep: ${flags.sleepSeconds}s between iterations`,
14298
- ` fresh context: ${freshContext}`,
14299
- ` stop before spending on: empty backlog, max-iterations${budgetStop}${redTestsStop}, no-progress (${flags.noProgressLimit})`,
14300
- ` roster: ${rosterSummary}`
14301
- ];
14302
- for (const role of plan2.guaranteed) {
14303
- const mark = role.exists ? "✓" : "✗ missing — run 'openteam setup'";
14304
- lines.push(` role ${role.roleID} → agent ${role.agentName}: ${mark}`);
14305
- }
14306
- return lines.join(`
14307
- `);
14308
- }
14309
- function backlogPrompt(freshContext, items) {
14310
- const next = uncheckedBacklogItems(items)[0];
14311
- const header = freshContext ? "You are ralph, the loop driver. Fresh context each iteration." : "You are ralph, the loop driver.";
14312
- const focus = next !== undefined ? `Work the next backlog item to completion: ${next.text}` : "The backlog is empty.";
14313
- return `${header}
14314
- ${focus}`;
14315
- }
14316
- async function runLoop(argv, deps, configPath, configResolution) {
14317
- const parsed = parseLoopFlags(argv);
14318
- if (!parsed.ok) {
14319
- return { exitCode: 1, stdout: `${parsed.error}
14320
-
14321
- ${LOOP_HELP}` };
14322
- }
14323
- const { flags } = parsed;
14324
- if (deps.loopRuntime === undefined) {
12722
+ if (plan2.kind === "none") {
14325
12723
  return {
14326
- exitCode: 1,
14327
- stdout: "The loop runtime is not configured in this environment."
12724
+ exitCode: 0,
12725
+ stdout: `openteam graph recover: ${plan2.reason}`
14328
12726
  };
14329
12727
  }
14330
- const runtime = deps.loopRuntime;
14331
- const config = await deps.loadConfig(configPath, configResolution);
14332
- const backlogText = await runtime.readBacklog(flags.backlogPath) ?? "";
14333
- const items = parseBacklog2(backlogText);
14334
- const unchecked = uncheckedBacklogItems(items);
14335
- const rosterFile = await runtime.loadRosterFile();
14336
- const guaranteed = await guaranteedAgentStatuses(runtime);
14337
- const plannedIterations = Math.max(0, Math.min(flags.maxIterations, unchecked.length));
14338
- const plan2 = {
14339
- flags,
14340
- uncheckedCount: unchecked.length,
14341
- plannedIterations,
14342
- rosterFile,
14343
- guaranteed
14344
- };
14345
- if (flags.dryRun) {
14346
- return { exitCode: 0, stdout: renderLoopPlan(plan2) };
14347
- }
14348
- if (!flags.yes) {
12728
+ if (plan2.kind === "blocked") {
14349
12729
  return {
14350
12730
  exitCode: 1,
14351
- stdout: "Refusing to run unattended without confirmation. Re-run with --dry-run to preview the plan, or --yes to run."
12731
+ stdout: `openteam graph recover: ${plan2.reason}`
14352
12732
  };
14353
12733
  }
14354
- const preStop = evaluateLoopStop({
14355
- uncheckedCount: unchecked.length,
14356
- iteration: 0,
14357
- maxIterations: flags.maxIterations,
14358
- ...flags.budgetUsd !== undefined ? { budgetUsd: flags.budgetUsd } : {},
14359
- spentUsd: 0,
14360
- stopOnRedTests: flags.stopOnRedTests,
14361
- testsRed: false,
14362
- noProgressCount: 0,
14363
- noProgressLimit: flags.noProgressLimit
14364
- });
14365
- if (preStop.stop) {
12734
+ const config = await deps.loadConfig(configPath, configResolution);
12735
+ const policy = await resolveOperatorGraphPolicy(config, operator);
12736
+ const blocked = checkOperatorGate(policy);
12737
+ if (blocked !== undefined) {
12738
+ return blocked;
12739
+ }
12740
+ const recovered = await operator.recovery.recover(config);
12741
+ return {
12742
+ exitCode: 0,
12743
+ stdout: `openteam graph recover: ${recovered.reason} (${recovered.tail})`
12744
+ };
12745
+ }
12746
+ async function runGraphWorktrees(deps, configPath, configResolution) {
12747
+ const operator = requireGraphOperator(deps);
12748
+ const config = await deps.loadConfig(configPath, configResolution);
12749
+ const enabled = config.graph?.worktrees?.enabled ?? false;
12750
+ if (!enabled) {
14366
12751
  return {
14367
12752
  exitCode: 0,
14368
- stdout: `openteam loop: nothing to do (${preStop.reason}).`
12753
+ stdout: renderWorktreeReconciliation(planWorktreeReconciliation({ enabled: false, worktrees: [] }))
14369
12754
  };
14370
12755
  }
14371
- const ralphAgent = guaranteedAgentName(RALPH_ROLE_ID);
14372
- if (!await runtime.agentFileExists(ralphAgent)) {
14373
- return { exitCode: 1, stdout: missingAgentFileMessage(ralphAgent) };
12756
+ const policy = await resolveOperatorGraphPolicy(config, operator);
12757
+ const blocked = checkOperatorGate(policy);
12758
+ if (blocked !== undefined) {
12759
+ return blocked;
14374
12760
  }
14375
- await runtime.loadOrGenerateRoster();
14376
- const result = await runtime.run({
14377
- prompt: backlogPrompt(flags.freshContext, items),
14378
- maxIterations: Math.max(1, plannedIterations),
14379
- config,
14380
- freshContext: flags.freshContext
12761
+ const observations = await operator.worktrees.listObservations(config);
12762
+ const report = planWorktreeReconciliation({
12763
+ enabled,
12764
+ worktrees: observations
14381
12765
  });
14382
- let updated = backlogText;
14383
- const completed = Math.min(result.iterations, unchecked.length);
14384
- for (let n = 0;n < completed; n += 1) {
14385
- const item = unchecked[n];
14386
- if (item !== undefined) {
14387
- updated = markBacklogLineDone(updated, item.line);
14388
- }
12766
+ if (report.plans.length === 0) {
12767
+ return { exitCode: 0, stdout: renderWorktreeReconciliation(report) };
14389
12768
  }
14390
- if (updated !== backlogText) {
14391
- await runtime.writeBacklog(flags.backlogPath, updated);
12769
+ for (const plan2 of report.plans) {
12770
+ if (!plan2.safe || plan2.action === "retain" || plan2.action === "block") {
12771
+ continue;
12772
+ }
12773
+ await operator.worktrees.apply(plan2.action, plan2.runID);
14392
12774
  }
14393
12775
  return {
14394
- exitCode: 0,
14395
- stdout: `openteam loop: ${result.iterations} iteration(s), terminated on ${result.termination}; marked ${completed} backlog item(s) done.`
12776
+ exitCode: report.blocked > 0 ? 1 : 0,
12777
+ stdout: renderWorktreeReconciliation(report)
14396
12778
  };
14397
12779
  }
14398
12780
  async function runCli(argv, deps) {
@@ -14537,12 +12919,6 @@ async function runCli(argv, deps) {
14537
12919
  if (command === "graph") {
14538
12920
  return await runGraph(parsed.positionals, deps, configPath, configResolution);
14539
12921
  }
14540
- if (command === "loop") {
14541
- if (parsed.positionals.includes("--help") || parsed.positionals.includes("-h")) {
14542
- return { exitCode: 0, stdout: LOOP_HELP };
14543
- }
14544
- return await runLoop(argv, deps, configPath, configResolution);
14545
- }
14546
12922
  if (command === "console") {
14547
12923
  const config = await deps.loadConfig(configPath, configResolution);
14548
12924
  return { exitCode: 0, stdout: renderConsoleStatus(config.console) };
@@ -14620,6 +12996,167 @@ ${HELP}`
14620
12996
  }
14621
12997
  }
14622
12998
 
12999
+ // src/orchestrator/roster.ts
13000
+ import { z as z19 } from "zod";
13001
+ var RosterEntrySchema = z19.object({
13002
+ roleID: z19.string().min(1),
13003
+ agentName: z19.string().min(1)
13004
+ }).strict();
13005
+ var RosterSchema = z19.object({
13006
+ universe: z19.string().min(1),
13007
+ entries: z19.array(RosterEntrySchema)
13008
+ }).strict();
13009
+ var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
13010
+
13011
+ class RosterParseError extends Error {
13012
+ code;
13013
+ constructor(code, message) {
13014
+ super(message);
13015
+ this.name = "RosterParseError";
13016
+ this.code = code;
13017
+ }
13018
+ }
13019
+ function parseRoster(text2) {
13020
+ const result = parseRosterResult(text2);
13021
+ if (!result.ok) {
13022
+ throw result.error;
13023
+ }
13024
+ return result.roster;
13025
+ }
13026
+ function parseRosterResult(text2) {
13027
+ const match = ROSTER_JSON_FENCE.exec(text2);
13028
+ if (match === null || match[1] === undefined) {
13029
+ return {
13030
+ ok: false,
13031
+ error: new RosterParseError("missing-json-block", "openteam roster: no JSON block found in roster file")
13032
+ };
13033
+ }
13034
+ let data;
13035
+ try {
13036
+ data = JSON.parse(match[1]);
13037
+ } catch (error) {
13038
+ return {
13039
+ ok: false,
13040
+ error: new RosterParseError("malformed-json", `openteam roster: malformed JSON in roster file: ${String(error)}`)
13041
+ };
13042
+ }
13043
+ const parsed = RosterSchema.safeParse(data);
13044
+ if (!parsed.success) {
13045
+ return {
13046
+ ok: false,
13047
+ error: new RosterParseError("invalid-roster-schema", `openteam roster: invalid roster schema: ${parsed.error.message}`)
13048
+ };
13049
+ }
13050
+ return { ok: true, roster: parsed.data };
13051
+ }
13052
+ var GUARANTEED_ROLE_IDS = [
13053
+ "orchestrator",
13054
+ "guardian",
13055
+ "scribe",
13056
+ "ralph"
13057
+ ];
13058
+ function isGuaranteedRoleID(roleID) {
13059
+ return GUARANTEED_ROLE_IDS.includes(roleID);
13060
+ }
13061
+ function missingGuaranteedRoles(roster) {
13062
+ const present = new Set(roster.entries.map((entry) => entry.roleID));
13063
+ return GUARANTEED_ROLE_IDS.filter((roleID) => !present.has(roleID));
13064
+ }
13065
+ function isRoleProfileLike(value) {
13066
+ return typeof value === "object" && value !== null && "roleID" in value && "defaultTier" in value;
13067
+ }
13068
+ function configuredRolesFromAuditInput(input) {
13069
+ if ("configuredRoles" in input && !isRoleProfileLike(input.configuredRoles)) {
13070
+ return input.configuredRoles;
13071
+ }
13072
+ return input;
13073
+ }
13074
+ function auditRoster(rosterOrText, configuredRolesOrOptions = {}) {
13075
+ const configuredRoles = configuredRolesFromAuditInput(configuredRolesOrOptions);
13076
+ if (typeof rosterOrText === "string") {
13077
+ const parsed = parseRosterResult(rosterOrText);
13078
+ if (!parsed.ok) {
13079
+ return {
13080
+ findings: [
13081
+ {
13082
+ kind: "unparseable-roster",
13083
+ roleID: "roster",
13084
+ code: parsed.error.code,
13085
+ message: parsed.error.message
13086
+ }
13087
+ ],
13088
+ hasAllGuaranteedRoles: false,
13089
+ missingGuaranteedRoleIDs: [],
13090
+ unprofiledRoleEntries: [],
13091
+ misassignedGuaranteedRoleEntries: []
13092
+ };
13093
+ }
13094
+ return auditParsedRoster(parsed.roster, configuredRoles);
13095
+ }
13096
+ return auditParsedRoster(rosterOrText, configuredRoles);
13097
+ }
13098
+ function auditParsedRoster(roster, configuredRoles) {
13099
+ const missingGuaranteedRoleIDs = missingGuaranteedRoles(roster);
13100
+ const missing = new Set(missingGuaranteedRoleIDs);
13101
+ const findings = missingGuaranteedRoleIDs.map((roleID) => ({
13102
+ kind: "missing-guaranteed-role",
13103
+ roleID
13104
+ }));
13105
+ const unprofiledRoleEntries = [];
13106
+ const misassignedGuaranteedRoleEntries = [];
13107
+ roster.entries.forEach((entry) => {
13108
+ const profile = getRoleProfile(entry.roleID, configuredRoles);
13109
+ if (profile === undefined && !isKnownNonWorkerRole(entry.roleID)) {
13110
+ unprofiledRoleEntries.push({
13111
+ roleID: entry.roleID,
13112
+ agentName: entry.agentName,
13113
+ severity: "info"
13114
+ });
13115
+ findings.push({
13116
+ kind: "unresolved-role-profile",
13117
+ roleID: entry.roleID,
13118
+ agentName: entry.agentName
13119
+ });
13120
+ }
13121
+ if (isGuaranteedRoleID(entry.agentName) && entry.roleID !== entry.agentName && missing.has(entry.agentName)) {
13122
+ misassignedGuaranteedRoleEntries.push({
13123
+ roleID: entry.roleID,
13124
+ agentName: entry.agentName,
13125
+ missingRoleID: entry.agentName,
13126
+ reason: "agentNameMatchesMissingGuaranteedRole"
13127
+ });
13128
+ findings.push({
13129
+ kind: "non-canonical-role-id",
13130
+ roleID: entry.roleID,
13131
+ agentName: entry.agentName,
13132
+ canonicalRoleID: entry.agentName
13133
+ });
13134
+ return;
13135
+ }
13136
+ if (profile !== undefined && isGuaranteedRoleID(profile.roleID) && profile.roleID !== entry.roleID && missing.has(profile.roleID)) {
13137
+ misassignedGuaranteedRoleEntries.push({
13138
+ roleID: entry.roleID,
13139
+ agentName: entry.agentName,
13140
+ missingRoleID: profile.roleID,
13141
+ reason: "roleIDResolvesToMissingGuaranteedRole"
13142
+ });
13143
+ findings.push({
13144
+ kind: "non-canonical-role-id",
13145
+ roleID: entry.roleID,
13146
+ agentName: entry.agentName,
13147
+ canonicalRoleID: profile.roleID
13148
+ });
13149
+ }
13150
+ });
13151
+ return {
13152
+ findings,
13153
+ hasAllGuaranteedRoles: missingGuaranteedRoleIDs.length === 0,
13154
+ missingGuaranteedRoleIDs,
13155
+ unprofiledRoleEntries,
13156
+ misassignedGuaranteedRoleEntries
13157
+ };
13158
+ }
13159
+
14623
13160
  // src/commands/rolesInit.ts
14624
13161
  var ROLES_INIT_LOCAL_ONLY = "local-only";
14625
13162
  var ROLES_INIT_FRONTIER_OK = "frontier-ok";
@@ -14957,9 +13494,6 @@ function planStartupHygieneMigration(exists) {
14957
13494
  // src/index.ts
14958
13495
  import { readFile as readFile2 } from "node:fs/promises";
14959
13496
  import { basename as basename2, dirname as dirname5 } from "node:path";
14960
- import {
14961
- tool as tool2
14962
- } from "@opencode-ai/plugin";
14963
13497
 
14964
13498
  // src/graph/certificate.ts
14965
13499
  import { z as z20 } from "zod";
@@ -15679,58 +14213,6 @@ var realCacheAdapter = {
15679
14213
  isReparsePath
15680
14214
  };
15681
14215
 
15682
- // src/orchestrator/rosterPersistence.ts
15683
- function rosterFence(roster) {
15684
- const data = { universe: roster.universe, entries: roster.entries };
15685
- return `\`\`\`json
15686
- ${JSON.stringify(data, null, 2)}
15687
- \`\`\``;
15688
- }
15689
- function serializeRoster(roster) {
15690
- return `# openteam roster
15691
-
15692
- Universe: ${roster.universe}
15693
-
15694
- ${rosterFence(roster)}
15695
- `;
15696
- }
15697
- async function persistRoster(storage, roster, path4 = OPENTEAM_ROSTER_PATH) {
15698
- await storage.write(path4, serializeRoster(roster));
15699
- return path4;
15700
- }
15701
- async function loadRosterFile(storage, path4 = OPENTEAM_ROSTER_PATH) {
15702
- const content = await storage.read(path4);
15703
- if (content === undefined) {
15704
- return { status: "absent", path: path4 };
15705
- }
15706
- const parsed = parseRosterResult(content);
15707
- if (!parsed.ok) {
15708
- return { status: "invalid", path: path4, error: parsed.error };
15709
- }
15710
- return {
15711
- status: "loaded",
15712
- path: path4,
15713
- roster: enforceGuaranteedRoles(parsed.roster)
15714
- };
15715
- }
15716
- async function loadOrGenerateRoster(storage, generate, path4 = OPENTEAM_ROSTER_PATH) {
15717
- const existing = await loadRosterFile(storage, path4);
15718
- if (existing.status === "loaded") {
15719
- return { roster: existing.roster, regenerated: false };
15720
- }
15721
- const roster = enforceGuaranteedRoles(await generate());
15722
- if (existing.status === "absent") {
15723
- await persistRoster(storage, roster, path4);
15724
- return { roster, regenerated: true };
15725
- }
15726
- return {
15727
- roster,
15728
- regenerated: true,
15729
- recoveredFromInvalidPersisted: true,
15730
- invalidPersistedRosterError: existing.error.message
15731
- };
15732
- }
15733
-
15734
14216
  // src/memory/types.ts
15735
14217
  import { z as z21 } from "zod";
15736
14218
  var SHARED_OWNER_KEY = "*";
@@ -16342,51 +14824,6 @@ async function recallMemory(deps, queryText, query) {
16342
14824
  return recall(recordRead.records, queryEmbedding, query, recallDeps);
16343
14825
  }
16344
14826
 
16345
- // src/plugin/orchestrateTool.ts
16346
- import { tool } from "@opencode-ai/plugin";
16347
- import { z as z22 } from "zod";
16348
- var _pluginWarnedRoles = new Set;
16349
- var EMPTY_ROLE_ID_SET = new Set;
16350
- var RoleAssignmentSchema = z22.object({
16351
- roleID: z22.string().min(1).describe("Role identifier from the roster"),
16352
- prompt: z22.string().min(1).describe("Work prompt for this role"),
16353
- title: z22.string().min(1).optional().describe("Human-readable title"),
16354
- dependsOn: z22.array(z22.string().min(1)).optional().describe("Role IDs this assignment depends on (DAG edges)")
16355
- }).strict();
16356
- var OrchestratePayloadSchema = z22.object({
16357
- assignments: z22.array(RoleAssignmentSchema).min(1).describe("Role assignments to distribute"),
16358
- parentSessionID: z22.string().min(1).optional().describe("Parent session for subsessions"),
16359
- directory: z22.string().min(1).optional().describe("Working directory override")
16360
- }).strict();
16361
- function adaptSdkClient(sdk) {
16362
- return {
16363
- session: {
16364
- create: async (args) => {
16365
- const result = await sdk.session.create(args);
16366
- const out = {};
16367
- if (result.data !== undefined) {
16368
- out.data = result.data;
16369
- }
16370
- if (result.error !== undefined) {
16371
- out.error = result.error;
16372
- }
16373
- return out;
16374
- },
16375
- prompt: async (args) => {
16376
- const result = await sdk.session.prompt(args);
16377
- const out = {};
16378
- if (result.data !== undefined) {
16379
- out.data = result.data;
16380
- }
16381
- if (result.error !== undefined) {
16382
- out.error = result.error;
16383
- }
16384
- return out;
16385
- }
16386
- }
16387
- };
16388
- }
16389
-
16390
14827
  // src/local/embeddings.ts
16391
14828
  function parseEmbeddingsResponse(payload) {
16392
14829
  if (typeof payload !== "object" || payload === null || !Array.isArray(payload.data)) {
@@ -16550,7 +14987,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
16550
14987
  // package.json
16551
14988
  var package_default = {
16552
14989
  name: "@jmanuelcorral/openteam",
16553
- version: "0.17.5",
14990
+ version: "0.18.0",
16554
14991
  packageManager: "bun@1.3.14",
16555
14992
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
16556
14993
  license: "MIT",
@@ -16898,11 +15335,11 @@ function applyLimit(list, limit) {
16898
15335
  return list.slice(0, limit);
16899
15336
  }
16900
15337
  function queryDecisions(memory, query = {}) {
16901
- const filtered = memory.decisions.filter((decision2) => {
16902
- if (query.agent !== undefined && decision2.agent !== query.agent) {
15338
+ const filtered = memory.decisions.filter((decision) => {
15339
+ if (query.agent !== undefined && decision.agent !== query.agent) {
16903
15340
  return false;
16904
15341
  }
16905
- if (query.tag !== undefined && !(decision2.tags ?? []).includes(query.tag)) {
15342
+ if (query.tag !== undefined && !(decision.tags ?? []).includes(query.tag)) {
16906
15343
  return false;
16907
15344
  }
16908
15345
  return true;
@@ -17568,7 +16005,6 @@ var deps = {
17568
16005
  agentDir: AGENT_DIR2,
17569
16006
  cachePort: realCacheAdapter,
17570
16007
  purge: createCliPurgeRuntime(),
17571
- loopRuntime: createCliLoopRuntime(),
17572
16008
  loadRosterForDoctor: readStoredRosterForDoctor,
17573
16009
  auditRoster: auditStoredRosterForDoctor,
17574
16010
  readGitignore: () => storage.read(OPENCODE_GITIGNORE_PATH),
@@ -17586,71 +16022,6 @@ function createCliPurgeRuntime() {
17586
16022
  sessionsDir: DEFAULT_SESSIONS_DIR
17587
16023
  };
17588
16024
  }
17589
- var fallbackRosterSource = () => ({
17590
- universe: "openteam",
17591
- entries: Object.entries(TEAM_ROLES).filter(([, profile]) => profile.opencodeAgent !== undefined).map(([roleID, profile]) => ({
17592
- roleID,
17593
- agentName: profile.opencodeAgent
17594
- }))
17595
- });
17596
- function ralphResultIsDone(result) {
17597
- const response2 = result.subsession.response;
17598
- if (typeof response2 === "string") {
17599
- return /\bDONE\b/i.test(response2);
17600
- }
17601
- if (response2 !== null && typeof response2 === "object") {
17602
- const done = response2.done;
17603
- if (typeof done === "boolean") {
17604
- return done;
17605
- }
17606
- }
17607
- return false;
17608
- }
17609
- async function runCliRalphLoop(input) {
17610
- const client = adaptSdkClient(createOpencodeClient());
17611
- const sink = createNullEventSink();
17612
- const registry = new RuntimeRegistry({
17613
- fetch: globalThis.fetch,
17614
- clock: () => new Date().toISOString(),
17615
- foundryDiscovery: foundryDiscoveryFromExec(nodeExec2)
17616
- });
17617
- const localRuntimeReachable = async () => {
17618
- const snapshots = await registry.probe(input.config.local.runtimes.filter((runtime) => runtime.enabled));
17619
- return anyRuntimeReachable(snapshots);
17620
- };
17621
- const task = {
17622
- promptChars: input.prompt.length,
17623
- hasCode: false
17624
- };
17625
- const result = await runRalphLoop({
17626
- task,
17627
- prompt: input.prompt,
17628
- config: input.config,
17629
- maxIterations: input.maxIterations
17630
- }, {
17631
- client,
17632
- sink,
17633
- now: () => Date.now(),
17634
- newDecisionID: () => crypto.randomUUID(),
17635
- localRuntimeReachable,
17636
- isDone: ralphResultIsDone
17637
- });
17638
- return { iterations: result.iterations, termination: result.termination };
17639
- }
17640
- function createCliLoopRuntime() {
17641
- const agentFileExists = (agentName) => storage.exists(`${AGENT_DIR2}/${agentName}.md`);
17642
- const generate = () => generateRoster("openteam initial roster", fallbackRosterSource);
17643
- return {
17644
- loadRosterFile: () => loadRosterFile(storage),
17645
- loadOrGenerateRoster: async () => (await loadOrGenerateRoster(storage, generate)).roster,
17646
- agentFileExists,
17647
- readBacklog: (path4) => storage.read(path4),
17648
- writeBacklog: async (path4, contents) => {
17649
- await storage.write(path4, contents);
17650
- },
17651
- run: (loopInput) => runCliRalphLoop(loopInput)
17652
- };
17653
- }
17654
16025
  function createSetupDeps() {
17655
16026
  return {
17656
16027
  detect: createDetect(nodeExec2),