@ordewell/web 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ordewell/web
2
2
 
3
- The local API server for **[Ordewell](https://ordewell.ai)** — a Hono HTTP +
3
+ The local API server for **[Ordewell](https://ordewell.ai)**: a Hono HTTP +
4
4
  WebSocket daemon bound to `127.0.0.1:3742` that the Ordewell CLI, terminal UI
5
5
  and VS Code extension all drive.
6
6
 
@@ -8,7 +8,7 @@ and VS Code extension all drive.
8
8
  port in a browser will show you an API response, not a dashboard. A browser UI
9
9
  may come later; it does not exist today.
10
10
 
11
- You almost certainly do not need to install this directly —
11
+ You almost certainly do not need to install this directly.
12
12
  [@ordewell/cli](https://www.npmjs.com/package/@ordewell/cli) depends on it and
13
13
  starts it on demand.
14
14
 
@@ -23,12 +23,15 @@ ordewell web --daemon # background; logs to ~/.config/ordewell/server.log
23
23
  `/api/commands`, `/api/workspaces`, `/api/approvals`, plus a WebSocket stream
24
24
  for live planner and execution events.
25
25
 
26
+ Skills are read by the planner and the task orchestrator, not served as a
27
+ route; the format is in [Writing skills](https://github.com/ordewell/ordewell/blob/main/docs/skills.md).
28
+
26
29
  ## Security
27
30
 
28
31
  The server is **unauthenticated by design** and binds the loopback interface
29
32
  only, on the assumption that local access is trusted. Do not expose it to a
30
33
  network. If you find a way to reach it off-host, or from a browser page, that's
31
- a vulnerability — see
34
+ a vulnerability; see
32
35
  [SECURITY.md](https://github.com/ordewell/ordewell/blob/main/SECURITY.md).
33
36
 
34
37
  Licensed under the [Apache License 2.0](./LICENSE).
@@ -336,20 +336,22 @@ ${err}` : out;
336
336
  return { success: true, output: combined || "(no output)", truncated: combined.length > 2e4 };
337
337
  }
338
338
  };
339
+
340
+ // server/adapters/PoolAwareRunner.ts
339
341
  var PoolAwareRunner = class {
340
342
  constructor(sessionId, broadcast, inner) {
341
343
  this.sessionId = sessionId;
342
344
  this.broadcast = broadcast;
343
- this.inner = inner ?? new core.HeadlessRunner();
345
+ this.inner = inner;
344
346
  }
345
347
  sessionId;
346
348
  broadcast;
347
349
  inner;
348
350
  /**
349
- * Sessions this plan spawned. The inner runner may be shared by every plan
350
- * in the pool (one tmux session per daemon), so "stop everything" and
351
- * "how many are running" must be answered from this set, never delegated —
352
- * delegating would let one plan's /stop kill another plan's tasks.
351
+ * Sessions this plan spawned. The inner runner is shared by every plan in
352
+ * the pool, so "stop everything" and "how many are running" must be
353
+ * answered from this set, never delegated — delegating would let one plan's
354
+ * /stop kill another plan's tasks.
353
355
  */
354
356
  owned = /* @__PURE__ */ new Set();
355
357
  get activeCount() {
@@ -374,65 +376,22 @@ var PoolAwareRunner = class {
374
376
  this.owned.clear();
375
377
  }
376
378
  };
377
- var TMUX_MISSING_ADVICE = "tmux is not installed, so terminal-transport tasks run without a terminal window you can open or type into. Install tmux, or keep the structured transport (/transport structured).";
378
- function startTerminalHost(port2, deps = {}) {
379
- const hasTmuxImpl = deps.hasTmuxImpl ?? core.hasTmux;
380
- if (!hasTmuxImpl()) return { advice: TMUX_MISSING_ADVICE };
381
- const runner = (deps.createTmuxRunner ?? ((p) => new core.TmuxRunner({ port: p })))(port2);
382
- runner.ensureSession().catch((err) => {
383
- console.error(`[web] tmux session setup failed (will retry on next task spawn): ${err?.message ?? err}`);
384
- });
385
- return { runner };
386
- }
387
- var AdvisingRunner = class {
388
- constructor(inner, advice, advise) {
389
- this.inner = inner;
390
- this.advice = advice;
391
- this.advise = advise;
392
- }
393
- inner;
394
- advice;
395
- advise;
396
- advised = false;
397
- get activeCount() {
398
- return this.inner.activeCount;
399
- }
400
- spawn(opts) {
401
- if (!this.advised) {
402
- this.advised = true;
403
- this.advise(this.advice);
404
- }
405
- return this.inner.spawn(opts);
406
- }
407
- stop(sessionId) {
408
- this.inner.stop(sessionId);
409
- }
410
- stopAll() {
411
- this.inner.stopAll();
412
- }
413
- };
414
379
 
415
380
  // server/pool/orchestratorPool.ts
416
381
  var OrchestratorPool = class {
417
382
  sessions = /* @__PURE__ */ new Map();
418
383
  clients = /* @__PURE__ */ new Map();
419
- registry = (() => {
420
- const r = new core.RunnerRegistry();
421
- r.loadUserPlugins();
422
- return r;
423
- })();
384
+ registry = new core.RunnerRegistry();
424
385
  modelResolver;
425
386
  runnerInstallation = new core.RunnerInstallation(this.registry);
426
387
  cachedProviderLists;
427
388
  settingsService = new core.SettingsService();
428
389
  plannerModelMemory = new core.PlannerModelMemory(this.settingsService);
429
- sharedRunner;
430
- structuredRunner;
431
- terminalAdvice;
390
+ runner;
432
391
  constructor(deps = {}) {
433
- this.sharedRunner = deps.runner;
434
- this.structuredRunner = deps.structuredRunner ?? new core.StructuredRunner();
435
- this.terminalAdvice = deps.terminalAdvice;
392
+ const pluginNotice = core.removedPluginNotice();
393
+ if (pluginNotice) console.warn(pluginNotice);
394
+ this.runner = deps.runner ?? new core.StructuredRunner();
436
395
  this.modelResolver = deps.modelResolver ?? new core.ModelResolver(this.registry, new WebConfig());
437
396
  }
438
397
  subscribe(sessionId, ws) {
@@ -498,12 +457,7 @@ var OrchestratorPool = class {
498
457
  });
499
458
  const fsAdapter = new PoolFileSystem(workspace);
500
459
  const broadcast = (msg) => this.broadcast(sessionId, msg);
501
- const terminal = this.sharedRunner ?? new core.HeadlessRunner();
502
- const router = new core.TransportRouter({
503
- terminal: this.terminalAdvice ? new AdvisingRunner(terminal, this.terminalAdvice, (message) => this.broadcast(sessionId, { type: "notice", level: "warn", message })) : terminal,
504
- structured: this.structuredRunner
505
- });
506
- const runner = new PoolAwareRunner(sessionId, broadcast, router);
460
+ const runner = new PoolAwareRunner(sessionId, broadcast, this.runner);
507
461
  return core.createSession({
508
462
  config,
509
463
  notifications: { info() {
@@ -557,14 +511,10 @@ var OrchestratorPool = class {
557
511
  // for a planner that is running at "high".
558
512
  plannerThinkingEffort: config.plannerThinkingEffort ?? "",
559
513
  maxParallel: config.maxParallelSessions,
560
- tdd: userSettings.tdd,
561
- verification: userSettings.verification,
562
514
  modelAllowlist: userSettings.modelAllowlist,
563
515
  // The model remembered per planner backend (this task), so a surface can
564
516
  // render what's remembered without a second round-trip.
565
- plannerModels: userSettings.plannerModels,
566
- // ADR-0018; a run copies it when it starts.
567
- runnerTransport: userSettings.runnerTransport
517
+ plannerModels: userSettings.plannerModels
568
518
  };
569
519
  }
570
520
  /**
@@ -592,22 +542,18 @@ var OrchestratorPool = class {
592
542
  const envChanges = admission2 ? admission2.accepted : void 0;
593
543
  const incomingAiProvider = typeof envChanges?.AI_PROVIDER === "string" ? envChanges.AI_PROVIDER : void 0;
594
544
  const switchedToProvider = incomingAiProvider && incomingAiProvider !== providerBefore ? incomingAiProvider : void 0;
595
- const switchRecall = switchedToProvider ? this.plannerModelMemory.recall(switchedToProvider, this.plannerCatalogFor(switchedToProvider)) : void 0;
545
+ const requestedModel = envChanges?.ORCHESTRATOR_MODEL ?? changes.orchestratorModel;
546
+ const explicitModel = typeof requestedModel === "string" && requestedModel.trim().length > 0;
547
+ const switchRecall = switchedToProvider && !explicitModel ? this.plannerModelMemory.recall(switchedToProvider, this.plannerCatalogFor(switchedToProvider)) : void 0;
548
+ if (switchedToProvider && explicitModel && typeof changes.plannerThinkingEffort !== "string" && envChanges?.ORDEWELL_PLANNER_EFFORT === void 0) {
549
+ process.env.ORDEWELL_PLANNER_EFFORT = "";
550
+ }
596
551
  if (typeof changes.orchestratorModel === "string") {
597
552
  process.env.ORCHESTRATOR_MODEL = changes.orchestratorModel;
598
553
  }
599
554
  if (typeof changes.plannerThinkingEffort === "string") {
600
555
  process.env.ORDEWELL_PLANNER_EFFORT = changes.plannerThinkingEffort;
601
556
  }
602
- if (changes.tdd && typeof changes.tdd.enabled === "boolean") {
603
- this.settingsService.setTdd(changes.tdd.enabled);
604
- }
605
- if (changes.verification && typeof changes.verification.enabled === "boolean") {
606
- this.settingsService.setVerification(changes.verification.enabled);
607
- }
608
- if (core.isRunnerTransport(changes.runnerTransport)) {
609
- this.settingsService.setRunnerTransport(changes.runnerTransport);
610
- }
611
557
  if (envChanges) {
612
558
  let touched = false;
613
559
  for (const [key, value] of Object.entries(envChanges)) {
@@ -640,7 +586,7 @@ var OrchestratorPool = class {
640
586
  }
641
587
  }
642
588
  }
643
- const envCarriesModelOrEffort = envChanges?.AI_PROVIDER === void 0 && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
589
+ const envCarriesModelOrEffort = (envChanges?.AI_PROVIDER === void 0 || explicitModel) && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
644
590
  if (typeof changes.orchestratorModel === "string" || typeof changes.plannerThinkingEffort === "string" || envCarriesModelOrEffort) {
645
591
  const rememberProvider = switchedToProvider ?? new WebConfig({
646
592
  enabledRunners: this.enabledRunnerOverride(),
@@ -707,11 +653,11 @@ var OrchestratorPool = class {
707
653
  async getInstalledRunners() {
708
654
  const config = new WebConfig({ enabledRunners: this.enabledRunnerOverride() });
709
655
  const enabled = new Set(config.enabledRunners);
710
- const plugins = this.registry.list();
656
+ const runners = this.registry.list();
711
657
  const installedIds = new Set(
712
- await this.runnerInstallation.filterInstalled(plugins.map((p) => p.manifest.name))
658
+ await this.runnerInstallation.filterInstalled(runners.map((p) => p.manifest.name))
713
659
  );
714
- return plugins.filter((p) => installedIds.has(p.manifest.name)).map((p) => ({
660
+ return runners.filter((p) => installedIds.has(p.manifest.name)).map((p) => ({
715
661
  id: p.manifest.name,
716
662
  name: p.manifest.displayName,
717
663
  enabled: enabled.has(p.manifest.name)
@@ -857,14 +803,14 @@ function sessionsRoute(pool2) {
857
803
  const id = c.req.param("id");
858
804
  const saved = core.loadSessionPlanState(id, ws);
859
805
  if (!saved) return refuse(c, 404, "Session not found", "session_not_found");
860
- return c.json({ meta: saved.meta, plan: pool2.getPlanState(id) ?? saved.plan });
806
+ return c.json({ meta: saved.meta, plan: core.surfacePlanState(pool2.getPlanState(id) ?? saved.plan) });
861
807
  });
862
808
  router.post("/:id/load", (c) => {
863
809
  const ws = c.req.query("workspace") || process.cwd();
864
810
  try {
865
811
  const id = c.req.param("id");
866
812
  const plan = pool2.adoptSavedSession(id, ws);
867
- return c.json({ ok: true, plan, goal: pool2.getGoal(id) });
813
+ return c.json({ ok: true, plan: core.surfacePlan(plan), goal: pool2.getGoal(id) });
868
814
  } catch (err) {
869
815
  return failure(c, err, "load session", { message: "Failed to load session" });
870
816
  }
@@ -877,7 +823,7 @@ function sessionsRoute(pool2) {
877
823
  const ws = c.req.query("workspace") || process.cwd();
878
824
  const attempt = Number(c.req.param("attempt"));
879
825
  if (!Number.isInteger(attempt) || attempt < 1) return refuse(c, 400, "attempt must be a positive integer");
880
- return c.json({ attempt, events: core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt) });
826
+ return c.json({ attempt, events: core.taskLogForSurface(core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt)) });
881
827
  });
882
828
  router.delete("/:id", (c) => {
883
829
  const ws = c.req.query("workspace") || process.cwd();
@@ -902,7 +848,8 @@ function plansRoute(pool2) {
902
848
  const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) : pool2.getRunnerState().enabledRunners;
903
849
  const plan = await pool2.generatePlan(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
904
850
  const { models, modelsByRunner } = await pool2.getProviderModels();
905
- return c.json({ plan, models, modelsByRunner });
851
+ const notes = (pool2.session(c.req.param("sessionId")).planState?.conversationHistory ?? []).filter((e) => e.kind === "system").map((e) => e.content);
852
+ return c.json({ plan: core.surfacePlanState(plan), models, modelsByRunner, ...notes.length > 0 ? { notes } : {} });
906
853
  } catch (err) {
907
854
  return failure(c, err, "generate", { message: "Plan generation failed" });
908
855
  }
@@ -1071,7 +1018,7 @@ function plansRoute(pool2) {
1071
1018
  router.post("/:sessionId/tasks/:taskId/resolve-conflict", async (c) => {
1072
1019
  try {
1073
1020
  const plan = await pool2.session(c.req.param("sessionId")).resolveConflictAsTask(c.req.param("taskId"));
1074
- return c.json({ plan });
1021
+ return c.json({ plan: plan && core.surfacePlan(plan) });
1075
1022
  } catch (err) {
1076
1023
  return failure(c, err, "task edit");
1077
1024
  }
@@ -1121,7 +1068,7 @@ function plansRoute(pool2) {
1121
1068
  router.post("/:sessionId/review/approve", async (c) => {
1122
1069
  try {
1123
1070
  const plan = await pool2.session(c.req.param("sessionId")).approveReview();
1124
- return c.json({ plan });
1071
+ return c.json({ plan: core.surfacePlan(plan) });
1125
1072
  } catch (err) {
1126
1073
  return failure(c, err, "review approve");
1127
1074
  }
@@ -1133,7 +1080,7 @@ function plansRoute(pool2) {
1133
1080
  return c.json({ error: "taskIds array with at least two ids is required" }, 400);
1134
1081
  }
1135
1082
  const plan = await pool2.session(c.req.param("sessionId")).requestMerge(taskIds);
1136
- return c.json({ plan });
1083
+ return c.json({ plan: core.surfacePlan(plan) });
1137
1084
  } catch (err) {
1138
1085
  return failure(c, err, "merge", { fallback: 400 });
1139
1086
  }
@@ -1141,7 +1088,7 @@ function plansRoute(pool2) {
1141
1088
  router.post("/:sessionId/tasks/:taskId/split", async (c) => {
1142
1089
  try {
1143
1090
  const plan = await pool2.session(c.req.param("sessionId")).requestSplit(c.req.param("taskId"));
1144
- return c.json({ plan });
1091
+ return c.json({ plan: core.surfacePlan(plan) });
1145
1092
  } catch (err) {
1146
1093
  return failure(c, err, "split", { fallback: 400 });
1147
1094
  }
@@ -1153,7 +1100,7 @@ function plansRoute(pool2) {
1153
1100
  const ws = workspace || c.req.query("workspace") || process.cwd();
1154
1101
  const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : pool2.getRunnerState().enabledRunners;
1155
1102
  const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
1156
- return c.json({ plan });
1103
+ return c.json({ plan: core.surfacePlan(plan) });
1157
1104
  } catch (err) {
1158
1105
  return failure(c, err, "converse start", { message: "Planning failed" });
1159
1106
  }
@@ -1163,14 +1110,15 @@ function plansRoute(pool2) {
1163
1110
  const { message } = await c.req.json();
1164
1111
  if (!message) return c.json({ error: "message is required" }, 400);
1165
1112
  const plan = await pool2.continuePlanning(c.req.param("sessionId"), message);
1166
- return c.json({ plan });
1113
+ return c.json({ plan: core.surfacePlan(plan) });
1167
1114
  } catch (err) {
1168
1115
  return failure(c, err, "conversation message");
1169
1116
  }
1170
1117
  });
1171
1118
  router.post("/:sessionId/conversation/fork", (c) => {
1172
1119
  try {
1173
- return c.json(pool2.forkConversation(c.req.param("sessionId")));
1120
+ const fork = pool2.forkConversation(c.req.param("sessionId"));
1121
+ return c.json({ ...fork, plan: core.surfacePlan(fork.plan) });
1174
1122
  } catch (err) {
1175
1123
  return failure(c, err, "conversation edit");
1176
1124
  }
@@ -1186,14 +1134,16 @@ function plansRoute(pool2) {
1186
1134
  try {
1187
1135
  const { index } = await c.req.json();
1188
1136
  if (!Number.isInteger(index) || index < 0) return c.json({ error: "index must be a non-negative integer" }, 400);
1189
- return c.json(pool2.rewindConversation(c.req.param("sessionId"), index));
1137
+ const rewound = pool2.rewindConversation(c.req.param("sessionId"), index);
1138
+ return c.json({ ...rewound, plan: core.surfacePlan(rewound.plan) });
1190
1139
  } catch (err) {
1191
1140
  return failure(c, err, "conversation edit");
1192
1141
  }
1193
1142
  });
1194
1143
  router.post("/:sessionId/conversation/compact", async (c) => {
1195
1144
  try {
1196
- return c.json(await pool2.compactConversation(c.req.param("sessionId")));
1145
+ const compacted = await pool2.compactConversation(c.req.param("sessionId"));
1146
+ return c.json({ ...compacted, plan: core.surfacePlan(compacted.plan) });
1197
1147
  } catch (err) {
1198
1148
  return failure(c, err, "conversation edit");
1199
1149
  }
@@ -1286,44 +1236,14 @@ function settingsRoute(pool2) {
1286
1236
  });
1287
1237
  return router;
1288
1238
  }
1289
- var COMMANDS = [
1290
- { name: "tdd", description: "Toggle Test-Driven Development mode (on|off|status)" },
1291
- { name: "verify", description: "Toggle verification mode \u2014 adds a final evidence-based verification task that runs the full suite (on|off|status)" },
1292
- { name: "transport", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run (terminal|structured|status)" }
1293
- ];
1294
- var BOOLEAN_TOGGLES = { tdd: "tdd", verify: "verification" };
1295
- function commandsRoute(pool2) {
1239
+ var COMMANDS = [];
1240
+ function commandsRoute() {
1296
1241
  const router = new hono.Hono();
1297
1242
  router.get("/", (c) => {
1298
1243
  return c.json({ commands: COMMANDS });
1299
1244
  });
1300
- router.post("/:name", async (c) => {
1245
+ router.post("/:name", (c) => {
1301
1246
  const name = c.req.param("name");
1302
- const body = await c.req.json().catch(() => ({}));
1303
- const args2 = body?.args || {};
1304
- const command = COMMANDS.find((cmd) => cmd.name === name);
1305
- if (!command) {
1306
- return refuse(c, 404, `Unknown command: ${name}`);
1307
- }
1308
- const toggle = BOOLEAN_TOGGLES[name];
1309
- if (toggle) {
1310
- const action = args2.action || "status";
1311
- if (action === "on") {
1312
- pool2.updateSettings({ [toggle]: { enabled: true } });
1313
- } else if (action === "off") {
1314
- pool2.updateSettings({ [toggle]: { enabled: false } });
1315
- }
1316
- return c.json({ ok: true, settings: pool2.getSettings() });
1317
- }
1318
- if (name === "transport") {
1319
- const action = args2.action || "status";
1320
- if (action === "terminal" || action === "structured") {
1321
- pool2.updateSettings({ runnerTransport: action });
1322
- } else if (action !== "status") {
1323
- return refuse(c, 400, `Unknown transport: ${action} (terminal|structured)`);
1324
- }
1325
- return c.json({ ok: true, settings: pool2.getSettings() });
1326
- }
1327
1247
  return refuse(c, 404, `Unknown command: ${name}`);
1328
1248
  });
1329
1249
  return router;
@@ -1430,7 +1350,7 @@ function createApp(pool2, admission2) {
1430
1350
  app2.route("/api/runners", runnersRoute(pool2));
1431
1351
  app2.route("/api/models", modelsRoute(pool2));
1432
1352
  app2.route("/api/settings", settingsRoute(pool2));
1433
- app2.route("/api/commands", commandsRoute(pool2));
1353
+ app2.route("/api/commands", commandsRoute());
1434
1354
  app2.route("/api/workspaces", workspacesRoute());
1435
1355
  app2.route("/api/approvals", approvalsRoute(pool2));
1436
1356
  return app2;
@@ -1518,10 +1438,9 @@ if (portIdx !== -1 && args[portIdx + 1]) {
1518
1438
  }
1519
1439
  var watchParentIdx = args.indexOf("--watch-parent");
1520
1440
  var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
1521
- var { runner: tmuxRunner, advice: terminalAdvice } = startTerminalHost(port);
1522
1441
  var { token, file: tokenFile } = core.mintDaemonToken(port);
1523
1442
  var admission = { port, token, tokenFile };
1524
- var pool = new OrchestratorPool({ runner: tmuxRunner, terminalAdvice, structuredRunner: new core.StructuredRunner() });
1443
+ var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
1525
1444
  var app = createApp(pool, admission);
1526
1445
  pool.getProviderModels().catch((err) => {
1527
1446
  console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
@@ -1532,11 +1451,9 @@ attachWsHandler(server, pool, admission);
1532
1451
  server.listen(port, "127.0.0.1", () => {
1533
1452
  console.error(`[web] Ordewell API server: http://localhost:${port}`);
1534
1453
  });
1535
- async function shutdown() {
1454
+ function shutdown() {
1536
1455
  core.clearDaemonToken(port);
1537
1456
  pool.destroyAll();
1538
- if (tmuxRunner) await tmuxRunner.killSession().catch(() => {
1539
- });
1540
1457
  server.close();
1541
1458
  process.exit(0);
1542
1459
  }