@jmanuelcorral/openteam 0.13.0 → 0.14.0

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
@@ -95,8 +95,8 @@ var AgentRoleProfileSchema = z.object({
95
95
  preferredFrontierModels: z.array(z.union([modelSelectionSchema, z.literal("auto")])),
96
96
  localFirst: z.boolean(),
97
97
  requiresLocalRuntime: z.boolean().optional(),
98
- localRuntimes: z.array(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"])).optional(),
99
- localModels: z.partialRecord(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"]), z.string().min(1)).optional()
98
+ localRuntimes: z.array(z.string().min(1)).optional(),
99
+ localModels: z.record(z.string().min(1), z.string().min(1)).optional()
100
100
  }).strict();
101
101
  var TEAM_ROLES = {
102
102
  architect: {
@@ -403,14 +403,33 @@ var defaultFrontierModel = {
403
403
  modelID: "claude-sonnet-4-5"
404
404
  };
405
405
  var DEFAULT_LOCAL_MAX_CONCURRENCY = 4;
406
+ var LocalRuntimeKindSchema = z2.enum([
407
+ "ollama",
408
+ "lmstudio",
409
+ "foundry-local",
410
+ "lemonade"
411
+ ]);
406
412
  var LocalRuntimeSchema = z2.object({
407
- id: z2.enum(["ollama", "lmstudio", "foundry-local", "lemonade"]),
413
+ id: z2.string().min(1),
414
+ kind: LocalRuntimeKindSchema.optional(),
408
415
  enabled: z2.boolean().default(true),
409
416
  baseURL: z2.string().url().optional(),
410
417
  discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
411
418
  defaultModel: ModelRefSchema,
412
419
  maxConcurrency: z2.number().int().positive().optional()
413
- }).strict();
420
+ }).strict().superRefine((runtime, ctx) => {
421
+ const resolvedKind = runtime.kind ?? runtime.id;
422
+ if (!LocalRuntimeKindSchema.safeParse(resolvedKind).success) {
423
+ ctx.addIssue({
424
+ code: z2.ZodIssueCode.custom,
425
+ path: ["kind"],
426
+ message: `local runtime "${runtime.id}" has no resolvable kind: add "kind" ` + "(one of ollama, lmstudio, foundry-local, lemonade), since the id is " + "not itself a runtime kind."
427
+ });
428
+ }
429
+ });
430
+ function localRuntimeKind(runtime) {
431
+ return runtime.kind ?? runtime.id;
432
+ }
414
433
  var MemoryScopeSchema = z2.enum(["project", "user"]);
415
434
  var MemoryExtractionModeSchema = z2.enum([
416
435
  "local",
@@ -492,7 +511,23 @@ var OpenTeamConfigObjectSchema = z2.object({
492
511
  localOnly: false
493
512
  }),
494
513
  local: z2.object({
495
- runtimes: z2.array(LocalRuntimeSchema).default([])
514
+ runtimes: z2.array(LocalRuntimeSchema).default([]).superRefine((runtimes, ctx) => {
515
+ const seen = new Set;
516
+ for (let index = 0;index < runtimes.length; index += 1) {
517
+ const runtime = runtimes[index];
518
+ if (runtime === undefined) {
519
+ continue;
520
+ }
521
+ if (seen.has(runtime.id)) {
522
+ ctx.addIssue({
523
+ code: z2.ZodIssueCode.custom,
524
+ path: [index, "id"],
525
+ message: `duplicate local runtime id "${runtime.id}": each local.runtimes[].id must be unique within the array.`
526
+ });
527
+ }
528
+ seen.add(runtime.id);
529
+ }
530
+ })
496
531
  }).strict().default({
497
532
  runtimes: []
498
533
  }),
@@ -2681,6 +2716,7 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
2681
2716
  "local.runtimes[]",
2682
2717
  [
2683
2718
  "id",
2719
+ "kind",
2684
2720
  "enabled",
2685
2721
  "baseURL",
2686
2722
  "discovery",
@@ -3031,6 +3067,16 @@ var MeetingEventSchema = EventBaseSchema.extend({
3031
3067
  roles: z11.array(z11.string().min(1)),
3032
3068
  decisionIDs: z11.array(z11.string().min(1)).optional()
3033
3069
  });
3070
+ var QueueEventSchema = EventBaseSchema.extend({
3071
+ type: z11.literal("queue"),
3072
+ runtimeId: z11.string().min(1),
3073
+ waitedMs: z11.number().finite().min(0),
3074
+ activeCount: z11.number().int().min(0),
3075
+ queuedCount: z11.number().int().min(0),
3076
+ decisionID: z11.string().min(1).optional(),
3077
+ agent: z11.string().min(1).optional(),
3078
+ batchID: z11.string().min(1).optional()
3079
+ });
3034
3080
  var DecisionEventSchema = EventBaseSchema.extend({
3035
3081
  type: z11.literal("decision"),
3036
3082
  agent: z11.string().min(1).optional(),
@@ -3076,6 +3122,7 @@ var OpenTeamEventSchema = z11.discriminatedUnion("type", [
3076
3122
  MessageEventSchema,
3077
3123
  ToolcallEventSchema,
3078
3124
  MeetingEventSchema,
3125
+ QueueEventSchema,
3079
3126
  DecisionEventSchema,
3080
3127
  ActivityEventSchema,
3081
3128
  SessionEndpointEventSchema,
@@ -5052,11 +5099,14 @@ function createRuntimeConcurrencyLimiter(options) {
5052
5099
  acquire(runtimeId) {
5053
5100
  const immediate = tryAcquire(runtimeId);
5054
5101
  if (immediate !== undefined) {
5055
- return Promise.resolve(immediate);
5102
+ return Promise.resolve({ release: immediate, waitedMs: 0 });
5056
5103
  }
5057
5104
  const state = stateFor(runtimeId);
5105
+ const queuedAt = options.now();
5058
5106
  return new Promise((resolve3) => {
5059
- state.queue.push(resolve3);
5107
+ state.queue.push((release) => {
5108
+ resolve3({ release, waitedMs: options.now() - queuedAt });
5109
+ });
5060
5110
  });
5061
5111
  },
5062
5112
  activeCount(runtimeId) {
@@ -5414,10 +5464,11 @@ class RuntimeRegistry {
5414
5464
  continue;
5415
5465
  }
5416
5466
  const probedAt = this.clock();
5417
- const adapter = this.adapters[runtime.id];
5467
+ const adapter = this.adapters[localRuntimeKind(runtime)];
5418
5468
  const options = this.createProbeOptions(runtime, probedAt);
5419
5469
  try {
5420
- snapshots.push(await adapter.probe(options));
5470
+ const snapshot = await adapter.probe(options);
5471
+ snapshots.push({ ...snapshot, id: runtime.id });
5421
5472
  } catch (error) {
5422
5473
  snapshots.push(unavailableSnapshot({
5423
5474
  id: runtime.id,
@@ -5437,7 +5488,7 @@ class RuntimeRegistry {
5437
5488
  if (runtime.baseURL !== undefined) {
5438
5489
  options.baseURL = runtime.baseURL;
5439
5490
  }
5440
- if (runtime.id === "foundry-local" && runtime.discovery !== "manual" && this.foundryDiscovery !== undefined) {
5491
+ if (localRuntimeKind(runtime) === "foundry-local" && runtime.discovery !== "manual" && this.foundryDiscovery !== undefined) {
5441
5492
  options.discovery = this.foundryDiscovery;
5442
5493
  }
5443
5494
  return options;
@@ -6538,6 +6589,28 @@ async function safeEmit(sink, event) {
6538
6589
  await sink.emit(event);
6539
6590
  } catch {}
6540
6591
  }
6592
+ function buildQueueEvent(observation, sessionID, ts, correlation) {
6593
+ const event = {
6594
+ v: EVENT_SCHEMA_VERSION,
6595
+ type: "queue",
6596
+ ts,
6597
+ sessionID,
6598
+ runtimeId: observation.runtimeId,
6599
+ waitedMs: observation.waitedMs,
6600
+ activeCount: observation.activeCount,
6601
+ queuedCount: observation.queuedCount
6602
+ };
6603
+ if (correlation.decisionID !== undefined) {
6604
+ event.decisionID = correlation.decisionID;
6605
+ }
6606
+ if (correlation.agent !== undefined) {
6607
+ event.agent = correlation.agent;
6608
+ }
6609
+ if (correlation.batchID !== undefined) {
6610
+ event.batchID = correlation.batchID;
6611
+ }
6612
+ return event;
6613
+ }
6541
6614
  async function runRoleTaskWithCorrelation(input, deps, correlation, batch) {
6542
6615
  if (getRoleProfile(input.roleID, input.config.orchestrator?.roles) === undefined) {
6543
6616
  warnUnknownRole(input.roleID, deps.warn, deps.warnedRoles);
@@ -6575,16 +6648,40 @@ async function runRoleTaskWithCorrelation(input, deps, correlation, batch) {
6575
6648
  const subsessionDeps = deps.timeoutMs === undefined ? {} : { timeoutMs: deps.timeoutMs };
6576
6649
  const runtimeId = selection.runtimeId;
6577
6650
  let release;
6651
+ let queueObservation;
6578
6652
  if (runtimeId !== undefined && deps.runtimeLimiter !== undefined) {
6579
- release = deps.runtimeLimiter.tryAcquire(runtimeId);
6653
+ const limiter = deps.runtimeLimiter;
6654
+ release = limiter.tryAcquire(runtimeId);
6655
+ let waitedMs = 0;
6580
6656
  if (release === undefined) {
6581
- release = await deps.runtimeLimiter.acquire(runtimeId);
6582
- }
6657
+ const acquisition = await limiter.acquire(runtimeId);
6658
+ release = acquisition.release;
6659
+ waitedMs = acquisition.waitedMs;
6660
+ }
6661
+ queueObservation = {
6662
+ runtimeId,
6663
+ waitedMs,
6664
+ activeCount: limiter.activeCount(runtimeId),
6665
+ queuedCount: limiter.queuedCount(runtimeId)
6666
+ };
6583
6667
  }
6584
6668
  try {
6585
6669
  const subsession = await runSubsession(deps.client, request, subsessionDeps);
6586
6670
  const record = buildRecord(input, decision2, subsession, deps, correlation.decisionID, correlation.batchID);
6587
6671
  await safeEmit(deps.sink, correlatedCostRecordToRouteEvent(record));
6672
+ if (queueObservation !== undefined) {
6673
+ const sessionID = recordSessionID(input, subsession);
6674
+ if (sessionID !== undefined) {
6675
+ const queueCorrelation = {
6676
+ decisionID: correlation.decisionID,
6677
+ agent: input.roleID
6678
+ };
6679
+ if (correlation.batchID !== undefined) {
6680
+ queueCorrelation.batchID = correlation.batchID;
6681
+ }
6682
+ await safeEmit(deps.sink, buildQueueEvent(queueObservation, sessionID, deps.now(), queueCorrelation));
6683
+ }
6684
+ }
6588
6685
  return { decision: decision2, subsession, record };
6589
6686
  } finally {
6590
6687
  release?.();
@@ -7505,6 +7602,60 @@ function createTelemetryEventSink(options, deps) {
7505
7602
  return { sink, resolution };
7506
7603
  }
7507
7604
 
7605
+ // src/telemetry/queueWait.ts
7606
+ function percentile(sortedAsc, p) {
7607
+ const rank = Math.ceil(p / 100 * sortedAsc.length);
7608
+ const index = Math.min(Math.max(rank, 1), sortedAsc.length) - 1;
7609
+ return sortedAsc[index] ?? 0;
7610
+ }
7611
+ function summarizeGroup(runtimeId, waits, maxQueued, maxActive) {
7612
+ const sorted = [...waits].sort((a, b) => a - b);
7613
+ return {
7614
+ runtimeId,
7615
+ samples: sorted.length,
7616
+ contended: sorted.filter((wait) => wait > 0).length,
7617
+ p50Ms: percentile(sorted, 50),
7618
+ p95Ms: percentile(sorted, 95),
7619
+ maxMs: sorted[sorted.length - 1] ?? 0,
7620
+ maxQueued,
7621
+ maxActive
7622
+ };
7623
+ }
7624
+ function summarizeQueueWaits(events) {
7625
+ if (events.length === 0) {
7626
+ return;
7627
+ }
7628
+ const allWaits = [];
7629
+ let overallMaxQueued = 0;
7630
+ let overallMaxActive = 0;
7631
+ const byRuntime = new Map;
7632
+ for (const event of events) {
7633
+ allWaits.push(event.waitedMs);
7634
+ overallMaxQueued = Math.max(overallMaxQueued, event.queuedCount);
7635
+ overallMaxActive = Math.max(overallMaxActive, event.activeCount);
7636
+ let group = byRuntime.get(event.runtimeId);
7637
+ if (group === undefined) {
7638
+ group = { waits: [], maxQueued: 0, maxActive: 0 };
7639
+ byRuntime.set(event.runtimeId, group);
7640
+ }
7641
+ group.waits.push(event.waitedMs);
7642
+ group.maxQueued = Math.max(group.maxQueued, event.queuedCount);
7643
+ group.maxActive = Math.max(group.maxActive, event.activeCount);
7644
+ }
7645
+ const overall = summarizeGroup("*", allWaits, overallMaxQueued, overallMaxActive);
7646
+ const perRuntime = [...byRuntime.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([runtimeId, group]) => summarizeGroup(runtimeId, group.waits, group.maxQueued, group.maxActive));
7647
+ return {
7648
+ samples: overall.samples,
7649
+ contended: overall.contended,
7650
+ p50Ms: overall.p50Ms,
7651
+ p95Ms: overall.p95Ms,
7652
+ maxMs: overall.maxMs,
7653
+ maxQueued: overall.maxQueued,
7654
+ maxActive: overall.maxActive,
7655
+ perRuntime
7656
+ };
7657
+ }
7658
+
7508
7659
  // src/web/paths.ts
7509
7660
  var DEFAULT_DECISIONS_PATH = ".opencode/openteam/decisions.md";
7510
7661
 
@@ -7982,6 +8133,24 @@ function isNetworkRuntime(baseURL) {
7982
8133
  return false;
7983
8134
  }
7984
8135
  }
8136
+ function queueWaitSection(view) {
8137
+ const lines = [" local queue wait (#144, per openteam process):"];
8138
+ const summary = view.summary;
8139
+ if (summary === undefined || summary.samples === 0) {
8140
+ lines.push(" (no queue samples recorded yet — run an orchestrated load, then re-run doctor)");
8141
+ return lines;
8142
+ }
8143
+ const mark = summary.contended > 0 ? "⚠" : "✓";
8144
+ lines.push(` ${mark} ${summary.samples} dispatch(es), ${summary.contended} queued — ` + `p50 ${summary.p50Ms}ms · p95 ${summary.p95Ms}ms · max ${summary.maxMs}ms`);
8145
+ lines.push(` peak depth: ${summary.maxActive} active · ${summary.maxQueued} queued`);
8146
+ for (const runtime of summary.perRuntime) {
8147
+ lines.push(` ${runtime.runtimeId.padEnd(14)} ${runtime.samples} sample(s), ` + `${runtime.contended} queued — p50 ${runtime.p50Ms}ms · p95 ${runtime.p95Ms}ms · ` + `max ${runtime.maxMs}ms · peak ${runtime.maxActive} active/${runtime.maxQueued} queued`);
8148
+ }
8149
+ if (summary.contended > 0) {
8150
+ lines.push(" Lower a runtime's maxConcurrency in .opencode/openteam.json to leave slots for interactive traffic.");
8151
+ }
8152
+ return lines;
8153
+ }
7985
8154
  function failureReasonText(reason, searchedPaths) {
7986
8155
  switch (reason) {
7987
8156
  case "provider-not-configured": {
@@ -8141,6 +8310,20 @@ function rosterHealthSection(audit, loadError, rosterRoleCount) {
8141
8310
  }
8142
8311
  return lines;
8143
8312
  }
8313
+ function gitignoreSection(gitignore) {
8314
+ if (gitignore.missingRules.length === 0) {
8315
+ return [
8316
+ " gitignore:",
8317
+ ` ✓ ${gitignore.path} ignores openteam's generated runtime state.`
8318
+ ];
8319
+ }
8320
+ const quoted = gitignore.missingRules.map((rule) => `\`${rule}\``).join(", ");
8321
+ const lines = [" gitignore:"];
8322
+ lines.push(gitignore.exists ? ` ✗ ${gitignore.path} is missing openteam ignore rule(s): ${gitignore.missingRules.join(", ")}` : ` ✗ ${gitignore.path} does not exist, so no openteam ignore rule(s) are in place: ${gitignore.missingRules.join(", ")}`);
8323
+ lines.push(" problem: openteam writes machine-local runtime output (telemetry, graph state, soak recordings) that must not be committed.");
8324
+ lines.push(` remedy: add ${quoted} to ${gitignore.path} (or re-run \`openteam setup\`), then re-run \`openteam doctor\`.`);
8325
+ return lines;
8326
+ }
8144
8327
  function agentModelsSection(diagnostics, searchedPaths) {
8145
8328
  const lines = [" agent models:"];
8146
8329
  if (diagnostics.length === 0) {
@@ -8184,10 +8367,16 @@ function renderDoctor(input) {
8184
8367
  lines.push(` ⚠ ${snapshot.id} is reached over the network: its ${cap}-slot cap is enforced per openteam process, so`, " two processes at once (an opencode session plus 'openteam console') can together exceed it.");
8185
8368
  }
8186
8369
  lines.push(` telemetry: ${input.telemetryPath} — ${input.telemetryRecords} record(s)`);
8370
+ if (input.queueWait !== undefined) {
8371
+ lines.push(...queueWaitSection(input.queueWait));
8372
+ }
8187
8373
  if (input.diagnostics !== undefined) {
8188
8374
  lines.push(...diagnosticsSection(input.diagnostics));
8189
8375
  }
8190
8376
  lines.push(...rosterHealthSection(input.rosterAudit, input.rosterLoadError, input.rosterRoleCount));
8377
+ if (input.gitignore !== undefined) {
8378
+ lines.push(...gitignoreSection(input.gitignore));
8379
+ }
8191
8380
  if (input.otelBackend !== undefined) {
8192
8381
  const otel = input.otelBackend;
8193
8382
  lines.push(" opentelemetry:");
@@ -8453,6 +8642,143 @@ function renderMigration(report) {
8453
8642
  // src/commands/purge.ts
8454
8643
  import * as path3 from "node:path";
8455
8644
 
8645
+ // src/storage/graph/soakRecorder.ts
8646
+ var SOAK_EVIDENCE_DIR = ".opencode/soak";
8647
+ var SOAK_RECORDER_BASE_DIR = ".opencode";
8648
+ var SOAK_RECORDER_ID_FILENAME = "soak-recorder-id";
8649
+ var SOAK_RECORDER_ID_PATH = `${SOAK_RECORDER_BASE_DIR}/${SOAK_RECORDER_ID_FILENAME}`;
8650
+ async function ensureRecorderId(ports, basePath) {
8651
+ const idPath = `${basePath}/${SOAK_RECORDER_ID_FILENAME}`;
8652
+ if (await ports.fs.exists(idPath)) {
8653
+ const id = (await ports.fs.readFile(idPath)).trim();
8654
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) {
8655
+ return id;
8656
+ }
8657
+ }
8658
+ const newId = ports.id.randomUUID();
8659
+ await ports.fs.writeFile(idPath, newId);
8660
+ return newId;
8661
+ }
8662
+ async function readAllChains(ports, evidenceDir) {
8663
+ if (!await ports.fs.exists(evidenceDir)) {
8664
+ return { chains: [], rejectedLines: 0, rejections: [] };
8665
+ }
8666
+ const files = await ports.fs.readDir(evidenceDir);
8667
+ const chains = [];
8668
+ const rejections = [];
8669
+ for (const file of files) {
8670
+ if (!file.endsWith(".jsonl")) {
8671
+ continue;
8672
+ }
8673
+ const content = await ports.fs.readFile(`${evidenceDir}/${file}`);
8674
+ const lines = content.split(`
8675
+ `);
8676
+ const observations = [];
8677
+ for (const [index, line] of lines.entries()) {
8678
+ if (line.trim().length === 0) {
8679
+ continue;
8680
+ }
8681
+ const lineNumber = index + 1;
8682
+ let rawParsed;
8683
+ try {
8684
+ rawParsed = JSON.parse(line);
8685
+ } catch {
8686
+ rejections.push({ file, lineNumber, detail: "invalid JSON" });
8687
+ continue;
8688
+ }
8689
+ const parsed = SoakObservationSchema.safeParse(rawParsed);
8690
+ if (parsed.success) {
8691
+ observations.push(parsed.data);
8692
+ } else {
8693
+ rejections.push({
8694
+ file,
8695
+ lineNumber,
8696
+ detail: formatSoakObservationIssues(parsed.error.issues)
8697
+ });
8698
+ }
8699
+ }
8700
+ if (observations.length > 0) {
8701
+ chains.push(observations);
8702
+ }
8703
+ }
8704
+ return { chains, rejectedLines: rejections.length, rejections };
8705
+ }
8706
+ function formatSoakObservationIssues(issues) {
8707
+ return issues.map((i) => `${i.path.join(".")}: ${i.code}`).join("; ");
8708
+ }
8709
+ function canonicalObservationPreimage2(obs) {
8710
+ return JSON.stringify({
8711
+ version: obs.version,
8712
+ seq: obs.seq,
8713
+ timestamp: obs.timestamp,
8714
+ platform: obs.platform,
8715
+ opencodeVersion: obs.opencodeVersion,
8716
+ provenance: obs.provenance,
8717
+ traceDigest: obs.traceDigest,
8718
+ criticalDivergences: obs.criticalDivergences,
8719
+ privacy: obs.privacy,
8720
+ duplicateEffects: obs.duplicateEffects,
8721
+ modelVerification: obs.modelVerification
8722
+ });
8723
+ }
8724
+ async function appendObservation(ports, evidenceDir, recorderId, input) {
8725
+ if (input.opencodeVersion === "unknown") {
8726
+ throw new Error("opencode-version-unreadable");
8727
+ }
8728
+ await ports.fs.mkdir(evidenceDir);
8729
+ const chainPath = `${evidenceDir}/${recorderId}.jsonl`;
8730
+ let seq = 0;
8731
+ let prevChainDigest = "0".repeat(64);
8732
+ if (await ports.fs.exists(chainPath)) {
8733
+ const content = await ports.fs.readFile(chainPath);
8734
+ const lines = content.split(`
8735
+ `).filter((l) => l.trim().length > 0);
8736
+ if (lines.length > 0) {
8737
+ const lastLine = lines[lines.length - 1];
8738
+ let rawParsed;
8739
+ try {
8740
+ rawParsed = JSON.parse(lastLine);
8741
+ } catch {
8742
+ throw new Error("corrupt chain tail: invalid JSON");
8743
+ }
8744
+ const parsed = SoakObservationSchema.safeParse(rawParsed);
8745
+ if (!parsed.success) {
8746
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.code}`).join("; ");
8747
+ throw new Error(`corrupt chain tail: ${detail}`);
8748
+ }
8749
+ seq = parsed.data.seq + 1;
8750
+ prevChainDigest = parsed.data.chainDigest;
8751
+ }
8752
+ }
8753
+ const timestamp = ports.clock.now();
8754
+ const base = {
8755
+ version: 1,
8756
+ seq,
8757
+ timestamp,
8758
+ platform: input.platform,
8759
+ opencodeVersion: input.opencodeVersion,
8760
+ provenance: input.provenance,
8761
+ traceDigest: input.traceDigest,
8762
+ criticalDivergences: input.criticalDivergences,
8763
+ privacy: input.privacy,
8764
+ duplicateEffects: input.duplicateEffects,
8765
+ modelVerification: input.modelVerification
8766
+ };
8767
+ const preimage = canonicalObservationPreimage2(base);
8768
+ const chainDigest = sha256Hex(prevChainDigest + preimage);
8769
+ const observation = { ...base, chainDigest };
8770
+ assertReferenceOnly(observation);
8771
+ const line = JSON.stringify(observation);
8772
+ if (await ports.fs.exists(chainPath)) {
8773
+ const existing = await ports.fs.readFile(chainPath);
8774
+ await ports.fs.writeFile(chainPath, `${existing}
8775
+ ${line}`);
8776
+ } else {
8777
+ await ports.fs.writeFile(chainPath, line);
8778
+ }
8779
+ return observation;
8780
+ }
8781
+
8456
8782
  // src/commands/setup.ts
8457
8783
  var OPENTEAM_PLUGIN_SPEC = "@jmanuelcorral/openteam";
8458
8784
  var OPENCODE_GITIGNORE_PATH = ".opencode/.gitignore";
@@ -8464,6 +8790,11 @@ var GENERATED_OPENTEAM_STATE_PATHS = [
8464
8790
  DEFAULT_STORAGE_SQLITE_INDEX_PATH,
8465
8791
  DEFAULT_MEMORY_INDEX_PATH
8466
8792
  ];
8793
+ var GENERATED_OPENTEAM_STATE_GROUPS = [
8794
+ { kind: "dir", paths: GENERATED_OPENTEAM_STATE_PATHS },
8795
+ { kind: "dir", paths: [SOAK_EVIDENCE_DIR] },
8796
+ { kind: "file", paths: [SOAK_RECORDER_ID_PATH] }
8797
+ ];
8467
8798
  var OPENCODE_DIR = ".opencode";
8468
8799
  var AGENT_DIR = `${OPENCODE_DIR}/agent`;
8469
8800
  function splitSetupPath(path3) {
@@ -8484,18 +8815,27 @@ function commonGeneratedStateRoot(paths) {
8484
8815
  }
8485
8816
  return common.join("/");
8486
8817
  }
8487
- function generatedStateIgnoreRule() {
8488
- const root = commonGeneratedStateRoot(GENERATED_OPENTEAM_STATE_PATHS);
8818
+ function generatedStateIgnoreRule(group) {
8819
+ const root = commonGeneratedStateRoot(group.paths);
8489
8820
  const opencodePrefix = `${OPENCODE_DIR}/`;
8490
8821
  if (!root.startsWith(opencodePrefix)) {
8491
8822
  throw new Error(`generated openteam state path is outside ${OPENCODE_DIR}`);
8492
8823
  }
8493
- return `${root.slice(opencodePrefix.length)}/`;
8824
+ const rule = root.slice(opencodePrefix.length);
8825
+ return group.kind === "dir" ? `${rule}/` : rule;
8494
8826
  }
8495
- var OPENCODE_GITIGNORE_GENERATED_STATE_RULES = [
8496
- generatedStateIgnoreRule()
8497
- ];
8827
+ var OPENCODE_GITIGNORE_GENERATED_STATE_RULES = GENERATED_OPENTEAM_STATE_GROUPS.map(generatedStateIgnoreRule);
8498
8828
  var LEGACY_OPENCODE_GITIGNORE_STATE_RULES = ["openteam/"];
8829
+ function normalizeGitignoreRule(line) {
8830
+ return line.trim().replace(/^\/+/, "");
8831
+ }
8832
+ function activeGitignoreRules(gitignore) {
8833
+ return gitignore.split(/\r?\n/).map(normalizeGitignoreRule).filter((line) => line.length > 0 && !line.startsWith("#"));
8834
+ }
8835
+ function missingGeneratedStateRules(gitignore) {
8836
+ const present = new Set(activeGitignoreRules(gitignore));
8837
+ return OPENCODE_GITIGNORE_GENERATED_STATE_RULES.filter((rule) => !present.has(rule));
8838
+ }
8499
8839
 
8500
8840
  // src/commands/purge.ts
8501
8841
  var PURGE_CATEGORY_IDS = [
@@ -8543,7 +8883,7 @@ function resolveStaticPurgeItems(config, paths) {
8543
8883
  ...paths.opencodeConfigPaths.map((p) => ({ kind: "opencode-config", path: p })),
8544
8884
  { kind: "gitignore", path: paths.gitignorePath }
8545
8885
  ],
8546
- roster: [],
8886
+ roster: [{ kind: "file", path: OPENTEAM_ROSTER_PATH }],
8547
8887
  decisions: [
8548
8888
  { kind: "file", path: paths.decisionsPath },
8549
8889
  { kind: "file", path: paths.telemetryPath }
@@ -8835,8 +9175,12 @@ function categoryPrompt(id, items) {
8835
9175
  return "Remove openteam configuration and wiring?";
8836
9176
  case "roster": {
8837
9177
  const names = items.filter((item) => item.kind === "agent").map((item) => item.name);
8838
- const listed = names.join(", ");
8839
- return `Remove the agent roster? (${names.length} user agent${names.length === 1 ? "" : "s"}: ${listed})`;
9178
+ const hasRegistry = items.some((item) => item.kind === "file" && item.path === OPENTEAM_ROSTER_PATH);
9179
+ const plural = names.length === 1 ? "" : "s";
9180
+ const agentNames = names.length > 0 ? `: ${names.join(", ")}` : "";
9181
+ const agentClause = `${names.length} user agent${plural}${agentNames}`;
9182
+ const parts = hasRegistry ? [`roster registry (${OPENTEAM_ROSTER_PATH})`, agentClause] : [agentClause];
9183
+ return `Remove the agent roster? (${parts.join("; ")})`;
8840
9184
  }
8841
9185
  case "decisions":
8842
9186
  return "Remove decisions and telemetry?";
@@ -8898,7 +9242,9 @@ async function collectCategories(input) {
8898
9242
  const { runtime, config, paths } = input;
8899
9243
  const staticItems = resolveStaticPurgeItems(config, paths);
8900
9244
  const agents = await runtime.port.listAgents();
8901
- const rosterItems = agents.filter((agent) => path3.resolve(agent.path) !== path3.resolve(paths.orchestratorAgentPath)).map((agent) => ({ kind: "agent", path: agent.path, name: agent.name }));
9245
+ const discoveredAgents = agents.filter((agent) => path3.resolve(agent.path) !== path3.resolve(paths.orchestratorAgentPath)).map((agent) => ({ kind: "agent", path: agent.path, name: agent.name }));
9246
+ const rosterRegistry = await presentItemsFor(runtime.port, staticItems.roster);
9247
+ const rosterItems = [...rosterRegistry, ...discoveredAgents];
8902
9248
  const worktrees = await runtime.port.listWorktrees();
8903
9249
  const worktreeItems = worktrees.map((worktree) => ({
8904
9250
  kind: "worktree",
@@ -10051,6 +10397,15 @@ async function runCli(argv, deps) {
10051
10397
  ...defaultModel !== undefined ? { defaultModel } : {}
10052
10398
  });
10053
10399
  const legacyLayout = deps.detectLegacyLayout !== undefined ? await deps.detectLegacyLayout() : undefined;
10400
+ let gitignoreCoverage;
10401
+ if (deps.readGitignore !== undefined) {
10402
+ const raw = await deps.readGitignore();
10403
+ gitignoreCoverage = {
10404
+ path: OPENCODE_GITIGNORE_PATH,
10405
+ exists: raw !== undefined,
10406
+ missingRules: missingGeneratedStateRules(raw ?? "")
10407
+ };
10408
+ }
10054
10409
  const otelBackend = describeOtelBackendForDoctor({
10055
10410
  backend: "opentelemetry",
10056
10411
  connectionEnv: "APPLICATIONINSIGHTS_CONNECTION_STRING"
@@ -10067,6 +10422,12 @@ async function runCli(argv, deps) {
10067
10422
  rosterLoadError = err instanceof Error ? err.message : String(err);
10068
10423
  }
10069
10424
  }
10425
+ let queueWait;
10426
+ if (deps.readQueueEvents !== undefined) {
10427
+ const queueEvents = await deps.readQueueEvents();
10428
+ const summary = summarizeQueueWaits(queueEvents);
10429
+ queueWait = summary !== undefined ? { summary } : {};
10430
+ }
10070
10431
  return {
10071
10432
  exitCode: 0,
10072
10433
  stdout: renderDoctor({
@@ -10076,6 +10437,7 @@ async function runCli(argv, deps) {
10076
10437
  telemetryRecords: records.length,
10077
10438
  agentModels,
10078
10439
  opencodeConfigPaths,
10440
+ ...queueWait !== undefined ? { queueWait } : {},
10079
10441
  ...rosterAudit !== undefined ? { rosterAudit } : {},
10080
10442
  ...rosterRoleCount !== undefined ? { rosterRoleCount } : {},
10081
10443
  ...rosterLoadError !== undefined ? { rosterLoadError } : {},
@@ -10083,7 +10445,8 @@ async function runCli(argv, deps) {
10083
10445
  ...opencodeConfigError !== undefined ? { opencodeConfigError } : {},
10084
10446
  ...cacheEntries !== undefined ? { cacheEntries, cliVersion: deps.version } : {},
10085
10447
  ...legacyLayout !== undefined && legacyLayout.length > 0 ? { legacyLayout } : {},
10086
- ...otelBackend !== undefined ? { otelBackend } : {}
10448
+ ...otelBackend !== undefined ? { otelBackend } : {},
10449
+ ...gitignoreCoverage !== undefined ? { gitignore: gitignoreCoverage } : {}
10087
10450
  })
10088
10451
  };
10089
10452
  }
@@ -10759,139 +11122,6 @@ function compareShadowRun(legacy, shadow) {
10759
11122
  };
10760
11123
  }
10761
11124
 
10762
- // src/storage/graph/soakRecorder.ts
10763
- async function ensureRecorderId(ports, basePath) {
10764
- const idPath = `${basePath}/soak-recorder-id`;
10765
- if (await ports.fs.exists(idPath)) {
10766
- const id = (await ports.fs.readFile(idPath)).trim();
10767
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) {
10768
- return id;
10769
- }
10770
- }
10771
- const newId = ports.id.randomUUID();
10772
- await ports.fs.writeFile(idPath, newId);
10773
- return newId;
10774
- }
10775
- async function readAllChains(ports, evidenceDir) {
10776
- if (!await ports.fs.exists(evidenceDir)) {
10777
- return { chains: [], rejectedLines: 0, rejections: [] };
10778
- }
10779
- const files = await ports.fs.readDir(evidenceDir);
10780
- const chains = [];
10781
- const rejections = [];
10782
- for (const file of files) {
10783
- if (!file.endsWith(".jsonl")) {
10784
- continue;
10785
- }
10786
- const content = await ports.fs.readFile(`${evidenceDir}/${file}`);
10787
- const lines = content.split(`
10788
- `);
10789
- const observations = [];
10790
- for (const [index, line] of lines.entries()) {
10791
- if (line.trim().length === 0) {
10792
- continue;
10793
- }
10794
- const lineNumber = index + 1;
10795
- let rawParsed;
10796
- try {
10797
- rawParsed = JSON.parse(line);
10798
- } catch {
10799
- rejections.push({ file, lineNumber, detail: "invalid JSON" });
10800
- continue;
10801
- }
10802
- const parsed = SoakObservationSchema.safeParse(rawParsed);
10803
- if (parsed.success) {
10804
- observations.push(parsed.data);
10805
- } else {
10806
- rejections.push({
10807
- file,
10808
- lineNumber,
10809
- detail: formatSoakObservationIssues(parsed.error.issues)
10810
- });
10811
- }
10812
- }
10813
- if (observations.length > 0) {
10814
- chains.push(observations);
10815
- }
10816
- }
10817
- return { chains, rejectedLines: rejections.length, rejections };
10818
- }
10819
- function formatSoakObservationIssues(issues) {
10820
- return issues.map((i) => `${i.path.join(".")}: ${i.code}`).join("; ");
10821
- }
10822
- function canonicalObservationPreimage2(obs) {
10823
- return JSON.stringify({
10824
- version: obs.version,
10825
- seq: obs.seq,
10826
- timestamp: obs.timestamp,
10827
- platform: obs.platform,
10828
- opencodeVersion: obs.opencodeVersion,
10829
- provenance: obs.provenance,
10830
- traceDigest: obs.traceDigest,
10831
- criticalDivergences: obs.criticalDivergences,
10832
- privacy: obs.privacy,
10833
- duplicateEffects: obs.duplicateEffects,
10834
- modelVerification: obs.modelVerification
10835
- });
10836
- }
10837
- async function appendObservation(ports, evidenceDir, recorderId, input) {
10838
- if (input.opencodeVersion === "unknown") {
10839
- throw new Error("opencode-version-unreadable");
10840
- }
10841
- await ports.fs.mkdir(evidenceDir);
10842
- const chainPath = `${evidenceDir}/${recorderId}.jsonl`;
10843
- let seq = 0;
10844
- let prevChainDigest = "0".repeat(64);
10845
- if (await ports.fs.exists(chainPath)) {
10846
- const content = await ports.fs.readFile(chainPath);
10847
- const lines = content.split(`
10848
- `).filter((l) => l.trim().length > 0);
10849
- if (lines.length > 0) {
10850
- const lastLine = lines[lines.length - 1];
10851
- let rawParsed;
10852
- try {
10853
- rawParsed = JSON.parse(lastLine);
10854
- } catch {
10855
- throw new Error("corrupt chain tail: invalid JSON");
10856
- }
10857
- const parsed = SoakObservationSchema.safeParse(rawParsed);
10858
- if (!parsed.success) {
10859
- const detail = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.code}`).join("; ");
10860
- throw new Error(`corrupt chain tail: ${detail}`);
10861
- }
10862
- seq = parsed.data.seq + 1;
10863
- prevChainDigest = parsed.data.chainDigest;
10864
- }
10865
- }
10866
- const timestamp = ports.clock.now();
10867
- const base = {
10868
- version: 1,
10869
- seq,
10870
- timestamp,
10871
- platform: input.platform,
10872
- opencodeVersion: input.opencodeVersion,
10873
- provenance: input.provenance,
10874
- traceDigest: input.traceDigest,
10875
- criticalDivergences: input.criticalDivergences,
10876
- privacy: input.privacy,
10877
- duplicateEffects: input.duplicateEffects,
10878
- modelVerification: input.modelVerification
10879
- };
10880
- const preimage = canonicalObservationPreimage2(base);
10881
- const chainDigest = sha256Hex(prevChainDigest + preimage);
10882
- const observation = { ...base, chainDigest };
10883
- assertReferenceOnly(observation);
10884
- const line = JSON.stringify(observation);
10885
- if (await ports.fs.exists(chainPath)) {
10886
- const existing = await ports.fs.readFile(chainPath);
10887
- await ports.fs.writeFile(chainPath, `${existing}
10888
- ${line}`);
10889
- } else {
10890
- await ports.fs.writeFile(chainPath, line);
10891
- }
10892
- return observation;
10893
- }
10894
-
10895
11125
  // src/plugin/soakObserver.ts
10896
11126
  function resolvePlatform(platform) {
10897
11127
  if (platform === "win32")
@@ -13036,7 +13266,7 @@ var createOtlpSpanExporter = (connection, config) => {
13036
13266
  // package.json
13037
13267
  var package_default = {
13038
13268
  name: "@jmanuelcorral/openteam",
13039
- version: "0.13.0",
13269
+ version: "0.14.0",
13040
13270
  packageManager: "bun@1.3.14",
13041
13271
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
13042
13272
  license: "MIT",
@@ -13212,7 +13442,6 @@ function warnSoakEvidenceRejections(result, warn) {
13212
13442
  var SHADOW_CERT_PATH = "artifacts/graph-shadow-certificate.json";
13213
13443
  var RELEASE_CERT_PATH = "artifacts/graph-release-certificate.json";
13214
13444
  var SOAK_CERT_PATH = "artifacts/graph-soak-certificate.json";
13215
- var SOAK_EVIDENCE_DIR = ".opencode/soak";
13216
13445
  async function readAndParseCertificate(storage, path4, parse2, digestExtractor) {
13217
13446
  try {
13218
13447
  const content = await storage.read(path4);
@@ -13735,14 +13964,14 @@ var server = async (ctx, rawOptions) => {
13735
13964
  let shadowDeps;
13736
13965
  if (graphConfig.effectiveMode !== "off") {
13737
13966
  const recorderPorts = recorderPortsFromStorage(storage);
13738
- const recorderId = await ensureRecorderId(recorderPorts, ".opencode");
13967
+ const recorderId = await ensureRecorderId(recorderPorts, SOAK_RECORDER_BASE_DIR);
13739
13968
  shadowDeps = {
13740
13969
  gate: {
13741
13970
  mode: graphConfig.configuredMode,
13742
13971
  killSwitch: graphConfig.killSwitch
13743
13972
  },
13744
13973
  ports: recorderPorts,
13745
- evidenceDir: ".opencode/soak",
13974
+ evidenceDir: SOAK_EVIDENCE_DIR,
13746
13975
  recorderId,
13747
13976
  resolveOpencodeVersion: getOpencodeVersion,
13748
13977
  criticalCodes: DEFAULT_SOAK_POLICY.criticalDivergenceCodes
@@ -13777,7 +14006,8 @@ var server = async (ctx, rawOptions) => {
13777
14006
  return reachableRuntimeIds(snapshots);
13778
14007
  };
13779
14008
  const runtimeLimiter = createRuntimeConcurrencyLimiter({
13780
- capacityFor: (id) => config.local.runtimes.find((runtime) => runtime.id === id)?.maxConcurrency ?? DEFAULT_LOCAL_MAX_CONCURRENCY
14009
+ capacityFor: (id) => config.local.runtimes.find((runtime) => runtime.id === id)?.maxConcurrency ?? DEFAULT_LOCAL_MAX_CONCURRENCY,
14010
+ now
13781
14011
  });
13782
14012
  const orchestrateTool = createOrchestrateTool({
13783
14013
  client: orchestrateClient,