@jmanuelcorral/openteam 0.9.1 → 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.
- package/README.es.md +1 -1
- package/README.md +1 -1
- package/dist/cli.js +362 -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 +361 -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.2`; 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.2`; 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,77 @@ 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
|
+
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
|
+
}
|
|
10349
10619
|
function agentModelsSection(diagnostics, searchedPaths) {
|
|
10350
10620
|
const lines = [" agent models:"];
|
|
10351
10621
|
if (diagnostics.length === 0) {
|
|
@@ -10384,6 +10654,7 @@ function renderDoctor(input) {
|
|
|
10384
10654
|
if (input.diagnostics !== undefined) {
|
|
10385
10655
|
lines.push(...diagnosticsSection(input.diagnostics));
|
|
10386
10656
|
}
|
|
10657
|
+
lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
|
|
10387
10658
|
if (input.otelBackend !== undefined) {
|
|
10388
10659
|
const otel = input.otelBackend;
|
|
10389
10660
|
lines.push(" opentelemetry:");
|
|
@@ -12609,13 +12880,23 @@ async function guaranteedAgentStatuses(runtime) {
|
|
|
12609
12880
|
}
|
|
12610
12881
|
return statuses;
|
|
12611
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
|
+
}
|
|
12612
12893
|
function renderLoopPlan(plan2) {
|
|
12613
12894
|
const { flags } = plan2;
|
|
12614
12895
|
const budget = flags.budgetUsd !== undefined ? `$${flags.budgetUsd}` : "unbounded";
|
|
12615
12896
|
const freshContext = flags.freshContext ? "on (pure Ralph pattern)" : "off";
|
|
12616
12897
|
const budgetStop = flags.budgetUsd !== undefined ? ", budget" : "";
|
|
12617
12898
|
const redTestsStop = flags.stopOnRedTests ? ", red tests" : "";
|
|
12618
|
-
const rosterSummary =
|
|
12899
|
+
const rosterSummary = rosterPlanSummary(plan2.rosterFile);
|
|
12619
12900
|
const lines = [
|
|
12620
12901
|
"openteam loop — plan (dry run, nothing dispatched):",
|
|
12621
12902
|
` backlog: ${flags.backlogPath} (${plan2.uncheckedCount} unchecked item(s))`,
|
|
@@ -12659,14 +12940,14 @@ ${LOOP_HELP}` };
|
|
|
12659
12940
|
const backlogText = await runtime.readBacklog(flags.backlogPath) ?? "";
|
|
12660
12941
|
const items = parseBacklog2(backlogText);
|
|
12661
12942
|
const unchecked = uncheckedBacklogItems(items);
|
|
12662
|
-
const
|
|
12943
|
+
const rosterFile = await runtime.loadRosterFile();
|
|
12663
12944
|
const guaranteed = await guaranteedAgentStatuses(runtime);
|
|
12664
12945
|
const plannedIterations = Math.max(0, Math.min(flags.maxIterations, unchecked.length));
|
|
12665
12946
|
const plan2 = {
|
|
12666
12947
|
flags,
|
|
12667
12948
|
uncheckedCount: unchecked.length,
|
|
12668
12949
|
plannedIterations,
|
|
12669
|
-
|
|
12950
|
+
rosterFile,
|
|
12670
12951
|
guaranteed
|
|
12671
12952
|
};
|
|
12672
12953
|
if (flags.dryRun) {
|
|
@@ -12744,13 +13025,15 @@ async function runCli(argv, deps) {
|
|
|
12744
13025
|
if (command === "doctor") {
|
|
12745
13026
|
const config = await deps.loadConfig(configPath, configResolution);
|
|
12746
13027
|
let opencodeConfigError;
|
|
13028
|
+
let rosterLoadError;
|
|
12747
13029
|
const [
|
|
12748
13030
|
snapshots,
|
|
12749
13031
|
records,
|
|
12750
13032
|
diagnostics,
|
|
12751
13033
|
cacheEntries,
|
|
12752
13034
|
agentFiles,
|
|
12753
|
-
opencodeConfig
|
|
13035
|
+
opencodeConfig,
|
|
13036
|
+
roster
|
|
12754
13037
|
] = await Promise.all([
|
|
12755
13038
|
deps.probe(config),
|
|
12756
13039
|
deps.readTelemetry(telemetryPath),
|
|
@@ -12762,7 +13045,11 @@ async function runCli(argv, deps) {
|
|
|
12762
13045
|
deps.readOpencodeConfig(opencodeConfigPaths).catch((err) => {
|
|
12763
13046
|
opencodeConfigError = err instanceof Error ? err.message : String(err);
|
|
12764
13047
|
return;
|
|
12765
|
-
})
|
|
13048
|
+
}),
|
|
13049
|
+
deps.loadRosterForDoctor !== undefined ? deps.loadRosterForDoctor().catch((err) => {
|
|
13050
|
+
rosterLoadError = err instanceof Error ? err.message : String(err);
|
|
13051
|
+
return;
|
|
13052
|
+
}) : Promise.resolve(undefined)
|
|
12766
13053
|
]);
|
|
12767
13054
|
const defaultModel = defaultModelFromOpencode(opencodeConfig);
|
|
12768
13055
|
const agentModels = diagnoseAgentModels(agentFiles, {
|
|
@@ -12777,6 +13064,18 @@ async function runCli(argv, deps) {
|
|
|
12777
13064
|
backend: "opentelemetry",
|
|
12778
13065
|
connectionEnv: "APPLICATIONINSIGHTS_CONNECTION_STRING"
|
|
12779
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
|
+
}
|
|
12780
13079
|
return {
|
|
12781
13080
|
exitCode: 0,
|
|
12782
13081
|
stdout: renderDoctor({
|
|
@@ -12786,6 +13085,9 @@ async function runCli(argv, deps) {
|
|
|
12786
13085
|
telemetryRecords: records.length,
|
|
12787
13086
|
agentModels,
|
|
12788
13087
|
opencodeConfigPaths,
|
|
13088
|
+
...rosterAudit !== undefined ? { rosterAudit } : {},
|
|
13089
|
+
...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
|
|
13090
|
+
...rosterLoadError !== undefined ? { rosterLoadError } : {},
|
|
12789
13091
|
...diagnostics !== undefined && diagnostics.length > 0 ? { diagnostics } : {},
|
|
12790
13092
|
...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
|
|
12791
13093
|
...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
|
|
@@ -14349,7 +14651,6 @@ async function* linesFromReadable(readable) {
|
|
|
14349
14651
|
}
|
|
14350
14652
|
|
|
14351
14653
|
// src/orchestrator/rosterPersistence.ts
|
|
14352
|
-
var ROSTER_JSON_FENCE = /```json\s*([\s\S]*?)```/;
|
|
14353
14654
|
function serializeRoster(roster) {
|
|
14354
14655
|
const data = { universe: roster.universe, entries: roster.entries };
|
|
14355
14656
|
const json = JSON.stringify(data, null, 2);
|
|
@@ -14362,38 +14663,41 @@ ${json}
|
|
|
14362
14663
|
\`\`\`
|
|
14363
14664
|
`;
|
|
14364
14665
|
}
|
|
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
14666
|
async function persistRoster(storage, roster, path4 = OPENTEAM_ROSTER_PATH) {
|
|
14379
14667
|
await storage.write(path4, serializeRoster(roster));
|
|
14380
14668
|
return path4;
|
|
14381
14669
|
}
|
|
14382
|
-
async function
|
|
14670
|
+
async function loadRosterFile(storage, path4 = OPENTEAM_ROSTER_PATH) {
|
|
14383
14671
|
const content = await storage.read(path4);
|
|
14384
14672
|
if (content === undefined) {
|
|
14385
|
-
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 };
|
|
14386
14678
|
}
|
|
14387
|
-
return
|
|
14679
|
+
return {
|
|
14680
|
+
status: "loaded",
|
|
14681
|
+
path: path4,
|
|
14682
|
+
roster: enforceGuaranteedRoles(parsed.roster)
|
|
14683
|
+
};
|
|
14388
14684
|
}
|
|
14389
14685
|
async function loadOrGenerateRoster(storage, generate, path4 = OPENTEAM_ROSTER_PATH) {
|
|
14390
|
-
const existing = await
|
|
14391
|
-
if (existing
|
|
14392
|
-
return { roster: existing, regenerated: false };
|
|
14686
|
+
const existing = await loadRosterFile(storage, path4);
|
|
14687
|
+
if (existing.status === "loaded") {
|
|
14688
|
+
return { roster: existing.roster, regenerated: false };
|
|
14689
|
+
}
|
|
14690
|
+
const roster = enforceGuaranteedRoles(await generate());
|
|
14691
|
+
if (existing.status === "absent") {
|
|
14692
|
+
await persistRoster(storage, roster, path4);
|
|
14693
|
+
return { roster, regenerated: true };
|
|
14393
14694
|
}
|
|
14394
|
-
|
|
14395
|
-
|
|
14396
|
-
|
|
14695
|
+
return {
|
|
14696
|
+
roster,
|
|
14697
|
+
regenerated: true,
|
|
14698
|
+
recoveredFromInvalidPersisted: true,
|
|
14699
|
+
invalidPersistedRosterError: existing.error.message
|
|
14700
|
+
};
|
|
14397
14701
|
}
|
|
14398
14702
|
|
|
14399
14703
|
// src/plugin/orchestrateTool.ts
|
|
@@ -14649,7 +14953,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
|
|
|
14649
14953
|
// package.json
|
|
14650
14954
|
var package_default = {
|
|
14651
14955
|
name: "@jmanuelcorral/openteam",
|
|
14652
|
-
version: "0.9.
|
|
14956
|
+
version: "0.9.2",
|
|
14653
14957
|
packageManager: "bun@1.3.14",
|
|
14654
14958
|
description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
|
|
14655
14959
|
license: "MIT",
|
|
@@ -14774,6 +15078,18 @@ function createGitLastCommit(exec) {
|
|
|
14774
15078
|
var execFileAsync2 = promisify2(execFile2);
|
|
14775
15079
|
var AGENT_DIR2 = dirname5(ORCHESTRATOR_AGENT_PATH);
|
|
14776
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);
|
|
14777
15093
|
var migratePort = createMigrateAdapter(process.cwd());
|
|
14778
15094
|
var nodeExec2 = async (command, args) => {
|
|
14779
15095
|
try {
|
|
@@ -14952,6 +15268,8 @@ var deps = {
|
|
|
14952
15268
|
cachePort: realCacheAdapter,
|
|
14953
15269
|
purge: createCliPurgeRuntime(),
|
|
14954
15270
|
loopRuntime: createCliLoopRuntime(),
|
|
15271
|
+
loadRosterForDoctor: readStoredRosterForDoctor,
|
|
15272
|
+
auditRoster: auditStoredRosterForDoctor,
|
|
14955
15273
|
version: PACKAGE_VERSION
|
|
14956
15274
|
};
|
|
14957
15275
|
function createCliPurgeRuntime() {
|
|
@@ -15021,7 +15339,7 @@ function createCliLoopRuntime() {
|
|
|
15021
15339
|
const agentFileExists = (agentName) => storage.exists(`${AGENT_DIR2}/${agentName}.md`);
|
|
15022
15340
|
const generate = () => generateRoster("openteam initial roster", fallbackRosterSource);
|
|
15023
15341
|
return {
|
|
15024
|
-
|
|
15342
|
+
loadRosterFile: () => loadRosterFile(storage),
|
|
15025
15343
|
loadOrGenerateRoster: async () => (await loadOrGenerateRoster(storage, generate)).roster,
|
|
15026
15344
|
agentFileExists,
|
|
15027
15345
|
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. */
|