@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/README.es.md +1 -1
- package/README.md +1 -1
- package/dist/cli.js +367 -44
- package/dist/commands/dispatch.d.ts +18 -4
- package/dist/commands/dispatch.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +34 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +366 -43
- package/dist/opencodeArtifacts/orchestratorAgent.d.ts +1 -1
- package/dist/opencodeArtifacts/orchestratorAgent.d.ts.map +1 -1
- package/dist/orchestrator/roles.d.ts.map +1 -1
- package/dist/orchestrator/roster.d.ts +85 -2
- package/dist/orchestrator/roster.d.ts.map +1 -1
- package/dist/orchestrator/rosterPersistence.d.ts +32 -5
- package/dist/orchestrator/rosterPersistence.d.ts.map +1 -1
- package/package.json +1 -1
package/README.es.md
CHANGED
|
@@ -819,7 +819,7 @@ Consulta [docs/compatibility.md](docs/compatibility.md). Resumen verificado el `
|
|
|
819
819
|
|
|
820
820
|
| Superficie | Versión / contrato |
|
|
821
821
|
| --- | --- |
|
|
822
|
-
| openteam | `0.9.
|
|
822
|
+
| openteam | `0.9.3`; versión actual del paquete |
|
|
823
823
|
| `@opencode-ai/plugin` | `1.18.19` |
|
|
824
824
|
| `@opencode-ai/sdk` | `1.18.19` |
|
|
825
825
|
| Hook de routing | `chat.message`; no `chat.params` para cambiar modelo |
|
package/README.md
CHANGED
|
@@ -700,7 +700,7 @@ See [docs/compatibility.md](docs/compatibility.md). Verified summary as of `2026
|
|
|
700
700
|
|
|
701
701
|
| Surface | Version / contract |
|
|
702
702
|
| --- | --- |
|
|
703
|
-
| openteam | `0.9.
|
|
703
|
+
| openteam | `0.9.3`; current package version |
|
|
704
704
|
| `@opencode-ai/plugin` | `1.18.19` |
|
|
705
705
|
| `@opencode-ai/sdk` | `1.18.19` |
|
|
706
706
|
| Routing hook | `chat.message`; not `chat.params` for model changes |
|
package/dist/cli.js
CHANGED
|
@@ -205,7 +205,7 @@ var LEGACY_TEAM_ROLE_ALIASES = {
|
|
|
205
205
|
basher: "routing-cost",
|
|
206
206
|
linus: "tester"
|
|
207
207
|
};
|
|
208
|
-
var KNOWN_NON_WORKER_ROLE_IDS = ["openteam"];
|
|
208
|
+
var KNOWN_NON_WORKER_ROLE_IDS = ["orchestrator", "openteam"];
|
|
209
209
|
function isKnownNonWorkerRole(roleID) {
|
|
210
210
|
return KNOWN_NON_WORKER_ROLE_IDS.includes(roleID);
|
|
211
211
|
}
|
|
@@ -8648,22 +8648,70 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
8648
8648
|
"1. When you create the team for the first time, **choose a thematic universe**",
|
|
8649
8649
|
" (e.g. a film, series, comic or mythology). If the user has a preference,",
|
|
8650
8650
|
" ask them for it; if not, propose one and move forward without blocking work.",
|
|
8651
|
-
"2. Give each subagent the **name of a character** from that universe;
|
|
8652
|
-
" file name (`.opencode/agent/<
|
|
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",
|
|
8653
8653
|
" `@mention`, and its role is clear from the `description`.",
|
|
8654
|
-
|
|
8655
|
-
"
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
"
|
|
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
|
+
"```",
|
|
8659
8705
|
"",
|
|
8660
8706
|
"## How to create the team when it does not exist",
|
|
8661
8707
|
"",
|
|
8662
8708
|
"1. Analyze the goal and explore the repository (`read`/`glob`/`grep`).",
|
|
8663
8709
|
"2. Break the request into tasks and design the **minimal team** needed:",
|
|
8664
|
-
" define only the roles the work requires
|
|
8665
|
-
"
|
|
8666
|
-
"
|
|
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`:",
|
|
8667
8715
|
" - Frontmatter: `description` (required, include role and universe),",
|
|
8668
8716
|
" `mode: subagent`, `temperature`, `permission` with the minimum needed,",
|
|
8669
8717
|
" and `model` **optional**.",
|
|
@@ -8684,8 +8732,10 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
8684
8732
|
"",
|
|
8685
8733
|
"## Standard team roles",
|
|
8686
8734
|
"",
|
|
8687
|
-
"
|
|
8688
|
-
"
|
|
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:",
|
|
8689
8739
|
"",
|
|
8690
8740
|
"- **scribe** — the team's silent memory. Records decisions and",
|
|
8691
8741
|
" learnings in a shared log (`.opencode/openteam/decisions.md`) without",
|
|
@@ -9896,16 +9946,165 @@ var RosterSchema = z17.object({
|
|
|
9896
9946
|
universe: z17.string().min(1),
|
|
9897
9947
|
entries: z17.array(RosterEntrySchema)
|
|
9898
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
|
+
}
|
|
9899
9992
|
var GUARANTEED_ROLE_IDS = [
|
|
9900
9993
|
"orchestrator",
|
|
9901
9994
|
"guardian",
|
|
9902
9995
|
"scribe",
|
|
9903
9996
|
"ralph"
|
|
9904
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
|
+
}
|
|
9905
10010
|
function missingGuaranteedRoles(roster) {
|
|
9906
10011
|
const present = new Set(roster.entries.map((entry) => entry.roleID));
|
|
9907
10012
|
return GUARANTEED_ROLE_IDS.filter((roleID) => !present.has(roleID));
|
|
9908
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
|
+
}
|
|
9909
10108
|
function enforceGuaranteedRoles(roster) {
|
|
9910
10109
|
const missing = missingGuaranteedRoles(roster);
|
|
9911
10110
|
if (missing.length === 0) {
|
|
@@ -9913,7 +10112,7 @@ function enforceGuaranteedRoles(roster) {
|
|
|
9913
10112
|
}
|
|
9914
10113
|
const added = missing.map((roleID) => ({
|
|
9915
10114
|
roleID,
|
|
9916
|
-
agentName: roleID
|
|
10115
|
+
agentName: defaultAgentNameForGuaranteedRole(roleID)
|
|
9917
10116
|
}));
|
|
9918
10117
|
return { universe: roster.universe, entries: [...roster.entries, ...added] };
|
|
9919
10118
|
}
|
|
@@ -10346,6 +10545,82 @@ function diagnosticsSection(diagnostics) {
|
|
|
10346
10545
|
lines.push(" note: plugin diagnostics are bounded classes recorded instead of writing to the opencode TUI console.");
|
|
10347
10546
|
return lines;
|
|
10348
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
|
+
const hasErrorLevelFindings = missing.length > 0 || misassigned.length > 0;
|
|
10596
|
+
const hasFindings = hasErrorLevelFindings || unprofiled.length > 0;
|
|
10597
|
+
if (!hasFindings) {
|
|
10598
|
+
return rosterRoleCount === undefined ? [] : [
|
|
10599
|
+
" roster health:",
|
|
10600
|
+
` ✓ ${OPENTEAM_ROSTER_PATH} read (${rosterRoleCount} roles); guaranteed roles present.`
|
|
10601
|
+
];
|
|
10602
|
+
}
|
|
10603
|
+
const lines = [" roster health:"];
|
|
10604
|
+
if (!hasErrorLevelFindings && rosterRoleCount !== undefined) {
|
|
10605
|
+
lines.push(` ✓ ${OPENTEAM_ROSTER_PATH} read (${rosterRoleCount} roles); guaranteed roles present.`);
|
|
10606
|
+
}
|
|
10607
|
+
if (missing.length > 0) {
|
|
10608
|
+
lines.push(` ✗ missing guaranteed roleID(s): ${missing.join(", ")}`);
|
|
10609
|
+
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" }.`);
|
|
10610
|
+
}
|
|
10611
|
+
for (const entry of misassigned) {
|
|
10612
|
+
const canonical2 = canonicalRoleID(entry);
|
|
10613
|
+
const target = canonical2 === undefined ? "the canonical functional roleID" : `"${canonical2}"`;
|
|
10614
|
+
lines.push(` ⚠ non-canonical roleID ${rosterEntryText(entry)}`);
|
|
10615
|
+
lines.push(` remedy: change roleID to ${target} and keep the themed name in agentName.`);
|
|
10616
|
+
}
|
|
10617
|
+
if (unprofiled.length > 0) {
|
|
10618
|
+
const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
|
|
10619
|
+
lines.push(` · unprofiled roleID(s): ${roleIDs}`);
|
|
10620
|
+
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.");
|
|
10621
|
+
}
|
|
10622
|
+
return lines;
|
|
10623
|
+
}
|
|
10349
10624
|
function agentModelsSection(diagnostics, searchedPaths) {
|
|
10350
10625
|
const lines = [" agent models:"];
|
|
10351
10626
|
if (diagnostics.length === 0) {
|
|
@@ -10384,6 +10659,7 @@ function renderDoctor(input) {
|
|
|
10384
10659
|
if (input.diagnostics !== undefined) {
|
|
10385
10660
|
lines.push(...diagnosticsSection(input.diagnostics));
|
|
10386
10661
|
}
|
|
10662
|
+
lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
|
|
10387
10663
|
if (input.otelBackend !== undefined) {
|
|
10388
10664
|
const otel = input.otelBackend;
|
|
10389
10665
|
lines.push(" opentelemetry:");
|
|
@@ -12609,13 +12885,23 @@ async function guaranteedAgentStatuses(runtime) {
|
|
|
12609
12885
|
}
|
|
12610
12886
|
return statuses;
|
|
12611
12887
|
}
|
|
12888
|
+
function rosterPlanSummary(rosterFile) {
|
|
12889
|
+
switch (rosterFile.status) {
|
|
12890
|
+
case "loaded":
|
|
12891
|
+
return `${rosterFile.roster.universe} (${rosterFile.roster.entries.length} roles)`;
|
|
12892
|
+
case "absent":
|
|
12893
|
+
return "none yet — one would be generated on the first real run";
|
|
12894
|
+
case "invalid":
|
|
12895
|
+
return `${rosterFile.path} is present but unparseable — run 'openteam doctor'`;
|
|
12896
|
+
}
|
|
12897
|
+
}
|
|
12612
12898
|
function renderLoopPlan(plan2) {
|
|
12613
12899
|
const { flags } = plan2;
|
|
12614
12900
|
const budget = flags.budgetUsd !== undefined ? `$${flags.budgetUsd}` : "unbounded";
|
|
12615
12901
|
const freshContext = flags.freshContext ? "on (pure Ralph pattern)" : "off";
|
|
12616
12902
|
const budgetStop = flags.budgetUsd !== undefined ? ", budget" : "";
|
|
12617
12903
|
const redTestsStop = flags.stopOnRedTests ? ", red tests" : "";
|
|
12618
|
-
const rosterSummary =
|
|
12904
|
+
const rosterSummary = rosterPlanSummary(plan2.rosterFile);
|
|
12619
12905
|
const lines = [
|
|
12620
12906
|
"openteam loop — plan (dry run, nothing dispatched):",
|
|
12621
12907
|
` backlog: ${flags.backlogPath} (${plan2.uncheckedCount} unchecked item(s))`,
|
|
@@ -12659,14 +12945,14 @@ ${LOOP_HELP}` };
|
|
|
12659
12945
|
const backlogText = await runtime.readBacklog(flags.backlogPath) ?? "";
|
|
12660
12946
|
const items = parseBacklog2(backlogText);
|
|
12661
12947
|
const unchecked = uncheckedBacklogItems(items);
|
|
12662
|
-
const
|
|
12948
|
+
const rosterFile = await runtime.loadRosterFile();
|
|
12663
12949
|
const guaranteed = await guaranteedAgentStatuses(runtime);
|
|
12664
12950
|
const plannedIterations = Math.max(0, Math.min(flags.maxIterations, unchecked.length));
|
|
12665
12951
|
const plan2 = {
|
|
12666
12952
|
flags,
|
|
12667
12953
|
uncheckedCount: unchecked.length,
|
|
12668
12954
|
plannedIterations,
|
|
12669
|
-
|
|
12955
|
+
rosterFile,
|
|
12670
12956
|
guaranteed
|
|
12671
12957
|
};
|
|
12672
12958
|
if (flags.dryRun) {
|
|
@@ -12744,13 +13030,15 @@ async function runCli(argv, deps) {
|
|
|
12744
13030
|
if (command === "doctor") {
|
|
12745
13031
|
const config = await deps.loadConfig(configPath, configResolution);
|
|
12746
13032
|
let opencodeConfigError;
|
|
13033
|
+
let rosterLoadError;
|
|
12747
13034
|
const [
|
|
12748
13035
|
snapshots,
|
|
12749
13036
|
records,
|
|
12750
13037
|
diagnostics,
|
|
12751
13038
|
cacheEntries,
|
|
12752
13039
|
agentFiles,
|
|
12753
|
-
opencodeConfig
|
|
13040
|
+
opencodeConfig,
|
|
13041
|
+
roster
|
|
12754
13042
|
] = await Promise.all([
|
|
12755
13043
|
deps.probe(config),
|
|
12756
13044
|
deps.readTelemetry(telemetryPath),
|
|
@@ -12762,7 +13050,11 @@ async function runCli(argv, deps) {
|
|
|
12762
13050
|
deps.readOpencodeConfig(opencodeConfigPaths).catch((err) => {
|
|
12763
13051
|
opencodeConfigError = err instanceof Error ? err.message : String(err);
|
|
12764
13052
|
return;
|
|
12765
|
-
})
|
|
13053
|
+
}),
|
|
13054
|
+
deps.loadRosterForDoctor !== undefined ? deps.loadRosterForDoctor().catch((err) => {
|
|
13055
|
+
rosterLoadError = err instanceof Error ? err.message : String(err);
|
|
13056
|
+
return;
|
|
13057
|
+
}) : Promise.resolve(undefined)
|
|
12766
13058
|
]);
|
|
12767
13059
|
const defaultModel = defaultModelFromOpencode(opencodeConfig);
|
|
12768
13060
|
const agentModels = diagnoseAgentModels(agentFiles, {
|
|
@@ -12777,6 +13069,18 @@ async function runCli(argv, deps) {
|
|
|
12777
13069
|
backend: "opentelemetry",
|
|
12778
13070
|
connectionEnv: "APPLICATIONINSIGHTS_CONNECTION_STRING"
|
|
12779
13071
|
}, (name) => process.env[name]);
|
|
13072
|
+
let rosterAudit;
|
|
13073
|
+
let rosterRoleCount;
|
|
13074
|
+
if (roster !== undefined && deps.auditRoster !== undefined) {
|
|
13075
|
+
if (typeof roster !== "string") {
|
|
13076
|
+
rosterRoleCount = roster.entries.length;
|
|
13077
|
+
}
|
|
13078
|
+
try {
|
|
13079
|
+
rosterAudit = deps.auditRoster(roster, config.orchestrator?.roles);
|
|
13080
|
+
} catch (err) {
|
|
13081
|
+
rosterLoadError = err instanceof Error ? err.message : String(err);
|
|
13082
|
+
}
|
|
13083
|
+
}
|
|
12780
13084
|
return {
|
|
12781
13085
|
exitCode: 0,
|
|
12782
13086
|
stdout: renderDoctor({
|
|
@@ -12786,6 +13090,9 @@ async function runCli(argv, deps) {
|
|
|
12786
13090
|
telemetryRecords: records.length,
|
|
12787
13091
|
agentModels,
|
|
12788
13092
|
opencodeConfigPaths,
|
|
13093
|
+
...rosterAudit !== undefined ? { rosterAudit } : {},
|
|
13094
|
+
...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
|
|
13095
|
+
...rosterLoadError !== undefined ? { rosterLoadError } : {},
|
|
12789
13096
|
...diagnostics !== undefined && diagnostics.length > 0 ? { diagnostics } : {},
|
|
12790
13097
|
...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
|
|
12791
13098
|
...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
|
|
@@ -14349,7 +14656,6 @@ async function* linesFromReadable(readable) {
|
|
|
14349
14656
|
}
|
|
14350
14657
|
|
|
14351
14658
|
// src/orchestrator/rosterPersistence.ts
|
|
14352
|
-
var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
|
|
14353
14659
|
function serializeRoster(roster) {
|
|
14354
14660
|
const data = { universe: roster.universe, entries: roster.entries };
|
|
14355
14661
|
const json = JSON.stringify(data, null, 2);
|
|
@@ -14362,38 +14668,41 @@ ${json}
|
|
|
14362
14668
|
\`\`\`
|
|
14363
14669
|
`;
|
|
14364
14670
|
}
|
|
14365
|
-
function parseRoster(text2) {
|
|
14366
|
-
const match = ROSTER_JSON_FENCE.exec(text2);
|
|
14367
|
-
if (match === null || match[1] === undefined) {
|
|
14368
|
-
throw new Error("openteam roster: no JSON block found in roster file");
|
|
14369
|
-
}
|
|
14370
|
-
let data;
|
|
14371
|
-
try {
|
|
14372
|
-
data = JSON.parse(match[1]);
|
|
14373
|
-
} catch (error) {
|
|
14374
|
-
throw new Error(`openteam roster: malformed JSON in roster file: ${String(error)}`);
|
|
14375
|
-
}
|
|
14376
|
-
return RosterSchema.parse(data);
|
|
14377
|
-
}
|
|
14378
14671
|
async function persistRoster(storage, roster, path4 = OPENTEAM_ROSTER_PATH) {
|
|
14379
14672
|
await storage.write(path4, serializeRoster(roster));
|
|
14380
14673
|
return path4;
|
|
14381
14674
|
}
|
|
14382
|
-
async function
|
|
14675
|
+
async function loadRosterFile(storage, path4 = OPENTEAM_ROSTER_PATH) {
|
|
14383
14676
|
const content = await storage.read(path4);
|
|
14384
14677
|
if (content === undefined) {
|
|
14385
|
-
return;
|
|
14678
|
+
return { status: "absent", path: path4 };
|
|
14679
|
+
}
|
|
14680
|
+
const parsed = parseRosterResult(content);
|
|
14681
|
+
if (!parsed.ok) {
|
|
14682
|
+
return { status: "invalid", path: path4, error: parsed.error };
|
|
14386
14683
|
}
|
|
14387
|
-
return
|
|
14684
|
+
return {
|
|
14685
|
+
status: "loaded",
|
|
14686
|
+
path: path4,
|
|
14687
|
+
roster: enforceGuaranteedRoles(parsed.roster)
|
|
14688
|
+
};
|
|
14388
14689
|
}
|
|
14389
14690
|
async function loadOrGenerateRoster(storage, generate, path4 = OPENTEAM_ROSTER_PATH) {
|
|
14390
|
-
const existing = await
|
|
14391
|
-
if (existing
|
|
14392
|
-
return { roster: existing, regenerated: false };
|
|
14691
|
+
const existing = await loadRosterFile(storage, path4);
|
|
14692
|
+
if (existing.status === "loaded") {
|
|
14693
|
+
return { roster: existing.roster, regenerated: false };
|
|
14694
|
+
}
|
|
14695
|
+
const roster = enforceGuaranteedRoles(await generate());
|
|
14696
|
+
if (existing.status === "absent") {
|
|
14697
|
+
await persistRoster(storage, roster, path4);
|
|
14698
|
+
return { roster, regenerated: true };
|
|
14393
14699
|
}
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14700
|
+
return {
|
|
14701
|
+
roster,
|
|
14702
|
+
regenerated: true,
|
|
14703
|
+
recoveredFromInvalidPersisted: true,
|
|
14704
|
+
invalidPersistedRosterError: existing.error.message
|
|
14705
|
+
};
|
|
14397
14706
|
}
|
|
14398
14707
|
|
|
14399
14708
|
// src/plugin/orchestrateTool.ts
|
|
@@ -14649,7 +14958,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
|
|
|
14649
14958
|
// package.json
|
|
14650
14959
|
var package_default = {
|
|
14651
14960
|
name: "@jmanuelcorral/openteam",
|
|
14652
|
-
version: "0.9.
|
|
14961
|
+
version: "0.9.3",
|
|
14653
14962
|
packageManager: "bun@1.3.14",
|
|
14654
14963
|
description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
|
|
14655
14964
|
license: "MIT",
|
|
@@ -14774,6 +15083,18 @@ function createGitLastCommit(exec) {
|
|
|
14774
15083
|
var execFileAsync2 = promisify2(execFile2);
|
|
14775
15084
|
var AGENT_DIR2 = dirname5(ORCHESTRATOR_AGENT_PATH);
|
|
14776
15085
|
var storage = createFsStorageProvider(process.cwd());
|
|
15086
|
+
async function readStoredRosterForDoctor() {
|
|
15087
|
+
const content = await storage.read(OPENTEAM_ROSTER_PATH);
|
|
15088
|
+
if (content === undefined) {
|
|
15089
|
+
return;
|
|
15090
|
+
}
|
|
15091
|
+
try {
|
|
15092
|
+
return parseRoster(content);
|
|
15093
|
+
} catch {
|
|
15094
|
+
return content;
|
|
15095
|
+
}
|
|
15096
|
+
}
|
|
15097
|
+
var auditStoredRosterForDoctor = (roster, configuredRoles) => typeof roster === "string" ? auditRoster(roster, configuredRoles) : auditRoster(roster, configuredRoles);
|
|
14777
15098
|
var migratePort = createMigrateAdapter(process.cwd());
|
|
14778
15099
|
var nodeExec2 = async (command, args) => {
|
|
14779
15100
|
try {
|
|
@@ -14952,6 +15273,8 @@ var deps = {
|
|
|
14952
15273
|
cachePort: realCacheAdapter,
|
|
14953
15274
|
purge: createCliPurgeRuntime(),
|
|
14954
15275
|
loopRuntime: createCliLoopRuntime(),
|
|
15276
|
+
loadRosterForDoctor: readStoredRosterForDoctor,
|
|
15277
|
+
auditRoster: auditStoredRosterForDoctor,
|
|
14955
15278
|
version: PACKAGE_VERSION
|
|
14956
15279
|
};
|
|
14957
15280
|
function createCliPurgeRuntime() {
|
|
@@ -15021,7 +15344,7 @@ function createCliLoopRuntime() {
|
|
|
15021
15344
|
const agentFileExists = (agentName) => storage.exists(`${AGENT_DIR2}/${agentName}.md`);
|
|
15022
15345
|
const generate = () => generateRoster("openteam initial roster", fallbackRosterSource);
|
|
15023
15346
|
return {
|
|
15024
|
-
|
|
15347
|
+
loadRosterFile: () => loadRosterFile(storage),
|
|
15025
15348
|
loadOrGenerateRoster: async () => (await loadOrGenerateRoster(storage, generate)).roster,
|
|
15026
15349
|
agentFileExists,
|
|
15027
15350
|
readBacklog: (path4) => storage.read(path4),
|
|
@@ -4,12 +4,14 @@ import type { LegacyLayoutFinding } from "../config/legacyPaths";
|
|
|
4
4
|
import type { OpenTeamConfig } from "../config/schema";
|
|
5
5
|
import type { LocalRuntimeSnapshot } from "../local/types";
|
|
6
6
|
import type { ActiveIngressCapability, GraphIngress, IngressPolicy } from "../orchestrator/graphIngress";
|
|
7
|
+
import { type AgentRoleProfile } from "../orchestrator/roles";
|
|
7
8
|
import { type Roster } from "../orchestrator/roster";
|
|
9
|
+
import type { RosterFileLoadResult } from "../orchestrator/rosterPersistence";
|
|
8
10
|
import { type WorktreeObservation, type WorktreeReconcileAction } from "../orchestrator/worktreeReconciler";
|
|
9
11
|
import type { CostRecord } from "../telemetry/types";
|
|
10
12
|
import { type AgentFile } from "./agents";
|
|
11
13
|
import type { CachePort } from "./clearCache";
|
|
12
|
-
import { type DoctorDiagnostic } from "./doctor";
|
|
14
|
+
import { type DoctorDiagnostic, type DoctorRosterAudit } from "./doctor";
|
|
13
15
|
import { type GraphHealthSnapshot } from "./graph";
|
|
14
16
|
import { type PurgeRuntime } from "./purge";
|
|
15
17
|
export type CliDeps = {
|
|
@@ -22,6 +24,18 @@ export type CliDeps = {
|
|
|
22
24
|
* older composition roots still run; when absent, doctor omits the section.
|
|
23
25
|
*/
|
|
24
26
|
readDiagnostics?: () => Promise<DoctorDiagnostic[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Reads the persisted roster for `openteam doctor`. This may be raw Markdown:
|
|
29
|
+
* the pure audit seam classifies unparseable table-only rosters without
|
|
30
|
+
* throwing, preserving the human-authored file while doctor reports a remedy.
|
|
31
|
+
*/
|
|
32
|
+
loadRosterForDoctor?: () => Promise<Roster | string | undefined>;
|
|
33
|
+
/**
|
|
34
|
+
* Pure roster audit seam from `src/orchestrator/roster.ts`. Doctor receives the
|
|
35
|
+
* structured result and only renders it; audit logic stays in the roster
|
|
36
|
+
* boundary.
|
|
37
|
+
*/
|
|
38
|
+
auditRoster?: (roster: Roster | string, configuredRoles?: Readonly<Record<string, AgentRoleProfile>>) => DoctorRosterAudit;
|
|
25
39
|
/**
|
|
26
40
|
* Reads and deep-merges all candidate opencode config files (later entries in
|
|
27
41
|
* `paths` have lower precedence). Returns `undefined` when none of the paths
|
|
@@ -197,8 +211,8 @@ export type LoopRunResult = {
|
|
|
197
211
|
* implementation (see `src/cli.ts`); tests inject a deterministic fake.
|
|
198
212
|
*/
|
|
199
213
|
export type LoopRuntime = {
|
|
200
|
-
/** Reads the persisted roster WITHOUT contacting a model.
|
|
201
|
-
|
|
214
|
+
/** Reads the persisted roster status WITHOUT contacting a model. */
|
|
215
|
+
loadRosterFile(): Promise<RosterFileLoadResult>;
|
|
202
216
|
/** Reuses the persisted roster, or generates + persists one (may call a model). */
|
|
203
217
|
loadOrGenerateRoster(): Promise<Roster>;
|
|
204
218
|
/** Whether `.opencode/agent/<agentName>.md` exists in the workspace. */
|
|
@@ -215,7 +229,7 @@ export type LoopPlan = {
|
|
|
215
229
|
readonly flags: LoopFlags;
|
|
216
230
|
readonly uncheckedCount: number;
|
|
217
231
|
readonly plannedIterations: number;
|
|
218
|
-
readonly
|
|
232
|
+
readonly rosterFile: RosterFileLoadResult;
|
|
219
233
|
readonly guaranteed: readonly GuaranteedAgentStatus[];
|
|
220
234
|
};
|
|
221
235
|
/** Renders the loop plan for `--dry-run`. Pure. */
|