@jmanuelcorral/openteam 0.16.0 → 0.16.1

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
@@ -2450,6 +2450,10 @@ function splitVirtualPath(path) {
2450
2450
 
2451
2451
  // src/cli/graphOperator.ts
2452
2452
  var ALLOWED_GATE = { allowed: true, violations: [] };
2453
+ var ALLOWED_EVALUATION = {
2454
+ kind: "evaluated",
2455
+ gate: ALLOWED_GATE
2456
+ };
2453
2457
  function journalRoot(config) {
2454
2458
  return config.graph?.journalRoot ?? DEFAULT_GRAPH_JOURNAL_ROOT;
2455
2459
  }
@@ -2539,7 +2543,7 @@ async function listWorktreeObservations(config, options) {
2539
2543
  })));
2540
2544
  }
2541
2545
  function createGraphOperator(options) {
2542
- const evaluateCutoverGate = options.evaluateCutoverGate ?? (async () => ALLOWED_GATE);
2546
+ const evaluateCutoverGate = options.evaluateCutoverGate ?? (async () => ALLOWED_EVALUATION);
2543
2547
  const worktreeOptions = options.exec === undefined ? undefined : {
2544
2548
  exec: options.exec,
2545
2549
  repoRoot: options.repoRoot ?? process.cwd(),
@@ -4298,6 +4302,36 @@ var SUPPORTED_OPENCODE_VERSIONS = [
4298
4302
  "1.18.18",
4299
4303
  "1.18.19"
4300
4304
  ];
4305
+ var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
4306
+ function parseSemver(version) {
4307
+ const normalized = version.trim().replace(/^[=^~v]+/, "");
4308
+ const match = SEMVER_RE.exec(normalized);
4309
+ if (match === null) {
4310
+ throw new Error(`invalid semver: "${version}"`);
4311
+ }
4312
+ return {
4313
+ major: Number(match[1]),
4314
+ minor: Number(match[2]),
4315
+ patch: Number(match[3])
4316
+ };
4317
+ }
4318
+ function isCertificateVersionCompatible(certifiedVersion, liveVersion) {
4319
+ if (certifiedVersion === liveVersion) {
4320
+ return true;
4321
+ }
4322
+ let certified;
4323
+ let live;
4324
+ try {
4325
+ certified = parseSemver(certifiedVersion);
4326
+ live = parseSemver(liveVersion);
4327
+ } catch {
4328
+ return false;
4329
+ }
4330
+ if (certified.major !== live.major || certified.minor !== live.minor) {
4331
+ return false;
4332
+ }
4333
+ return live.patch >= certified.patch;
4334
+ }
4301
4335
 
4302
4336
  // src/graph/soakLedger.ts
4303
4337
  import { z as z15 } from "zod";
@@ -4556,8 +4590,8 @@ function parseShadowCertificate(value, opencodeVersion) {
4556
4590
  if (sha256Hex(canonicalCertificate(unsigned)) !== digest) {
4557
4591
  throw new ShadowCertificateError("digest-mismatch", "certificate digest does not cover the presented evidence");
4558
4592
  }
4559
- if (certificate.opencodeVersion !== opencodeVersion) {
4560
- throw new ShadowCertificateError("opencode-mismatch", `certified for "${certificate.opencodeVersion}", expected "${opencodeVersion}"`);
4593
+ if (!isCertificateVersionCompatible(certificate.opencodeVersion, opencodeVersion)) {
4594
+ throw new ShadowCertificateError("opencode-mismatch", `certified for "${certificate.opencodeVersion}", running "${opencodeVersion}" — evidence is accepted only on ${certificate.opencodeVersion} or a newer patch of the same minor line`);
4561
4595
  }
4562
4596
  return certificate;
4563
4597
  }
@@ -4741,8 +4775,8 @@ function parseReleaseCertificate(value, opencodeVersion, expectedShadowDigest) {
4741
4775
  if (sha256Hex(canonicalReleaseCertificate(unsigned)) !== digest) {
4742
4776
  throw new ReleaseCertificateError("digest-mismatch", "certificate digest does not cover the presented evidence");
4743
4777
  }
4744
- if (certificate.opencodeVersion !== opencodeVersion) {
4745
- throw new ReleaseCertificateError("opencode-mismatch", `certified for "${certificate.opencodeVersion}", expected "${opencodeVersion}"`);
4778
+ if (!isCertificateVersionCompatible(certificate.opencodeVersion, opencodeVersion)) {
4779
+ throw new ReleaseCertificateError("opencode-mismatch", `certified for "${certificate.opencodeVersion}", running "${opencodeVersion}" — evidence is accepted only on ${certificate.opencodeVersion} or a newer patch of the same minor line`);
4746
4780
  }
4747
4781
  return certificate;
4748
4782
  }
@@ -8344,7 +8378,7 @@ function legacyRosterFindings(audit) {
8344
8378
  }
8345
8379
  return findings;
8346
8380
  }
8347
- function rosterHealthSection(audit, loadError, rosterRoleCount) {
8381
+ function rosterHealthSection(audit, loadError, rosterRoleCount, rosterMissing) {
8348
8382
  const auditFindings = audit?.findings ?? legacyRosterFindings(audit ?? {});
8349
8383
  const unparseable = auditFindings.find((finding) => finding.kind === "unparseable-roster");
8350
8384
  const unparseableMessage = loadError ?? unparseable?.message;
@@ -8357,6 +8391,14 @@ function rosterHealthSection(audit, loadError, rosterRoleCount) {
8357
8391
  ];
8358
8392
  }
8359
8393
  if (audit === undefined) {
8394
+ if (rosterMissing === true) {
8395
+ return [
8396
+ " roster health:",
8397
+ ` ✗ ${OPENTEAM_ROSTER_PATH} does not exist — no team roster has been cast.`,
8398
+ " consequence: without a roster openteam cannot distribute work across roles, so every request is handled by a single agent and no work splitting happens.",
8399
+ ` remedy: run \`openteam setup\` to cast a team and write the roster (${OPENTEAM_ROSTER_PATH}), then re-run \`openteam doctor\`.`
8400
+ ];
8401
+ }
8360
8402
  return [];
8361
8403
  }
8362
8404
  const missing = auditFindings.filter((finding) => finding.kind === "missing-guaranteed-role").map((finding) => finding.roleID);
@@ -8392,6 +8434,14 @@ function rosterHealthSection(audit, loadError, rosterRoleCount) {
8392
8434
  }
8393
8435
  return lines;
8394
8436
  }
8437
+ function orchestratorAgentSection() {
8438
+ return [
8439
+ " orchestrator agent:",
8440
+ ` ✗ ${ORCHESTRATOR_AGENT_PATH} does not exist — opencode has no openteam coordinator agent.`,
8441
+ " consequence: this is the primary agent that reads the roster and calls `openteam-orchestrate` to fan work out across roles; without it opencode has no openteam coordinator, so every request is handled by a single agent and no work splitting happens.",
8442
+ ` remedy: run \`openteam setup\` to generate ${ORCHESTRATOR_AGENT_PATH}, then re-run \`openteam doctor\`.`
8443
+ ];
8444
+ }
8395
8445
  function gitignoreSection(gitignore) {
8396
8446
  if (gitignore.missingRules.length === 0) {
8397
8447
  return [
@@ -8455,7 +8505,10 @@ function renderDoctor(input) {
8455
8505
  if (input.diagnostics !== undefined) {
8456
8506
  lines.push(...diagnosticsSection(input.diagnostics));
8457
8507
  }
8458
- lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
8508
+ lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount, input.rosterMissing));
8509
+ if (input.orchestratorAgentMissing === true) {
8510
+ lines.push(...orchestratorAgentSection());
8511
+ }
8459
8512
  if (input.gitignore !== undefined) {
8460
8513
  lines.push(...gitignoreSection(input.gitignore));
8461
8514
  }
@@ -8539,14 +8592,31 @@ function leaseLine(lease) {
8539
8592
  const state = lease.released ? "released" : "active";
8540
8593
  return ` lease: ${lease.holder} → ${lease.runID} (${state})`;
8541
8594
  }
8542
- function renderGraphStatus(snapshot) {
8595
+ function appendEffectiveLines(lines, gate) {
8596
+ switch (gate.kind) {
8597
+ case "not-applicable":
8598
+ return;
8599
+ case "passed":
8600
+ lines.push(" effective: active");
8601
+ return;
8602
+ case "unevaluable":
8603
+ lines.push(" effective: unknown — could not evaluate the cutover gate");
8604
+ lines.push(` ${gate.reason}`);
8605
+ return;
8606
+ case "denied":
8607
+ lines.push(" effective: off — cutover gate denied");
8608
+ for (const violation of gate.violations) {
8609
+ lines.push(` • ${violation.code}: ${violation.detail}`);
8610
+ }
8611
+ }
8612
+ }
8613
+ function renderGraphStatus(snapshot, gate = { kind: "not-applicable" }) {
8543
8614
  const lines = [
8544
8615
  "openteam graph status:",
8545
- ` config: ${snapshot.effectiveMode} (source: ${snapshot.source})`,
8546
- leaseLine(snapshot.lease),
8547
- ` journal: ${snapshot.journal}`,
8548
- ` effects: ${snapshot.pendingEffects} pending, ${snapshot.unknownEffects} unknown`
8616
+ ` config: ${snapshot.effectiveMode} (source: ${snapshot.source})`
8549
8617
  ];
8618
+ appendEffectiveLines(lines, gate);
8619
+ lines.push(leaseLine(snapshot.lease), ` journal: ${snapshot.journal}`, ` effects: ${snapshot.pendingEffects} pending, ${snapshot.unknownEffects} unknown`);
8550
8620
  const needsReconciliation = isRecoveryRequired(snapshot.journal, snapshot.unknownEffects);
8551
8621
  if (needsReconciliation) {
8552
8622
  lines.push(" ⚠ Reconciliation required: journal or effects in an uncertain state.");
@@ -9990,8 +10060,7 @@ async function runAgents(deps, configPath, opencodeConfigPaths, configResolution
9990
10060
  async function runGraph(positionals, deps, configPath, configResolution) {
9991
10061
  const sub = positionals[1];
9992
10062
  if (sub === "status") {
9993
- const snapshot = await deps.readGraphSnapshot(configPath, configResolution);
9994
- return { exitCode: 0, stdout: renderGraphStatus(snapshot) };
10063
+ return runGraphStatus(deps, configPath, configResolution);
9995
10064
  }
9996
10065
  if (sub === "cancel") {
9997
10066
  return runGraphCancel(positionals, deps, configPath, configResolution);
@@ -10046,7 +10115,9 @@ function renderGraphOutcome(outcome) {
10046
10115
  async function resolveOperatorGraphPolicy(config, operator) {
10047
10116
  let graph = resolveGraphConfig(config, "file");
10048
10117
  if (isGateApplicable(graph.effectiveMode)) {
10049
- graph = applyGraphGate(graph, await operator.evaluateCutoverGate());
10118
+ const evaluation = await operator.evaluateCutoverGate(config);
10119
+ const gate = evaluation.kind === "evaluated" ? evaluation.gate : { allowed: false, violations: [] };
10120
+ graph = applyGraphGate(graph, gate);
10050
10121
  }
10051
10122
  return {
10052
10123
  graph,
@@ -10064,6 +10135,32 @@ function checkOperatorGate(policy) {
10064
10135
  stdout: renderLegacyGraphReason(decision2)
10065
10136
  };
10066
10137
  }
10138
+ async function runGraphStatus(deps, configPath, configResolution) {
10139
+ const snapshot = await deps.readGraphSnapshot(configPath, configResolution);
10140
+ if (!isGateApplicable(snapshot.effectiveMode)) {
10141
+ return { exitCode: 0, stdout: renderGraphStatus(snapshot) };
10142
+ }
10143
+ const operator = deps.graphOperator;
10144
+ if (operator === undefined) {
10145
+ const gate2 = {
10146
+ kind: "unevaluable",
10147
+ reason: "graph operator ports are not configured"
10148
+ };
10149
+ return { exitCode: 0, stdout: renderGraphStatus(snapshot, gate2) };
10150
+ }
10151
+ const config = await deps.loadConfig(configPath, configResolution);
10152
+ const evaluation = await operator.evaluateCutoverGate(config);
10153
+ if (evaluation.kind === "unevaluable") {
10154
+ const gate2 = {
10155
+ kind: "unevaluable",
10156
+ reason: evaluation.reason
10157
+ };
10158
+ return { exitCode: 0, stdout: renderGraphStatus(snapshot, gate2) };
10159
+ }
10160
+ const resolved = applyGraphGate(resolveGraphConfig(config, "file"), evaluation.gate);
10161
+ const gate = resolved.gateDenied === true ? { kind: "denied", violations: resolved.gateViolations ?? [] } : { kind: "passed" };
10162
+ return { exitCode: 0, stdout: renderGraphStatus(snapshot, gate) };
10163
+ }
10067
10164
  async function runGraphCancel(positionals, deps, configPath, configResolution) {
10068
10165
  const runID = positionals[2];
10069
10166
  if (runID === undefined || runID.trim() === "") {
@@ -10510,6 +10607,9 @@ async function runCli(argv, deps) {
10510
10607
  const summary = summarizeQueueWaits(queueEvents);
10511
10608
  queueWait = summary !== undefined ? { summary } : {};
10512
10609
  }
10610
+ const rosterMissing = deps.loadRosterForDoctor !== undefined && roster === undefined && rosterLoadError === undefined;
10611
+ const orchestratorAgentName = ORCHESTRATOR_AGENT_PATH.slice(ORCHESTRATOR_AGENT_PATH.lastIndexOf("/") + 1).replace(/\.md$/, "");
10612
+ const orchestratorAgentMissing = !agentFiles.some((file) => file.name === orchestratorAgentName);
10513
10613
  return {
10514
10614
  exitCode: 0,
10515
10615
  stdout: renderDoctor({
@@ -10523,6 +10623,8 @@ async function runCli(argv, deps) {
10523
10623
  ...rosterAudit !== undefined ? { rosterAudit } : {},
10524
10624
  ...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
10525
10625
  ...rosterLoadError !== undefined ? { rosterLoadError } : {},
10626
+ ...rosterMissing ? { rosterMissing } : {},
10627
+ ...orchestratorAgentMissing ? { orchestratorAgentMissing } : {},
10526
10628
  ...diagnostics !== undefined && diagnostics.length > 0 ? { diagnostics } : {},
10527
10629
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
10528
10630
  ...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
@@ -13387,7 +13489,7 @@ var createOtlpSpanExporter = (connection, config) => {
13387
13489
  // package.json
13388
13490
  var package_default = {
13389
13491
  name: "@jmanuelcorral/openteam",
13390
- version: "0.16.0",
13492
+ version: "0.16.1",
13391
13493
  packageManager: "bun@1.3.14",
13392
13494
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
13393
13495
  license: "MIT",
@@ -13751,6 +13853,14 @@ async function buildGateInput(config, storage, opencodeVersion, warn) {
13751
13853
  const operatorApproval = config.graph?.operatorApproval ?? false;
13752
13854
  return { shadow, release, soak, migration, operatorApproval };
13753
13855
  }
13856
+ var OPENCODE_VERSION_UNRESOLVED_REASON = "the opencode version is not determinable outside a running opencode session, " + "so the cutover gate cannot be verified here — the running plugin logs the " + "authoritative gate result and warns loudly on a real denial";
13857
+ async function evaluateCutoverGateForBoundary(config, storage, version) {
13858
+ if (version === undefined) {
13859
+ return { kind: "unevaluable", reason: OPENCODE_VERSION_UNRESOLVED_REASON };
13860
+ }
13861
+ const gateInput = await buildGateInput(config, storage, version, () => {});
13862
+ return { kind: "evaluated", gate: evaluateGraphGate(gateInput) };
13863
+ }
13754
13864
  function recorderPortsFromStorage(storage) {
13755
13865
  return {
13756
13866
  fs: {
@@ -13838,7 +13948,7 @@ async function readStoredRosterForDoctor(storage) {
13838
13948
  }
13839
13949
  }
13840
13950
  var auditStoredRosterForDoctor = (roster, configuredRoles) => typeof roster === "string" ? auditRoster(roster, configuredRoles) : auditRoster(roster, configuredRoles);
13841
- function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SESSIONS_DIR, storage = createFsStorageProvider(process.cwd()), fallbackSource = "options", rawOptions) {
13951
+ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SESSIONS_DIR, storage = createFsStorageProvider(process.cwd()), fallbackSource = "options", rawOptions, resolveOpencodeVersion) {
13842
13952
  return {
13843
13953
  loadConfig: async (path4, resolution = { explicit: false }) => {
13844
13954
  const content = await readCliConfigFromStorage(storage, path4, resolution);
@@ -13912,7 +14022,8 @@ function createCliDeps(config, registry, telemetryPath, sessionsDir = DEFAULT_SE
13912
14022
  }),
13913
14023
  graphOperator: createGraphOperator({
13914
14024
  storage,
13915
- holder: "openteam-cli"
14025
+ holder: "openteam-cli",
14026
+ evaluateCutoverGate: async (gateConfig) => evaluateCutoverGateForBoundary(gateConfig, storage, resolveOpencodeVersion === undefined ? undefined : await resolveOpencodeVersion())
13916
14027
  }),
13917
14028
  configPath: DEFAULT_CONFIG_PATH,
13918
14029
  telemetryPath,
@@ -14042,7 +14153,14 @@ var server = async (ctx, rawOptions) => {
14042
14153
  injectMemory,
14043
14154
  onRoute: (sid, model) => sessionTracker.trackRoutedModel(sid, model)
14044
14155
  });
14045
- const cliDeps = createCliDeps(config, registry, telemetryPath, sessionsDir, storage, runtimeConfig.source, rawOptions);
14156
+ const getOpencodeVersion = createLazyOpencodeVersionReader(ctx.serverUrl, {
14157
+ client: ctx.client,
14158
+ onFailure: createOpencodeVersionReadFailureLogger(MAX_LOGGED_OPENCODE_VERSION_FAILURE_CAUSES, diagnostics.warn("opencode-version-read-failed", { toast: false }))
14159
+ });
14160
+ const cliDeps = createCliDeps(config, registry, telemetryPath, sessionsDir, storage, runtimeConfig.source, rawOptions, async () => {
14161
+ const version = await getOpencodeVersion();
14162
+ return version === "unknown" ? undefined : version;
14163
+ });
14046
14164
  const toolcalls = createToolcallTracker({ sink });
14047
14165
  const announcedSessions = new Set;
14048
14166
  const announceEndpoint = (sessionID) => {
@@ -14065,10 +14183,6 @@ var server = async (ctx, rawOptions) => {
14065
14183
  if (config.privacyMode === "forceLocalOnSensitive" && localDefault(config) === null) {
14066
14184
  warnInertPrivacySetting(runtimeConfig.searchedPaths, diagnostics.warn("privacy-inert"));
14067
14185
  }
14068
- const getOpencodeVersion = createLazyOpencodeVersionReader(ctx.serverUrl, {
14069
- client: ctx.client,
14070
- onFailure: createOpencodeVersionReadFailureLogger(MAX_LOGGED_OPENCODE_VERSION_FAILURE_CAUSES, diagnostics.warn("opencode-version-read-failed", { toast: false }))
14071
- });
14072
14186
  if (isGateApplicable(graphConfig.effectiveMode)) {
14073
14187
  const opencodeVersion = await getOpencodeVersion();
14074
14188
  const gateInput = await buildGateInput(config, storage, opencodeVersion, diagnostics.warn("soak-evidence-rejected", { toast: false }));
@@ -14233,6 +14347,7 @@ export {
14233
14347
  logOpencodeServerUrl,
14234
14348
  logAvailabilityRefreshError,
14235
14349
  isMissingFile,
14350
+ evaluateCutoverGateForBoundary,
14236
14351
  src_default as default,
14237
14352
  createOpencodeVersionReadFailureLogger,
14238
14353
  createLazyOpencodeVersionReader,
@@ -14240,5 +14355,6 @@ export {
14240
14355
  createCliDeps,
14241
14356
  buildGateInput,
14242
14357
  PACKAGE_VERSION,
14358
+ OPENCODE_VERSION_UNRESOLVED_REASON,
14243
14359
  MAX_LOGGED_OPENCODE_VERSION_FAILURE_CAUSES
14244
14360
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmanuelcorral/openteam",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "packageManager": "bun@1.3.14",
5
5
  "description": "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
6
6
  "license": "MIT",