@enderfga/claw-orchestrator 4.7.0 → 4.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.
Files changed (49) hide show
  1. package/README.md +8 -9
  2. package/configs/autoloop-coder-prompt.md +4 -4
  3. package/configs/autoloop-planner-prompt.md +10 -10
  4. package/configs/autoloop-reviewer-prompt.md +3 -4
  5. package/dist/src/autoloop/dispatcher.d.ts +59 -10
  6. package/dist/src/autoloop/dispatcher.js +261 -40
  7. package/dist/src/autoloop/dispatcher.js.map +1 -1
  8. package/dist/src/autoloop/planner-tools.d.ts +4 -3
  9. package/dist/src/autoloop/planner-tools.js +35 -2
  10. package/dist/src/autoloop/planner-tools.js.map +1 -1
  11. package/dist/src/autoloop/types.d.ts +2 -0
  12. package/dist/src/autoloop/types.js.map +1 -1
  13. package/dist/src/dashboard/index.html +185 -94
  14. package/dist/src/embedded-server.js +67 -9
  15. package/dist/src/embedded-server.js.map +1 -1
  16. package/dist/src/index.js +84 -52
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/persistent-agy-session.js +4 -1
  19. package/dist/src/persistent-agy-session.js.map +1 -1
  20. package/dist/src/persistent-codex-session.js +3 -0
  21. package/dist/src/persistent-codex-session.js.map +1 -1
  22. package/dist/src/persistent-cursor-session.d.ts +6 -0
  23. package/dist/src/persistent-cursor-session.js +98 -3
  24. package/dist/src/persistent-cursor-session.js.map +1 -1
  25. package/dist/src/persistent-custom-session.js +13 -0
  26. package/dist/src/persistent-custom-session.js.map +1 -1
  27. package/dist/src/persistent-gemini-session.d.ts +4 -0
  28. package/dist/src/persistent-gemini-session.js +55 -2
  29. package/dist/src/persistent-gemini-session.js.map +1 -1
  30. package/dist/src/persistent-opencode-session.d.ts +5 -5
  31. package/dist/src/persistent-opencode-session.js +58 -6
  32. package/dist/src/persistent-opencode-session.js.map +1 -1
  33. package/dist/src/persistent-session.js +6 -1
  34. package/dist/src/persistent-session.js.map +1 -1
  35. package/dist/src/session-manager.d.ts +44 -5
  36. package/dist/src/session-manager.js +190 -18
  37. package/dist/src/session-manager.js.map +1 -1
  38. package/dist/src/types.d.ts +3 -2
  39. package/package.json +1 -1
  40. package/skills/SKILL.md +18 -26
  41. package/skills/references/autoloop.md +49 -6
  42. package/skills/references/claude-cli-tracking.md +2 -1
  43. package/skills/references/cli.md +2 -2
  44. package/skills/references/council.md +1 -1
  45. package/skills/references/getting-started.md +4 -4
  46. package/skills/references/multi-engine.md +21 -34
  47. package/skills/references/openai-compat.md +1 -1
  48. package/skills/references/sessions.md +3 -3
  49. package/skills/references/tools.md +39 -20
@@ -27,7 +27,12 @@
27
27
  margin: 0;
28
28
  background: var(--bg);
29
29
  color: var(--text);
30
- font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
30
+ font:
31
+ 14px/1.5 -apple-system,
32
+ BlinkMacSystemFont,
33
+ 'Segoe UI',
34
+ Roboto,
35
+ sans-serif;
31
36
  }
32
37
  a {
33
38
  color: var(--info);
@@ -270,8 +275,7 @@
270
275
  .entryBody {
271
276
  white-space: pre-wrap;
272
277
  word-wrap: break-word;
273
- font-family:
274
- ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
278
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
275
279
  font-size: 12px;
276
280
  background: #fafafa;
277
281
  padding: 8px 10px;
@@ -379,11 +383,23 @@
379
383
  align-self: flex-start;
380
384
  }
381
385
  .forge-mode[data-mode='build'],
382
- .forge-mode[data-mode='building'] { background: var(--info); color: white; }
386
+ .forge-mode[data-mode='building'] {
387
+ background: var(--info);
388
+ color: white;
389
+ }
383
390
  .forge-mode[data-mode='done'],
384
- .forge-mode[data-mode='build-complete'] { background: var(--accent); color: white; }
385
- .forge-mode[data-mode='failed'] { background: var(--error); color: white; }
386
- .forge-mode[data-mode='queued'] { background: var(--warn); color: white; }
391
+ .forge-mode[data-mode='build-complete'] {
392
+ background: var(--accent);
393
+ color: white;
394
+ }
395
+ .forge-mode[data-mode='failed'] {
396
+ background: var(--error);
397
+ color: white;
398
+ }
399
+ .forge-mode[data-mode='queued'] {
400
+ background: var(--warn);
401
+ color: white;
402
+ }
387
403
  .forge-empty {
388
404
  margin: auto;
389
405
  max-width: 440px;
@@ -402,9 +418,19 @@
402
418
  border-radius: 4px;
403
419
  font-size: 13px;
404
420
  }
405
- .forge-msg.user { background: #e0eaff; align-self: flex-end; }
406
- .forge-msg.assistant { background: var(--surface); border: 1px solid var(--border); }
407
- .forge-msg.system { background: var(--surface-2); color: var(--muted); font-size: 12px; }
421
+ .forge-msg.user {
422
+ background: #e0eaff;
423
+ align-self: flex-end;
424
+ }
425
+ .forge-msg.assistant {
426
+ background: var(--surface);
427
+ border: 1px solid var(--border);
428
+ }
429
+ .forge-msg.system {
430
+ background: var(--surface-2);
431
+ color: var(--muted);
432
+ font-size: 12px;
433
+ }
408
434
  .forge-share {
409
435
  background: var(--surface);
410
436
  border: 1px solid var(--accent);
@@ -412,9 +438,20 @@
412
438
  padding: 12px;
413
439
  margin: 8px 0;
414
440
  }
415
- .forge-share strong { color: var(--accent); display: block; margin-bottom: 6px; }
416
- .forge-share a { color: var(--accent); word-break: break-all; }
417
- .forge-share-actions { display: flex; gap: 8px; margin-top: 10px; }
441
+ .forge-share strong {
442
+ color: var(--accent);
443
+ display: block;
444
+ margin-bottom: 6px;
445
+ }
446
+ .forge-share a {
447
+ color: var(--accent);
448
+ word-break: break-all;
449
+ }
450
+ .forge-share-actions {
451
+ display: flex;
452
+ gap: 8px;
453
+ margin-top: 10px;
454
+ }
418
455
  .forge-share-actions button {
419
456
  padding: 4px 12px;
420
457
  font-size: 12px;
@@ -436,13 +473,17 @@
436
473
  font-size: 12px;
437
474
  overflow-x: auto;
438
475
  }
439
- .forge-public-modal h4 { margin-bottom: 4px; }
476
+ .forge-public-modal h4 {
477
+ margin-bottom: 4px;
478
+ }
440
479
  .runItem .lifecycle-actions {
441
480
  display: none;
442
481
  gap: 4px;
443
482
  margin-top: 4px;
444
483
  }
445
- .runItem:hover .lifecycle-actions { display: flex; }
484
+ .runItem:hover .lifecycle-actions {
485
+ display: flex;
486
+ }
446
487
  .lifecycle-actions button {
447
488
  font-size: 10px;
448
489
  padding: 2px 6px;
@@ -451,7 +492,9 @@
451
492
  border-radius: 3px;
452
493
  cursor: pointer;
453
494
  }
454
- .lifecycle-actions button:hover { background: var(--surface-2); }
495
+ .lifecycle-actions button:hover {
496
+ background: var(--surface-2);
497
+ }
455
498
  .forge-question {
456
499
  background: var(--surface);
457
500
  border: 1px solid var(--border);
@@ -459,9 +502,21 @@
459
502
  padding: 12px;
460
503
  margin: 8px 0;
461
504
  }
462
- .forge-question .ctx { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
463
- .forge-question .q { font-weight: 600; margin-bottom: 8px; }
464
- .forge-question .opts { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
505
+ .forge-question .ctx {
506
+ color: var(--muted);
507
+ font-size: 12px;
508
+ margin-bottom: 8px;
509
+ }
510
+ .forge-question .q {
511
+ font-weight: 600;
512
+ margin-bottom: 8px;
513
+ }
514
+ .forge-question .opts {
515
+ display: flex;
516
+ flex-wrap: wrap;
517
+ gap: 6px;
518
+ margin-bottom: 8px;
519
+ }
465
520
  .forge-question .opt {
466
521
  padding: 4px 10px;
467
522
  border: 1px solid var(--border);
@@ -470,8 +525,14 @@
470
525
  font-size: 12px;
471
526
  background: var(--surface);
472
527
  }
473
- .forge-question .opt.recommended { border-color: var(--accent); }
474
- .forge-question .opt.selected { background: var(--accent); color: white; border-color: var(--accent); }
528
+ .forge-question .opt.recommended {
529
+ border-color: var(--accent);
530
+ }
531
+ .forge-question .opt.selected {
532
+ background: var(--accent);
533
+ color: white;
534
+ border-color: var(--accent);
535
+ }
475
536
  .forge-input {
476
537
  display: grid;
477
538
  grid-template-columns: 1fr auto;
@@ -500,7 +561,10 @@
500
561
  color: white;
501
562
  cursor: pointer;
502
563
  }
503
- .forge-input button:disabled { opacity: 0.4; cursor: not-allowed; }
564
+ .forge-input button:disabled {
565
+ opacity: 0.4;
566
+ cursor: not-allowed;
567
+ }
504
568
  .forge-spec {
505
569
  border-left: 1px solid var(--border);
506
570
  padding: 16px;
@@ -509,7 +573,9 @@
509
573
  display: flex;
510
574
  flex-direction: column;
511
575
  }
512
- .forge-spec h3 { margin: 0 0 8px 0; }
576
+ .forge-spec h3 {
577
+ margin: 0 0 8px 0;
578
+ }
513
579
  .forge-spec textarea {
514
580
  width: 100%;
515
581
  font-family: ui-monospace, SFMono-Regular, monospace;
@@ -532,7 +598,10 @@
532
598
  background: var(--surface);
533
599
  cursor: pointer;
534
600
  }
535
- .forge-spec-msg { font-size: 11px; color: var(--muted); }
601
+ .forge-spec-msg {
602
+ font-size: 11px;
603
+ color: var(--muted);
604
+ }
536
605
  #forgeNew,
537
606
  #councilNew,
538
607
  #autoloopNew {
@@ -561,7 +630,9 @@
561
630
  justify-content: center;
562
631
  z-index: 100;
563
632
  }
564
- .modal-root[hidden] { display: none; }
633
+ .modal-root[hidden] {
634
+ display: none;
635
+ }
565
636
  .modal {
566
637
  background: var(--surface);
567
638
  border-radius: 8px;
@@ -576,7 +647,10 @@
576
647
  padding: 14px 20px;
577
648
  border-bottom: 1px solid var(--border);
578
649
  }
579
- .modal-header h2 { margin: 0; font-size: 15px; }
650
+ .modal-header h2 {
651
+ margin: 0;
652
+ font-size: 15px;
653
+ }
580
654
  .modal-header button {
581
655
  background: transparent;
582
656
  border: none;
@@ -637,8 +711,14 @@
637
711
  margin: 80px auto 0;
638
712
  max-width: 420px;
639
713
  }
640
- .empty-cta h2 { margin: 0 0 8px; font-size: 16px; }
641
- .empty-cta p { margin: 0 0 16px; color: var(--muted); }
714
+ .empty-cta h2 {
715
+ margin: 0 0 8px;
716
+ font-size: 16px;
717
+ }
718
+ .empty-cta p {
719
+ margin: 0 0 16px;
720
+ color: var(--muted);
721
+ }
642
722
  .empty-cta button {
643
723
  padding: 8px 16px;
644
724
  border-radius: 4px;
@@ -658,10 +738,24 @@
658
738
  cursor: pointer;
659
739
  font-weight: 600;
660
740
  }
661
- #forgeStartBuild:disabled { opacity: 0.4; cursor: not-allowed; }
662
- .forge-versions { margin-top: 12px; }
663
- .forge-versions h4 { margin: 0 0 6px 0; font-size: 12px; color: var(--muted); text-transform: uppercase; }
664
- .version-list { list-style: none; padding: 0; margin: 0; }
741
+ #forgeStartBuild:disabled {
742
+ opacity: 0.4;
743
+ cursor: not-allowed;
744
+ }
745
+ .forge-versions {
746
+ margin-top: 12px;
747
+ }
748
+ .forge-versions h4 {
749
+ margin: 0 0 6px 0;
750
+ font-size: 12px;
751
+ color: var(--muted);
752
+ text-transform: uppercase;
753
+ }
754
+ .version-list {
755
+ list-style: none;
756
+ padding: 0;
757
+ margin: 0;
758
+ }
665
759
  .version-row {
666
760
  display: flex;
667
761
  gap: 8px;
@@ -677,8 +771,14 @@
677
771
  font-family: monospace;
678
772
  font-weight: 600;
679
773
  }
680
- .version-meta { color: var(--muted); }
681
- .version-row a { color: var(--accent); text-decoration: none; word-break: break-all; }
774
+ .version-meta {
775
+ color: var(--muted);
776
+ }
777
+ .version-row a {
778
+ color: var(--accent);
779
+ text-decoration: none;
780
+ word-break: break-all;
781
+ }
682
782
  .version-row button {
683
783
  margin-left: auto;
684
784
  padding: 2px 8px;
@@ -688,7 +788,10 @@
688
788
  border-radius: 3px;
689
789
  cursor: pointer;
690
790
  }
691
- .version-row button:disabled { opacity: 0.4; cursor: not-allowed; }
791
+ .version-row button:disabled {
792
+ opacity: 0.4;
793
+ cursor: not-allowed;
794
+ }
692
795
  </style>
693
796
  </head>
694
797
  <body>
@@ -733,19 +836,11 @@
733
836
  <div data-for-tab="council" style="display: contents">
734
837
  <label>
735
838
  Task
736
- <textarea
737
- name="task"
738
- rows="3"
739
- placeholder="What should the council achieve?"
740
- ></textarea>
839
+ <textarea name="task" rows="3" placeholder="What should the council achieve?"></textarea>
741
840
  </label>
742
841
  <label>
743
842
  Project directory
744
- <input
745
- name="projectDir"
746
- type="text"
747
- placeholder="/abs/path/to/workspace"
748
- />
843
+ <input name="projectDir" type="text" placeholder="/abs/path/to/workspace" />
749
844
  </label>
750
845
  <label>
751
846
  Max rounds
@@ -756,11 +851,7 @@
756
851
  <div data-for-tab="autoloop" style="display: contents">
757
852
  <label>
758
853
  Workspace
759
- <input
760
- name="workspace"
761
- type="text"
762
- placeholder="/abs/path/to/git/workspace"
763
- />
854
+ <input name="workspace" type="text" placeholder="/abs/path/to/git/workspace" />
764
855
  </label>
765
856
  <label>
766
857
  Planner model (optional)
@@ -807,11 +898,11 @@
807
898
  };
808
899
 
809
900
  const $ = (id) => document.getElementById(id);
810
- const fmtTime = (iso) =>
811
- iso ? new Date(iso).toLocaleTimeString(undefined, { hour12: false }) : '';
901
+ const fmtTime = (iso) => (iso ? new Date(iso).toLocaleTimeString(undefined, { hour12: false }) : '');
812
902
  const escapeHtml = (s) =>
813
- String(s ?? '').replace(/[&<>"']/g, (c) =>
814
- ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
903
+ String(s ?? '').replace(
904
+ /[&<>"']/g,
905
+ (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
815
906
  );
816
907
 
817
908
  // ─── Tab switching ────────────────────────────────────────────
@@ -826,11 +917,7 @@
826
917
  b.setAttribute('aria-selected', String(b.dataset.tab === tab));
827
918
  });
828
919
  $('sidebarTitle').textContent =
829
- tab === 'autoloop'
830
- ? 'Autoloop runs'
831
- : tab === 'council'
832
- ? 'Council sessions'
833
- : 'Forge apps';
920
+ tab === 'autoloop' ? 'Autoloop runs' : tab === 'council' ? 'Council sessions' : 'Forge apps';
834
921
  $('forgeNew').hidden = tab !== 'forge';
835
922
  $('councilNew').hidden = tab !== 'council';
836
923
  $('autoloopNew').hidden = tab !== 'autoloop';
@@ -879,11 +966,7 @@
879
966
  const noun = labels[state.tab] || 'run';
880
967
  if (state.runs.length === 0) {
881
968
  const triggerId =
882
- state.tab === 'autoloop'
883
- ? 'autoloopNew'
884
- : state.tab === 'council'
885
- ? 'councilNew'
886
- : 'forgeNew';
969
+ state.tab === 'autoloop' ? 'autoloopNew' : state.tab === 'council' ? 'councilNew' : 'forgeNew';
887
970
  $('main').innerHTML = `
888
971
  <div class="empty empty-cta">
889
972
  <h2>No ${escapeHtml(noun)}s yet</h2>
@@ -892,8 +975,7 @@
892
975
  </div>
893
976
  `;
894
977
  } else {
895
- $('main').innerHTML =
896
- '<div class="empty" style="margin-top:80px">Select a run to view.</div>';
978
+ $('main').innerHTML = '<div class="empty" style="margin-top:80px">Select a run to view.</div>';
897
979
  }
898
980
  }
899
981
 
@@ -901,20 +983,15 @@
901
983
  async function refreshList() {
902
984
  try {
903
985
  const url =
904
- state.tab === 'autoloop'
905
- ? '/autoloop/list'
906
- : state.tab === 'council'
907
- ? '/council/list'
908
- : '/ultraapp/list';
909
- const opts = state.tab === 'forge' ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' } : {};
986
+ state.tab === 'autoloop' ? '/autoloop/list' : state.tab === 'council' ? '/council/list' : '/ultraapp/list';
987
+ const opts =
988
+ state.tab === 'forge'
989
+ ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }
990
+ : {};
910
991
  const r = await fetch(url, opts);
911
992
  const j = await r.json();
912
993
  state.runs =
913
- state.tab === 'autoloop'
914
- ? j.runs || []
915
- : state.tab === 'council'
916
- ? j.councils || []
917
- : j.runs || [];
994
+ state.tab === 'autoloop' ? j.runs || [] : state.tab === 'council' ? j.councils || [] : j.runs || [];
918
995
  renderList();
919
996
  // Refresh the empty-state CTA — runs may have just appeared or
920
997
  // disappeared, and renderEmptyMain's text depends on state.runs.length.
@@ -929,12 +1006,8 @@
929
1006
  function renderList() {
930
1007
  const ul = $('runlist');
931
1008
  if (state.runs.length === 0) {
932
- const noun =
933
- state.tab === 'autoloop' ? 'runs' : state.tab === 'council' ? 'councils' : 'apps';
934
- ul.innerHTML =
935
- '<li class="empty" style="padding:20px;list-style:none">No active ' +
936
- noun +
937
- '.</li>';
1009
+ const noun = state.tab === 'autoloop' ? 'runs' : state.tab === 'council' ? 'councils' : 'apps';
1010
+ ul.innerHTML = '<li class="empty" style="padding:20px;list-style:none">No active ' + noun + '.</li>';
938
1011
  return;
939
1012
  }
940
1013
  const html = state.runs
@@ -998,12 +1071,26 @@
998
1071
  const id = el.dataset.id;
999
1072
  if (state.tab === 'autoloop') {
1000
1073
  if (action !== 'delete') return;
1001
- if (!confirm(`Delete autoloop run "${id}"? The registry entry is removed; the ledger directory under tasks/${id}/ is kept on disk.`)) return;
1074
+ if (
1075
+ !confirm(
1076
+ `Delete autoloop run "${id}"? The registry entry is removed; the ledger directory under tasks/${id}/ is kept on disk.`,
1077
+ )
1078
+ )
1079
+ return;
1002
1080
  try {
1003
- const r = await fetch(`/autoloop/${encodeURIComponent(id)}/delete`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
1081
+ const r = await fetch(`/autoloop/${encodeURIComponent(id)}/delete`, {
1082
+ method: 'POST',
1083
+ headers: { 'content-type': 'application/json' },
1084
+ body: '{}',
1085
+ });
1004
1086
  if (!r.ok) {
1005
1087
  let detail = `HTTP ${r.status}`;
1006
- try { const j = await r.json(); if (j && j.error) detail = j.error; } catch { /* ignore */ }
1088
+ try {
1089
+ const j = await r.json();
1090
+ if (j && j.error) detail = j.error;
1091
+ } catch {
1092
+ /* ignore */
1093
+ }
1007
1094
  alert(`Delete failed: ${detail}`);
1008
1095
  return;
1009
1096
  }
@@ -1020,7 +1107,11 @@
1020
1107
  refreshList();
1021
1108
  return;
1022
1109
  }
1023
- if (action === 'delete' && !confirm('Delete this run? Container, image, and on-disk artifacts are removed.')) return;
1110
+ if (
1111
+ action === 'delete' &&
1112
+ !confirm('Delete this run? Container, image, and on-disk artifacts are removed.')
1113
+ )
1114
+ return;
1024
1115
  await forgeLifecycleAction(id, action);
1025
1116
  refreshList();
1026
1117
  });
@@ -1272,7 +1363,11 @@
1272
1363
  });
1273
1364
  const j = await r.json();
1274
1365
  if (!j.ok) {
1275
- renderChatEntry({ role: 'system', kind: 'error', text: '[error] ' + action + ': ' + (j.error || 'unknown') });
1366
+ renderChatEntry({
1367
+ role: 'system',
1368
+ kind: 'error',
1369
+ text: '[error] ' + action + ': ' + (j.error || 'unknown'),
1370
+ });
1276
1371
  }
1277
1372
  if (action === 'delete' && j.ok && state.forge.runId === runId) {
1278
1373
  // Run is gone — close it and refresh sidebar
@@ -1856,10 +1951,7 @@
1856
1951
  const s = state.council.session;
1857
1952
  if (!s) return;
1858
1953
  $('c-status').innerHTML = `<span class="badge ${s.status}">${s.status}</span>`;
1859
- const rounds =
1860
- s.responses && s.responses.length > 0
1861
- ? Math.max(...s.responses.map((r) => r.round))
1862
- : 0;
1954
+ const rounds = s.responses && s.responses.length > 0 ? Math.max(...s.responses.map((r) => r.round)) : 0;
1863
1955
  $('c-round').textContent = rounds;
1864
1956
  $('c-agents').textContent = (s.config?.agents || []).map((a) => a.name).join(', ');
1865
1957
  $('c-task').textContent = s.task || '';
@@ -1944,8 +2036,7 @@
1944
2036
  // both, switching visible fields by state.tab.
1945
2037
  function openLauncherModal() {
1946
2038
  const root = $('modalRoot');
1947
- $('modalTitle').textContent =
1948
- state.tab === 'council' ? 'New Council session' : 'New Autoloop run';
2039
+ $('modalTitle').textContent = state.tab === 'council' ? 'New Council session' : 'New Autoloop run';
1949
2040
  root.querySelectorAll('[data-for-tab]').forEach((el) => {
1950
2041
  const visible = el.dataset.forTab === state.tab;
1951
2042
  el.style.display = visible ? 'contents' : 'none';
@@ -18,6 +18,46 @@ import { DEFAULT_SERVER_PORT, MAX_BODY_SIZE, RATE_LIMIT_MAX_REQUESTS, RATE_LIMIT
18
18
  // Grace period for server.close() to drain connections before lingering
19
19
  // SSE/keep-alive sockets are force-dropped (otherwise close() hangs forever).
20
20
  const SERVER_CLOSE_GRACE_MS = 5000;
21
+ function autoloopErrorStatus(error) {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ if (/^Autoloop run '.+' not found in registry$/.test(message))
24
+ return 404;
25
+ if (/^Autoloop with id '.+' (?:already exists|is being deleted|is still starting)$/.test(message))
26
+ return 409;
27
+ if (/^Autoloop session name '.+' is already in use$/.test(message))
28
+ return 409;
29
+ if (/^(?:Planner|Coder|Reviewer) engine '.+' is not supported$/.test(message))
30
+ return 400;
31
+ // Any custom-engine config complaint is caller error, not a server fault.
32
+ // The old form pinned a single dotted segment and the verb "must be", so
33
+ // `config.args.permissionMode must be a string` and `config.env must contain
34
+ // only string values` both fell through to 500 — the opposite of the split
35
+ // this helper exists to provide.
36
+ if (/^(?:Planner|Coder|Reviewer) custom engine config\b/.test(message))
37
+ return 400;
38
+ return 500;
39
+ }
40
+ /**
41
+ * Custom engines name an arbitrary executable (`bin`) plus argv and env. That
42
+ * is fine for a local, programmatic caller, but this HTTP surface is routinely
43
+ * reverse-tunnelled to a public hostname, and its auth token is a *monitoring*
44
+ * credential — it was never meant to confer "run any binary on the host".
45
+ * Accepting a custom-engine object from the request body would turn any
46
+ * dashboard session into remote code execution, so the network surface refuses
47
+ * it outright. Built-in engines (the actual ask in #72) stay fully selectable.
48
+ */
49
+ const CUSTOM_ENGINE_BODY_KEYS = ['planner_custom_engine', 'coder_custom_engine', 'reviewer_custom_engine'];
50
+ function rejectCustomEngineOverHttp(body) {
51
+ if (typeof body !== 'object' || body === null)
52
+ return null;
53
+ const record = body;
54
+ for (const key of CUSTOM_ENGINE_BODY_KEYS) {
55
+ if (record[key] !== undefined && record[key] !== null) {
56
+ return `${key} is not accepted over HTTP: a custom engine names an executable to spawn, so it may only be configured by a local caller (MCP tool / SessionManager API). Use a built-in engine here.`;
57
+ }
58
+ }
59
+ return null;
60
+ }
21
61
  export class EmbeddedServer {
22
62
  server = null;
23
63
  manager;
@@ -636,12 +676,18 @@ export class EmbeddedServer {
636
676
  }
637
677
  // ─── Autoloop — launch new (dashboard "+ New" button) ───────
638
678
  //
639
- // Minimal contract: { workspace, run_id?, planner_model?, send_timeout_ms? }.
679
+ // Contract: workspace/run_id plus optional per-role engine/model/custom-engine config.
640
680
  // When run_id is omitted or malformed, the server generates
641
681
  // `auto-{timestamp}-{4-byte-hex}`. Power users wanting a meaningful id
642
682
  // (e.g. "ml-refactor-v2") can pass run_id explicitly.
643
683
  if (path === '/autoloop/new') {
644
- const workspace = body.workspace;
684
+ const customEngineRejection = rejectCustomEngineOverHttp(body);
685
+ if (customEngineRejection) {
686
+ json(400, { ok: false, error: customEngineRejection });
687
+ return;
688
+ }
689
+ const input = body;
690
+ const workspace = input.workspace;
645
691
  if (typeof workspace !== 'string' || !workspace.trim()) {
646
692
  json(400, { ok: false, error: 'workspace (string) required' });
647
693
  return;
@@ -651,7 +697,7 @@ export class EmbeddedServer {
651
697
  json(400, { ok: false, error: 'workspace failed sanitization' });
652
698
  return;
653
699
  }
654
- const explicitId = body.run_id;
700
+ const explicitId = input.run_id;
655
701
  const runId = typeof explicitId === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(explicitId)
656
702
  ? explicitId
657
703
  : `auto-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
@@ -659,8 +705,13 @@ export class EmbeddedServer {
659
705
  const result = await this.manager.autoloopStart({
660
706
  runId,
661
707
  workspace: safeWorkspace,
662
- plannerModel: body.planner_model,
663
- sendTimeoutMs: body.send_timeout_ms,
708
+ plannerEngine: input.planner_engine,
709
+ plannerModel: input.planner_model,
710
+ coderEngine: input.coder_engine,
711
+ coderModel: input.coder_model,
712
+ reviewerEngine: input.reviewer_engine,
713
+ reviewerModel: input.reviewer_model,
714
+ sendTimeoutMs: input.send_timeout_ms,
664
715
  });
665
716
  json(200, {
666
717
  ok: true,
@@ -669,7 +720,7 @@ export class EmbeddedServer {
669
720
  });
670
721
  }
671
722
  catch (err) {
672
- json(400, { ok: false, error: err.message });
723
+ json(autoloopErrorStatus(err), { ok: false, error: err.message });
673
724
  }
674
725
  return;
675
726
  }
@@ -907,13 +958,20 @@ export class EmbeddedServer {
907
958
  const v2ResumeMatch = path.match(/^\/autoloop\/([^/]+)\/resume$/);
908
959
  if (v2ResumeMatch) {
909
960
  const id = v2ResumeMatch[1];
961
+ const resumeRejection = rejectCustomEngineOverHttp(body);
962
+ if (resumeRejection) {
963
+ json(400, { ok: false, error: resumeRejection });
964
+ return;
965
+ }
910
966
  try {
911
- const state = await this.manager.autoloopResume(id);
967
+ // No custom-engine configs here by design (see rejectCustomEngineOverHttp).
968
+ // A run whose roles used custom engines therefore cannot be resumed over
969
+ // HTTP; autoloopResume reports that as a caller error, not a 500.
970
+ const state = await this.manager.autoloopResume(id, {});
912
971
  json(200, { ok: true, state });
913
972
  }
914
973
  catch (err) {
915
- const msg = err.message;
916
- json(/not found/i.test(msg) ? 404 : 500, { ok: false, error: msg });
974
+ json(autoloopErrorStatus(err), { ok: false, error: err.message });
917
975
  }
918
976
  return;
919
977
  }