@jmanuelcorral/openteam 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -199,7 +199,7 @@ var LEGACY_TEAM_ROLE_ALIASES = {
199
199
  basher: "routing-cost",
200
200
  linus: "tester"
201
201
  };
202
- var KNOWN_NON_WORKER_ROLE_IDS = ["openteam"];
202
+ var KNOWN_NON_WORKER_ROLE_IDS = ["orchestrator", "openteam"];
203
203
  function isKnownNonWorkerRole(roleID) {
204
204
  return KNOWN_NON_WORKER_ROLE_IDS.includes(roleID);
205
205
  }
@@ -3661,22 +3661,70 @@ function buildOrchestratorAgent(frontier, options = {}) {
3661
3661
  "1. When you create the team for the first time, **choose a thematic universe**",
3662
3662
  " (e.g. a film, series, comic or mythology). If the user has a preference,",
3663
3663
  " ask them for it; if not, propose one and move forward without blocking work.",
3664
- "2. Give each subagent the **name of a character** from that universe; the",
3665
- " 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",
3666
3666
  " `@mention`, and its role is clear from the `description`.",
3667
- `3. **Register the cast** in \`${OPENTEAM_ROSTER_PATH}\` (universe + table`,
3668
- " name role model) so that the names **persist** across sessions.",
3669
- " This file goes **outside** `.opencode/agent/` (otherwise opencode would load it",
3670
- " as a phantom agent). Always reuse the same cast; do not re-cast",
3671
- " 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
+ "```",
3672
3718
  "",
3673
3719
  "## How to create the team when it does not exist",
3674
3720
  "",
3675
3721
  "1. Analyze the goal and explore the repository (`read`/`glob`/`grep`).",
3676
3722
  "2. Break the request into tasks and design the **minimal team** needed:",
3677
- " define only the roles the work requires (e.g. architect, backend,",
3678
- " frontend, reviewer, tester, docs). Do not invent roles you will not use.",
3679
- "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`:",
3680
3728
  " - Frontmatter: `description` (required, include role and universe),",
3681
3729
  " `mode: subagent`, `temperature`, `permission` with the minimum needed,",
3682
3730
  " and `model` **optional**.",
@@ -3697,8 +3745,10 @@ function buildOrchestratorAgent(frontier, options = {}) {
3697
3745
  "",
3698
3746
  "## Standard team roles",
3699
3747
  "",
3700
- "Besides the project-specific roles, include these standard roles",
3701
- "(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:",
3702
3752
  "",
3703
3753
  "- **scribe** — the team's silent memory. Records decisions and",
3704
3754
  " learnings in a shared log (`.opencode/openteam/decisions.md`) without",
@@ -6608,12 +6658,67 @@ var RosterSchema = z18.object({
6608
6658
  universe: z18.string().min(1),
6609
6659
  entries: z18.array(RosterEntrySchema)
6610
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
+ }
6611
6704
  var GUARANTEED_ROLE_IDS = [
6612
6705
  "orchestrator",
6613
6706
  "guardian",
6614
6707
  "scribe",
6615
6708
  "ralph"
6616
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
+ }
6617
6722
  function missingGuaranteedRoles(roster) {
6618
6723
  const present = new Set(roster.entries.map((entry) => entry.roleID));
6619
6724
  return GUARANTEED_ROLE_IDS.filter((roleID) => !present.has(roleID));
@@ -6621,6 +6726,100 @@ function missingGuaranteedRoles(roster) {
6621
6726
  function hasAllGuaranteedRoles(roster) {
6622
6727
  return missingGuaranteedRoles(roster).length === 0;
6623
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
+ }
6624
6823
  function enforceGuaranteedRoles(roster) {
6625
6824
  const missing = missingGuaranteedRoles(roster);
6626
6825
  if (missing.length === 0) {
@@ -6628,7 +6827,7 @@ function enforceGuaranteedRoles(roster) {
6628
6827
  }
6629
6828
  const added = missing.map((roleID) => ({
6630
6829
  roleID,
6631
- agentName: roleID
6830
+ agentName: defaultAgentNameForGuaranteedRole(roleID)
6632
6831
  }));
6633
6832
  return { universe: roster.universe, entries: [...roster.entries, ...added] };
6634
6833
  }
@@ -6660,7 +6859,6 @@ async function dispatchGuaranteedRole(agentName, agentFileExists, dispatch) {
6660
6859
  }
6661
6860
 
6662
6861
  // src/orchestrator/rosterPersistence.ts
6663
- var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
6664
6862
  function serializeRoster(roster) {
6665
6863
  const data = { universe: roster.universe, entries: roster.entries };
6666
6864
  const json = JSON.stringify(data, null, 2);
@@ -6673,38 +6871,41 @@ ${json}
6673
6871
  \`\`\`
6674
6872
  `;
6675
6873
  }
6676
- function parseRoster(text) {
6677
- const match = ROSTER_JSON_FENCE.exec(text);
6678
- if (match === null || match[1] === undefined) {
6679
- throw new Error("openteam roster: no JSON block found in roster file");
6680
- }
6681
- let data;
6682
- try {
6683
- data = JSON.parse(match[1]);
6684
- } catch (error) {
6685
- throw new Error(`openteam roster: malformed JSON in roster file: ${String(error)}`);
6686
- }
6687
- return RosterSchema.parse(data);
6688
- }
6689
6874
  async function persistRoster(storage, roster, path2 = OPENTEAM_ROSTER_PATH) {
6690
6875
  await storage.write(path2, serializeRoster(roster));
6691
6876
  return path2;
6692
6877
  }
6693
- async function loadRoster(storage, path2 = OPENTEAM_ROSTER_PATH) {
6878
+ async function loadRosterFile(storage, path2 = OPENTEAM_ROSTER_PATH) {
6694
6879
  const content = await storage.read(path2);
6695
6880
  if (content === undefined) {
6696
- return;
6881
+ return { status: "absent", path: path2 };
6882
+ }
6883
+ const parsed = parseRosterResult(content);
6884
+ if (!parsed.ok) {
6885
+ return { status: "invalid", path: path2, error: parsed.error };
6697
6886
  }
6698
- return parseRoster(content);
6887
+ return {
6888
+ status: "loaded",
6889
+ path: path2,
6890
+ roster: enforceGuaranteedRoles(parsed.roster)
6891
+ };
6699
6892
  }
6700
6893
  async function loadOrGenerateRoster(storage, generate, path2 = OPENTEAM_ROSTER_PATH) {
6701
- const existing = await loadRoster(storage, path2);
6702
- if (existing !== undefined) {
6703
- return { roster: existing, regenerated: false };
6894
+ const existing = await loadRosterFile(storage, path2);
6895
+ if (existing.status === "loaded") {
6896
+ return { roster: existing.roster, regenerated: false };
6897
+ }
6898
+ const roster = enforceGuaranteedRoles(await generate());
6899
+ if (existing.status === "absent") {
6900
+ await persistRoster(storage, roster, path2);
6901
+ return { roster, regenerated: true };
6704
6902
  }
6705
- const roster = await generate();
6706
- await persistRoster(storage, roster, path2);
6707
- return { roster, regenerated: true };
6903
+ return {
6904
+ roster,
6905
+ regenerated: true,
6906
+ recoveredFromInvalidPersisted: true,
6907
+ invalidPersistedRosterError: existing.error.message
6908
+ };
6708
6909
  }
6709
6910
 
6710
6911
  // src/plugin/availability.ts
@@ -7760,6 +7961,82 @@ function diagnosticsSection(diagnostics) {
7760
7961
  lines.push(" note: plugin diagnostics are bounded classes recorded instead of writing to the opencode TUI console.");
7761
7962
  return lines;
7762
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
+ const hasErrorLevelFindings = missing.length > 0 || misassigned.length > 0;
8012
+ const hasFindings = hasErrorLevelFindings || unprofiled.length > 0;
8013
+ if (!hasFindings) {
8014
+ return rosterRoleCount === undefined ? [] : [
8015
+ " roster health:",
8016
+ ` ✓ ${OPENTEAM_ROSTER_PATH} read (${rosterRoleCount} roles); guaranteed roles present.`
8017
+ ];
8018
+ }
8019
+ const lines = [" roster health:"];
8020
+ if (!hasErrorLevelFindings && rosterRoleCount !== undefined) {
8021
+ lines.push(` ✓ ${OPENTEAM_ROSTER_PATH} read (${rosterRoleCount} roles); guaranteed roles present.`);
8022
+ }
8023
+ if (missing.length > 0) {
8024
+ lines.push(` ✗ missing guaranteed roleID(s): ${missing.join(", ")}`);
8025
+ 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" }.`);
8026
+ }
8027
+ for (const entry of misassigned) {
8028
+ const canonical2 = canonicalRoleID(entry);
8029
+ const target = canonical2 === undefined ? "the canonical functional roleID" : `"${canonical2}"`;
8030
+ lines.push(` ⚠ non-canonical roleID ${rosterEntryText(entry)}`);
8031
+ lines.push(` remedy: change roleID to ${target} and keep the themed name in agentName.`);
8032
+ }
8033
+ if (unprofiled.length > 0) {
8034
+ const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
8035
+ lines.push(` · unprofiled roleID(s): ${roleIDs}`);
8036
+ 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.");
8037
+ }
8038
+ return lines;
8039
+ }
7763
8040
  function agentModelsSection(diagnostics, searchedPaths) {
7764
8041
  const lines = [" agent models:"];
7765
8042
  if (diagnostics.length === 0) {
@@ -7798,6 +8075,7 @@ function renderDoctor(input) {
7798
8075
  if (input.diagnostics !== undefined) {
7799
8076
  lines.push(...diagnosticsSection(input.diagnostics));
7800
8077
  }
8078
+ lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
7801
8079
  if (input.otelBackend !== undefined) {
7802
8080
  const otel = input.otelBackend;
7803
8081
  lines.push(" opentelemetry:");
@@ -9442,13 +9720,23 @@ async function guaranteedAgentStatuses(runtime) {
9442
9720
  }
9443
9721
  return statuses;
9444
9722
  }
9723
+ function rosterPlanSummary(rosterFile) {
9724
+ switch (rosterFile.status) {
9725
+ case "loaded":
9726
+ return `${rosterFile.roster.universe} (${rosterFile.roster.entries.length} roles)`;
9727
+ case "absent":
9728
+ return "none yet — one would be generated on the first real run";
9729
+ case "invalid":
9730
+ return `${rosterFile.path} is present but unparseable — run 'openteam doctor'`;
9731
+ }
9732
+ }
9445
9733
  function renderLoopPlan(plan2) {
9446
9734
  const { flags } = plan2;
9447
9735
  const budget = flags.budgetUsd !== undefined ? `$${flags.budgetUsd}` : "unbounded";
9448
9736
  const freshContext = flags.freshContext ? "on (pure Ralph pattern)" : "off";
9449
9737
  const budgetStop = flags.budgetUsd !== undefined ? ", budget" : "";
9450
9738
  const redTestsStop = flags.stopOnRedTests ? ", red tests" : "";
9451
- const rosterSummary = plan2.roster !== undefined ? `${plan2.roster.universe} (${plan2.roster.entries.length} roles)` : "none yet — one would be generated on the first real run";
9739
+ const rosterSummary = rosterPlanSummary(plan2.rosterFile);
9452
9740
  const lines = [
9453
9741
  "openteam loop — plan (dry run, nothing dispatched):",
9454
9742
  ` backlog: ${flags.backlogPath} (${plan2.uncheckedCount} unchecked item(s))`,
@@ -9492,14 +9780,14 @@ ${LOOP_HELP}` };
9492
9780
  const backlogText = await runtime.readBacklog(flags.backlogPath) ?? "";
9493
9781
  const items = parseBacklog(backlogText);
9494
9782
  const unchecked = uncheckedBacklogItems(items);
9495
- const roster = await runtime.loadRoster();
9783
+ const rosterFile = await runtime.loadRosterFile();
9496
9784
  const guaranteed = await guaranteedAgentStatuses(runtime);
9497
9785
  const plannedIterations = Math.max(0, Math.min(flags.maxIterations, unchecked.length));
9498
9786
  const plan2 = {
9499
9787
  flags,
9500
9788
  uncheckedCount: unchecked.length,
9501
9789
  plannedIterations,
9502
- roster,
9790
+ rosterFile,
9503
9791
  guaranteed
9504
9792
  };
9505
9793
  if (flags.dryRun) {
@@ -9577,13 +9865,15 @@ async function runCli(argv, deps) {
9577
9865
  if (command === "doctor") {
9578
9866
  const config = await deps.loadConfig(configPath, configResolution);
9579
9867
  let opencodeConfigError;
9868
+ let rosterLoadError;
9580
9869
  const [
9581
9870
  snapshots,
9582
9871
  records,
9583
9872
  diagnostics,
9584
9873
  cacheEntries,
9585
9874
  agentFiles,
9586
- opencodeConfig
9875
+ opencodeConfig,
9876
+ roster
9587
9877
  ] = await Promise.all([
9588
9878
  deps.probe(config),
9589
9879
  deps.readTelemetry(telemetryPath),
@@ -9595,7 +9885,11 @@ async function runCli(argv, deps) {
9595
9885
  deps.readOpencodeConfig(opencodeConfigPaths).catch((err) => {
9596
9886
  opencodeConfigError = err instanceof Error ? err.message : String(err);
9597
9887
  return;
9598
- })
9888
+ }),
9889
+ deps.loadRosterForDoctor !== undefined ? deps.loadRosterForDoctor().catch((err) => {
9890
+ rosterLoadError = err instanceof Error ? err.message : String(err);
9891
+ return;
9892
+ }) : Promise.resolve(undefined)
9599
9893
  ]);
9600
9894
  const defaultModel = defaultModelFromOpencode(opencodeConfig);
9601
9895
  const agentModels = diagnoseAgentModels(agentFiles, {
@@ -9610,6 +9904,18 @@ async function runCli(argv, deps) {
9610
9904
  backend: "opentelemetry",
9611
9905
  connectionEnv: "APPLICATIONINSIGHTS_CONNECTION_STRING"
9612
9906
  }, (name) => process.env[name]);
9907
+ let rosterAudit;
9908
+ let rosterRoleCount;
9909
+ if (roster !== undefined && deps.auditRoster !== undefined) {
9910
+ if (typeof roster !== "string") {
9911
+ rosterRoleCount = roster.entries.length;
9912
+ }
9913
+ try {
9914
+ rosterAudit = deps.auditRoster(roster, config.orchestrator?.roles);
9915
+ } catch (err) {
9916
+ rosterLoadError = err instanceof Error ? err.message : String(err);
9917
+ }
9918
+ }
9613
9919
  return {
9614
9920
  exitCode: 0,
9615
9921
  stdout: renderDoctor({
@@ -9619,6 +9925,9 @@ async function runCli(argv, deps) {
9619
9925
  telemetryRecords: records.length,
9620
9926
  agentModels,
9621
9927
  opencodeConfigPaths,
9928
+ ...rosterAudit !== undefined ? { rosterAudit } : {},
9929
+ ...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
9930
+ ...rosterLoadError !== undefined ? { rosterLoadError } : {},
9622
9931
  ...diagnostics !== undefined && diagnostics.length > 0 ? { diagnostics } : {},
9623
9932
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
9624
9933
  ...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
@@ -12504,7 +12813,7 @@ var createOtlpSpanExporter = (connection, config) => {
12504
12813
  // package.json
12505
12814
  var package_default = {
12506
12815
  name: "@jmanuelcorral/openteam",
12507
- version: "0.9.1",
12816
+ version: "0.9.3",
12508
12817
  packageManager: "bun@1.3.14",
12509
12818
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
12510
12819
  license: "MIT",
@@ -12944,6 +13253,18 @@ async function readCliConfigFromStorage(storage, path4, resolution) {
12944
13253
  }
12945
13254
  return content;
12946
13255
  }
13256
+ async function readStoredRosterForDoctor(storage) {
13257
+ const content = await storage.read(OPENTEAM_ROSTER_PATH);
13258
+ if (content === undefined) {
13259
+ return;
13260
+ }
13261
+ try {
13262
+ return parseRoster(content);
13263
+ } catch {
13264
+ return content;
13265
+ }
13266
+ }
13267
+ var auditStoredRosterForDoctor = (roster, configuredRoles) => typeof roster === "string" ? auditRoster(roster, configuredRoles) : auditRoster(roster, configuredRoles);
12947
13268
  function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SESSIONS_DIR, storage = createFsStorageProvider(process.cwd()), fallbackSource = "options", rawOptions) {
12948
13269
  return {
12949
13270
  loadConfig: async (path4, resolution = { explicit: false }) => {
@@ -13027,6 +13348,8 @@ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SE
13027
13348
  agentDir: dirname4(ORCHESTRATOR_AGENT_PATH),
13028
13349
  cachePort: realCacheAdapter,
13029
13350
  purge: createPluginPurgeRuntime(sessionsDir),
13351
+ loadRosterForDoctor: () => readStoredRosterForDoctor(storage),
13352
+ auditRoster: auditStoredRosterForDoctor,
13030
13353
  version: PACKAGE_VERSION
13031
13354
  };
13032
13355
  }
@@ -5,7 +5,7 @@ import type { FrontierChoice } from "../commands/setup";
5
5
  */
6
6
  export declare const ORCHESTRATOR_AGENT_PATH = ".opencode/agent/openteam.md";
7
7
  /**
8
- * Path to the casting registry (universe + namerole→model table). Lives
8
+ * Path to the casting registry (universe + roleIDagentName JSON roster). Lives
9
9
  * **outside** `.opencode/agent/` intentionally: opencode loads as an agent every
10
10
  * `.md` in that folder, so a stray roster markdown file inside would appear as a
11
11
  * phantom agent in the Tab-switcher. It is portable team-state (P15b), so it lives
@@ -1 +1 @@
1
- {"version":3,"file":"orchestratorAgent.d.ts","sourceRoot":"","sources":["../../src/opencodeArtifacts/orchestratorAgent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,iCAAiC,CAAC;AAInE;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,MAAM,CAiOR"}
1
+ {"version":3,"file":"orchestratorAgent.d.ts","sourceRoot":"","sources":["../../src/opencodeArtifacts/orchestratorAgent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,iCAAiC,CAAC;AAInE;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,MAAM,CAmRR"}
@@ -1 +1 @@
1
- {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/orchestrator/roles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,cAAc,CAAC;IAC5B,gBAAgB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC,kBAAkB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtC,yBAAyB,EAAE,OAAO,CAAC;IACnC,uBAAuB,EAAE,KAAK,CAAC,cAAc,GAAG,MAAM,CAAC,CAAC;IACxD,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;;;OASG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;CAClC,CAAC;AAiBF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;kBAkBxB,CAAC;AAGZ,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA8FvD,CAAC;AAYF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI5D;AA6BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GAC3D,gBAAgB,GAAG,SAAS,CAO9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAW1E;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,WAAW,EACjB,IAAI,CAAC,EAAE,cAAc,GACpB,qBAAqB,CAevB"}
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/orchestrator/roles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,cAAc,CAAC;IAC5B,gBAAgB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC,kBAAkB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtC,yBAAyB,EAAE,OAAO,CAAC;IACnC,uBAAuB,EAAE,KAAK,CAAC,cAAc,GAAG,MAAM,CAAC,CAAC;IACxD,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;;;OASG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;CAClC,CAAC;AAiBF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;kBAkBxB,CAAC;AAGZ,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA8FvD,CAAC;AAcF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI5D;AA6BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GAC3D,gBAAgB,GAAG,SAAS,CAO9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAW1E;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,WAAW,EACjB,IAAI,CAAC,EAAE,cAAc,GACpB,qBAAqB,CAevB"}