@jmanuelcorral/openteam 0.23.0 → 0.24.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.
Files changed (33) hide show
  1. package/README.es.md +22 -8
  2. package/README.md +19 -7
  3. package/dist/certificates/graph-release-certificate.json +2 -2
  4. package/dist/certificates/graph-shadow-certificate.json +2 -2
  5. package/dist/cli.js +131 -84
  6. package/dist/commands/setup.d.ts +0 -19
  7. package/dist/commands/setup.d.ts.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +508 -54
  10. package/dist/messages/executionSetup.d.ts +0 -2
  11. package/dist/messages/executionSetup.d.ts.map +1 -1
  12. package/dist/messages/orchestration.d.ts +1 -0
  13. package/dist/messages/orchestration.d.ts.map +1 -1
  14. package/dist/messages/reload.d.ts +21 -0
  15. package/dist/messages/reload.d.ts.map +1 -0
  16. package/dist/opencodeArtifacts/createRosterReload.d.ts +2 -0
  17. package/dist/opencodeArtifacts/createRosterReload.d.ts.map +1 -0
  18. package/dist/opencodeArtifacts/orchestratorAgent.d.ts.map +1 -1
  19. package/dist/opencodeArtifacts/slashCommand.d.ts.map +1 -1
  20. package/dist/opencodeArtifacts/teamPreparation.d.ts.map +1 -1
  21. package/dist/orchestrator/roster.d.ts +0 -7
  22. package/dist/orchestrator/roster.d.ts.map +1 -1
  23. package/dist/orchestrator/sessionStatus.d.ts +12 -0
  24. package/dist/orchestrator/sessionStatus.d.ts.map +1 -0
  25. package/dist/orchestrator/subsessions.d.ts +2 -10
  26. package/dist/orchestrator/subsessions.d.ts.map +1 -1
  27. package/dist/plugin/reloadTool.d.ts +33 -0
  28. package/dist/plugin/reloadTool.d.ts.map +1 -0
  29. package/dist/telemetry/diagnostics.d.ts +1 -1
  30. package/dist/telemetry/diagnostics.d.ts.map +1 -1
  31. package/dist/telemetry/events.d.ts +24 -0
  32. package/dist/telemetry/events.d.ts.map +1 -1
  33. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4103,6 +4103,28 @@ import { z as z13 } from "zod";
4103
4103
  // src/telemetry/events.ts
4104
4104
  import { z as z11 } from "zod";
4105
4105
 
4106
+ // src/messages/reload.ts
4107
+ var reloadMessages = {
4108
+ description: "Queue a project-instance reload only after the user explicitly answers the fresh /create_roster reload question affirmatively. General tool approval, YOLO and earlier project-description answers are not consent. End this turn after queuing; do not dispatch work. Cancel withdraws a pending request.",
4109
+ action: "Request a confirmed reload or cancel this session's queued request.",
4110
+ confirmed: "True only for an unambiguous affirmative human answer to the current reload question, after successful preparation and the full Agent/Role/Model and file summaries. This field records consent; it does not independently prove a human answered.",
4111
+ invalid: "Reload not queued: provide only action (request or cancel) and explicit boolean confirmed. No target directory or other arguments are accepted.",
4112
+ declined: "Reload not queued: explicit affirmative consent to the current reload question is required. No files or policies were changed.",
4113
+ denied: "Reload is restricted to the openteam primary in this plugin's project.",
4114
+ closed: "This reload controller is no longer active. Check the current opencode instance before requesting another reload; agent discovery has not been verified.",
4115
+ other: "A reload request is already pending for another session. This call does not replace or extend its confirmation.",
4116
+ authorizing: "Host permission for this project's reload is still pending. This call queues nothing and does not extend the confirmation deadline.",
4117
+ none: "There is no queued reload for this session to cancel.",
4118
+ "reload-queued": "Reload queued, not completed. Waiting up to 30 seconds for this tool and confirming turn to finish and for all project sessions to be idle. End this turn now; no project work or agent readiness is implied.",
4119
+ "reload-waiting": "Reload is still waiting: project sessions are busy or retrying. They will not be aborted; the original 30-second deadline is unchanged.",
4120
+ "reload-accepted": "opencode accepted the project-instance reload request. Agent discovery and readiness have not been verified; existing discovery safeguards still apply.",
4121
+ "reload-cancelled": "The pending reload was cancelled before disposal was requested. Prepared files, history and policies are unchanged.",
4122
+ "reload-timeout": "Reload was not requested: the 30-second idle deadline expired. A new explicit confirmation is required; nothing will retry automatically.",
4123
+ "reload-status-failed": "Reload was not requested: project activity was missing, malformed, unavailable or did not respond within 5 seconds. No idle state was assumed and no automatic retry is armed.",
4124
+ "reload-dispose-failed": "opencode did not confirm acceptance of the reload request. No automatic retry is armed; check the current instance before trying again. Agent readiness is unverified.",
4125
+ "reload-unconfirmed": "Reload acceptance is unconfirmed because the request timed out, was cancelled or the instance was disposed while it was in flight. The host may still finish it; no automatic retry is armed. Check the current instance and agent discovery."
4126
+ };
4127
+
4106
4128
  // src/telemetry/diagnostics.ts
4107
4129
  var DIAGNOSTIC_CODES = [
4108
4130
  "availability-refresh-failed",
@@ -4115,6 +4137,14 @@ var DIAGNOSTIC_CODES = [
4115
4137
  "opencode-server-url",
4116
4138
  "opencode-version-read-failed",
4117
4139
  "privacy-inert",
4140
+ "reload-queued",
4141
+ "reload-waiting",
4142
+ "reload-accepted",
4143
+ "reload-cancelled",
4144
+ "reload-timeout",
4145
+ "reload-status-failed",
4146
+ "reload-dispose-failed",
4147
+ "reload-unconfirmed",
4118
4148
  "soak-evidence-rejected",
4119
4149
  "telemetry-warning",
4120
4150
  "telemetry-write-failed",
@@ -4142,6 +4172,15 @@ function diagnosticDescription(code) {
4142
4172
  return "openteam could not read the live opencode version; graph gates fail closed when this happens.";
4143
4173
  case "privacy-inert":
4144
4174
  return routingMessages.privacyModeDeprecated;
4175
+ case "reload-queued":
4176
+ case "reload-waiting":
4177
+ case "reload-accepted":
4178
+ case "reload-cancelled":
4179
+ case "reload-timeout":
4180
+ case "reload-status-failed":
4181
+ case "reload-dispose-failed":
4182
+ case "reload-unconfirmed":
4183
+ return reloadMessages[code];
4145
4184
  case "soak-evidence-rejected":
4146
4185
  return "Graph soak evidence contained rejected observation lines; cutover gate evidence may be incomplete.";
4147
4186
  case "telemetry-warning":
@@ -4894,6 +4933,48 @@ function withDeterministicGitLocale(env) {
4894
4933
  };
4895
4934
  }
4896
4935
 
4936
+ // src/opencodeArtifacts/createRosterReload.ts
4937
+ var CREATE_ROSTER_RELOAD_INSTRUCTIONS = [
4938
+ "## Reload confirmation — /create_roster only",
4939
+ "",
4940
+ "Only after successful persisted registration and verified agent files may you offer reload:",
4941
+ "read back the parseable persisted roster, including guaranteed roles; actually create each missing",
4942
+ "subagent file and verify every required existing or created file.",
4943
+ "First show the complete Agent/Role/Model table returned by `openteam-register-cast`,",
4944
+ "including primary, guaranteed and preserved agents with their unchanged tool-returned policy values.",
4945
+ "Then show the created-versus-preserved file summary and any warnings. Both must be visible",
4946
+ "BEFORE asking a fresh explicit reload question in this opencode conversation.",
4947
+ "If registration, file creation or verification failed or is partial, do not ask the reload question:",
4948
+ "report the exact blocker and do not claim readiness. If registration succeeded, still show its",
4949
+ "complete table even when file preparation later fails.",
4950
+ "",
4951
+ "Ask whether to reload this project's opencode agent definitions now, explaining the request is",
4952
+ "queued/waiting until the requesting turn completes and all project sessions are idle.",
4953
+ "Use the host question tool when available; otherwise ask a normal conversational question",
4954
+ "in opencode and WAIT for the user's answer. Never use a terminal wizard, tool permission",
4955
+ "approval or YOLO permission as reload consent. A greenfield project-description reply is NOT reload consent.",
4956
+ "Only an explicit affirmative answer to this specific reload question authorizes",
4957
+ '`openteam-reload` with `{ action: "request", confirmed: true }`.',
4958
+ "False, no, cancel, an ambiguous answer or no reply means no reload request.",
4959
+ "A decline before anything is queued needs no tool call; do not turn ambiguity into confirmation.",
4960
+ "To cancel an already queued request at the user's direction, use",
4961
+ '`openteam-reload` with `{ action: "cancel", confirmed: false }` and report its actual result.',
4962
+ "",
4963
+ "This internal tool is restricted to the product primary `openteam` and its bound project.",
4964
+ "Do not supply a directory, project, session or other model-selected target.",
4965
+ "Report queued/waiting honestly and end the confirming turn immediately.",
4966
+ "Do not await your own idle state, poll or hold the turn open. Do not start work or call",
4967
+ "`task` or `openteam-orchestrate`; do not rewrite/recast files or change models, permissions",
4968
+ "or execution policies. The idle wait has a finite timeout; surface timeout and failure results",
4969
+ "when the runtime reports them, including rejected requests, rather than promising success or silently retrying.",
4970
+ "SDK acceptance is not verified discovery or readiness. Preserve all existing discovery guards:",
4971
+ "if new agents remain undiscovered, surface the reload/discovery blocker;",
4972
+ "do not dispatch through a generic substitute or claim readiness.",
4973
+ "This workflow and all its replies remain composition only. After completion, decline or cancellation,",
4974
+ "wait for a separate work request; neither a reload answer nor queued acceptance starts project work."
4975
+ ].join(`
4976
+ `);
4977
+
4897
4978
  // src/opencodeArtifacts/rosterSummary.ts
4898
4979
  var ROSTER_SUMMARY_INSTRUCTIONS = [
4899
4980
  "After every successful cast creation, repair or extension, show the complete Agent/Role/Model table",
@@ -4926,6 +5007,20 @@ var TEAM_PREPARATION_INSTRUCTIONS = [
4926
5007
  "machine bindings stay unchanged; do not require a terminal or manually edit policy to finish hiring.",
4927
5008
  "Never infer execution domains from prose, relax permissions, or invent a concrete auto model.",
4928
5009
  "Registration/review is idempotent: a capable configured team needs no extra members or file rewrites.",
5010
+ "`agentName`, not `roleID`, determines each worker filename.",
5011
+ "Create `.opencode/agent/<agentName>.md` only for registered roster entries.",
5012
+ "A built-in role profile is not an extra agent.",
5013
+ "Do not create a parallel `<roleID>.md` when the roster maps that role to another alias.",
5014
+ "An existing roster alias equal to its roleID is still valid; do not rename it.",
5015
+ "Do not run `openteam setup` to hire workers or repair their files.",
5016
+ "Preserve unrelated and legacy generic files; do not delete, overwrite or silently register them.",
5017
+ "Registration writes roster/profile state, not worker Markdown.",
5018
+ "Report only verified file changes; do not call pre-existing files newly created.",
5019
+ "When creating a scribe worker, put the following memory instructions in its charter:",
5020
+ "Use `openteam-memory` for durable, reusable facts, entities and typed relations.",
5021
+ "Send only redacted knowledge messages, never raw conversations, prompts or credentials.",
5022
+ "Honor operator permissions and memory settings; do not broaden them.",
5023
+ "A disabled-memory or nothing-recorded result is expected; do not retry it.",
4929
5024
  "Create only missing agent definitions; preserve customized files. Show the registration table",
4930
5025
  "and verify preparation before dispatch. If opencode cannot discover a new agent, surface",
4931
5026
  "the reload/discovery blocker; do not dispatch through a generic substitute or claim readiness."
@@ -4967,33 +5062,43 @@ function buildOrchestratorAgent(model, options = {}) {
4967
5062
  "primary agent: there is no pre-created team. You **build the team of",
4968
5063
  "subagents on demand** according to the goal of the user and the repository.",
4969
5064
  "",
4970
- "## Golden rule: check the team on EVERY request",
4971
- "",
4972
- "Before answering or delegating **any** request, your **first action",
4973
- "always** is to check whether a team already exists, even if the user does not",
4974
- "explicitly ask you to create it. Never assume there is a team or that there is not:",
4975
- `verify it by looking at \`${OPENTEAM_ROSTER_PATH}\` and the \`.opencode/agent/*.md\``,
4976
- "files with `mode: subagent` (use `read`/`glob`/`list`).",
4977
- "",
4978
- ROSTER_SUMMARY_INSTRUCTIONS,
4979
- "",
4980
- TEAM_PREPARATION_INSTRUCTIONS,
4981
- "",
4982
5065
  "## Roster composition takes precedence",
4983
5066
  "",
4984
5067
  "For `/create_roster`, including replies to its pending questions, follow",
4985
5068
  "the command's roster-only workflow: do not distribute tasks or start implementation.",
5069
+ "These rules override the ordinary golden rule and capability checks below.",
5070
+ "A bare yes to a pending reload question continues only reload confirmation:",
5071
+ "do not restart roster reads, capability preparation or registration.",
4986
5072
  "Reuse or conservatively extend the existing cast and preserve customized agent files.",
4987
5073
  "Use a supplied description; otherwise inspect real project source read-only.",
4988
5074
  "If there is no meaningful source and no description, ask what the user wants to",
4989
5075
  "build in this conversation and wait before any writes or cast registration.",
4990
5076
  "Register through `openteam-register-cast`, create missing subagent files, verify",
4991
- "the persisted roster and files, then stop. Do not call `openteam-orchestrate` or",
5077
+ "the persisted roster and files, then follow the reload confirmation below.",
5078
+ "Do not call `openteam-orchestrate` or",
4992
5079
  "`task` for this workflow. A later answer to your question continues composition",
4993
5080
  "only until it completes or the user cancels; it is not an implementation request.",
4994
5081
  "",
5082
+ CREATE_ROSTER_RELOAD_INSTRUCTIONS,
5083
+ "",
5084
+ "## Golden rule: check the team on EVERY request",
5085
+ "",
5086
+ "The roster composition and pending reload-reply rules above take precedence.",
5087
+ "Before answering or delegating **any ordinary work** request, your **first action",
5088
+ "always** is to check whether a team already exists, even if the user does not",
5089
+ "explicitly ask you to create it. Never assume there is a team or that there is not:",
5090
+ `verify it by looking at \`${OPENTEAM_ROSTER_PATH}\` and the \`.opencode/agent/*.md\``,
5091
+ "files with `mode: subagent` (use `read`/`glob`/`list`).",
5092
+ "",
5093
+ ROSTER_SUMMARY_INSTRUCTIONS,
5094
+ "",
5095
+ TEAM_PREPARATION_INSTRUCTIONS,
5096
+ "",
4995
5097
  "## For work requests, including analysis, review and documentation",
4996
5098
  "",
5099
+ "Ordinary automatic hiring must NOT ask for reload or automatically reset the project.",
5100
+ "If discovery is blocked, surface the blocker and preserve the discovery guards instead.",
5101
+ "",
4997
5102
  "- **If the team ALREADY exists** → review capability coverage, reuse suitable",
4998
5103
  " members and extend only for uncovered needs. Complete any existing member's",
4999
5104
  " profile through `openteam-register-cast`; do not hire for a configuration gap.",
@@ -5368,8 +5473,10 @@ function buildCreateRosterCommand() {
5368
5473
  "required agent file exists. If anything failed, report the incomplete state and exact",
5369
5474
  "blocker; do not delete preserved state or claim the team is ready.",
5370
5475
  "Summarize the cast, role rationale/evidence, preserved versus newly created files, and",
5371
- "any warnings. New subagents may require an opencode reload before discovery.",
5372
- "Stop after composition; wait for a separate request before starting project work.",
5476
+ "any warnings. Continue only to the reload confirmation below, including its pending replies;",
5477
+ "do not restart preparation when the user answers that reload question.",
5478
+ "",
5479
+ CREATE_ROSTER_RELOAD_INSTRUCTIONS,
5373
5480
  "",
5374
5481
  "## Project description (user input)",
5375
5482
  "",
@@ -5636,7 +5743,7 @@ import { z as z15 } from "zod";
5636
5743
  // package.json
5637
5744
  var package_default = {
5638
5745
  name: "@jmanuelcorral/openteam",
5639
- version: "0.23.0",
5746
+ version: "0.24.0",
5640
5747
  packageManager: "bun@1.3.14",
5641
5748
  description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
5642
5749
  license: "MIT",
@@ -6015,12 +6122,14 @@ var executionSetupMessages = {
6015
6122
  `Local runtimes: ${params.localRuntimes}`,
6016
6123
  `YOLO mode: ${params.yolo ? "enabled (auto-approves permissions)" : "disabled"}`,
6017
6124
  "Web Console: launched separately with 'openteam console' (multi-session, loopback)",
6018
- `Wrote: ${params.opencodePath}, ${params.configPath}, ${params.agentPath}, ${params.agentDir}/*.md (${params.roleAgentCount} standard subagents), ${params.commandDir}/*.md (${params.commandCount} commands); ensured ${params.gitignorePath}`,
6125
+ `Wrote: ${params.opencodePath}, ${params.configPath}, ${params.agentPath}, ${params.commandDir}/*.md (${params.commandCount} commands); ensured ${params.gitignorePath}`,
6126
+ "Worker files are created through conversational roster preparation, not setup; existing worker files were left untouched.",
6019
6127
  "",
6020
6128
  "Next steps:",
6021
6129
  params.executionMode === "local" ? " 1. No frontier authentication is required while local execution is active" : " 1. Authenticate the frontier provider: opencode auth login",
6022
6130
  params.executionMode === "local" ? executionSetupMessages.localRuntimeNextStep : params.executionMode === "mixed" ? executionSetupMessages.mixedNextStep : executionSetupMessages.frontierNextStep,
6023
- " 3. Press Tab and pick the 'openteam' agent, or type / and pick an /openteam… command"
6131
+ " 3. Press Tab and pick the 'openteam' agent, or type / and pick an /openteam… command",
6132
+ " 4. Use /create_roster to compose the team without starting project work, or describe a task to prepare the team and begin work"
6024
6133
  ],
6025
6134
  configuredTitle: "openteam configured",
6026
6135
  complete: (paths) => `openteam setup complete: ${paths}`,
@@ -10294,8 +10403,11 @@ var UNAUTHORIZED_MODEL_SWITCH_REASON = "routing-model-switch-not-authorized";
10294
10403
  function invalidRoleIdentifier(roleID) {
10295
10404
  return `invalid role identifier "${roleID}" — use letters, numbers, underscores, or hyphens, starting with a letter or number`;
10296
10405
  }
10406
+ function agentPreparationRemedy(agentName) {
10407
+ return "Use `/create_roster` in opencode to complete the missing roster agent " + "definitions without changing existing aliases. " + `If \`.opencode/agent/${agentName}.md\` already exists, reload opencode ` + "to discover it before retrying; do not substitute an unrelated agent.";
10408
+ }
10297
10409
  function missingAgentDefinition(agentName, roleID) {
10298
- return `agent definition "${agentName}" is not installed for role "${roleID}". ` + `Create .opencode/agent/${agentName}.md or choose an installed builtin agent before retrying.`;
10410
+ return `agent definition "${agentName}" is not installed for role "${roleID}". ` + agentPreparationRemedy(agentName);
10299
10411
  }
10300
10412
  function dependencyValidationError(code, detail) {
10301
10413
  return `dependency validation error: ${code} (${detail})`;
@@ -10548,6 +10660,50 @@ function lifecycleRecordingAdvisory(status) {
10548
10660
  return `Lifecycle history recording is ${status.state}${failure}; model execution continued without claiming complete history.`;
10549
10661
  }
10550
10662
 
10663
+ // src/orchestrator/sessionStatus.ts
10664
+ function dataProperties(value) {
10665
+ if (typeof value !== "object" || value === null || Array.isArray(value))
10666
+ return;
10667
+ try {
10668
+ const prototype = Object.getPrototypeOf(value);
10669
+ if (prototype !== Object.prototype && prototype !== null)
10670
+ return;
10671
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string"))
10672
+ return;
10673
+ const properties = Object.getOwnPropertyDescriptors(value);
10674
+ return Object.values(properties).every((property) => ("value" in property) && property.enumerable === true) ? properties : undefined;
10675
+ } catch {
10676
+ return;
10677
+ }
10678
+ }
10679
+ function parseSessionStatus(value) {
10680
+ const properties = dataProperties(value);
10681
+ if (properties === undefined)
10682
+ return;
10683
+ const type = properties.type?.value;
10684
+ if (type === "idle" || type === "busy")
10685
+ return { type };
10686
+ const attempt = properties.attempt?.value;
10687
+ const message = properties.message?.value;
10688
+ const next = properties.next?.value;
10689
+ if (type === "retry" && typeof attempt === "number" && Number.isFinite(attempt) && typeof message === "string" && typeof next === "number" && Number.isFinite(next))
10690
+ return { type, attempt, message, next };
10691
+ return;
10692
+ }
10693
+ function parseSessionStatusMap(value) {
10694
+ const properties = dataProperties(value);
10695
+ if (properties === undefined)
10696
+ return;
10697
+ const entries = [];
10698
+ for (const [sessionID, property] of Object.entries(properties)) {
10699
+ const status = parseSessionStatus(property.value);
10700
+ if (status === undefined)
10701
+ return;
10702
+ entries.push([sessionID, status]);
10703
+ }
10704
+ return Object.fromEntries(entries);
10705
+ }
10706
+
10551
10707
  // src/orchestrator/subsessions.ts
10552
10708
  var DEFAULT_ABORT_GRACE_MS = 2000;
10553
10709
  var DEFAULT_STATUS_POLL_MS = 100;
@@ -10773,28 +10929,6 @@ function statusArgs(directory, signal) {
10773
10929
  function abortArgs(sessionID, directory, signal) {
10774
10930
  return directory === undefined ? { path: { id: sessionID }, signal } : { path: { id: sessionID }, query: { directory }, signal };
10775
10931
  }
10776
- function isSessionStatus(value) {
10777
- if (!isObject3(value))
10778
- return false;
10779
- const type = safeGet(value, "type");
10780
- if (type === "idle" || type === "busy")
10781
- return true;
10782
- if (type !== "retry")
10783
- return false;
10784
- return typeof safeGet(value, "attempt") === "number" && typeof safeGet(value, "message") === "string" && typeof safeGet(value, "next") === "number";
10785
- }
10786
- function isSessionStatusMap(value) {
10787
- if (!isObject3(value) || Array.isArray(value))
10788
- return false;
10789
- try {
10790
- const prototype = Object.getPrototypeOf(value);
10791
- if (prototype !== Object.prototype && prototype !== null)
10792
- return false;
10793
- return Object.values(value).every(isSessionStatus);
10794
- } catch {
10795
- return false;
10796
- }
10797
- }
10798
10932
  async function confirmSessionStopped(client, sessionID, directory, deps, deadline) {
10799
10933
  const abortSession = client.session.abort;
10800
10934
  if (abortSession === undefined) {
@@ -10843,8 +10977,8 @@ async function confirmSessionStopped(client, sessionID, directory, deps, deadlin
10843
10977
  if (safeGet(statuses, "error") !== undefined) {
10844
10978
  return { confirmed: false, cause: "status-error" };
10845
10979
  }
10846
- const snapshot = safeGet(statuses, "data");
10847
- if (!isSessionStatusMap(snapshot)) {
10980
+ const snapshot = parseSessionStatusMap(safeGet(statuses, "data"));
10981
+ if (snapshot === undefined) {
10848
10982
  return { confirmed: false, cause: "status-missing" };
10849
10983
  }
10850
10984
  if (!Object.hasOwn(snapshot, sessionID)) {
@@ -13174,7 +13308,6 @@ var GENERATED_OPENTEAM_STATE_GROUPS = [
13174
13308
  { kind: "file", paths: [SOAK_RECORDER_ID_PATH] }
13175
13309
  ];
13176
13310
  var OPENCODE_DIR = ".opencode";
13177
- var AGENT_DIR = `${OPENCODE_DIR}/agent`;
13178
13311
  function splitSetupPath(path3) {
13179
13312
  return path3.split("/").filter((segment) => segment.length > 0);
13180
13313
  }
@@ -20343,6 +20476,307 @@ function createRegisterCastTool(deps) {
20343
20476
  });
20344
20477
  }
20345
20478
 
20479
+ // src/plugin/reloadTool.ts
20480
+ import { resolve as resolve9 } from "node:path";
20481
+ import { tool as tool7 } from "@opencode-ai/plugin/tool";
20482
+ var WAIT_MS = 30000;
20483
+ var SDK_TIMEOUT_MS = 5000;
20484
+ var POLL_MS2 = 1000;
20485
+ var schema = tool7.schema.object({
20486
+ action: tool7.schema.enum(["request", "cancel"]).describe(reloadMessages.action),
20487
+ confirmed: tool7.schema.boolean().describe(reloadMessages.confirmed)
20488
+ }).strict();
20489
+ function sdkData(value) {
20490
+ if (typeof value !== "object" || value === null)
20491
+ return;
20492
+ try {
20493
+ const prototype = Object.getPrototypeOf(value);
20494
+ if (prototype !== Object.prototype && prototype !== null)
20495
+ return;
20496
+ const error = Object.getOwnPropertyDescriptor(value, "error");
20497
+ const data = Object.getOwnPropertyDescriptor(value, "data");
20498
+ if (error !== undefined && (!("value" in error) || error.value !== undefined))
20499
+ return;
20500
+ return data !== undefined && "value" in data ? data.value : undefined;
20501
+ } catch {
20502
+ return;
20503
+ }
20504
+ }
20505
+ function createReloadController(deps) {
20506
+ const scheduler = deps.scheduler ?? {
20507
+ now: () => Date.now(),
20508
+ schedule: (run, ms) => {
20509
+ const timer = setTimeout(run, ms);
20510
+ timer.unref();
20511
+ return () => clearTimeout(timer);
20512
+ }
20513
+ };
20514
+ const directory = resolve9(deps.directory);
20515
+ let pending;
20516
+ let authorizing;
20517
+ let closed = false;
20518
+ function current(request) {
20519
+ return pending === request && !request.abort.signal.aborted;
20520
+ }
20521
+ function finish(request, code) {
20522
+ if (!current(request))
20523
+ return;
20524
+ pending = undefined;
20525
+ request.stopDeadline();
20526
+ request.stopPoll?.();
20527
+ request.unlinkCaller();
20528
+ request.abort.abort();
20529
+ deps.notify(code);
20530
+ }
20531
+ function revokeAuthorization() {
20532
+ authorizing?.abort.abort();
20533
+ authorizing = undefined;
20534
+ }
20535
+ function cancel() {
20536
+ revokeAuthorization();
20537
+ if (pending !== undefined)
20538
+ finish(pending, pending.disposing ? "reload-unconfirmed" : "reload-cancelled");
20539
+ }
20540
+ async function requestPermission(context, authorization) {
20541
+ let finishInterruption;
20542
+ const interrupted = new Promise((done) => {
20543
+ finishInterruption = done;
20544
+ });
20545
+ const onAbort = () => finishInterruption("cancelled");
20546
+ const stopTimer = scheduler.schedule(() => finishInterruption("timeout"), Math.max(0, authorization.deadline - scheduler.now()));
20547
+ context.abort.addEventListener("abort", onAbort, { once: true });
20548
+ authorization.abort.signal.addEventListener("abort", onAbort, {
20549
+ once: true
20550
+ });
20551
+ try {
20552
+ return await Promise.race([
20553
+ context.ask({
20554
+ permission: "openteam-reload",
20555
+ patterns: [directory],
20556
+ always: [directory],
20557
+ metadata: { directory }
20558
+ }).then(() => "approved"),
20559
+ interrupted
20560
+ ]);
20561
+ } finally {
20562
+ stopTimer();
20563
+ context.abort.removeEventListener("abort", onAbort);
20564
+ authorization.abort.signal.removeEventListener("abort", onAbort);
20565
+ }
20566
+ }
20567
+ function bounded(request, run) {
20568
+ return new Promise((done) => {
20569
+ const operation = new AbortController;
20570
+ let settled = false;
20571
+ const settle = (outcome) => {
20572
+ if (settled)
20573
+ return;
20574
+ settled = true;
20575
+ stopTimer();
20576
+ request.abort.signal.removeEventListener("abort", onAbort);
20577
+ if (outcome.kind !== "settled")
20578
+ operation.abort();
20579
+ done(outcome);
20580
+ };
20581
+ const onAbort = () => settle({ kind: "cancelled" });
20582
+ const stopTimer = scheduler.schedule(() => settle({ kind: "timeout" }), Math.max(0, Math.min(SDK_TIMEOUT_MS, request.deadline - scheduler.now())));
20583
+ request.abort.signal.addEventListener("abort", onAbort, { once: true });
20584
+ try {
20585
+ run(operation.signal).then((value) => settle({ kind: "settled", value }), () => settle({ kind: "failed" }));
20586
+ } catch {
20587
+ settle({ kind: "failed" });
20588
+ }
20589
+ });
20590
+ }
20591
+ function poll(request) {
20592
+ if (!current(request) || request.stopPoll !== undefined)
20593
+ return;
20594
+ request.stopPoll = scheduler.schedule(() => {
20595
+ request.stopPoll = undefined;
20596
+ probe(request);
20597
+ }, POLL_MS2);
20598
+ }
20599
+ async function probe(request) {
20600
+ if (!current(request) || !request.callerIdle || request.probing || request.disposing)
20601
+ return;
20602
+ if (scheduler.now() >= request.deadline) {
20603
+ finish(request, "reload-timeout");
20604
+ return;
20605
+ }
20606
+ request.stopPoll?.();
20607
+ request.stopPoll = undefined;
20608
+ request.probing = true;
20609
+ const revision = request.revision;
20610
+ try {
20611
+ for (let check = 0;check < 2; check++) {
20612
+ const outcome2 = await bounded(request, (signal) => deps.client.session.status({ query: { directory }, signal }));
20613
+ if (!current(request))
20614
+ return;
20615
+ if (outcome2.kind !== "settled") {
20616
+ finish(request, "reload-status-failed");
20617
+ return;
20618
+ }
20619
+ const snapshot = parseSessionStatusMap(sdkData(outcome2.value));
20620
+ if (snapshot === undefined) {
20621
+ finish(request, "reload-status-failed");
20622
+ return;
20623
+ }
20624
+ if (revision !== request.revision || !request.callerIdle) {
20625
+ poll(request);
20626
+ return;
20627
+ }
20628
+ if (Object.values(snapshot).some((status) => status.type !== "idle")) {
20629
+ if (Object.hasOwn(snapshot, request.sessionID) && snapshot[request.sessionID]?.type !== "idle")
20630
+ request.callerIdle = false;
20631
+ if (!request.waitingReported) {
20632
+ request.waitingReported = true;
20633
+ deps.notify("reload-waiting");
20634
+ }
20635
+ poll(request);
20636
+ return;
20637
+ }
20638
+ }
20639
+ if (scheduler.now() >= request.deadline) {
20640
+ finish(request, "reload-timeout");
20641
+ return;
20642
+ }
20643
+ request.disposing = true;
20644
+ closed = true;
20645
+ const outcome = await bounded(request, (signal) => deps.client.instance.dispose({ query: { directory }, signal }));
20646
+ if (!current(request))
20647
+ return;
20648
+ finish(request, outcome.kind === "timeout" ? "reload-unconfirmed" : outcome.kind === "settled" && sdkData(outcome.value) === true ? "reload-accepted" : "reload-dispose-failed");
20649
+ } finally {
20650
+ request.probing = false;
20651
+ if (current(request) && request.callerIdle && !request.disposing && revision !== request.revision)
20652
+ probe(request);
20653
+ }
20654
+ }
20655
+ const definition = {
20656
+ description: reloadMessages.description,
20657
+ args: schema.shape,
20658
+ async execute(args, context) {
20659
+ const parsed = schema.safeParse(args);
20660
+ if (!parsed.success)
20661
+ return reloadMessages.invalid;
20662
+ if (context.agent !== "openteam" || resolve9(context.directory) !== directory || context.sessionID.trim().length === 0)
20663
+ return reloadMessages.denied;
20664
+ if (parsed.data.action === "cancel") {
20665
+ if ((pending?.sessionID ?? authorizing?.sessionID) !== context.sessionID)
20666
+ return reloadMessages.none;
20667
+ const message = pending?.disposing ? reloadMessages["reload-unconfirmed"] : reloadMessages["reload-cancelled"];
20668
+ cancel();
20669
+ return message;
20670
+ }
20671
+ if (!parsed.data.confirmed) {
20672
+ const inFlight = pending?.sessionID === context.sessionID && pending.disposing;
20673
+ if ((pending?.sessionID ?? authorizing?.sessionID) === context.sessionID)
20674
+ cancel();
20675
+ return inFlight ? reloadMessages["reload-unconfirmed"] : reloadMessages.declined;
20676
+ }
20677
+ if (context.abort.aborted)
20678
+ return reloadMessages["reload-cancelled"];
20679
+ if (closed)
20680
+ return reloadMessages.closed;
20681
+ if (authorizing !== undefined)
20682
+ return authorizing.sessionID === context.sessionID ? reloadMessages.authorizing : reloadMessages.other;
20683
+ if (pending !== undefined) {
20684
+ if (pending.sessionID !== context.sessionID)
20685
+ return reloadMessages.other;
20686
+ pending.toolDone = false;
20687
+ pending.callerIdle = false;
20688
+ pending.revision++;
20689
+ return reloadMessages["reload-queued"];
20690
+ }
20691
+ const authorization = {
20692
+ sessionID: context.sessionID,
20693
+ deadline: scheduler.now() + WAIT_MS,
20694
+ abort: new AbortController
20695
+ };
20696
+ authorizing = authorization;
20697
+ try {
20698
+ const permission = await requestPermission(context, authorization);
20699
+ if (closed)
20700
+ return reloadMessages.closed;
20701
+ if (authorizing !== authorization || context.abort.aborted || permission === "cancelled")
20702
+ return reloadMessages["reload-cancelled"];
20703
+ if (permission === "timeout" || scheduler.now() >= authorization.deadline) {
20704
+ deps.notify("reload-timeout");
20705
+ return reloadMessages["reload-timeout"];
20706
+ }
20707
+ const onAbort = () => {
20708
+ if (pending === request)
20709
+ cancel();
20710
+ };
20711
+ const request = {
20712
+ sessionID: context.sessionID,
20713
+ deadline: authorization.deadline,
20714
+ abort: new AbortController,
20715
+ unlinkCaller: () => context.abort.removeEventListener("abort", onAbort),
20716
+ stopDeadline: () => {},
20717
+ stopPoll: undefined,
20718
+ toolDone: false,
20719
+ callerIdle: false,
20720
+ probing: false,
20721
+ disposing: false,
20722
+ waitingReported: false,
20723
+ revision: 0
20724
+ };
20725
+ pending = request;
20726
+ context.abort.addEventListener("abort", onAbort, { once: true });
20727
+ request.stopDeadline = scheduler.schedule(() => finish(request, request.disposing ? "reload-unconfirmed" : "reload-timeout"), request.deadline - scheduler.now());
20728
+ deps.notify("reload-queued");
20729
+ return reloadMessages["reload-queued"];
20730
+ } finally {
20731
+ if (authorizing === authorization)
20732
+ authorizing = undefined;
20733
+ }
20734
+ }
20735
+ };
20736
+ return {
20737
+ tool: definition,
20738
+ toolCompleted(sessionID) {
20739
+ if (pending?.sessionID !== sessionID)
20740
+ return;
20741
+ pending.unlinkCaller();
20742
+ pending.toolDone = true;
20743
+ pending.callerIdle = false;
20744
+ pending.revision++;
20745
+ },
20746
+ userTurn(_sessionID) {
20747
+ cancel();
20748
+ },
20749
+ event(event) {
20750
+ const sessionID = pending?.sessionID ?? authorizing?.sessionID;
20751
+ if (sessionID === undefined)
20752
+ return;
20753
+ if (event.type === "session.error" && (event.properties.sessionID === undefined || event.properties.sessionID === sessionID) || event.type === "session.deleted" && event.properties.info.id === sessionID) {
20754
+ cancel();
20755
+ return;
20756
+ }
20757
+ if (event.type === "session.created" || event.type === "session.status" && event.properties.status.type !== "idle")
20758
+ revokeAuthorization();
20759
+ const request = pending;
20760
+ if (request === undefined)
20761
+ return;
20762
+ if (event.type === "session.created" || event.type === "session.status") {
20763
+ request.revision++;
20764
+ if (event.type === "session.status" && event.properties.sessionID === request.sessionID && event.properties.status.type !== "idle")
20765
+ request.callerIdle = false;
20766
+ }
20767
+ if (event.type === "session.idle" || event.type === "session.status" && event.properties.status.type === "idle") {
20768
+ if (event.properties.sessionID === request.sessionID && request.toolDone)
20769
+ request.callerIdle = true;
20770
+ probe(request);
20771
+ }
20772
+ },
20773
+ dispose() {
20774
+ closed = true;
20775
+ cancel();
20776
+ }
20777
+ };
20778
+ }
20779
+
20346
20780
  // src/plugin/sessionTracker.ts
20347
20781
  function createSessionTracker() {
20348
20782
  const knownSessions = new Set;
@@ -20580,7 +21014,7 @@ import {
20580
21014
  statSync,
20581
21015
  unlinkSync as unlinkSync2
20582
21016
  } from "node:fs";
20583
- import { join as join15, resolve as resolve10 } from "node:path";
21017
+ import { join as join15, resolve as resolve11 } from "node:path";
20584
21018
  import { ZodError } from "zod";
20585
21019
 
20586
21020
  // src/storage/lifecycle/codec.ts
@@ -20699,7 +21133,7 @@ import {
20699
21133
  unlinkSync,
20700
21134
  writeSync as writeSync2
20701
21135
  } from "node:fs";
20702
- import { basename as basename3, dirname as dirname7, join as join14, relative as relative3, resolve as resolve9 } from "node:path";
21136
+ import { basename as basename3, dirname as dirname7, join as join14, relative as relative3, resolve as resolve10 } from "node:path";
20703
21137
  var SAFE_RUN_ID = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u;
20704
21138
 
20705
21139
  class LifecycleFsError extends Error {
@@ -20723,7 +21157,7 @@ function validateRunID(runID) {
20723
21157
  }
20724
21158
  function resolvedLifecycleRoot(root) {
20725
21159
  try {
20726
- const absolute = resolve9(root);
21160
+ const absolute = resolve10(root);
20727
21161
  mkdirSync2(absolute, { recursive: true });
20728
21162
  const rootStat = lstatSync2(absolute);
20729
21163
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
@@ -20766,7 +21200,7 @@ function assertExistingSafeRun(root, runID) {
20766
21200
  throw new LifecycleFsError("unavailable");
20767
21201
  }
20768
21202
  const child = relative3(root, real);
20769
- if (child === "" || child.startsWith("..") || resolve9(root, child) !== real || dirname7(real) !== root) {
21203
+ if (child === "" || child.startsWith("..") || resolve10(root, child) !== real || dirname7(real) !== root) {
20770
21204
  throw new LifecycleFsError("corrupt");
20771
21205
  }
20772
21206
  return real;
@@ -21006,7 +21440,7 @@ function canDeleteTerminalRun(runDir, runID, now) {
21006
21440
  return now >= lease.expiresAt;
21007
21441
  }
21008
21442
  var createFsLifecycleJournal = (options) => {
21009
- const configuredRoot = resolve10(options.root);
21443
+ const configuredRoot = resolve11(options.root);
21010
21444
  const now = options.now ?? Date.now;
21011
21445
  const mutateRun = options.withMutationLock ?? withRunMutationLock;
21012
21446
  return {
@@ -21638,8 +22072,8 @@ async function readOpencodeVersion(serverUrl, options = {}) {
21638
22072
  }
21639
22073
  var DEFAULT_OPENCODE_VERSION_BACKOFF_MS = [50, 250];
21640
22074
  function defaultSleep(ms) {
21641
- return new Promise((resolve11) => {
21642
- setTimeout(resolve11, ms);
22075
+ return new Promise((resolve12) => {
22076
+ setTimeout(resolve12, ms);
21643
22077
  });
21644
22078
  }
21645
22079
  function createLazyOpencodeVersionReader(serverUrl, options = {}) {
@@ -22163,8 +22597,8 @@ var server = async (ctx, rawOptions) => {
22163
22597
  lifecycleRecorder,
22164
22598
  abortGraceMs: DEFAULT_ABORT_GRACE_MS,
22165
22599
  statusPollMs: DEFAULT_STATUS_POLL_MS,
22166
- delay: (ms) => new Promise((resolve11) => {
22167
- setTimeout(resolve11, ms);
22600
+ delay: (ms) => new Promise((resolve12) => {
22601
+ setTimeout(resolve12, ms);
22168
22602
  }),
22169
22603
  warn: diagnostics.warn("unknown-role")
22170
22604
  });
@@ -22184,23 +22618,43 @@ var server = async (ctx, rawOptions) => {
22184
22618
  options: rawOptions
22185
22619
  })
22186
22620
  });
22621
+ const reload = createReloadController({
22622
+ directory: ctx.directory,
22623
+ client: ctx.client,
22624
+ notify: (code) => diagnostics.emit({
22625
+ code,
22626
+ level: code === "reload-timeout" || code === "reload-status-failed" || code === "reload-dispose-failed" || code === "reload-unconfirmed" ? "warn" : "info"
22627
+ }, { toast: code !== "reload-queued" })
22628
+ });
22187
22629
  return {
22188
22630
  ...hooks,
22631
+ "chat.message": async (input, output) => {
22632
+ reload.userTurn(input.sessionID);
22633
+ await hooks["chat.message"]?.(input, output);
22634
+ },
22635
+ dispose: async () => {
22636
+ reload.dispose();
22637
+ await hooks.dispose?.();
22638
+ },
22189
22639
  tool: {
22190
22640
  openteam: createCommandTool(cliDeps),
22191
22641
  "openteam-console": createConsoleTool(),
22192
22642
  "openteam-graph": graphSurface.definition,
22193
22643
  "openteam-orchestrate": orchestrateTool,
22194
22644
  "openteam-memory": memoryTool,
22195
- "openteam-register-cast": registerCastTool
22645
+ "openteam-register-cast": registerCastTool,
22646
+ "openteam-reload": reload.tool
22196
22647
  },
22197
22648
  "tool.execute.before": async (input) => {
22198
22649
  toolcalls.before(input);
22199
22650
  },
22200
22651
  "tool.execute.after": async (input) => {
22201
22652
  await toolcalls.after(input);
22653
+ if (input.tool === "openteam-reload")
22654
+ reload.toolCompleted(input.sessionID);
22202
22655
  },
22203
22656
  event: async ({ event }) => {
22657
+ reload.event(event);
22204
22658
  if (event.type === "session.created" || event.type === "session.idle") {
22205
22659
  cache.refresh();
22206
22660
  }