@jmanuelcorral/openteam 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.es.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +522 -67
  4. package/dist/commands/dispatch.d.ts +23 -3
  5. package/dist/commands/dispatch.d.ts.map +1 -1
  6. package/dist/commands/doctor.d.ts +47 -0
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/index.d.ts +11 -9
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +665 -103
  11. package/dist/opencodeArtifacts/orchestratorAgent.d.ts +1 -1
  12. package/dist/opencodeArtifacts/orchestratorAgent.d.ts.map +1 -1
  13. package/dist/orchestrator/coordinator.d.ts +8 -5
  14. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  15. package/dist/orchestrator/permissions.d.ts.map +1 -1
  16. package/dist/orchestrator/roles.d.ts +23 -4
  17. package/dist/orchestrator/roles.d.ts.map +1 -1
  18. package/dist/orchestrator/roster.d.ts +85 -2
  19. package/dist/orchestrator/roster.d.ts.map +1 -1
  20. package/dist/orchestrator/rosterPersistence.d.ts +32 -5
  21. package/dist/orchestrator/rosterPersistence.d.ts.map +1 -1
  22. package/dist/plugin/diagnostics.d.ts +30 -0
  23. package/dist/plugin/diagnostics.d.ts.map +1 -0
  24. package/dist/plugin/orchestrateTool.d.ts +3 -1
  25. package/dist/plugin/orchestrateTool.d.ts.map +1 -1
  26. package/dist/storage/index/memoryIndex.d.ts +2 -2
  27. package/dist/storage/index/memoryIndex.d.ts.map +1 -1
  28. package/dist/telemetry/diagnostics.d.ts +10 -0
  29. package/dist/telemetry/diagnostics.d.ts.map +1 -0
  30. package/dist/telemetry/eventLog.d.ts +3 -1
  31. package/dist/telemetry/eventLog.d.ts.map +1 -1
  32. package/dist/telemetry/events.d.ts +77 -0
  33. package/dist/telemetry/events.d.ts.map +1 -1
  34. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -98,8 +98,8 @@ var AgentRoleProfileSchema = z.object({
98
98
  localRuntimes: z.array(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"])).optional()
99
99
  }).strict();
100
100
  var TEAM_ROLES = {
101
- rusty: {
102
- roleID: "rusty",
101
+ architect: {
102
+ roleID: "architect",
103
103
  opencodeAgent: "architect",
104
104
  defaultTier: "hard",
105
105
  minReasoningTier: 5,
@@ -108,8 +108,8 @@ var TEAM_ROLES = {
108
108
  preferredFrontierModels: ["auto"],
109
109
  localFirst: false
110
110
  },
111
- livingston: {
112
- roleID: "livingston",
111
+ integration: {
112
+ roleID: "integration",
113
113
  opencodeAgent: "integration",
114
114
  defaultTier: "moderate",
115
115
  minReasoningTier: 4,
@@ -118,8 +118,8 @@ var TEAM_ROLES = {
118
118
  preferredFrontierModels: ["auto"],
119
119
  localFirst: false
120
120
  },
121
- yen: {
122
- roleID: "yen",
121
+ "local-runtime": {
122
+ roleID: "local-runtime",
123
123
  opencodeAgent: "local-runtime",
124
124
  defaultTier: "moderate",
125
125
  minReasoningTier: 3,
@@ -128,8 +128,8 @@ var TEAM_ROLES = {
128
128
  preferredFrontierModels: ["auto"],
129
129
  localFirst: true
130
130
  },
131
- basher: {
132
- roleID: "basher",
131
+ "routing-cost": {
132
+ roleID: "routing-cost",
133
133
  opencodeAgent: "routing-cost",
134
134
  defaultTier: "moderate",
135
135
  minReasoningTier: 3,
@@ -171,8 +171,8 @@ var TEAM_ROLES = {
171
171
  localFirst: true,
172
172
  requiresLocalRuntime: true
173
173
  },
174
- linus: {
175
- roleID: "linus",
174
+ tester: {
175
+ roleID: "tester",
176
176
  opencodeAgent: "tester",
177
177
  defaultTier: "simple",
178
178
  minReasoningTier: 2,
@@ -192,8 +192,35 @@ var TEAM_ROLES = {
192
192
  localFirst: true
193
193
  }
194
194
  };
195
+ var LEGACY_TEAM_ROLE_ALIASES = {
196
+ rusty: "architect",
197
+ livingston: "integration",
198
+ yen: "local-runtime",
199
+ basher: "routing-cost",
200
+ linus: "tester"
201
+ };
202
+ var KNOWN_NON_WORKER_ROLE_IDS = ["orchestrator", "openteam"];
203
+ function isKnownNonWorkerRole(roleID) {
204
+ return KNOWN_NON_WORKER_ROLE_IDS.includes(roleID);
205
+ }
206
+ function getConfiguredRoleProfileByAlias(roleID, roles) {
207
+ if (roles === undefined) {
208
+ return;
209
+ }
210
+ for (const key of Object.keys(roles).sort()) {
211
+ const profile = roles[key];
212
+ if (profile?.opencodeAgent === roleID) {
213
+ return profile;
214
+ }
215
+ }
216
+ return;
217
+ }
218
+ function getLegacyTeamRoleProfile(roleID) {
219
+ const canonicalRoleID = LEGACY_TEAM_ROLE_ALIASES[roleID];
220
+ return canonicalRoleID === undefined ? undefined : TEAM_ROLES[canonicalRoleID];
221
+ }
195
222
  function getRoleProfile(roleID, configuredRoles) {
196
- return configuredRoles?.[roleID] ?? TEAM_ROLES[roleID];
223
+ return configuredRoles?.[roleID] ?? TEAM_ROLES[roleID] ?? getConfiguredRoleProfileByAlias(roleID, configuredRoles) ?? getLegacyTeamRoleProfile(roleID);
197
224
  }
198
225
  function synthesiseFallbackProfile(roleID) {
199
226
  return {
@@ -2878,6 +2905,50 @@ var ModelCapabilityProfileSchema = z10.object({
2878
2905
  availability: z10.enum(["available", "degraded", "unavailable"])
2879
2906
  }).strict();
2880
2907
 
2908
+ // src/telemetry/diagnostics.ts
2909
+ var DIAGNOSTIC_CODES = [
2910
+ "availability-refresh-failed",
2911
+ "config-missing",
2912
+ "graph-gate-denied",
2913
+ "memory-extraction-warning",
2914
+ "memory-runtime-warning",
2915
+ "opencode-server-url",
2916
+ "opencode-version-read-failed",
2917
+ "privacy-inert",
2918
+ "soak-evidence-rejected",
2919
+ "telemetry-warning",
2920
+ "telemetry-write-failed",
2921
+ "unknown-role"
2922
+ ];
2923
+ function diagnosticDescription(code) {
2924
+ switch (code) {
2925
+ case "availability-refresh-failed":
2926
+ return "Local runtime availability refresh failed; routing will continue with the previous or empty availability snapshot.";
2927
+ case "config-missing":
2928
+ return "openteam did not find its runtime config file; defaults or plugin options were used.";
2929
+ case "graph-gate-denied":
2930
+ return "The graph cutover gate denied active mode and openteam degraded graph mode to off.";
2931
+ case "memory-extraction-warning":
2932
+ return "Semantic memory extraction reported a non-fatal local-runtime warning; no raw conversation content was recorded.";
2933
+ case "memory-runtime-warning":
2934
+ return "Semantic memory runtime setup reported a non-fatal warning; memory recall may be incomplete.";
2935
+ case "opencode-server-url":
2936
+ return "openteam observed the opencode server URL during plugin initialization.";
2937
+ case "opencode-version-read-failed":
2938
+ return "openteam could not read the live opencode version; graph gates fail closed when this happens.";
2939
+ case "privacy-inert":
2940
+ return "forceLocalOnSensitive is configured but no local runtime is configured, so the setting cannot keep sensitive prompts local.";
2941
+ case "soak-evidence-rejected":
2942
+ return "Graph soak evidence contained rejected observation lines; cutover gate evidence may be incomplete.";
2943
+ case "telemetry-warning":
2944
+ return "An optional telemetry backend warning occurred; local JSONL telemetry remains the fallback when enabled.";
2945
+ case "telemetry-write-failed":
2946
+ return "Writing an openteam telemetry event failed; plugin execution continued.";
2947
+ case "unknown-role":
2948
+ return "An unknown role ID was encountered; openteam used a neutral frontier-eligible fallback. If intentional, define the role in orchestrator.roles.";
2949
+ }
2950
+ }
2951
+
2881
2952
  // src/telemetry/events.ts
2882
2953
  var EVENT_SCHEMA_VERSION = 1;
2883
2954
  var EventBaseSchema = z11.object({
@@ -2971,6 +3042,13 @@ var ShadowDiagnosticEventSchema = EventBaseSchema.extend({
2971
3042
  "unknown"
2972
3043
  ])
2973
3044
  });
3045
+ var DiagnosticLevelSchema = z11.enum(["debug", "info", "warn", "error"]);
3046
+ var DiagnosticCodeSchema = z11.enum(DIAGNOSTIC_CODES);
3047
+ var DiagnosticEventSchema = EventBaseSchema.extend({
3048
+ type: z11.literal("diagnostic"),
3049
+ level: DiagnosticLevelSchema,
3050
+ code: DiagnosticCodeSchema
3051
+ });
2974
3052
  var OpenTeamEventSchema = z11.discriminatedUnion("type", [
2975
3053
  RouteEventSchema,
2976
3054
  MessageEventSchema,
@@ -2979,7 +3057,8 @@ var OpenTeamEventSchema = z11.discriminatedUnion("type", [
2979
3057
  DecisionEventSchema,
2980
3058
  ActivityEventSchema,
2981
3059
  SessionEndpointEventSchema,
2982
- ShadowDiagnosticEventSchema
3060
+ ShadowDiagnosticEventSchema,
3061
+ DiagnosticEventSchema
2983
3062
  ]);
2984
3063
 
2985
3064
  // src/telemetry/types.ts
@@ -3162,6 +3241,9 @@ async function readRouteCostRecords(dir, deps) {
3162
3241
  }
3163
3242
  return records;
3164
3243
  }
3244
+ async function readDiagnosticEvents(dir, deps) {
3245
+ return (await readSessionEvents(dir, deps)).filter((event) => event.type === "diagnostic");
3246
+ }
3165
3247
  async function readSessionEvents(dir, deps) {
3166
3248
  const files = (await deps.storage.list(dir)).filter((file) => file.endsWith(".jsonl"));
3167
3249
  const perFile = await Promise.all(files.map(async (file) => {
@@ -3579,22 +3661,70 @@ function buildOrchestratorAgent(frontier, options = {}) {
3579
3661
  "1. When you create the team for the first time, **choose a thematic universe**",
3580
3662
  " (e.g. a film, series, comic or mythology). If the user has a preference,",
3581
3663
  " ask them for it; if not, propose one and move forward without blocking work.",
3582
- "2. Give each subagent the **name of a character** from that universe; the",
3583
- " file name (`.opencode/agent/<name>.md`) is its alias for",
3664
+ "2. Give each subagent an `agentName`: the **name of a character** from that universe;",
3665
+ " the file name (`.opencode/agent/<agentName>.md`) is its alias for",
3584
3666
  " `@mention`, and its role is clear from the `description`.",
3585
- `3. **Register the cast** in \`${OPENTEAM_ROSTER_PATH}\` (universe + table`,
3586
- " name role model) so that the names **persist** across sessions.",
3587
- " This file goes **outside** `.opencode/agent/` (otherwise opencode would load it",
3588
- " as a phantom agent). Always reuse the same cast; do not re-cast",
3589
- " without reason.",
3667
+ "3. Give each entry a `roleID`: the stable functional routing key. `roleID` is",
3668
+ " **not** the themed name. Worked example: C-3PO as the scribe is",
3669
+ ' `{ "roleID": "scribe", "agentName": "c3po" }`, never',
3670
+ ' `{ "roleID": "c3po", "agentName": "c3po" }`.',
3671
+ "4. Prefer existing curated `roleID` keys when they fit:",
3672
+ " `architect`, `integration`, `tester`, `reviewer`, `local-runtime`,",
3673
+ " `routing-cost` (plus the guaranteed `orchestrator`, `guardian`,",
3674
+ " `scribe`, `ralph`). The roleID space is open: use free-form project",
3675
+ " roleIDs only when no curated role fits.",
3676
+ `5. **Register the cast** in \`${OPENTEAM_ROSTER_PATH}\` so the names persist`,
3677
+ " across sessions. This file goes **outside** `.opencode/agent/` (otherwise",
3678
+ " opencode would load it as a phantom agent). Always reuse the same cast;",
3679
+ " do not re-cast without reason.",
3680
+ "",
3681
+ "## Roster file format",
3682
+ "",
3683
+ "Write the roster as Markdown with exactly one fenced `json` block matching",
3684
+ "`{ universe, entries: [{ roleID, agentName }] }`. Do not write a table.",
3685
+ "Minimal parseable example:",
3686
+ "",
3687
+ "# openteam roster",
3688
+ "",
3689
+ "Universe: star-wars",
3690
+ "",
3691
+ "```json",
3692
+ "{",
3693
+ ' "universe": "star-wars",',
3694
+ ' "entries": [',
3695
+ " {",
3696
+ ' "roleID": "orchestrator",',
3697
+ ' "agentName": "openteam"',
3698
+ " },",
3699
+ " {",
3700
+ ' "roleID": "guardian",',
3701
+ ' "agentName": "leia"',
3702
+ " },",
3703
+ " {",
3704
+ ' "roleID": "scribe",',
3705
+ ' "agentName": "c3po"',
3706
+ " },",
3707
+ " {",
3708
+ ' "roleID": "ralph",',
3709
+ ' "agentName": "r2d2"',
3710
+ " },",
3711
+ " {",
3712
+ ' "roleID": "tester",',
3713
+ ' "agentName": "rex"',
3714
+ " }",
3715
+ " ]",
3716
+ "}",
3717
+ "```",
3590
3718
  "",
3591
3719
  "## How to create the team when it does not exist",
3592
3720
  "",
3593
3721
  "1. Analyze the goal and explore the repository (`read`/`glob`/`grep`).",
3594
3722
  "2. Break the request into tasks and design the **minimal team** needed:",
3595
- " define only the roles the work requires (e.g. architect, backend,",
3596
- " frontend, reviewer, tester, docs). Do not invent roles you will not use.",
3597
- "3. Create each subagent **on demand** by writing `.opencode/agent/<name>.md`:",
3723
+ " define only the roles the work requires. Reuse curated roleIDs such as",
3724
+ " `architect`, `integration`, `reviewer`, `tester`, `local-runtime` and",
3725
+ " `routing-cost`; use custom roleIDs for uncovered project specialties.",
3726
+ " Do not invent roles you will not use.",
3727
+ "3. Create each subagent **on demand** by writing `.opencode/agent/<agentName>.md`:",
3598
3728
  " - Frontmatter: `description` (required, include role and universe),",
3599
3729
  " `mode: subagent`, `temperature`, `permission` with the minimum needed,",
3600
3730
  " and `model` **optional**.",
@@ -3615,8 +3745,10 @@ function buildOrchestratorAgent(frontier, options = {}) {
3615
3745
  "",
3616
3746
  "## Standard team roles",
3617
3747
  "",
3618
- "Besides the project-specific roles, include these standard roles",
3619
- "(create them on demand, with their universe name) when they add value:",
3748
+ "Every roster MUST include these guaranteed canonical roleIDs exactly:",
3749
+ "`orchestrator`, `guardian`, `scribe`, and `ralph`. Map `orchestrator` to",
3750
+ "agentName `openteam` (the primary agent). Create the `guardian`, `scribe`,",
3751
+ "and `ralph` subagent files on demand with their themed `agentName` values:",
3620
3752
  "",
3621
3753
  "- **scribe** — the team's silent memory. Records decisions and",
3622
3754
  " learnings in a shared log (`.opencode/openteam/decisions.md`) without",
@@ -5728,14 +5860,21 @@ function hashPrompt(prompt) {
5728
5860
 
5729
5861
  // src/orchestrator/permissions.ts
5730
5862
  var ROLE_PERMISSIONS = {
5731
- rusty: ["read", "edit", "multiFileEdit", "destructive", "network", "shell"],
5732
- livingston: ["read", "edit", "multiFileEdit", "network", "shell"],
5733
- yen: ["read", "edit", "network", "shell"],
5734
- basher: ["read", "edit"],
5863
+ architect: [
5864
+ "read",
5865
+ "edit",
5866
+ "multiFileEdit",
5867
+ "destructive",
5868
+ "network",
5869
+ "shell"
5870
+ ],
5871
+ integration: ["read", "edit", "multiFileEdit", "network", "shell"],
5872
+ "local-runtime": ["read", "edit", "network", "shell"],
5873
+ "routing-cost": ["read", "edit"],
5735
5874
  scribe: ["read", "edit"],
5736
5875
  ralph: ["read", "edit", "shell"],
5737
5876
  guardian: ["read"],
5738
- linus: ["read", "edit", "shell"],
5877
+ tester: ["read", "edit", "shell"],
5739
5878
  reviewer: ["read"]
5740
5879
  };
5741
5880
  var elevatedTier = {
@@ -6031,6 +6170,8 @@ async function runSubsession(client, req, deps = {}) {
6031
6170
 
6032
6171
  // src/orchestrator/coordinator.ts
6033
6172
  function warnUnknownRole(roleID, warn, warnedRoles) {
6173
+ if (isKnownNonWorkerRole(roleID))
6174
+ return;
6034
6175
  if (warnedRoles !== undefined) {
6035
6176
  if (warnedRoles.has(roleID))
6036
6177
  return;
@@ -6109,7 +6250,7 @@ function routingInput(input, role) {
6109
6250
  ...input.task,
6110
6251
  prompt: input.task.prompt ?? input.prompt
6111
6252
  }).tier;
6112
- const permissionTier = elevateTierForPermissions(classifiedTier, permissionsFor(input.roleID));
6253
+ const permissionTier = elevateTierForPermissions(classifiedTier, permissionsFor(role.roleID));
6113
6254
  const tier = permissionTier;
6114
6255
  const requirement = roleToRequirement(role, input.task, tier);
6115
6256
  const task = roleAdjustedTask(role, input, requirement);
@@ -6517,12 +6658,67 @@ var RosterSchema = z18.object({
6517
6658
  universe: z18.string().min(1),
6518
6659
  entries: z18.array(RosterEntrySchema)
6519
6660
  }).strict();
6661
+ var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
6662
+
6663
+ class RosterParseError extends Error {
6664
+ code;
6665
+ constructor(code, message) {
6666
+ super(message);
6667
+ this.name = "RosterParseError";
6668
+ this.code = code;
6669
+ }
6670
+ }
6671
+ function parseRoster(text) {
6672
+ const result = parseRosterResult(text);
6673
+ if (!result.ok) {
6674
+ throw result.error;
6675
+ }
6676
+ return result.roster;
6677
+ }
6678
+ function parseRosterResult(text) {
6679
+ const match = ROSTER_JSON_FENCE.exec(text);
6680
+ if (match === null || match[1] === undefined) {
6681
+ return {
6682
+ ok: false,
6683
+ error: new RosterParseError("missing-json-block", "openteam roster: no JSON block found in roster file")
6684
+ };
6685
+ }
6686
+ let data;
6687
+ try {
6688
+ data = JSON.parse(match[1]);
6689
+ } catch (error) {
6690
+ return {
6691
+ ok: false,
6692
+ error: new RosterParseError("malformed-json", `openteam roster: malformed JSON in roster file: ${String(error)}`)
6693
+ };
6694
+ }
6695
+ const parsed = RosterSchema.safeParse(data);
6696
+ if (!parsed.success) {
6697
+ return {
6698
+ ok: false,
6699
+ error: new RosterParseError("invalid-roster-schema", `openteam roster: invalid roster schema: ${parsed.error.message}`)
6700
+ };
6701
+ }
6702
+ return { ok: true, roster: parsed.data };
6703
+ }
6520
6704
  var GUARANTEED_ROLE_IDS = [
6521
6705
  "orchestrator",
6522
6706
  "guardian",
6523
6707
  "scribe",
6524
6708
  "ralph"
6525
6709
  ];
6710
+ var GUARANTEED_ROLE_DEFAULT_AGENT_NAMES = {
6711
+ orchestrator: "openteam",
6712
+ guardian: "guardian",
6713
+ scribe: "scribe",
6714
+ ralph: "ralph"
6715
+ };
6716
+ function defaultAgentNameForGuaranteedRole(roleID) {
6717
+ return GUARANTEED_ROLE_DEFAULT_AGENT_NAMES[roleID];
6718
+ }
6719
+ function isGuaranteedRoleID(roleID) {
6720
+ return GUARANTEED_ROLE_IDS.includes(roleID);
6721
+ }
6526
6722
  function missingGuaranteedRoles(roster) {
6527
6723
  const present = new Set(roster.entries.map((entry) => entry.roleID));
6528
6724
  return GUARANTEED_ROLE_IDS.filter((roleID) => !present.has(roleID));
@@ -6530,6 +6726,100 @@ function missingGuaranteedRoles(roster) {
6530
6726
  function hasAllGuaranteedRoles(roster) {
6531
6727
  return missingGuaranteedRoles(roster).length === 0;
6532
6728
  }
6729
+ function isRoleProfileLike(value) {
6730
+ return typeof value === "object" && value !== null && "roleID" in value && "defaultTier" in value;
6731
+ }
6732
+ function configuredRolesFromAuditInput(input) {
6733
+ if ("configuredRoles" in input && !isRoleProfileLike(input.configuredRoles)) {
6734
+ return input.configuredRoles;
6735
+ }
6736
+ return input;
6737
+ }
6738
+ function auditRoster(rosterOrText, configuredRolesOrOptions = {}) {
6739
+ const configuredRoles = configuredRolesFromAuditInput(configuredRolesOrOptions);
6740
+ if (typeof rosterOrText === "string") {
6741
+ const parsed = parseRosterResult(rosterOrText);
6742
+ if (!parsed.ok) {
6743
+ return {
6744
+ findings: [
6745
+ {
6746
+ kind: "unparseable-roster",
6747
+ roleID: "roster",
6748
+ code: parsed.error.code,
6749
+ message: parsed.error.message
6750
+ }
6751
+ ],
6752
+ hasAllGuaranteedRoles: false,
6753
+ missingGuaranteedRoleIDs: [],
6754
+ unprofiledRoleEntries: [],
6755
+ misassignedGuaranteedRoleEntries: []
6756
+ };
6757
+ }
6758
+ return auditParsedRoster(parsed.roster, configuredRoles);
6759
+ }
6760
+ return auditParsedRoster(rosterOrText, configuredRoles);
6761
+ }
6762
+ function auditParsedRoster(roster, configuredRoles) {
6763
+ const missingGuaranteedRoleIDs = missingGuaranteedRoles(roster);
6764
+ const missing = new Set(missingGuaranteedRoleIDs);
6765
+ const findings = missingGuaranteedRoleIDs.map((roleID) => ({
6766
+ kind: "missing-guaranteed-role",
6767
+ roleID
6768
+ }));
6769
+ const unprofiledRoleEntries = [];
6770
+ const misassignedGuaranteedRoleEntries = [];
6771
+ roster.entries.forEach((entry) => {
6772
+ const profile = getRoleProfile(entry.roleID, configuredRoles);
6773
+ if (profile === undefined && !isKnownNonWorkerRole(entry.roleID)) {
6774
+ unprofiledRoleEntries.push({
6775
+ roleID: entry.roleID,
6776
+ agentName: entry.agentName,
6777
+ severity: "info"
6778
+ });
6779
+ findings.push({
6780
+ kind: "unresolved-role-profile",
6781
+ roleID: entry.roleID,
6782
+ agentName: entry.agentName
6783
+ });
6784
+ }
6785
+ if (isGuaranteedRoleID(entry.agentName) && entry.roleID !== entry.agentName && missing.has(entry.agentName)) {
6786
+ misassignedGuaranteedRoleEntries.push({
6787
+ roleID: entry.roleID,
6788
+ agentName: entry.agentName,
6789
+ missingRoleID: entry.agentName,
6790
+ reason: "agentNameMatchesMissingGuaranteedRole"
6791
+ });
6792
+ findings.push({
6793
+ kind: "non-canonical-role-id",
6794
+ roleID: entry.roleID,
6795
+ agentName: entry.agentName,
6796
+ canonicalRoleID: entry.agentName
6797
+ });
6798
+ return;
6799
+ }
6800
+ if (profile !== undefined && isGuaranteedRoleID(profile.roleID) && profile.roleID !== entry.roleID && missing.has(profile.roleID)) {
6801
+ misassignedGuaranteedRoleEntries.push({
6802
+ roleID: entry.roleID,
6803
+ agentName: entry.agentName,
6804
+ missingRoleID: profile.roleID,
6805
+ reason: "roleIDResolvesToMissingGuaranteedRole"
6806
+ });
6807
+ findings.push({
6808
+ kind: "non-canonical-role-id",
6809
+ roleID: entry.roleID,
6810
+ agentName: entry.agentName,
6811
+ canonicalRoleID: profile.roleID
6812
+ });
6813
+ }
6814
+ });
6815
+ return {
6816
+ findings,
6817
+ hasAllGuaranteedRoles: missingGuaranteedRoleIDs.length === 0,
6818
+ missingGuaranteedRoleIDs,
6819
+ unprofiledRoleEntries,
6820
+ misassignedGuaranteedRoleEntries
6821
+ };
6822
+ }
6533
6823
  function enforceGuaranteedRoles(roster) {
6534
6824
  const missing = missingGuaranteedRoles(roster);
6535
6825
  if (missing.length === 0) {
@@ -6537,7 +6827,7 @@ function enforceGuaranteedRoles(roster) {
6537
6827
  }
6538
6828
  const added = missing.map((roleID) => ({
6539
6829
  roleID,
6540
- agentName: roleID
6830
+ agentName: defaultAgentNameForGuaranteedRole(roleID)
6541
6831
  }));
6542
6832
  return { universe: roster.universe, entries: [...roster.entries, ...added] };
6543
6833
  }
@@ -6569,7 +6859,6 @@ async function dispatchGuaranteedRole(agentName, agentFileExists, dispatch) {
6569
6859
  }
6570
6860
 
6571
6861
  // src/orchestrator/rosterPersistence.ts
6572
- var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
6573
6862
  function serializeRoster(roster) {
6574
6863
  const data = { universe: roster.universe, entries: roster.entries };
6575
6864
  const json = JSON.stringify(data, null, 2);
@@ -6582,38 +6871,41 @@ ${json}
6582
6871
  \`\`\`
6583
6872
  `;
6584
6873
  }
6585
- function parseRoster(text) {
6586
- const match = ROSTER_JSON_FENCE.exec(text);
6587
- if (match === null || match[1] === undefined) {
6588
- throw new Error("openteam roster: no JSON block found in roster file");
6589
- }
6590
- let data;
6591
- try {
6592
- data = JSON.parse(match[1]);
6593
- } catch (error) {
6594
- throw new Error(`openteam roster: malformed JSON in roster file: ${String(error)}`);
6595
- }
6596
- return RosterSchema.parse(data);
6597
- }
6598
6874
  async function persistRoster(storage, roster, path2 = OPENTEAM_ROSTER_PATH) {
6599
6875
  await storage.write(path2, serializeRoster(roster));
6600
6876
  return path2;
6601
6877
  }
6602
- async function loadRoster(storage, path2 = OPENTEAM_ROSTER_PATH) {
6878
+ async function loadRosterFile(storage, path2 = OPENTEAM_ROSTER_PATH) {
6603
6879
  const content = await storage.read(path2);
6604
6880
  if (content === undefined) {
6605
- return;
6881
+ return { status: "absent", path: path2 };
6606
6882
  }
6607
- return parseRoster(content);
6883
+ const parsed = parseRosterResult(content);
6884
+ if (!parsed.ok) {
6885
+ return { status: "invalid", path: path2, error: parsed.error };
6886
+ }
6887
+ return {
6888
+ status: "loaded",
6889
+ path: path2,
6890
+ roster: enforceGuaranteedRoles(parsed.roster)
6891
+ };
6608
6892
  }
6609
6893
  async function loadOrGenerateRoster(storage, generate, path2 = OPENTEAM_ROSTER_PATH) {
6610
- const existing = await loadRoster(storage, path2);
6611
- if (existing !== undefined) {
6612
- return { roster: existing, regenerated: false };
6894
+ const existing = await loadRosterFile(storage, path2);
6895
+ if (existing.status === "loaded") {
6896
+ return { roster: existing.roster, regenerated: false };
6613
6897
  }
6614
- const roster = await generate();
6615
- await persistRoster(storage, roster, path2);
6616
- return { roster, regenerated: true };
6898
+ const roster = enforceGuaranteedRoles(await generate());
6899
+ if (existing.status === "absent") {
6900
+ await persistRoster(storage, roster, path2);
6901
+ return { roster, regenerated: true };
6902
+ }
6903
+ return {
6904
+ roster,
6905
+ regenerated: true,
6906
+ recoveredFromInvalidPersisted: true,
6907
+ invalidPersistedRosterError: existing.error.message
6908
+ };
6617
6909
  }
6618
6910
 
6619
6911
  // src/plugin/availability.ts
@@ -7641,6 +7933,105 @@ function agentModelLine(diagnostic, nameWidth, searchedPaths) {
7641
7933
  }
7642
7934
  return ` ${mark} ${name} [${diagnostic.mode}] ${modelText2} ${provenance} ${parts.join(" · ")}`;
7643
7935
  }
7936
+ function diagnosticMark(level) {
7937
+ switch (level) {
7938
+ case "error":
7939
+ return "✗";
7940
+ case "warn":
7941
+ return "⚠";
7942
+ default:
7943
+ return "·";
7944
+ }
7945
+ }
7946
+ function diagnosticsSection(diagnostics) {
7947
+ const notable = diagnostics.filter((diagnostic) => diagnostic.level === "warn" || diagnostic.level === "error");
7948
+ if (notable.length === 0) {
7949
+ return [];
7950
+ }
7951
+ const recent = notable.slice(-5);
7952
+ const lines = [
7953
+ ` plugin diagnostics: ${notable.length} warning/error event(s)`
7954
+ ];
7955
+ for (const diagnostic of recent) {
7956
+ lines.push(` ${diagnosticMark(diagnostic.level)} [${diagnostic.code}] ${diagnosticDescription(diagnostic.code)}`);
7957
+ }
7958
+ if (notable.length > recent.length) {
7959
+ lines.push(` … ${notable.length - recent.length} older diagnostic event(s) omitted`);
7960
+ }
7961
+ lines.push(" note: plugin diagnostics are bounded classes recorded instead of writing to the opencode TUI console.");
7962
+ return lines;
7963
+ }
7964
+ function rosterEntryText(entry) {
7965
+ return entry.agentName === undefined ? `"${entry.roleID}"` : `"${entry.roleID}" (agentName "${entry.agentName}")`;
7966
+ }
7967
+ function canonicalRoleID(entry) {
7968
+ return entry.canonicalRoleID ?? entry.missingRoleID;
7969
+ }
7970
+ function legacyRosterFindings(audit) {
7971
+ const findings = [];
7972
+ for (const roleID of audit.missingGuaranteedRoleIDs ?? []) {
7973
+ findings.push({ kind: "missing-guaranteed-role", roleID });
7974
+ }
7975
+ for (const entry of audit.misassignedGuaranteedRoleEntries ?? []) {
7976
+ const canonical2 = canonicalRoleID(entry);
7977
+ findings.push({
7978
+ kind: "non-canonical-role-id",
7979
+ roleID: entry.roleID,
7980
+ ...entry.agentName !== undefined ? { agentName: entry.agentName } : {},
7981
+ ...canonical2 !== undefined ? { canonicalRoleID: canonical2 } : {}
7982
+ });
7983
+ }
7984
+ for (const entry of audit.unprofiledRoleEntries ?? []) {
7985
+ findings.push({
7986
+ kind: "unresolved-role-profile",
7987
+ roleID: entry.roleID,
7988
+ ...entry.agentName !== undefined ? { agentName: entry.agentName } : {}
7989
+ });
7990
+ }
7991
+ return findings;
7992
+ }
7993
+ function rosterHealthSection(audit, loadError, rosterRoleCount) {
7994
+ const auditFindings = audit?.findings ?? legacyRosterFindings(audit ?? {});
7995
+ const unparseable = auditFindings.find((finding) => finding.kind === "unparseable-roster");
7996
+ const unparseableMessage = loadError ?? unparseable?.message;
7997
+ if (unparseableMessage !== undefined) {
7998
+ return [
7999
+ " roster health:",
8000
+ ` ✗ unparseable roster: ${OPENTEAM_ROSTER_PATH} — ${unparseableMessage}`,
8001
+ " problem: the roster is present but is not machine-readable by openteam.",
8002
+ " remedy: keep any human notes outside the JSON fence, add one fenced `json` block containing `{ universe, entries: [{ roleID, agentName }] }`, then re-run `openteam doctor`."
8003
+ ];
8004
+ }
8005
+ if (audit === undefined) {
8006
+ return [];
8007
+ }
8008
+ const missing = auditFindings.filter((finding) => finding.kind === "missing-guaranteed-role").map((finding) => finding.roleID);
8009
+ const misassigned = auditFindings.filter((finding) => finding.kind === "non-canonical-role-id");
8010
+ const unprofiled = auditFindings.filter((finding) => finding.kind === "unresolved-role-profile");
8011
+ if (missing.length === 0 && misassigned.length === 0 && unprofiled.length === 0) {
8012
+ return rosterRoleCount === undefined ? [] : [
8013
+ " roster health:",
8014
+ ` ✓ ${OPENTEAM_ROSTER_PATH} read (${rosterRoleCount} roles); guaranteed roles present.`
8015
+ ];
8016
+ }
8017
+ const lines = [" roster health:"];
8018
+ if (missing.length > 0) {
8019
+ lines.push(` ✗ missing guaranteed roleID(s): ${missing.join(", ")}`);
8020
+ lines.push(` remedy: edit ${OPENTEAM_ROSTER_PATH}'s JSON block to add these canonical roleIDs with their themed agentName values; keep roleID canonical, e.g. { "roleID": "scribe", "agentName": "c3po" }.`);
8021
+ }
8022
+ for (const entry of misassigned) {
8023
+ const canonical2 = canonicalRoleID(entry);
8024
+ const target = canonical2 === undefined ? "the canonical functional roleID" : `"${canonical2}"`;
8025
+ lines.push(` ⚠ non-canonical roleID ${rosterEntryText(entry)}`);
8026
+ lines.push(` remedy: change roleID to ${target} and keep the themed name in agentName.`);
8027
+ }
8028
+ if (unprofiled.length > 0) {
8029
+ const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
8030
+ lines.push(` · unprofiled roleID(s): ${roleIDs}`);
8031
+ lines.push(" remedy: if intentional project roles, no action is required; for curated routing, change roleID to a TEAM_ROLES key or add a profile under orchestrator.roles in .opencode/openteam.json.");
8032
+ }
8033
+ return lines;
8034
+ }
7644
8035
  function agentModelsSection(diagnostics, searchedPaths) {
7645
8036
  const lines = [" agent models:"];
7646
8037
  if (diagnostics.length === 0) {
@@ -7676,6 +8067,10 @@ function renderDoctor(input) {
7676
8067
  }
7677
8068
  }
7678
8069
  lines.push(` telemetry: ${input.telemetryPath} — ${input.telemetryRecords} record(s)`);
8070
+ if (input.diagnostics !== undefined) {
8071
+ lines.push(...diagnosticsSection(input.diagnostics));
8072
+ }
8073
+ lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
7679
8074
  if (input.otelBackend !== undefined) {
7680
8075
  const otel = input.otelBackend;
7681
8076
  lines.push(" opentelemetry:");
@@ -9320,13 +9715,23 @@ async function guaranteedAgentStatuses(runtime) {
9320
9715
  }
9321
9716
  return statuses;
9322
9717
  }
9718
+ function rosterPlanSummary(rosterFile) {
9719
+ switch (rosterFile.status) {
9720
+ case "loaded":
9721
+ return `${rosterFile.roster.universe} (${rosterFile.roster.entries.length} roles)`;
9722
+ case "absent":
9723
+ return "none yet — one would be generated on the first real run";
9724
+ case "invalid":
9725
+ return `${rosterFile.path} is present but unparseable — run 'openteam doctor'`;
9726
+ }
9727
+ }
9323
9728
  function renderLoopPlan(plan2) {
9324
9729
  const { flags } = plan2;
9325
9730
  const budget = flags.budgetUsd !== undefined ? `$${flags.budgetUsd}` : "unbounded";
9326
9731
  const freshContext = flags.freshContext ? "on (pure Ralph pattern)" : "off";
9327
9732
  const budgetStop = flags.budgetUsd !== undefined ? ", budget" : "";
9328
9733
  const redTestsStop = flags.stopOnRedTests ? ", red tests" : "";
9329
- const rosterSummary = plan2.roster !== undefined ? `${plan2.roster.universe} (${plan2.roster.entries.length} roles)` : "none yet — one would be generated on the first real run";
9734
+ const rosterSummary = rosterPlanSummary(plan2.rosterFile);
9330
9735
  const lines = [
9331
9736
  "openteam loop — plan (dry run, nothing dispatched):",
9332
9737
  ` backlog: ${flags.backlogPath} (${plan2.uncheckedCount} unchecked item(s))`,
@@ -9370,14 +9775,14 @@ ${LOOP_HELP}` };
9370
9775
  const backlogText = await runtime.readBacklog(flags.backlogPath) ?? "";
9371
9776
  const items = parseBacklog(backlogText);
9372
9777
  const unchecked = uncheckedBacklogItems(items);
9373
- const roster = await runtime.loadRoster();
9778
+ const rosterFile = await runtime.loadRosterFile();
9374
9779
  const guaranteed = await guaranteedAgentStatuses(runtime);
9375
9780
  const plannedIterations = Math.max(0, Math.min(flags.maxIterations, unchecked.length));
9376
9781
  const plan2 = {
9377
9782
  flags,
9378
9783
  uncheckedCount: unchecked.length,
9379
9784
  plannedIterations,
9380
- roster,
9785
+ rosterFile,
9381
9786
  guaranteed
9382
9787
  };
9383
9788
  if (flags.dryRun) {
@@ -9455,9 +9860,19 @@ async function runCli(argv, deps) {
9455
9860
  if (command === "doctor") {
9456
9861
  const config = await deps.loadConfig(configPath, configResolution);
9457
9862
  let opencodeConfigError;
9458
- const [snapshots, records, cacheEntries, agentFiles, opencodeConfig] = await Promise.all([
9863
+ let rosterLoadError;
9864
+ const [
9865
+ snapshots,
9866
+ records,
9867
+ diagnostics,
9868
+ cacheEntries,
9869
+ agentFiles,
9870
+ opencodeConfig,
9871
+ roster
9872
+ ] = await Promise.all([
9459
9873
  deps.probe(config),
9460
9874
  deps.readTelemetry(telemetryPath),
9875
+ deps.readDiagnostics !== undefined ? deps.readDiagnostics() : Promise.resolve(undefined),
9461
9876
  deps.cachePort !== undefined ? deps.cachePort.listEntries(deps.cachePort.resolveCacheRoot(parsed.cacheRoot)).catch(() => {
9462
9877
  return;
9463
9878
  }) : Promise.resolve(undefined),
@@ -9465,7 +9880,11 @@ async function runCli(argv, deps) {
9465
9880
  deps.readOpencodeConfig(opencodeConfigPaths).catch((err) => {
9466
9881
  opencodeConfigError = err instanceof Error ? err.message : String(err);
9467
9882
  return;
9468
- })
9883
+ }),
9884
+ deps.loadRosterForDoctor !== undefined ? deps.loadRosterForDoctor().catch((err) => {
9885
+ rosterLoadError = err instanceof Error ? err.message : String(err);
9886
+ return;
9887
+ }) : Promise.resolve(undefined)
9469
9888
  ]);
9470
9889
  const defaultModel = defaultModelFromOpencode(opencodeConfig);
9471
9890
  const agentModels = diagnoseAgentModels(agentFiles, {
@@ -9480,6 +9899,18 @@ async function runCli(argv, deps) {
9480
9899
  backend: "opentelemetry",
9481
9900
  connectionEnv: "APPLICATIONINSIGHTS_CONNECTION_STRING"
9482
9901
  }, (name) => process.env[name]);
9902
+ let rosterAudit;
9903
+ let rosterRoleCount;
9904
+ if (roster !== undefined && deps.auditRoster !== undefined) {
9905
+ if (typeof roster !== "string") {
9906
+ rosterRoleCount = roster.entries.length;
9907
+ }
9908
+ try {
9909
+ rosterAudit = deps.auditRoster(roster, config.orchestrator?.roles);
9910
+ } catch (err) {
9911
+ rosterLoadError = err instanceof Error ? err.message : String(err);
9912
+ }
9913
+ }
9483
9914
  return {
9484
9915
  exitCode: 0,
9485
9916
  stdout: renderDoctor({
@@ -9489,6 +9920,10 @@ async function runCli(argv, deps) {
9489
9920
  telemetryRecords: records.length,
9490
9921
  agentModels,
9491
9922
  opencodeConfigPaths,
9923
+ ...rosterAudit !== undefined ? { rosterAudit } : {},
9924
+ ...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
9925
+ ...rosterLoadError !== undefined ? { rosterLoadError } : {},
9926
+ ...diagnostics !== undefined && diagnostics.length > 0 ? { diagnostics } : {},
9492
9927
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
9493
9928
  ...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
9494
9929
  ...legacyLayout !== undefined && legacyLayout.length > 0 ? { legacyLayout } : {},
@@ -9650,6 +10085,96 @@ function createCommandTool(deps) {
9650
10085
  });
9651
10086
  }
9652
10087
 
10088
+ // src/plugin/diagnostics.ts
10089
+ var PLUGIN_DIAGNOSTIC_SESSION_ID = "plugin";
10090
+ function asDiagnosticClient(client) {
10091
+ if (typeof client !== "object" || client === null) {
10092
+ return {};
10093
+ }
10094
+ return client;
10095
+ }
10096
+ function appLogOptions(diagnostic, directory) {
10097
+ return {
10098
+ body: {
10099
+ service: "openteam",
10100
+ level: diagnostic.level,
10101
+ message: diagnosticDescription(diagnostic.code),
10102
+ extra: {
10103
+ code: diagnostic.code,
10104
+ sessionID: diagnostic.sessionID,
10105
+ source: "plugin"
10106
+ }
10107
+ },
10108
+ ...directory !== undefined && directory.length > 0 ? { query: { directory } } : {}
10109
+ };
10110
+ }
10111
+ function toastOptions(diagnostic, directory) {
10112
+ return {
10113
+ body: {
10114
+ title: `openteam ${diagnostic.level}`,
10115
+ message: `${diagnosticDescription(diagnostic.code)} Run \`openteam doctor\` for details.`,
10116
+ variant: diagnostic.level === "error" ? "error" : "warning",
10117
+ duration: 8000
10118
+ },
10119
+ ...directory !== undefined && directory.length > 0 ? { query: { directory } } : {}
10120
+ };
10121
+ }
10122
+ function fireAndForget(run) {
10123
+ try {
10124
+ Promise.resolve(run()).catch(() => {});
10125
+ } catch {}
10126
+ }
10127
+ function shouldToast(diagnostic, options) {
10128
+ if (options?.toast !== undefined) {
10129
+ return options.toast;
10130
+ }
10131
+ return diagnostic.level === "warn" || diagnostic.level === "error";
10132
+ }
10133
+ function createPluginDiagnosticChannel(deps) {
10134
+ const client = asDiagnosticClient(deps.client);
10135
+ const toastKeys = new Set;
10136
+ let eventSink = deps.sink ?? createNullEventSink();
10137
+ const emit = (input, options = {}) => {
10138
+ const diagnostic = {
10139
+ v: EVENT_SCHEMA_VERSION,
10140
+ type: "diagnostic",
10141
+ ts: deps.now(),
10142
+ sessionID: input.sessionID ?? PLUGIN_DIAGNOSTIC_SESSION_ID,
10143
+ level: input.level,
10144
+ code: input.code
10145
+ };
10146
+ if (options.persist !== false) {
10147
+ fireAndForget(() => eventSink.emit(diagnostic));
10148
+ }
10149
+ const log = client.app?.log;
10150
+ if (typeof log === "function") {
10151
+ fireAndForget(() => log.call(client.app, appLogOptions(diagnostic, deps.directory)));
10152
+ }
10153
+ if (shouldToast(diagnostic, options) && !toastKeys.has(input.code)) {
10154
+ const showToast = client.tui?.showToast;
10155
+ toastKeys.add(input.code);
10156
+ if (typeof showToast === "function") {
10157
+ fireAndForget(() => showToast.call(client.tui, toastOptions(diagnostic, deps.directory)));
10158
+ }
10159
+ }
10160
+ };
10161
+ return {
10162
+ setEventSink: (sink) => {
10163
+ eventSink = sink;
10164
+ },
10165
+ emit,
10166
+ sink: (level, code, options = {}) => (message) => {
10167
+ emit({ level, code }, options);
10168
+ },
10169
+ warn: (code, options = {}) => (message) => {
10170
+ emit({ level: "warn", code }, options);
10171
+ },
10172
+ info: (code, options = {}) => (message) => {
10173
+ emit({ level: "info", code }, options);
10174
+ }
10175
+ };
10176
+ }
10177
+
9653
10178
  // src/plugin/graphTool.ts
9654
10179
  import { tool as tool2 } from "@opencode-ai/plugin";
9655
10180
 
@@ -11111,16 +11636,16 @@ function insertRecords(db, records) {
11111
11636
  throw error;
11112
11637
  }
11113
11638
  }
11114
- function warnMemoryIndexRecordRejections(result) {
11639
+ function warnMemoryIndexRecordRejections(result, warn) {
11115
11640
  if (result.rejectedRows === 0) {
11116
11641
  return;
11117
11642
  }
11118
- console.warn(`[openteam] memory index reader rejected ${result.rejectedRows} row(s); semantic memory may be incomplete.`);
11643
+ (warn ?? console.warn)(`[openteam] memory index reader rejected ${result.rejectedRows} row(s); semantic memory may be incomplete.`);
11119
11644
  for (const rejection of result.rejections) {
11120
- console.warn(`[openteam] • memory_records:${rejection.recordId}: ${rejection.detail}`);
11645
+ (warn ?? console.warn)(`[openteam] • memory_records:${rejection.recordId}: ${rejection.detail}`);
11121
11646
  }
11122
11647
  }
11123
- function createMemoryIndex(db) {
11648
+ function createMemoryIndex(db, warn) {
11124
11649
  db.exec(SCHEMA);
11125
11650
  const allRecordsFn = () => {
11126
11651
  const rows = db.query("SELECT id, payload FROM memory_records ORDER BY created_at ASC, id ASC").all();
@@ -11148,7 +11673,7 @@ function createMemoryIndex(db) {
11148
11673
  };
11149
11674
  const allRecordsWithWarning = () => {
11150
11675
  const result = allRecordsFn();
11151
- warnMemoryIndexRecordRejections(result);
11676
+ warnMemoryIndexRecordRejections(result, warn);
11152
11677
  return result.records;
11153
11678
  };
11154
11679
  const state = () => {
@@ -11987,6 +12512,7 @@ function createOrchestrateTool(deps) {
11987
12512
  sink: deps.sink,
11988
12513
  now: deps.now,
11989
12514
  newDecisionID: deps.newDecisionID,
12515
+ warn: deps.warn,
11990
12516
  warnedRoles: _pluginWarnedRoles,
11991
12517
  localRuntimeReachable: deps.localRuntimeReachable,
11992
12518
  reachableRuntimeIds: deps.reachableRuntimeIds,
@@ -12217,7 +12743,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
12217
12743
  const warn = deps.warn ?? console.warn;
12218
12744
  try {
12219
12745
  const db = await openDatabase(semantic.indexPath);
12220
- const index = createMemoryIndex(db);
12746
+ const index = createMemoryIndex(db, warn);
12221
12747
  const rebuildResult = await rebuildMemoryIndexFromStorage(index, semantic.logPath, { storage: deps.storage });
12222
12748
  if (rebuildResult.rejectedLines > 0) {
12223
12749
  warn(`[openteam] memory log rejected ${rebuildResult.rejectedLines} line(s); semantic memory may be incomplete.`);
@@ -12282,7 +12808,7 @@ var createOtlpSpanExporter = (connection, config) => {
12282
12808
  // package.json
12283
12809
  var package_default = {
12284
12810
  name: "@jmanuelcorral/openteam",
12285
- version: "0.9.0",
12811
+ version: "0.9.2",
12286
12812
  packageManager: "bun@1.3.14",
12287
12813
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
12288
12814
  license: "MIT",
@@ -12395,24 +12921,24 @@ function createShellExec($) {
12395
12921
  };
12396
12922
  };
12397
12923
  }
12398
- function logAvailabilityRefreshError(error) {
12924
+ function logAvailabilityRefreshError(error, warn) {
12399
12925
  const message = error instanceof Error ? error.message : String(error);
12400
- console.warn(`[openteam] availability refresh failed: ${message}`);
12926
+ (warn ?? console.warn)(`[openteam] availability refresh failed: ${message}`);
12401
12927
  }
12402
- function logTelemetryError(error) {
12928
+ function logTelemetryError(error, warn) {
12403
12929
  const message = error instanceof Error ? error.message : String(error);
12404
- console.warn(`[openteam] telemetry write failed: ${message}`);
12930
+ (warn ?? console.warn)(`[openteam] telemetry write failed: ${message}`);
12405
12931
  }
12406
- function logTelemetryWarning(message) {
12407
- console.warn(message);
12932
+ function logTelemetryWarning(message, warn) {
12933
+ (warn ?? console.warn)(message);
12408
12934
  }
12409
12935
  var opencodeServerUrlLogged = false;
12410
- function logOpencodeServerUrl(serverUrl) {
12936
+ function logOpencodeServerUrl(serverUrl, warn) {
12411
12937
  if (opencodeServerUrlLogged) {
12412
12938
  return;
12413
12939
  }
12414
12940
  opencodeServerUrlLogged = true;
12415
- console.warn(`[openteam] plugin initialisation ctx.serverUrl: ${serverUrl.toString()}`);
12941
+ (warn ?? console.warn)(`[openteam] plugin initialisation ctx.serverUrl: ${serverUrl.toString()}`);
12416
12942
  }
12417
12943
  var MAX_LOGGED_OPENCODE_VERSION_FAILURE_CAUSES = 64;
12418
12944
  function createOpencodeVersionReadFailureLogger(maxCauses = MAX_LOGGED_OPENCODE_VERSION_FAILURE_CAUSES, warn) {
@@ -12434,26 +12960,25 @@ function createOpencodeVersionReadFailureLogger(maxCauses = MAX_LOGGED_OPENCODE_
12434
12960
  (warn ?? console.warn)(`[openteam] opencode version read failed: ${message}`);
12435
12961
  };
12436
12962
  }
12437
- var logOpencodeVersionReadFailure = createOpencodeVersionReadFailureLogger();
12438
- function warnNoRuntimeConfigFound(paths, optionsOnly, localModel) {
12963
+ function warnNoRuntimeConfigFound(paths, optionsOnly, localModel, warn) {
12439
12964
  const searched = paths.join(", ");
12440
12965
  const modelRef = localModel !== null ? `\`${localModel.providerID}/${localModel.modelID}\`` : "none (no local runtime configured)";
12441
12966
  if (optionsOnly) {
12442
- console.warn(`[openteam] config file not found (searched: ${searched}); ` + `routing is configured by plugin options only — local runtimes declared ` + `in ${paths[0] ?? searched} are not loaded. ` + `Local model in use: ${modelRef}. ` + `Create ${DEFAULT_CONFIG_PATH} to persist your local runtime configuration.`);
12967
+ (warn ?? console.warn)(`[openteam] config file not found (searched: ${searched}); ` + `routing is configured by plugin options only — local runtimes declared ` + `in ${paths[0] ?? searched} are not loaded. ` + `Local model in use: ${modelRef}. ` + `Create ${DEFAULT_CONFIG_PATH} to persist your local runtime configuration.`);
12443
12968
  } else {
12444
- console.warn(`[openteam] no config file found at ${searched}. ` + `Routing uses schema defaults — local model: ${modelRef}. ` + `Privacy-sensitive tasks under forceLocalOnSensitive will be routed to frontier ` + `(no local runtime configured). Run \`openteam setup\` or create ${DEFAULT_CONFIG_PATH}.`);
12969
+ (warn ?? console.warn)(`[openteam] no config file found at ${searched}. ` + `Routing uses schema defaults — local model: ${modelRef}. ` + `Privacy-sensitive tasks under forceLocalOnSensitive will be routed to frontier ` + `(no local runtime configured). Run \`openteam setup\` or create ${DEFAULT_CONFIG_PATH}.`);
12445
12970
  }
12446
12971
  }
12447
12972
  function warnInertPrivacySetting(paths, warn) {
12448
12973
  (warn ?? console.warn)(`[openteam] privacyMode is "forceLocalOnSensitive" but no local runtime is configured ` + `(searched: ${paths.join(", ")}). ` + `Privacy-sensitive tasks will be routed to frontier and labelled "frontier" in telemetry ` + `(rationale: "privacy-force-local-none-configured"). ` + `To keep sensitive prompts local, configure a local runtime in ` + `${paths[0] ?? DEFAULT_CONFIG_PATH} or use the alwaysLocal override per request.`);
12449
12974
  }
12450
- function warnSoakEvidenceRejections(result) {
12975
+ function warnSoakEvidenceRejections(result, warn) {
12451
12976
  if (result.rejectedLines === 0) {
12452
12977
  return;
12453
12978
  }
12454
- console.warn(`[openteam] soak evidence reader rejected ${result.rejectedLines} observation line(s); cutover gate evidence may be incomplete.`);
12979
+ (warn ?? console.warn)(`[openteam] soak evidence reader rejected ${result.rejectedLines} observation line(s); cutover gate evidence may be incomplete.`);
12455
12980
  for (const rejection of result.rejections) {
12456
- console.warn(`[openteam] • ${rejection.file}:${rejection.lineNumber}: ${rejection.detail}`);
12981
+ (warn ?? console.warn)(`[openteam] • ${rejection.file}:${rejection.lineNumber}: ${rejection.detail}`);
12457
12982
  }
12458
12983
  }
12459
12984
  var SHADOW_CERT_PATH = "artifacts/graph-shadow-certificate.json";
@@ -12630,7 +13155,7 @@ function createLazyOpencodeVersionReader(serverUrl, options = {}) {
12630
13155
  }
12631
13156
  };
12632
13157
  }
12633
- async function buildGateInput(config, storage, opencodeVersion) {
13158
+ async function buildGateInput(config, storage, opencodeVersion, warn) {
12634
13159
  const shadow = await readAndParseCertificate(storage, SHADOW_CERT_PATH, (raw) => parseShadowCertificate(raw, opencodeVersion), (cert) => shadowCertificateBindingDigest(cert.evidence));
12635
13160
  const shadowDigest = shadow.status === "valid" ? shadow.digest : "0".repeat(64);
12636
13161
  const release = await readAndParseCertificate(storage, RELEASE_CERT_PATH, (raw) => parseReleaseCertificate(raw, opencodeVersion, shadowDigest), (cert) => cert.digest);
@@ -12638,7 +13163,7 @@ async function buildGateInput(config, storage, opencodeVersion) {
12638
13163
  try {
12639
13164
  const recorderPorts = recorderPortsFromStorage(storage);
12640
13165
  const chainRead = await readAllChains(recorderPorts, SOAK_EVIDENCE_DIR);
12641
- warnSoakEvidenceRejections(chainRead);
13166
+ warnSoakEvidenceRejections(chainRead, warn);
12642
13167
  if (chainRead.chains.length > 0) {
12643
13168
  soakLedgerDigest = canonicalLedger(chainRead.chains);
12644
13169
  }
@@ -12667,8 +13192,8 @@ function recorderPortsFromStorage(storage) {
12667
13192
  id: { randomUUID: () => crypto.randomUUID() }
12668
13193
  };
12669
13194
  }
12670
- async function buildMemoryInjectorFromConfig(config, storage) {
12671
- const runtime = await buildMemoryRuntimeFromConfig(config, { storage }, { requireInjectionEnabled: true });
13195
+ async function buildMemoryInjectorFromConfig(config, storage, warn) {
13196
+ const runtime = await buildMemoryRuntimeFromConfig(config, { storage, ...warn !== undefined ? { warn } : {} }, { requireInjectionEnabled: true });
12672
13197
  if (runtime === undefined) {
12673
13198
  return;
12674
13199
  }
@@ -12685,8 +13210,10 @@ async function buildMemoryInjectorFromConfig(config, storage) {
12685
13210
  }
12686
13211
  });
12687
13212
  }
12688
- function createEventSink(rawOptions, storage = createFsStorageProvider(process.cwd())) {
13213
+ function createEventSink(rawOptions, storage = createFsStorageProvider(process.cwd()), loggers = {}) {
12689
13214
  const options = telemetryOptions(rawOptions);
13215
+ const onError = loggers.onError !== undefined ? loggers.onError : (error) => logTelemetryError(error);
13216
+ const onWarn = loggers.onWarn !== undefined ? loggers.onWarn : (message) => logTelemetryWarning(message);
12690
13217
  const { sink } = createTelemetryEventSink({
12691
13218
  enabled: options.enabled,
12692
13219
  otel: OtelBackendConfigSchema.parse({
@@ -12697,13 +13224,13 @@ function createEventSink(rawOptions, storage = createFsStorageProvider(process.c
12697
13224
  eventLog: {
12698
13225
  dir: options.sessionsDir,
12699
13226
  storage,
12700
- onError: logTelemetryError
13227
+ onError
12701
13228
  },
12702
13229
  otel: {
12703
13230
  env: (name) => process.env[name],
12704
13231
  buildExporter: createOtlpSpanExporter,
12705
- onError: logTelemetryError,
12706
- onWarn: logTelemetryWarning
13232
+ onError,
13233
+ onWarn
12707
13234
  }
12708
13235
  });
12709
13236
  return sink;
@@ -12721,6 +13248,18 @@ async function readCliConfigFromStorage(storage, path4, resolution) {
12721
13248
  }
12722
13249
  return content;
12723
13250
  }
13251
+ async function readStoredRosterForDoctor(storage) {
13252
+ const content = await storage.read(OPENTEAM_ROSTER_PATH);
13253
+ if (content === undefined) {
13254
+ return;
13255
+ }
13256
+ try {
13257
+ return parseRoster(content);
13258
+ } catch {
13259
+ return content;
13260
+ }
13261
+ }
13262
+ var auditStoredRosterForDoctor = (roster, configuredRoles) => typeof roster === "string" ? auditRoster(roster, configuredRoles) : auditRoster(roster, configuredRoles);
12724
13263
  function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SESSIONS_DIR, storage = createFsStorageProvider(process.cwd()), fallbackSource = "options", rawOptions) {
12725
13264
  return {
12726
13265
  loadConfig: async (path4, resolution = { explicit: false }) => {
@@ -12783,6 +13322,9 @@ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SE
12783
13322
  storage,
12784
13323
  legacyTelemetryPath: path4
12785
13324
  }),
13325
+ readDiagnostics: () => readDiagnosticEvents(sessionsDir, {
13326
+ storage
13327
+ }),
12786
13328
  readGraphSnapshot: createGraphStatusSnapshotReader({
12787
13329
  storage,
12788
13330
  fallbackConfig: config,
@@ -12801,6 +13343,8 @@ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SE
12801
13343
  agentDir: dirname4(ORCHESTRATOR_AGENT_PATH),
12802
13344
  cachePort: realCacheAdapter,
12803
13345
  purge: createPluginPurgeRuntime(sessionsDir),
13346
+ loadRosterForDoctor: () => readStoredRosterForDoctor(storage),
13347
+ auditRoster: auditStoredRosterForDoctor,
12804
13348
  version: PACKAGE_VERSION
12805
13349
  };
12806
13350
  }
@@ -12876,7 +13420,13 @@ function createLoopTool(deps) {
12876
13420
  });
12877
13421
  }
12878
13422
  var server = async (ctx, rawOptions) => {
12879
- logOpencodeServerUrl(ctx.serverUrl);
13423
+ const now = () => Date.now();
13424
+ const diagnostics = createPluginDiagnosticChannel({
13425
+ client: ctx.client,
13426
+ directory: ctx.directory,
13427
+ now
13428
+ });
13429
+ logOpencodeServerUrl(ctx.serverUrl, diagnostics.info("opencode-server-url", { persist: false, toast: false }));
12880
13430
  const storage = createFsStorageProvider(ctx.directory || ctx.worktree || process.cwd());
12881
13431
  const runtimeConfig = await loadRuntimeOpenTeamConfig(storage, rawOptions);
12882
13432
  const config = runtimeConfig.config;
@@ -12888,13 +13438,23 @@ var server = async (ctx, rawOptions) => {
12888
13438
  const cache = createAvailabilityCache({
12889
13439
  config,
12890
13440
  registry,
12891
- onRefreshError: logAvailabilityRefreshError
13441
+ onRefreshError: (error) => logAvailabilityRefreshError(error, diagnostics.warn("availability-refresh-failed", { toast: false }))
12892
13442
  });
12893
13443
  cache.refresh();
12894
13444
  const telemetryPath = telemetryOptions(rawOptions).path;
12895
13445
  const sessionsDir = telemetryOptions(rawOptions).sessionsDir;
12896
- const sink = createEventSink(rawOptions, storage);
12897
- const injectMemory = await buildMemoryInjectorFromConfig(config, storage);
13446
+ const sink = createEventSink(rawOptions, storage, {
13447
+ onError: (error) => logTelemetryError(error, diagnostics.warn("telemetry-write-failed", {
13448
+ persist: false,
13449
+ toast: false
13450
+ })),
13451
+ onWarn: (message) => logTelemetryWarning(message, diagnostics.warn("telemetry-warning", {
13452
+ persist: false,
13453
+ toast: false
13454
+ }))
13455
+ });
13456
+ diagnostics.setEventSink(sink);
13457
+ const injectMemory = await buildMemoryInjectorFromConfig(config, storage, diagnostics.warn("memory-runtime-warning", { toast: false }));
12898
13458
  const sessionTracker = createSessionTracker();
12899
13459
  const hooks = createHooks(config, cache.get, injectMemory === undefined ? {
12900
13460
  sink,
@@ -12906,7 +13466,6 @@ var server = async (ctx, rawOptions) => {
12906
13466
  });
12907
13467
  const cliDeps = createCliDeps(config, registry, telemetryPath, sessionsDir, storage, runtimeConfig.source, rawOptions);
12908
13468
  const toolcalls = createToolcallTracker({ sink });
12909
- const now = () => Date.now();
12910
13469
  const announcedSessions = new Set;
12911
13470
  const announceEndpoint = (sessionID) => {
12912
13471
  if (sessionID === undefined || announcedSessions.has(sessionID)) {
@@ -12923,24 +13482,25 @@ var server = async (ctx, rawOptions) => {
12923
13482
  };
12924
13483
  let graphConfig = resolveGraphConfig(config, runtimeConfig.source);
12925
13484
  if (!runtimeConfig.fileFound) {
12926
- warnNoRuntimeConfigFound(runtimeConfig.searchedPaths, runtimeConfig.optionsFound, localDefault(config));
13485
+ warnNoRuntimeConfigFound(runtimeConfig.searchedPaths, runtimeConfig.optionsFound, localDefault(config), diagnostics.warn("config-missing"));
12927
13486
  }
12928
13487
  if (config.privacyMode === "forceLocalOnSensitive" && localDefault(config) === null) {
12929
- warnInertPrivacySetting(runtimeConfig.searchedPaths);
13488
+ warnInertPrivacySetting(runtimeConfig.searchedPaths, diagnostics.warn("privacy-inert"));
12930
13489
  }
12931
13490
  const getOpencodeVersion = createLazyOpencodeVersionReader(ctx.serverUrl, {
12932
13491
  client: ctx.client,
12933
- onFailure: logOpencodeVersionReadFailure
13492
+ onFailure: createOpencodeVersionReadFailureLogger(MAX_LOGGED_OPENCODE_VERSION_FAILURE_CAUSES, diagnostics.warn("opencode-version-read-failed", { toast: false }))
12934
13493
  });
12935
13494
  if (isGateApplicable(graphConfig.effectiveMode)) {
12936
13495
  const opencodeVersion = await getOpencodeVersion();
12937
- const gateInput = await buildGateInput(config, storage, opencodeVersion);
13496
+ const gateInput = await buildGateInput(config, storage, opencodeVersion, diagnostics.warn("soak-evidence-rejected", { toast: false }));
12938
13497
  const gateResult = evaluateGraphGate(gateInput);
12939
13498
  graphConfig = applyGraphGate(graphConfig, gateResult);
12940
13499
  if (graphConfig.gateDenied === true && graphConfig.gateViolations !== undefined) {
12941
- console.warn("[openteam] ⛔ cutover gate denied active mode — degrading to off");
13500
+ const graphGateWarn = diagnostics.warn("graph-gate-denied");
13501
+ graphGateWarn("[openteam] ⛔ cutover gate denied active mode — degrading to off");
12942
13502
  for (const v of graphConfig.gateViolations) {
12943
- console.warn(`[openteam] • ${v.code}: ${v.detail}`);
13503
+ graphGateWarn(`[openteam] • ${v.code}: ${v.detail}`);
12944
13504
  }
12945
13505
  }
12946
13506
  }
@@ -13002,14 +13562,16 @@ var server = async (ctx, rawOptions) => {
13002
13562
  observeLegacyExecution: graphSurface.observeLegacyExecution,
13003
13563
  localRuntimeReachable,
13004
13564
  reachableRuntimeIds: probeReachableRuntimeIds,
13005
- runtimeLimiter
13565
+ runtimeLimiter,
13566
+ warn: diagnostics.warn("unknown-role")
13006
13567
  });
13007
13568
  const memoryTool = createMemoryTool({
13008
13569
  config,
13009
13570
  storage,
13010
13571
  now,
13011
13572
  localRuntimeReachable,
13012
- fetch: globalThis.fetch
13573
+ fetch: globalThis.fetch,
13574
+ warn: diagnostics.warn("memory-extraction-warning", { toast: false })
13013
13575
  });
13014
13576
  const loopTool = createLoopTool({
13015
13577
  storage,
@@ -13019,6 +13581,7 @@ var server = async (ctx, rawOptions) => {
13019
13581
  sink,
13020
13582
  now,
13021
13583
  newDecisionID: () => crypto.randomUUID(),
13584
+ warn: diagnostics.warn("unknown-role"),
13022
13585
  localRuntimeReachable,
13023
13586
  reachableRuntimeIds: probeReachableRuntimeIds,
13024
13587
  runtimeLimiter
@@ -13075,7 +13638,6 @@ export {
13075
13638
  readOpencodeVersion,
13076
13639
  logTelemetryWarning,
13077
13640
  logTelemetryError,
13078
- logOpencodeVersionReadFailure,
13079
13641
  logOpencodeServerUrl,
13080
13642
  logAvailabilityRefreshError,
13081
13643
  isMissingFile,