paseo-bm 0.2.0-alpha.1 → 0.3.0-alpha.4

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +156 -18
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugin/client/agent-tree.ts +5 -3
  6. package/plugin/client/beads-header-button.ts +11 -8
  7. package/plugin/client/chat-card.tsx +146 -2
  8. package/plugin/client/chat-cards.ts +382 -6
  9. package/plugin/client/launch-manager.ts +6 -0
  10. package/plugin/client/setup-model.ts +487 -7
  11. package/plugin/client/setup-screen.tsx +590 -9
  12. package/plugin/client/waiting-pills-model.ts +74 -6
  13. package/plugin/client/waiting-pills.tsx +70 -26
  14. package/plugin/index.server.ts +49 -3
  15. package/plugin/roles/manager.md +12 -5
  16. package/plugin/roles/worker.md +20 -8
  17. package/plugin/server/agent-labels.ts +16 -0
  18. package/plugin/server/agent-role.ts +14 -3
  19. package/plugin/server/chat-peers.ts +3 -0
  20. package/plugin/server/chat-rpc.ts +7 -1
  21. package/plugin/server/chat-waiting.ts +63 -11
  22. package/plugin/server/collector.ts +11 -12
  23. package/plugin/server/config-writer.ts +222 -0
  24. package/plugin/server/cost.ts +28 -6
  25. package/plugin/server/dashboard-rpc.ts +72 -6
  26. package/plugin/server/fallback-detect.ts +183 -0
  27. package/plugin/server/fallback-handover.ts +365 -0
  28. package/plugin/server/fallback-manager.ts +170 -0
  29. package/plugin/server/fallback-reviewer.ts +198 -0
  30. package/plugin/server/fallback-rpc.ts +306 -0
  31. package/plugin/server/fallback-settings.ts +322 -0
  32. package/plugin/server/fallback-state.ts +518 -0
  33. package/plugin/server/fallback-switch.ts +191 -0
  34. package/plugin/server/fallback-wait.ts +188 -0
  35. package/plugin/server/manager-instructions.ts +1 -1
  36. package/plugin/server/manager.ts +206 -25
  37. package/plugin/server/model-costs.ts +238 -0
  38. package/plugin/server/notice-queue.ts +315 -0
  39. package/plugin/server/notices.ts +25 -0
  40. package/plugin/server/review-budget.ts +18 -9
  41. package/plugin/server/role-choices.ts +161 -0
  42. package/plugin/server/role-extras.ts +77 -4
  43. package/plugin/server/role-hook.ts +170 -26
  44. package/plugin/server/role-mode.ts +190 -11
  45. package/plugin/server/role-settings-rpc.ts +325 -0
  46. package/plugin/server/settings-notices.ts +112 -0
  47. package/plugin/server/setup-rpc.ts +2 -0
  48. package/plugin/server/setup-skills.ts +18 -3
  49. package/plugin/server/stop-propagation.ts +44 -32
  50. package/plugin/server/tools-check.ts +118 -0
  51. package/plugin/server/traces.ts +27 -3
  52. package/plugin/server/worker-instructions.ts +1 -1
  53. package/plugin/shared/bm-fallback.ts +91 -0
  54. package/plugin/shared/contracts.ts +344 -4
  55. package/plugin/shared/fallback-patterns.ts +201 -0
  56. package/plugin/shared/fallback.ts +46 -0
  57. package/plugin/shared/sole-worker.ts +4 -3
  58. package/plugin/shared/version.ts +1 -1
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - **Reviewer** — checks each stage and returns a verdict. It never edits anything.
10
10
  - **Screens in Paseo** — **Metric** (what each request did and cost), **Beads** (the workspace's beads) and **Setup** (tools, skills and your own role instructions).
11
11
 
12
- > **Status:** prerelease (`0.2.0-alpha.*`). Command names, flags, exit codes and the `--json` shape are a public contract, but expect rough edges.
12
+ > **Status:** prerelease (`0.3.0-alpha.*`). Command names, flags, exit codes and the `--json` shape are a public contract, but expect rough edges.
13
13
 
14
14
  ## Quick start
15
15
 
package/dist/index.js CHANGED
@@ -3574,8 +3574,13 @@ var MISSING_CONFIG = (path, present, readable) => ({
3574
3574
  mcpInjectIntoAgents: false,
3575
3575
  agentProviderIds: [],
3576
3576
  agentProfileIds: [],
3577
- providerPaseoTools: {}
3577
+ providerPaseoTools: {},
3578
+ providerExtends: {},
3579
+ profileModels: {}
3578
3580
  });
3581
+ function nonEmptyString(value) {
3582
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3583
+ }
3579
3584
  var readPaseoConfigFacts = async (configFile) => {
3580
3585
  let text;
3581
3586
  try {
@@ -3598,8 +3603,19 @@ var readPaseoConfigFacts = async (configFile) => {
3598
3603
  const providers = asRecordObject(asRecordObject(root["agents"])?.["providers"]) ?? {};
3599
3604
  const profiles = Array.isArray(daemon?.["agentProfiles"]) ? daemon["agentProfiles"] : [];
3600
3605
  const providerPaseoTools = {};
3606
+ const providerExtends = {};
3601
3607
  for (const [id, value] of Object.entries(providers)) {
3602
- providerPaseoTools[id] = paseoToolsEnabled(asRecordObject(value)?.["paseoTools"]);
3608
+ const provider = asRecordObject(value);
3609
+ providerPaseoTools[id] = paseoToolsEnabled(provider?.["paseoTools"]);
3610
+ const base = nonEmptyString(provider?.["extends"]);
3611
+ if (base !== void 0) providerExtends[id] = base;
3612
+ }
3613
+ const profileModels = {};
3614
+ for (const entry of profiles) {
3615
+ const profile = asRecordObject(entry);
3616
+ const id = nonEmptyString(profile?.["id"]);
3617
+ const model = nonEmptyString(profile?.["model"]);
3618
+ if (id !== void 0 && model !== void 0 && !(id in profileModels)) profileModels[id] = model;
3603
3619
  }
3604
3620
  return {
3605
3621
  path: configFile,
@@ -3609,7 +3625,9 @@ var readPaseoConfigFacts = async (configFile) => {
3609
3625
  mcpInjectIntoAgents: mcp?.["injectIntoAgents"] === true,
3610
3626
  agentProviderIds: Object.keys(providers),
3611
3627
  agentProfileIds: profiles.map((entry) => asRecordObject(entry)?.["id"]).filter((id) => typeof id === "string" && id.length > 0),
3612
- providerPaseoTools
3628
+ providerPaseoTools,
3629
+ providerExtends,
3630
+ profileModels
3613
3631
  };
3614
3632
  };
3615
3633
  var probeOwnedFiles = async (record, installHome) => {
@@ -3915,6 +3933,9 @@ function switchChecks(config) {
3915
3933
  ];
3916
3934
  }
3917
3935
  function roleChecks(record, config) {
3936
+ return [...presenceChecks(record, config), ...changedInAppChecks(record, config)];
3937
+ }
3938
+ function presenceChecks(record, config) {
3918
3939
  return ROLE_NAMES.map((role) => {
3919
3940
  const id = roleId(role);
3920
3941
  const checkId = `role-${id}`;
@@ -3958,6 +3979,32 @@ function roleChecks(record, config) {
3958
3979
  );
3959
3980
  });
3960
3981
  }
3982
+ var CHANGED_IN_APP_SEVERITY = "ok";
3983
+ function changedInAppChecks(record, config) {
3984
+ if (!config.readable) return [];
3985
+ const notes = [];
3986
+ for (const role of ROLE_NAMES) {
3987
+ const entry = record.roles.find((candidate) => candidate.role === role);
3988
+ if (entry === void 0) continue;
3989
+ if (!config.agentProviderIds.includes(entry.providerId) || !config.agentProfileIds.includes(entry.profileId)) {
3990
+ continue;
3991
+ }
3992
+ const base = config.providerExtends[entry.providerId];
3993
+ const model = config.profileModels[entry.profileId];
3994
+ const baseChanged = base !== void 0 && base !== entry.baseProvider;
3995
+ const modelChanged = model !== void 0 && model !== entry.model;
3996
+ if (!baseChanged && !modelChanged) continue;
3997
+ notes.push(
3998
+ check(
3999
+ "roles.changed-in-app",
4000
+ CHANGED_IN_APP_SEVERITY,
4001
+ `${roleId(role)}: base provider or model differs from what the installer wrote (changed in the app).`,
4002
+ `Nothing to do if the change was on purpose: installing again keeps role settings changed in the app. The installer wrote ${entry.baseProvider}/${entry.model}; Paseo's configuration has ${base ?? "(not set)"}/${model ?? "(not set)"}.`
4003
+ )
4004
+ );
4005
+ }
4006
+ return notes;
4007
+ }
3961
4008
  function inventoryChecks(record) {
3962
4009
  const versions = record.versions.length;
3963
4010
  const active = record.versions.find((entry) => entry.active)?.version ?? "none";
@@ -4688,8 +4735,10 @@ function applyProviders(config, edit, changedPaths) {
4688
4735
  const providers = isPlainObject(existing) ? existing : {};
4689
4736
  parent[leaf] = providers;
4690
4737
  for (const [id, value] of entries) {
4691
- if (!deepEqual(providers[id], value)) {
4692
- providers[id] = structuredClone(value);
4738
+ const current = providers[id];
4739
+ const next = mergedEntry(current, value, edit.providerMerge?.[id]);
4740
+ if (!deepEqual(current, next)) {
4741
+ providers[id] = next;
4693
4742
  changedPaths.push(`${PROVIDERS_PATH}.${id}`);
4694
4743
  }
4695
4744
  }
@@ -4725,8 +4774,10 @@ function applyProfiles(config, edit, changedPaths) {
4725
4774
  changedPaths.push(profileField(profile.id));
4726
4775
  continue;
4727
4776
  }
4728
- if (!deepEqual(list[index], profile)) {
4729
- list[index] = structuredClone(profile);
4777
+ const current = list[index];
4778
+ const next = mergedEntry(current, profile, edit.profileMerge?.[profile.id]);
4779
+ if (!deepEqual(current, next)) {
4780
+ list[index] = next;
4730
4781
  changed = true;
4731
4782
  changedPaths.push(profileField(profile.id));
4732
4783
  }
@@ -4747,11 +4798,45 @@ function applyProfiles(config, edit, changedPaths) {
4747
4798
  function profileField(id) {
4748
4799
  return `${AGENT_PROFILES_PATH}[${id}]`;
4749
4800
  }
4801
+ function mergedEntry(current, entry, rule) {
4802
+ if (!isPlainObject(current) || !isPlainObject(entry)) {
4803
+ return structuredClone(entry);
4804
+ }
4805
+ const merged = structuredClone(current);
4806
+ for (const [key, value] of Object.entries(entry)) {
4807
+ const mode = rule?.keys?.[key] ?? "set";
4808
+ const existing = merged[key];
4809
+ switch (mode) {
4810
+ case "create-only":
4811
+ break;
4812
+ case "fill":
4813
+ if (existing === void 0 || existing === null) {
4814
+ merged[key] = structuredClone(value);
4815
+ }
4816
+ break;
4817
+ case "merge":
4818
+ merged[key] = isPlainObject(existing) && isPlainObject(value) ? { ...existing, ...structuredClone(value) } : structuredClone(value);
4819
+ break;
4820
+ case "set":
4821
+ merged[key] = structuredClone(value);
4822
+ break;
4823
+ }
4824
+ }
4825
+ const drop = rule?.drop;
4826
+ if (drop !== void 0 && !deepEqual(current[drop.whenChanged], merged[drop.whenChanged])) {
4827
+ for (const key of drop.keys) {
4828
+ delete merged[key];
4829
+ }
4830
+ }
4831
+ return merged;
4832
+ }
4750
4833
  function assertOnlyOurKeys(edit) {
4751
4834
  const ids = [
4752
4835
  ...Object.keys(edit.providers ?? {}),
4836
+ ...Object.keys(edit.providerMerge ?? {}),
4753
4837
  ...edit.removeProviders ?? [],
4754
4838
  ...(edit.profiles ?? []).map((profile) => profile.id),
4839
+ ...Object.keys(edit.profileMerge ?? {}),
4755
4840
  ...edit.removeProfiles ?? []
4756
4841
  ];
4757
4842
  for (const path of edit.removeEmptyContainers ?? []) {
@@ -6776,6 +6861,8 @@ var PROVIDER_LOGIN_COMMANDS = {
6776
6861
  codex: { executable: "codex", args: ["login"] },
6777
6862
  opencode: { executable: "opencode", args: ["providers", "login"] }
6778
6863
  };
6864
+ var PI_PROVIDER_ID = "pi";
6865
+ var PI_SIGN_IN_GUIDANCE = "Pi has no login command paseo-bm knows; sign in the way Pi's own documentation describes, then run doctor.";
6779
6866
  function loginCommandFor(providerId) {
6780
6867
  return Object.prototype.hasOwnProperty.call(PROVIDER_LOGIN_COMMANDS, providerId) ? PROVIDER_LOGIN_COMMANDS[providerId] : void 0;
6781
6868
  }
@@ -6864,6 +6951,22 @@ async function ensureProviderLogins(options) {
6864
6951
  }
6865
6952
  const providers = [];
6866
6953
  for (const [provider, roles] of rolesByProvider) {
6954
+ if (provider === PI_PROVIDER_ID) {
6955
+ print(PI_SIGN_IN_GUIDANCE);
6956
+ providers.push({
6957
+ provider,
6958
+ roles,
6959
+ before: "unknown",
6960
+ after: "unknown",
6961
+ command: null,
6962
+ outcome: "unknown",
6963
+ ran: false,
6964
+ run: null,
6965
+ manualInstructions: [PI_SIGN_IN_GUIDANCE],
6966
+ warning: null
6967
+ });
6968
+ continue;
6969
+ }
6867
6970
  const before = (await checkProviderLogin(options.runner, provider)).state;
6868
6971
  const login = loginCommandFor(provider);
6869
6972
  const command = login === void 0 ? null : formatLoginCommand(login);
@@ -6965,6 +7068,32 @@ function roleProfileEntry(selection) {
6965
7068
  notes: ROLE_PROFILE_NOTES[selection.role]
6966
7069
  };
6967
7070
  }
7071
+ function isNewRoleChoice(selection) {
7072
+ return selection.source !== "record";
7073
+ }
7074
+ function roleProviderMerge(selection) {
7075
+ const chosen = isNewRoleChoice(selection);
7076
+ return {
7077
+ keys: {
7078
+ extends: chosen ? "set" : "fill",
7079
+ label: chosen ? "set" : "create-only",
7080
+ paseoTools: "merge"
7081
+ }
7082
+ };
7083
+ }
7084
+ function roleProfileMerge(selection) {
7085
+ const chosen = isNewRoleChoice(selection);
7086
+ return {
7087
+ keys: {
7088
+ id: "set",
7089
+ name: chosen ? "set" : "fill",
7090
+ provider: "set",
7091
+ model: chosen ? "set" : "fill",
7092
+ notes: "create-only"
7093
+ },
7094
+ ...chosen ? { drop: { whenChanged: "model", keys: ["thinkingOptionId"] } } : {}
7095
+ };
7096
+ }
6968
7097
  var RoleRegistrationError = class extends Error {
6969
7098
  constructor(message) {
6970
7099
  super(message);
@@ -6974,7 +7103,9 @@ var RoleRegistrationError = class extends Error {
6974
7103
  function roleRegistrationEdit(selections) {
6975
7104
  const seen = /* @__PURE__ */ new Set();
6976
7105
  const providers = {};
7106
+ const providerMerge = {};
6977
7107
  const profiles = [];
7108
+ const profileMerge = {};
6978
7109
  for (const selection of selections) {
6979
7110
  if (seen.has(selection.role)) {
6980
7111
  throw new RoleRegistrationError(
@@ -6982,10 +7113,13 @@ function roleRegistrationEdit(selections) {
6982
7113
  );
6983
7114
  }
6984
7115
  seen.add(selection.role);
6985
- providers[roleId(selection.role)] = roleProviderEntry(selection);
7116
+ const id = roleId(selection.role);
7117
+ providers[id] = roleProviderEntry(selection);
7118
+ providerMerge[id] = roleProviderMerge(selection);
6986
7119
  profiles.push(roleProfileEntry(selection));
7120
+ profileMerge[id] = roleProfileMerge(selection);
6987
7121
  }
6988
- return { providers, profiles };
7122
+ return { providers, profiles, providerMerge, profileMerge };
6989
7123
  }
6990
7124
  async function registerRoles(options) {
6991
7125
  const edit = roleRegistrationEdit(options.selections);
@@ -7184,25 +7318,29 @@ function nonBlank(value) {
7184
7318
  return typeof value === "string" && value.trim().length > 0 ? value : void 0;
7185
7319
  }
7186
7320
  function roleActions(config, selections) {
7187
- const edit = roleRegistrationEdit(selections);
7188
- const changed = new Set(editConfig(config, edit).changedPaths);
7189
- const providers = asObject2(asObject2(config["agents"])?.["providers"]);
7190
- const profiles = asObject2(config["daemon"])?.["agentProfiles"];
7321
+ const edited = editConfig(config, roleRegistrationEdit(selections));
7322
+ const changed = new Set(edited.changedPaths);
7191
7323
  const actions = [];
7192
7324
  for (const selection of selections) {
7193
7325
  const id = roleId(selection.role);
7194
7326
  const providerPath = `${PROVIDERS_PATH}.${id}`;
7195
- const providerFrom = providers?.[id];
7196
7327
  actions.push(
7197
- configAction(providerPath, changed.has(providerPath), providerFrom, edit.providers?.[id] ?? null)
7328
+ configAction(providerPath, changed.has(providerPath), providerOf(config, id), providerOf(edited.config, id) ?? null)
7198
7329
  );
7199
7330
  const profilePath = `${AGENT_PROFILES_PATH}[${id}]`;
7200
- const profileFrom = Array.isArray(profiles) ? profiles.find((entry) => asObject2(entry)?.["id"] === id) : void 0;
7201
- const profileTo = (edit.profiles ?? []).find((entry) => entry.id === id) ?? null;
7202
- actions.push(configAction(profilePath, changed.has(profilePath), profileFrom, profileTo));
7331
+ actions.push(
7332
+ configAction(profilePath, changed.has(profilePath), profileOf(config, id), profileOf(edited.config, id) ?? null)
7333
+ );
7203
7334
  }
7204
7335
  return actions;
7205
7336
  }
7337
+ function providerOf(config, id) {
7338
+ return asObject2(asObject2(config["agents"])?.["providers"])?.[id];
7339
+ }
7340
+ function profileOf(config, id) {
7341
+ const profiles = asObject2(config["daemon"])?.["agentProfiles"];
7342
+ return Array.isArray(profiles) ? profiles.find((entry) => asObject2(entry)?.["id"] === id) : void 0;
7343
+ }
7206
7344
  function configAction(key, changes, from, to) {
7207
7345
  const target = `paseoHome/config.json#${key}`;
7208
7346
  if (!changes) {