@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/cli.js CHANGED
@@ -104,8 +104,8 @@ var AgentRoleProfileSchema = z.object({
104
104
  localRuntimes: z.array(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"])).optional()
105
105
  }).strict();
106
106
  var TEAM_ROLES = {
107
- rusty: {
108
- roleID: "rusty",
107
+ architect: {
108
+ roleID: "architect",
109
109
  opencodeAgent: "architect",
110
110
  defaultTier: "hard",
111
111
  minReasoningTier: 5,
@@ -114,8 +114,8 @@ var TEAM_ROLES = {
114
114
  preferredFrontierModels: ["auto"],
115
115
  localFirst: false
116
116
  },
117
- livingston: {
118
- roleID: "livingston",
117
+ integration: {
118
+ roleID: "integration",
119
119
  opencodeAgent: "integration",
120
120
  defaultTier: "moderate",
121
121
  minReasoningTier: 4,
@@ -124,8 +124,8 @@ var TEAM_ROLES = {
124
124
  preferredFrontierModels: ["auto"],
125
125
  localFirst: false
126
126
  },
127
- yen: {
128
- roleID: "yen",
127
+ "local-runtime": {
128
+ roleID: "local-runtime",
129
129
  opencodeAgent: "local-runtime",
130
130
  defaultTier: "moderate",
131
131
  minReasoningTier: 3,
@@ -134,8 +134,8 @@ var TEAM_ROLES = {
134
134
  preferredFrontierModels: ["auto"],
135
135
  localFirst: true
136
136
  },
137
- basher: {
138
- roleID: "basher",
137
+ "routing-cost": {
138
+ roleID: "routing-cost",
139
139
  opencodeAgent: "routing-cost",
140
140
  defaultTier: "moderate",
141
141
  minReasoningTier: 3,
@@ -177,8 +177,8 @@ var TEAM_ROLES = {
177
177
  localFirst: true,
178
178
  requiresLocalRuntime: true
179
179
  },
180
- linus: {
181
- roleID: "linus",
180
+ tester: {
181
+ roleID: "tester",
182
182
  opencodeAgent: "tester",
183
183
  defaultTier: "simple",
184
184
  minReasoningTier: 2,
@@ -198,8 +198,35 @@ var TEAM_ROLES = {
198
198
  localFirst: true
199
199
  }
200
200
  };
201
+ var LEGACY_TEAM_ROLE_ALIASES = {
202
+ rusty: "architect",
203
+ livingston: "integration",
204
+ yen: "local-runtime",
205
+ basher: "routing-cost",
206
+ linus: "tester"
207
+ };
208
+ var KNOWN_NON_WORKER_ROLE_IDS = ["orchestrator", "openteam"];
209
+ function isKnownNonWorkerRole(roleID) {
210
+ return KNOWN_NON_WORKER_ROLE_IDS.includes(roleID);
211
+ }
212
+ function getConfiguredRoleProfileByAlias(roleID, roles) {
213
+ if (roles === undefined) {
214
+ return;
215
+ }
216
+ for (const key of Object.keys(roles).sort()) {
217
+ const profile = roles[key];
218
+ if (profile?.opencodeAgent === roleID) {
219
+ return profile;
220
+ }
221
+ }
222
+ return;
223
+ }
224
+ function getLegacyTeamRoleProfile(roleID) {
225
+ const canonicalRoleID = LEGACY_TEAM_ROLE_ALIASES[roleID];
226
+ return canonicalRoleID === undefined ? undefined : TEAM_ROLES[canonicalRoleID];
227
+ }
201
228
  function getRoleProfile(roleID, configuredRoles) {
202
- return configuredRoles?.[roleID] ?? TEAM_ROLES[roleID];
229
+ return configuredRoles?.[roleID] ?? TEAM_ROLES[roleID] ?? getConfiguredRoleProfileByAlias(roleID, configuredRoles) ?? getLegacyTeamRoleProfile(roleID);
203
230
  }
204
231
  function synthesiseFallbackProfile(roleID) {
205
232
  return {
@@ -967,6 +994,50 @@ var ModelCapabilityProfileSchema = z3.object({
967
994
  availability: z3.enum(["available", "degraded", "unavailable"])
968
995
  }).strict();
969
996
 
997
+ // src/telemetry/diagnostics.ts
998
+ var DIAGNOSTIC_CODES = [
999
+ "availability-refresh-failed",
1000
+ "config-missing",
1001
+ "graph-gate-denied",
1002
+ "memory-extraction-warning",
1003
+ "memory-runtime-warning",
1004
+ "opencode-server-url",
1005
+ "opencode-version-read-failed",
1006
+ "privacy-inert",
1007
+ "soak-evidence-rejected",
1008
+ "telemetry-warning",
1009
+ "telemetry-write-failed",
1010
+ "unknown-role"
1011
+ ];
1012
+ function diagnosticDescription(code) {
1013
+ switch (code) {
1014
+ case "availability-refresh-failed":
1015
+ return "Local runtime availability refresh failed; routing will continue with the previous or empty availability snapshot.";
1016
+ case "config-missing":
1017
+ return "openteam did not find its runtime config file; defaults or plugin options were used.";
1018
+ case "graph-gate-denied":
1019
+ return "The graph cutover gate denied active mode and openteam degraded graph mode to off.";
1020
+ case "memory-extraction-warning":
1021
+ return "Semantic memory extraction reported a non-fatal local-runtime warning; no raw conversation content was recorded.";
1022
+ case "memory-runtime-warning":
1023
+ return "Semantic memory runtime setup reported a non-fatal warning; memory recall may be incomplete.";
1024
+ case "opencode-server-url":
1025
+ return "openteam observed the opencode server URL during plugin initialization.";
1026
+ case "opencode-version-read-failed":
1027
+ return "openteam could not read the live opencode version; graph gates fail closed when this happens.";
1028
+ case "privacy-inert":
1029
+ return "forceLocalOnSensitive is configured but no local runtime is configured, so the setting cannot keep sensitive prompts local.";
1030
+ case "soak-evidence-rejected":
1031
+ return "Graph soak evidence contained rejected observation lines; cutover gate evidence may be incomplete.";
1032
+ case "telemetry-warning":
1033
+ return "An optional telemetry backend warning occurred; local JSONL telemetry remains the fallback when enabled.";
1034
+ case "telemetry-write-failed":
1035
+ return "Writing an openteam telemetry event failed; plugin execution continued.";
1036
+ case "unknown-role":
1037
+ return "An unknown role ID was encountered; openteam used a neutral frontier-eligible fallback. If intentional, define the role in orchestrator.roles.";
1038
+ }
1039
+ }
1040
+
970
1041
  // src/telemetry/events.ts
971
1042
  var EVENT_SCHEMA_VERSION = 1;
972
1043
  var EventBaseSchema = z4.object({
@@ -1060,6 +1131,13 @@ var ShadowDiagnosticEventSchema = EventBaseSchema.extend({
1060
1131
  "unknown"
1061
1132
  ])
1062
1133
  });
1134
+ var DiagnosticLevelSchema = z4.enum(["debug", "info", "warn", "error"]);
1135
+ var DiagnosticCodeSchema = z4.enum(DIAGNOSTIC_CODES);
1136
+ var DiagnosticEventSchema = EventBaseSchema.extend({
1137
+ type: z4.literal("diagnostic"),
1138
+ level: DiagnosticLevelSchema,
1139
+ code: DiagnosticCodeSchema
1140
+ });
1063
1141
  var OpenTeamEventSchema = z4.discriminatedUnion("type", [
1064
1142
  RouteEventSchema,
1065
1143
  MessageEventSchema,
@@ -1068,7 +1146,8 @@ var OpenTeamEventSchema = z4.discriminatedUnion("type", [
1068
1146
  DecisionEventSchema,
1069
1147
  ActivityEventSchema,
1070
1148
  SessionEndpointEventSchema,
1071
- ShadowDiagnosticEventSchema
1149
+ ShadowDiagnosticEventSchema,
1150
+ DiagnosticEventSchema
1072
1151
  ]);
1073
1152
 
1074
1153
  // src/storage/fsStorageProvider.ts
@@ -1328,6 +1407,9 @@ async function readRouteCostRecords(dir, deps) {
1328
1407
  }
1329
1408
  return records;
1330
1409
  }
1410
+ async function readDiagnosticEvents(dir, deps) {
1411
+ return (await readSessionEvents(dir, deps)).filter((event) => event.type === "diagnostic");
1412
+ }
1331
1413
  async function readSessionEvents(dir, deps) {
1332
1414
  const files = (await deps.storage.list(dir)).filter((file) => file.endsWith(".jsonl"));
1333
1415
  const perFile = await Promise.all(files.map(async (file) => {
@@ -1368,14 +1450,21 @@ function hashPrompt(prompt) {
1368
1450
 
1369
1451
  // src/orchestrator/permissions.ts
1370
1452
  var ROLE_PERMISSIONS = {
1371
- rusty: ["read", "edit", "multiFileEdit", "destructive", "network", "shell"],
1372
- livingston: ["read", "edit", "multiFileEdit", "network", "shell"],
1373
- yen: ["read", "edit", "network", "shell"],
1374
- basher: ["read", "edit"],
1453
+ architect: [
1454
+ "read",
1455
+ "edit",
1456
+ "multiFileEdit",
1457
+ "destructive",
1458
+ "network",
1459
+ "shell"
1460
+ ],
1461
+ integration: ["read", "edit", "multiFileEdit", "network", "shell"],
1462
+ "local-runtime": ["read", "edit", "network", "shell"],
1463
+ "routing-cost": ["read", "edit"],
1375
1464
  scribe: ["read", "edit"],
1376
1465
  ralph: ["read", "edit", "shell"],
1377
1466
  guardian: ["read"],
1378
- linus: ["read", "edit", "shell"],
1467
+ tester: ["read", "edit", "shell"],
1379
1468
  reviewer: ["read"]
1380
1469
  };
1381
1470
  var elevatedTier = {
@@ -1671,6 +1760,8 @@ async function runSubsession(client, req, deps = {}) {
1671
1760
 
1672
1761
  // src/orchestrator/coordinator.ts
1673
1762
  function warnUnknownRole(roleID, warn, warnedRoles) {
1763
+ if (isKnownNonWorkerRole(roleID))
1764
+ return;
1674
1765
  if (warnedRoles !== undefined) {
1675
1766
  if (warnedRoles.has(roleID))
1676
1767
  return;
@@ -1749,7 +1840,7 @@ function routingInput(input, role) {
1749
1840
  ...input.task,
1750
1841
  prompt: input.task.prompt ?? input.prompt
1751
1842
  }).tier;
1752
- const permissionTier = elevateTierForPermissions(classifiedTier, permissionsFor(input.roleID));
1843
+ const permissionTier = elevateTierForPermissions(classifiedTier, permissionsFor(role.roleID));
1753
1844
  const tier = permissionTier;
1754
1845
  const requirement = roleToRequirement(role, input.task, tier);
1755
1846
  const task = roleAdjustedTask(role, input, requirement);
@@ -8557,22 +8648,70 @@ function buildOrchestratorAgent(frontier, options = {}) {
8557
8648
  "1. When you create the team for the first time, **choose a thematic universe**",
8558
8649
  " (e.g. a film, series, comic or mythology). If the user has a preference,",
8559
8650
  " ask them for it; if not, propose one and move forward without blocking work.",
8560
- "2. Give each subagent the **name of a character** from that universe; the",
8561
- " file name (`.opencode/agent/<name>.md`) is its alias for",
8651
+ "2. Give each subagent an `agentName`: the **name of a character** from that universe;",
8652
+ " the file name (`.opencode/agent/<agentName>.md`) is its alias for",
8562
8653
  " `@mention`, and its role is clear from the `description`.",
8563
- `3. **Register the cast** in \`${OPENTEAM_ROSTER_PATH}\` (universe + table`,
8564
- " name role model) so that the names **persist** across sessions.",
8565
- " This file goes **outside** `.opencode/agent/` (otherwise opencode would load it",
8566
- " as a phantom agent). Always reuse the same cast; do not re-cast",
8567
- " without reason.",
8654
+ "3. Give each entry a `roleID`: the stable functional routing key. `roleID` is",
8655
+ " **not** the themed name. Worked example: C-3PO as the scribe is",
8656
+ ' `{ "roleID": "scribe", "agentName": "c3po" }`, never',
8657
+ ' `{ "roleID": "c3po", "agentName": "c3po" }`.',
8658
+ "4. Prefer existing curated `roleID` keys when they fit:",
8659
+ " `architect`, `integration`, `tester`, `reviewer`, `local-runtime`,",
8660
+ " `routing-cost` (plus the guaranteed `orchestrator`, `guardian`,",
8661
+ " `scribe`, `ralph`). The roleID space is open: use free-form project",
8662
+ " roleIDs only when no curated role fits.",
8663
+ `5. **Register the cast** in \`${OPENTEAM_ROSTER_PATH}\` so the names persist`,
8664
+ " across sessions. This file goes **outside** `.opencode/agent/` (otherwise",
8665
+ " opencode would load it as a phantom agent). Always reuse the same cast;",
8666
+ " do not re-cast without reason.",
8667
+ "",
8668
+ "## Roster file format",
8669
+ "",
8670
+ "Write the roster as Markdown with exactly one fenced `json` block matching",
8671
+ "`{ universe, entries: [{ roleID, agentName }] }`. Do not write a table.",
8672
+ "Minimal parseable example:",
8673
+ "",
8674
+ "# openteam roster",
8675
+ "",
8676
+ "Universe: star-wars",
8677
+ "",
8678
+ "```json",
8679
+ "{",
8680
+ ' "universe": "star-wars",',
8681
+ ' "entries": [',
8682
+ " {",
8683
+ ' "roleID": "orchestrator",',
8684
+ ' "agentName": "openteam"',
8685
+ " },",
8686
+ " {",
8687
+ ' "roleID": "guardian",',
8688
+ ' "agentName": "leia"',
8689
+ " },",
8690
+ " {",
8691
+ ' "roleID": "scribe",',
8692
+ ' "agentName": "c3po"',
8693
+ " },",
8694
+ " {",
8695
+ ' "roleID": "ralph",',
8696
+ ' "agentName": "r2d2"',
8697
+ " },",
8698
+ " {",
8699
+ ' "roleID": "tester",',
8700
+ ' "agentName": "rex"',
8701
+ " }",
8702
+ " ]",
8703
+ "}",
8704
+ "```",
8568
8705
  "",
8569
8706
  "## How to create the team when it does not exist",
8570
8707
  "",
8571
8708
  "1. Analyze the goal and explore the repository (`read`/`glob`/`grep`).",
8572
8709
  "2. Break the request into tasks and design the **minimal team** needed:",
8573
- " define only the roles the work requires (e.g. architect, backend,",
8574
- " frontend, reviewer, tester, docs). Do not invent roles you will not use.",
8575
- "3. Create each subagent **on demand** by writing `.opencode/agent/<name>.md`:",
8710
+ " define only the roles the work requires. Reuse curated roleIDs such as",
8711
+ " `architect`, `integration`, `reviewer`, `tester`, `local-runtime` and",
8712
+ " `routing-cost`; use custom roleIDs for uncovered project specialties.",
8713
+ " Do not invent roles you will not use.",
8714
+ "3. Create each subagent **on demand** by writing `.opencode/agent/<agentName>.md`:",
8576
8715
  " - Frontmatter: `description` (required, include role and universe),",
8577
8716
  " `mode: subagent`, `temperature`, `permission` with the minimum needed,",
8578
8717
  " and `model` **optional**.",
@@ -8593,8 +8732,10 @@ function buildOrchestratorAgent(frontier, options = {}) {
8593
8732
  "",
8594
8733
  "## Standard team roles",
8595
8734
  "",
8596
- "Besides the project-specific roles, include these standard roles",
8597
- "(create them on demand, with their universe name) when they add value:",
8735
+ "Every roster MUST include these guaranteed canonical roleIDs exactly:",
8736
+ "`orchestrator`, `guardian`, `scribe`, and `ralph`. Map `orchestrator` to",
8737
+ "agentName `openteam` (the primary agent). Create the `guardian`, `scribe`,",
8738
+ "and `ralph` subagent files on demand with their themed `agentName` values:",
8598
8739
  "",
8599
8740
  "- **scribe** — the team's silent memory. Records decisions and",
8600
8741
  " learnings in a shared log (`.opencode/openteam/decisions.md`) without",
@@ -9805,16 +9946,165 @@ var RosterSchema = z17.object({
9805
9946
  universe: z17.string().min(1),
9806
9947
  entries: z17.array(RosterEntrySchema)
9807
9948
  }).strict();
9949
+ var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
9950
+
9951
+ class RosterParseError extends Error {
9952
+ code;
9953
+ constructor(code, message) {
9954
+ super(message);
9955
+ this.name = "RosterParseError";
9956
+ this.code = code;
9957
+ }
9958
+ }
9959
+ function parseRoster(text2) {
9960
+ const result = parseRosterResult(text2);
9961
+ if (!result.ok) {
9962
+ throw result.error;
9963
+ }
9964
+ return result.roster;
9965
+ }
9966
+ function parseRosterResult(text2) {
9967
+ const match = ROSTER_JSON_FENCE.exec(text2);
9968
+ if (match === null || match[1] === undefined) {
9969
+ return {
9970
+ ok: false,
9971
+ error: new RosterParseError("missing-json-block", "openteam roster: no JSON block found in roster file")
9972
+ };
9973
+ }
9974
+ let data;
9975
+ try {
9976
+ data = JSON.parse(match[1]);
9977
+ } catch (error) {
9978
+ return {
9979
+ ok: false,
9980
+ error: new RosterParseError("malformed-json", `openteam roster: malformed JSON in roster file: ${String(error)}`)
9981
+ };
9982
+ }
9983
+ const parsed = RosterSchema.safeParse(data);
9984
+ if (!parsed.success) {
9985
+ return {
9986
+ ok: false,
9987
+ error: new RosterParseError("invalid-roster-schema", `openteam roster: invalid roster schema: ${parsed.error.message}`)
9988
+ };
9989
+ }
9990
+ return { ok: true, roster: parsed.data };
9991
+ }
9808
9992
  var GUARANTEED_ROLE_IDS = [
9809
9993
  "orchestrator",
9810
9994
  "guardian",
9811
9995
  "scribe",
9812
9996
  "ralph"
9813
9997
  ];
9998
+ var GUARANTEED_ROLE_DEFAULT_AGENT_NAMES = {
9999
+ orchestrator: "openteam",
10000
+ guardian: "guardian",
10001
+ scribe: "scribe",
10002
+ ralph: "ralph"
10003
+ };
10004
+ function defaultAgentNameForGuaranteedRole(roleID) {
10005
+ return GUARANTEED_ROLE_DEFAULT_AGENT_NAMES[roleID];
10006
+ }
10007
+ function isGuaranteedRoleID(roleID) {
10008
+ return GUARANTEED_ROLE_IDS.includes(roleID);
10009
+ }
9814
10010
  function missingGuaranteedRoles(roster) {
9815
10011
  const present = new Set(roster.entries.map((entry) => entry.roleID));
9816
10012
  return GUARANTEED_ROLE_IDS.filter((roleID) => !present.has(roleID));
9817
10013
  }
10014
+ function isRoleProfileLike(value) {
10015
+ return typeof value === "object" && value !== null && "roleID" in value && "defaultTier" in value;
10016
+ }
10017
+ function configuredRolesFromAuditInput(input) {
10018
+ if ("configuredRoles" in input && !isRoleProfileLike(input.configuredRoles)) {
10019
+ return input.configuredRoles;
10020
+ }
10021
+ return input;
10022
+ }
10023
+ function auditRoster(rosterOrText, configuredRolesOrOptions = {}) {
10024
+ const configuredRoles = configuredRolesFromAuditInput(configuredRolesOrOptions);
10025
+ if (typeof rosterOrText === "string") {
10026
+ const parsed = parseRosterResult(rosterOrText);
10027
+ if (!parsed.ok) {
10028
+ return {
10029
+ findings: [
10030
+ {
10031
+ kind: "unparseable-roster",
10032
+ roleID: "roster",
10033
+ code: parsed.error.code,
10034
+ message: parsed.error.message
10035
+ }
10036
+ ],
10037
+ hasAllGuaranteedRoles: false,
10038
+ missingGuaranteedRoleIDs: [],
10039
+ unprofiledRoleEntries: [],
10040
+ misassignedGuaranteedRoleEntries: []
10041
+ };
10042
+ }
10043
+ return auditParsedRoster(parsed.roster, configuredRoles);
10044
+ }
10045
+ return auditParsedRoster(rosterOrText, configuredRoles);
10046
+ }
10047
+ function auditParsedRoster(roster, configuredRoles) {
10048
+ const missingGuaranteedRoleIDs = missingGuaranteedRoles(roster);
10049
+ const missing = new Set(missingGuaranteedRoleIDs);
10050
+ const findings = missingGuaranteedRoleIDs.map((roleID) => ({
10051
+ kind: "missing-guaranteed-role",
10052
+ roleID
10053
+ }));
10054
+ const unprofiledRoleEntries = [];
10055
+ const misassignedGuaranteedRoleEntries = [];
10056
+ roster.entries.forEach((entry) => {
10057
+ const profile = getRoleProfile(entry.roleID, configuredRoles);
10058
+ if (profile === undefined && !isKnownNonWorkerRole(entry.roleID)) {
10059
+ unprofiledRoleEntries.push({
10060
+ roleID: entry.roleID,
10061
+ agentName: entry.agentName,
10062
+ severity: "info"
10063
+ });
10064
+ findings.push({
10065
+ kind: "unresolved-role-profile",
10066
+ roleID: entry.roleID,
10067
+ agentName: entry.agentName
10068
+ });
10069
+ }
10070
+ if (isGuaranteedRoleID(entry.agentName) && entry.roleID !== entry.agentName && missing.has(entry.agentName)) {
10071
+ misassignedGuaranteedRoleEntries.push({
10072
+ roleID: entry.roleID,
10073
+ agentName: entry.agentName,
10074
+ missingRoleID: entry.agentName,
10075
+ reason: "agentNameMatchesMissingGuaranteedRole"
10076
+ });
10077
+ findings.push({
10078
+ kind: "non-canonical-role-id",
10079
+ roleID: entry.roleID,
10080
+ agentName: entry.agentName,
10081
+ canonicalRoleID: entry.agentName
10082
+ });
10083
+ return;
10084
+ }
10085
+ if (profile !== undefined && isGuaranteedRoleID(profile.roleID) && profile.roleID !== entry.roleID && missing.has(profile.roleID)) {
10086
+ misassignedGuaranteedRoleEntries.push({
10087
+ roleID: entry.roleID,
10088
+ agentName: entry.agentName,
10089
+ missingRoleID: profile.roleID,
10090
+ reason: "roleIDResolvesToMissingGuaranteedRole"
10091
+ });
10092
+ findings.push({
10093
+ kind: "non-canonical-role-id",
10094
+ roleID: entry.roleID,
10095
+ agentName: entry.agentName,
10096
+ canonicalRoleID: profile.roleID
10097
+ });
10098
+ }
10099
+ });
10100
+ return {
10101
+ findings,
10102
+ hasAllGuaranteedRoles: missingGuaranteedRoleIDs.length === 0,
10103
+ missingGuaranteedRoleIDs,
10104
+ unprofiledRoleEntries,
10105
+ misassignedGuaranteedRoleEntries
10106
+ };
10107
+ }
9818
10108
  function enforceGuaranteedRoles(roster) {
9819
10109
  const missing = missingGuaranteedRoles(roster);
9820
10110
  if (missing.length === 0) {
@@ -9822,7 +10112,7 @@ function enforceGuaranteedRoles(roster) {
9822
10112
  }
9823
10113
  const added = missing.map((roleID) => ({
9824
10114
  roleID,
9825
- agentName: roleID
10115
+ agentName: defaultAgentNameForGuaranteedRole(roleID)
9826
10116
  }));
9827
10117
  return { universe: roster.universe, entries: [...roster.entries, ...added] };
9828
10118
  }
@@ -10227,6 +10517,105 @@ function agentModelLine(diagnostic, nameWidth, searchedPaths) {
10227
10517
  }
10228
10518
  return ` ${mark} ${name} [${diagnostic.mode}] ${modelText2} ${provenance} ${parts.join(" · ")}`;
10229
10519
  }
10520
+ function diagnosticMark(level) {
10521
+ switch (level) {
10522
+ case "error":
10523
+ return "✗";
10524
+ case "warn":
10525
+ return "⚠";
10526
+ default:
10527
+ return "·";
10528
+ }
10529
+ }
10530
+ function diagnosticsSection(diagnostics) {
10531
+ const notable = diagnostics.filter((diagnostic) => diagnostic.level === "warn" || diagnostic.level === "error");
10532
+ if (notable.length === 0) {
10533
+ return [];
10534
+ }
10535
+ const recent = notable.slice(-5);
10536
+ const lines = [
10537
+ ` plugin diagnostics: ${notable.length} warning/error event(s)`
10538
+ ];
10539
+ for (const diagnostic of recent) {
10540
+ lines.push(` ${diagnosticMark(diagnostic.level)} [${diagnostic.code}] ${diagnosticDescription(diagnostic.code)}`);
10541
+ }
10542
+ if (notable.length > recent.length) {
10543
+ lines.push(` … ${notable.length - recent.length} older diagnostic event(s) omitted`);
10544
+ }
10545
+ lines.push(" note: plugin diagnostics are bounded classes recorded instead of writing to the opencode TUI console.");
10546
+ return lines;
10547
+ }
10548
+ function rosterEntryText(entry) {
10549
+ return entry.agentName === undefined ? `"${entry.roleID}"` : `"${entry.roleID}" (agentName "${entry.agentName}")`;
10550
+ }
10551
+ function canonicalRoleID(entry) {
10552
+ return entry.canonicalRoleID ?? entry.missingRoleID;
10553
+ }
10554
+ function legacyRosterFindings(audit) {
10555
+ const findings = [];
10556
+ for (const roleID of audit.missingGuaranteedRoleIDs ?? []) {
10557
+ findings.push({ kind: "missing-guaranteed-role", roleID });
10558
+ }
10559
+ for (const entry of audit.misassignedGuaranteedRoleEntries ?? []) {
10560
+ const canonical2 = canonicalRoleID(entry);
10561
+ findings.push({
10562
+ kind: "non-canonical-role-id",
10563
+ roleID: entry.roleID,
10564
+ ...entry.agentName !== undefined ? { agentName: entry.agentName } : {},
10565
+ ...canonical2 !== undefined ? { canonicalRoleID: canonical2 } : {}
10566
+ });
10567
+ }
10568
+ for (const entry of audit.unprofiledRoleEntries ?? []) {
10569
+ findings.push({
10570
+ kind: "unresolved-role-profile",
10571
+ roleID: entry.roleID,
10572
+ ...entry.agentName !== undefined ? { agentName: entry.agentName } : {}
10573
+ });
10574
+ }
10575
+ return findings;
10576
+ }
10577
+ function rosterHealthSection(audit, loadError, rosterRoleCount) {
10578
+ const auditFindings = audit?.findings ?? legacyRosterFindings(audit ?? {});
10579
+ const unparseable = auditFindings.find((finding) => finding.kind === "unparseable-roster");
10580
+ const unparseableMessage = loadError ?? unparseable?.message;
10581
+ if (unparseableMessage !== undefined) {
10582
+ return [
10583
+ " roster health:",
10584
+ ` ✗ unparseable roster: ${OPENTEAM_ROSTER_PATH} — ${unparseableMessage}`,
10585
+ " problem: the roster is present but is not machine-readable by openteam.",
10586
+ " remedy: keep any human notes outside the JSON fence, add one fenced `json` block containing `{ universe, entries: [{ roleID, agentName }] }`, then re-run `openteam doctor`."
10587
+ ];
10588
+ }
10589
+ if (audit === undefined) {
10590
+ return [];
10591
+ }
10592
+ const missing = auditFindings.filter((finding) => finding.kind === "missing-guaranteed-role").map((finding) => finding.roleID);
10593
+ const misassigned = auditFindings.filter((finding) => finding.kind === "non-canonical-role-id");
10594
+ const unprofiled = auditFindings.filter((finding) => finding.kind === "unresolved-role-profile");
10595
+ if (missing.length === 0 && misassigned.length === 0 && unprofiled.length === 0) {
10596
+ return rosterRoleCount === undefined ? [] : [
10597
+ " roster health:",
10598
+ ` ✓ ${OPENTEAM_ROSTER_PATH} read (${rosterRoleCount} roles); guaranteed roles present.`
10599
+ ];
10600
+ }
10601
+ const lines = [" roster health:"];
10602
+ if (missing.length > 0) {
10603
+ lines.push(` ✗ missing guaranteed roleID(s): ${missing.join(", ")}`);
10604
+ 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" }.`);
10605
+ }
10606
+ for (const entry of misassigned) {
10607
+ const canonical2 = canonicalRoleID(entry);
10608
+ const target = canonical2 === undefined ? "the canonical functional roleID" : `"${canonical2}"`;
10609
+ lines.push(` ⚠ non-canonical roleID ${rosterEntryText(entry)}`);
10610
+ lines.push(` remedy: change roleID to ${target} and keep the themed name in agentName.`);
10611
+ }
10612
+ if (unprofiled.length > 0) {
10613
+ const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
10614
+ lines.push(` · unprofiled roleID(s): ${roleIDs}`);
10615
+ 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.");
10616
+ }
10617
+ return lines;
10618
+ }
10230
10619
  function agentModelsSection(diagnostics, searchedPaths) {
10231
10620
  const lines = [" agent models:"];
10232
10621
  if (diagnostics.length === 0) {
@@ -10262,6 +10651,10 @@ function renderDoctor(input) {
10262
10651
  }
10263
10652
  }
10264
10653
  lines.push(` telemetry: ${input.telemetryPath} — ${input.telemetryRecords} record(s)`);
10654
+ if (input.diagnostics !== undefined) {
10655
+ lines.push(...diagnosticsSection(input.diagnostics));
10656
+ }
10657
+ lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
10265
10658
  if (input.otelBackend !== undefined) {
10266
10659
  const otel = input.otelBackend;
10267
10660
  lines.push(" opentelemetry:");
@@ -12487,13 +12880,23 @@ async function guaranteedAgentStatuses(runtime) {
12487
12880
  }
12488
12881
  return statuses;
12489
12882
  }
12883
+ function rosterPlanSummary(rosterFile) {
12884
+ switch (rosterFile.status) {
12885
+ case "loaded":
12886
+ return `${rosterFile.roster.universe} (${rosterFile.roster.entries.length} roles)`;
12887
+ case "absent":
12888
+ return "none yet — one would be generated on the first real run";
12889
+ case "invalid":
12890
+ return `${rosterFile.path} is present but unparseable — run 'openteam doctor'`;
12891
+ }
12892
+ }
12490
12893
  function renderLoopPlan(plan2) {
12491
12894
  const { flags } = plan2;
12492
12895
  const budget = flags.budgetUsd !== undefined ? `$${flags.budgetUsd}` : "unbounded";
12493
12896
  const freshContext = flags.freshContext ? "on (pure Ralph pattern)" : "off";
12494
12897
  const budgetStop = flags.budgetUsd !== undefined ? ", budget" : "";
12495
12898
  const redTestsStop = flags.stopOnRedTests ? ", red tests" : "";
12496
- const rosterSummary = plan2.roster !== undefined ? `${plan2.roster.universe} (${plan2.roster.entries.length} roles)` : "none yet — one would be generated on the first real run";
12899
+ const rosterSummary = rosterPlanSummary(plan2.rosterFile);
12497
12900
  const lines = [
12498
12901
  "openteam loop — plan (dry run, nothing dispatched):",
12499
12902
  ` backlog: ${flags.backlogPath} (${plan2.uncheckedCount} unchecked item(s))`,
@@ -12537,14 +12940,14 @@ ${LOOP_HELP}` };
12537
12940
  const backlogText = await runtime.readBacklog(flags.backlogPath) ?? "";
12538
12941
  const items = parseBacklog2(backlogText);
12539
12942
  const unchecked = uncheckedBacklogItems(items);
12540
- const roster = await runtime.loadRoster();
12943
+ const rosterFile = await runtime.loadRosterFile();
12541
12944
  const guaranteed = await guaranteedAgentStatuses(runtime);
12542
12945
  const plannedIterations = Math.max(0, Math.min(flags.maxIterations, unchecked.length));
12543
12946
  const plan2 = {
12544
12947
  flags,
12545
12948
  uncheckedCount: unchecked.length,
12546
12949
  plannedIterations,
12547
- roster,
12950
+ rosterFile,
12548
12951
  guaranteed
12549
12952
  };
12550
12953
  if (flags.dryRun) {
@@ -12622,9 +13025,19 @@ async function runCli(argv, deps) {
12622
13025
  if (command === "doctor") {
12623
13026
  const config = await deps.loadConfig(configPath, configResolution);
12624
13027
  let opencodeConfigError;
12625
- const [snapshots, records, cacheEntries, agentFiles, opencodeConfig] = await Promise.all([
13028
+ let rosterLoadError;
13029
+ const [
13030
+ snapshots,
13031
+ records,
13032
+ diagnostics,
13033
+ cacheEntries,
13034
+ agentFiles,
13035
+ opencodeConfig,
13036
+ roster
13037
+ ] = await Promise.all([
12626
13038
  deps.probe(config),
12627
13039
  deps.readTelemetry(telemetryPath),
13040
+ deps.readDiagnostics !== undefined ? deps.readDiagnostics() : Promise.resolve(undefined),
12628
13041
  deps.cachePort !== undefined ? deps.cachePort.listEntries(deps.cachePort.resolveCacheRoot(parsed.cacheRoot)).catch(() => {
12629
13042
  return;
12630
13043
  }) : Promise.resolve(undefined),
@@ -12632,7 +13045,11 @@ async function runCli(argv, deps) {
12632
13045
  deps.readOpencodeConfig(opencodeConfigPaths).catch((err) => {
12633
13046
  opencodeConfigError = err instanceof Error ? err.message : String(err);
12634
13047
  return;
12635
- })
13048
+ }),
13049
+ deps.loadRosterForDoctor !== undefined ? deps.loadRosterForDoctor().catch((err) => {
13050
+ rosterLoadError = err instanceof Error ? err.message : String(err);
13051
+ return;
13052
+ }) : Promise.resolve(undefined)
12636
13053
  ]);
12637
13054
  const defaultModel = defaultModelFromOpencode(opencodeConfig);
12638
13055
  const agentModels = diagnoseAgentModels(agentFiles, {
@@ -12647,6 +13064,18 @@ async function runCli(argv, deps) {
12647
13064
  backend: "opentelemetry",
12648
13065
  connectionEnv: "APPLICATIONINSIGHTS_CONNECTION_STRING"
12649
13066
  }, (name) => process.env[name]);
13067
+ let rosterAudit;
13068
+ let rosterRoleCount;
13069
+ if (roster !== undefined && deps.auditRoster !== undefined) {
13070
+ if (typeof roster !== "string") {
13071
+ rosterRoleCount = roster.entries.length;
13072
+ }
13073
+ try {
13074
+ rosterAudit = deps.auditRoster(roster, config.orchestrator?.roles);
13075
+ } catch (err) {
13076
+ rosterLoadError = err instanceof Error ? err.message : String(err);
13077
+ }
13078
+ }
12650
13079
  return {
12651
13080
  exitCode: 0,
12652
13081
  stdout: renderDoctor({
@@ -12656,6 +13085,10 @@ async function runCli(argv, deps) {
12656
13085
  telemetryRecords: records.length,
12657
13086
  agentModels,
12658
13087
  opencodeConfigPaths,
13088
+ ...rosterAudit !== undefined ? { rosterAudit } : {},
13089
+ ...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
13090
+ ...rosterLoadError !== undefined ? { rosterLoadError } : {},
13091
+ ...diagnostics !== undefined && diagnostics.length > 0 ? { diagnostics } : {},
12659
13092
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
12660
13093
  ...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
12661
13094
  ...legacyLayout !== undefined && legacyLayout.length > 0 ? { legacyLayout } : {},
@@ -13618,16 +14051,16 @@ function insertRecords(db, records) {
13618
14051
  throw error;
13619
14052
  }
13620
14053
  }
13621
- function warnMemoryIndexRecordRejections(result) {
14054
+ function warnMemoryIndexRecordRejections(result, warn) {
13622
14055
  if (result.rejectedRows === 0) {
13623
14056
  return;
13624
14057
  }
13625
- console.warn(`[openteam] memory index reader rejected ${result.rejectedRows} row(s); semantic memory may be incomplete.`);
14058
+ (warn ?? console.warn)(`[openteam] memory index reader rejected ${result.rejectedRows} row(s); semantic memory may be incomplete.`);
13626
14059
  for (const rejection of result.rejections) {
13627
- console.warn(`[openteam] • memory_records:${rejection.recordId}: ${rejection.detail}`);
14060
+ (warn ?? console.warn)(`[openteam] • memory_records:${rejection.recordId}: ${rejection.detail}`);
13628
14061
  }
13629
14062
  }
13630
- function createMemoryIndex(db) {
14063
+ function createMemoryIndex(db, warn) {
13631
14064
  db.exec(SCHEMA2);
13632
14065
  const allRecordsFn = () => {
13633
14066
  const rows = db.query("SELECT id, payload FROM memory_records ORDER BY created_at ASC, id ASC").all();
@@ -13655,7 +14088,7 @@ function createMemoryIndex(db) {
13655
14088
  };
13656
14089
  const allRecordsWithWarning = () => {
13657
14090
  const result = allRecordsFn();
13658
- warnMemoryIndexRecordRejections(result);
14091
+ warnMemoryIndexRecordRejections(result, warn);
13659
14092
  return result.records;
13660
14093
  };
13661
14094
  const state = () => {
@@ -14218,7 +14651,6 @@ async function* linesFromReadable(readable) {
14218
14651
  }
14219
14652
 
14220
14653
  // src/orchestrator/rosterPersistence.ts
14221
- var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
14222
14654
  function serializeRoster(roster) {
14223
14655
  const data = { universe: roster.universe, entries: roster.entries };
14224
14656
  const json = JSON.stringify(data, null, 2);
@@ -14231,38 +14663,41 @@ ${json}
14231
14663
  \`\`\`
14232
14664
  `;
14233
14665
  }
14234
- function parseRoster(text2) {
14235
- const match = ROSTER_JSON_FENCE.exec(text2);
14236
- if (match === null || match[1] === undefined) {
14237
- throw new Error("openteam roster: no JSON block found in roster file");
14238
- }
14239
- let data;
14240
- try {
14241
- data = JSON.parse(match[1]);
14242
- } catch (error) {
14243
- throw new Error(`openteam roster: malformed JSON in roster file: ${String(error)}`);
14244
- }
14245
- return RosterSchema.parse(data);
14246
- }
14247
14666
  async function persistRoster(storage, roster, path4 = OPENTEAM_ROSTER_PATH) {
14248
14667
  await storage.write(path4, serializeRoster(roster));
14249
14668
  return path4;
14250
14669
  }
14251
- async function loadRoster(storage, path4 = OPENTEAM_ROSTER_PATH) {
14670
+ async function loadRosterFile(storage, path4 = OPENTEAM_ROSTER_PATH) {
14252
14671
  const content = await storage.read(path4);
14253
14672
  if (content === undefined) {
14254
- return;
14673
+ return { status: "absent", path: path4 };
14674
+ }
14675
+ const parsed = parseRosterResult(content);
14676
+ if (!parsed.ok) {
14677
+ return { status: "invalid", path: path4, error: parsed.error };
14255
14678
  }
14256
- return parseRoster(content);
14679
+ return {
14680
+ status: "loaded",
14681
+ path: path4,
14682
+ roster: enforceGuaranteedRoles(parsed.roster)
14683
+ };
14257
14684
  }
14258
14685
  async function loadOrGenerateRoster(storage, generate, path4 = OPENTEAM_ROSTER_PATH) {
14259
- const existing = await loadRoster(storage, path4);
14260
- if (existing !== undefined) {
14261
- return { roster: existing, regenerated: false };
14686
+ const existing = await loadRosterFile(storage, path4);
14687
+ if (existing.status === "loaded") {
14688
+ return { roster: existing.roster, regenerated: false };
14262
14689
  }
14263
- const roster = await generate();
14264
- await persistRoster(storage, roster, path4);
14265
- return { roster, regenerated: true };
14690
+ const roster = enforceGuaranteedRoles(await generate());
14691
+ if (existing.status === "absent") {
14692
+ await persistRoster(storage, roster, path4);
14693
+ return { roster, regenerated: true };
14694
+ }
14695
+ return {
14696
+ roster,
14697
+ regenerated: true,
14698
+ recoveredFromInvalidPersisted: true,
14699
+ invalidPersistedRosterError: existing.error.message
14700
+ };
14266
14701
  }
14267
14702
 
14268
14703
  // src/plugin/orchestrateTool.ts
@@ -14497,7 +14932,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
14497
14932
  const warn = deps.warn ?? console.warn;
14498
14933
  try {
14499
14934
  const db = await openDatabase(semantic.indexPath);
14500
- const index = createMemoryIndex(db);
14935
+ const index = createMemoryIndex(db, warn);
14501
14936
  const rebuildResult = await rebuildMemoryIndexFromStorage(index, semantic.logPath, { storage: deps.storage });
14502
14937
  if (rebuildResult.rejectedLines > 0) {
14503
14938
  warn(`[openteam] memory log rejected ${rebuildResult.rejectedLines} line(s); semantic memory may be incomplete.`);
@@ -14518,7 +14953,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
14518
14953
  // package.json
14519
14954
  var package_default = {
14520
14955
  name: "@jmanuelcorral/openteam",
14521
- version: "0.9.0",
14956
+ version: "0.9.2",
14522
14957
  packageManager: "bun@1.3.14",
14523
14958
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
14524
14959
  license: "MIT",
@@ -14643,6 +15078,18 @@ function createGitLastCommit(exec) {
14643
15078
  var execFileAsync2 = promisify2(execFile2);
14644
15079
  var AGENT_DIR2 = dirname5(ORCHESTRATOR_AGENT_PATH);
14645
15080
  var storage = createFsStorageProvider(process.cwd());
15081
+ async function readStoredRosterForDoctor() {
15082
+ const content = await storage.read(OPENTEAM_ROSTER_PATH);
15083
+ if (content === undefined) {
15084
+ return;
15085
+ }
15086
+ try {
15087
+ return parseRoster(content);
15088
+ } catch {
15089
+ return content;
15090
+ }
15091
+ }
15092
+ var auditStoredRosterForDoctor = (roster, configuredRoles) => typeof roster === "string" ? auditRoster(roster, configuredRoles) : auditRoster(roster, configuredRoles);
14646
15093
  var migratePort = createMigrateAdapter(process.cwd());
14647
15094
  var nodeExec2 = async (command, args) => {
14648
15095
  try {
@@ -14798,6 +15245,9 @@ var deps = {
14798
15245
  storage,
14799
15246
  legacyTelemetryPath: path4
14800
15247
  }),
15248
+ readDiagnostics: () => readDiagnosticEvents(DEFAULT_SESSIONS_DIR, {
15249
+ storage
15250
+ }),
14801
15251
  readGraphSnapshot: createGraphStatusSnapshotReader({
14802
15252
  storage,
14803
15253
  fallbackConfig: loadOpenTeamConfig({}),
@@ -14818,6 +15268,8 @@ var deps = {
14818
15268
  cachePort: realCacheAdapter,
14819
15269
  purge: createCliPurgeRuntime(),
14820
15270
  loopRuntime: createCliLoopRuntime(),
15271
+ loadRosterForDoctor: readStoredRosterForDoctor,
15272
+ auditRoster: auditStoredRosterForDoctor,
14821
15273
  version: PACKAGE_VERSION
14822
15274
  };
14823
15275
  function createCliPurgeRuntime() {
@@ -14887,7 +15339,7 @@ function createCliLoopRuntime() {
14887
15339
  const agentFileExists = (agentName) => storage.exists(`${AGENT_DIR2}/${agentName}.md`);
14888
15340
  const generate = () => generateRoster("openteam initial roster", fallbackRosterSource);
14889
15341
  return {
14890
- loadRoster: () => loadRoster(storage),
15342
+ loadRosterFile: () => loadRosterFile(storage),
14891
15343
  loadOrGenerateRoster: async () => (await loadOrGenerateRoster(storage, generate)).roster,
14892
15344
  agentFileExists,
14893
15345
  readBacklog: (path4) => storage.read(path4),
@@ -14995,6 +15447,9 @@ async function main() {
14995
15447
  storage,
14996
15448
  legacyTelemetryPath: path4
14997
15449
  }),
15450
+ readDiagnostics: () => readDiagnosticEvents(effectiveSessionsDir, {
15451
+ storage
15452
+ }),
14998
15453
  detectLegacyLayout: async () => detectLegacyLayout(legacyExists),
14999
15454
  migrateLegacyLayout: () => migrateLegacyLayout(planLegacyMigration(legacyExists), migratePort)
15000
15455
  };