@yemi33/minions 0.1.2454 → 0.1.2455

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 (41) hide show
  1. package/bin/install-internal-minions.js +136 -6
  2. package/bin/minions.js +27 -13
  3. package/dashboard/js/agent-identity.js +121 -0
  4. package/dashboard/js/modal.js +4 -0
  5. package/dashboard/js/refresh.js +38 -3
  6. package/dashboard/js/render-agents.js +14 -2
  7. package/dashboard/js/render-dispatch.js +2 -2
  8. package/dashboard/js/render-other.js +147 -13
  9. package/dashboard/js/render-prd.js +6 -5
  10. package/dashboard/js/render-prs.js +1 -1
  11. package/dashboard/js/render-work-items.js +436 -21
  12. package/dashboard/js/utils.js +40 -0
  13. package/dashboard/pages/tools.html +1 -0
  14. package/dashboard/pages/work.html +13 -0
  15. package/dashboard/styles.css +73 -0
  16. package/dashboard-build.js +2 -2
  17. package/dashboard.js +93 -7
  18. package/docs/README.md +1 -0
  19. package/docs/copilot-cli-schema.md +1 -0
  20. package/docs/engine-restart.md +4 -2
  21. package/docs/internal-install.md +44 -5
  22. package/docs/named-agents.md +48 -0
  23. package/docs/runtime-adapters.md +39 -0
  24. package/docs/temporary-agents.md +172 -0
  25. package/engine/ado/comment.js +261 -4
  26. package/engine/agents/llm.js +26 -0
  27. package/engine/agents/playbook.js +2 -1
  28. package/engine/core/queries.js +27 -2
  29. package/engine/core/shared.js +148 -1
  30. package/engine/operations/cli.js +102 -1
  31. package/engine/orchestration/lifecycle.js +7 -0
  32. package/engine/orchestration/routing.js +4 -1
  33. package/engine/providers/gh-comment.js +159 -0
  34. package/engine/recovery/stop-stack.js +16 -2
  35. package/engine/runtimes/claude.js +3 -0
  36. package/engine/runtimes/codex.js +4 -0
  37. package/engine/runtimes/copilot.js +23 -2
  38. package/engine.js +2 -2
  39. package/package.json +1 -1
  40. package/playbooks/fix.md +23 -2
  41. package/playbooks/shared-rules.md +24 -2
@@ -1,6 +1,7 @@
1
1
  <section id="work-items-section" style="overflow:visible">
2
2
  <h2>Work Items <span class="count" id="wi-count">0</span>
3
3
  <button class="btn-add" style="margin-left:8px" onclick="openCreateWorkItemModal()">+ New</button>
4
+ <button id="wi-bulk-toggle" type="button" class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;margin-left:4px" onclick="wiToggleBulkMode()" aria-pressed="false" aria-controls="wi-bulk-bar" title="Enter bulk-selection mode">Bulk select</button>
4
5
  <button id="work-archive-toggle" type="button" class="pr-pager-btn" style="font-size:var(--text-sm);padding:2px 8px;margin-left:4px" onclick="toggleWorkItemArchive()" aria-expanded="false" aria-controls="work-items-archive">See Archive</button>
5
6
  <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">tasks dispatched to agents — auto-created from PRDs or added manually</span>
6
7
  </h2>
@@ -44,6 +45,18 @@
44
45
  <button class="pr-filter-reset" type="button" onclick="resetWiFilters()">Reset filters</button>
45
46
  <span class="pr-filter-summary" id="wi-filter-summary" aria-live="polite"></span>
46
47
  </div>
48
+ <div id="wi-bulk-bar" class="wi-bulk-bar" role="toolbar" aria-label="Bulk work-item actions" hidden>
49
+ <label class="wi-bulk-selectall"><input type="checkbox" id="wi-bulk-selectall-cb" onchange="wiToggleSelectAll(this)" aria-label="Select all filtered work items"> Select all</label>
50
+ <span id="wi-bulk-count" aria-live="polite">0 selected</span>
51
+ <span class="wi-bulk-actions">
52
+ <button type="button" class="pr-pager-btn" data-bulk-action="cancel" onclick="wiBulkAction('cancel')" disabled>Cancel</button>
53
+ <button type="button" class="pr-pager-btn" data-bulk-action="archive" onclick="wiBulkAction('archive')" disabled>Archive</button>
54
+ <button type="button" class="pr-pager-btn" data-bulk-action="retry" onclick="wiBulkAction('retry')" disabled>Retry</button>
55
+ <button type="button" class="btn-destructive" data-bulk-action="delete" onclick="wiBulkAction('delete')" disabled>Delete</button>
56
+ </span>
57
+ <button type="button" class="pr-filter-reset" onclick="wiClearSelection()">Clear</button>
58
+ <button type="button" class="pr-filter-reset" onclick="wiExitBulkMode()">Exit bulk mode</button>
59
+ </div>
47
60
  <div id="work-items-content"><p class="empty">No work items yet.</p></div>
48
61
  <div id="work-items-archive" style="display:none;margin-top:12px"></div>
49
62
  </section>
@@ -126,6 +126,10 @@
126
126
  .agent-name { font-weight: 600; font-size: var(--text-xl); }
127
127
  .agent-role { font-size: var(--text-base); color: var(--muted); margin-bottom: var(--space-4); }
128
128
  .status-badge { font-size: var(--text-sm); font-weight: 600; padding: var(--space-1) var(--space-4); border-radius: var(--radius-xl); text-transform: uppercase; letter-spacing: 0.5px; }
129
+ /* Temporary-agent marker — additive display identity next to a friendly call
130
+ sign. Neutral, low-emphasis pill so the temp status stays unmistakable
131
+ without competing with the agent name (W-msd8zdzb00i550a8). */
132
+ .agent-temp-badge { display: inline-block; font-size: var(--text-sm); font-weight: 600; padding: 0 var(--space-2); margin-left: var(--space-2); border-radius: var(--radius-sm); text-transform: uppercase; letter-spacing: 0.4px; vertical-align: middle; color: var(--muted); background: var(--surface); border: 1px solid var(--border); }
129
133
  .status-badge.idle { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
130
134
  .status-badge.working { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
131
135
  .status-badge.done { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
@@ -2008,3 +2012,72 @@
2008
2012
  height (no magic 64px, no vh). */
2009
2013
  html.embed-settings .modal-body.settings-body { flex: 1 1 auto; min-height: 0; }
2010
2014
  html.embed-settings .settings-layout { height: 100%; }
2015
+
2016
+ /* ── Work-item overflow menu + bulk-operations mode (W-mscjvm33007r79ef) ──
2017
+ * The overflow (⋯) menu is rendered via document.createElement in
2018
+ * wiOpenOverflowMenu (no innerHTML), appended to <body> and positioned under
2019
+ * the clicked button — mirroring .checkout-mode-menu above. The bulk bar and
2020
+ * selection affordances only appear when bulk mode is explicitly enabled. */
2021
+ .wi-overflow-btn { line-height: 1; }
2022
+ .wi-overflow-menu {
2023
+ position: absolute;
2024
+ z-index: 1000;
2025
+ min-width: 180px;
2026
+ background: var(--surface2);
2027
+ border: 1px solid var(--border);
2028
+ border-radius: var(--radius-md, 6px);
2029
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
2030
+ padding: 4px;
2031
+ display: flex;
2032
+ flex-direction: column;
2033
+ }
2034
+ .wi-overflow-item {
2035
+ display: block;
2036
+ width: 100%;
2037
+ text-align: left;
2038
+ padding: 6px 10px;
2039
+ border: none;
2040
+ background: none;
2041
+ border-radius: 4px;
2042
+ color: var(--text);
2043
+ font-size: var(--text-sm);
2044
+ font-family: inherit;
2045
+ cursor: pointer;
2046
+ }
2047
+ .wi-overflow-item:hover,
2048
+ .wi-overflow-item:focus {
2049
+ background: var(--surface);
2050
+ outline: none;
2051
+ }
2052
+ .wi-overflow-item.wi-overflow-item-destructive { color: var(--red); }
2053
+
2054
+ .wi-bulk-bar {
2055
+ display: flex;
2056
+ align-items: center;
2057
+ gap: var(--space-4, 8px);
2058
+ flex-wrap: wrap;
2059
+ margin: 8px 0;
2060
+ padding: 8px 10px;
2061
+ background: var(--surface2);
2062
+ border: 1px solid var(--border);
2063
+ border-radius: var(--radius-sm, 4px);
2064
+ font-size: var(--text-sm);
2065
+ }
2066
+ .wi-bulk-bar[hidden] { display: none; }
2067
+ .wi-bulk-selectall {
2068
+ display: inline-flex;
2069
+ align-items: center;
2070
+ gap: 6px;
2071
+ cursor: pointer;
2072
+ color: var(--text);
2073
+ }
2074
+ #wi-bulk-count { color: var(--muted); min-width: 80px; }
2075
+ .wi-bulk-actions { display: inline-flex; gap: 6px; flex-wrap: wrap; }
2076
+ .wi-bulk-actions button:disabled { opacity: 0.5; cursor: default; }
2077
+ #wi-bulk-toggle.active {
2078
+ background: var(--blue);
2079
+ color: #fff;
2080
+ border-color: var(--blue);
2081
+ }
2082
+ .wi-bulk-cell { width: 28px; text-align: center; }
2083
+ tr.wi-row-selected > td { background: rgba(80,140,255,0.12); }
@@ -38,7 +38,7 @@ const DASHBOARD_PAGE_SUB_FRAGMENTS = {
38
38
  // Every dashboard/js/*.js file belongs here unless it is lazy-loaded through a
39
39
  // dedicated route (only memory-search.js today, served at /assets/memory-search.js).
40
40
  const DASHBOARD_JS_FILES = [
41
- 'utils', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
41
+ 'utils', 'agent-identity', 'state', 'features-client', 'render-utils', 'charter-editor', 'detail-panel', 'live-stream',
42
42
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
43
43
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
44
44
  'render-other', 'render-managed', 'memory-panel', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
@@ -151,7 +151,7 @@ const SLIM_JS_ORDER = [
151
151
  // charter-editor MUST precede detail-panel (see the "charter-editor registered
152
152
  // before detail-panel" test).
153
153
  const SLIM_CLASSIC_PANEL_JS = [
154
- 'utils', 'render-utils', 'charter-editor', 'live-stream', 'render-agents', 'detail-panel',
154
+ 'utils', 'agent-identity', 'render-utils', 'charter-editor', 'live-stream', 'render-agents', 'detail-panel',
155
155
  ];
156
156
 
157
157
  // Cache for the assembled slim source fragments (layout/css/body/js). Keyed on
package/dashboard.js CHANGED
@@ -2538,6 +2538,9 @@ let _toolsInventoryCacheTs = 0;
2538
2538
  let _toolsInventoryRefreshPromise = null;
2539
2539
  let _toolsInventoryChild = null;
2540
2540
  let _toolsInventoryScanner = _scanToolsInventoryInChild;
2541
+ // Most recent failed refresh ({ message, ts }); cleared on the next success.
2542
+ // Surfaced as a response header so a stale-serve still exposes refresh failure.
2543
+ let _toolsInventoryLastError = null;
2541
2544
 
2542
2545
  function _validateToolsInventoryPayload(payload) {
2543
2546
  if (!payload || typeof payload !== 'object'
@@ -2600,11 +2603,12 @@ function _terminateToolsInventoryChild() {
2600
2603
  try { shared.terminateProcess('dashboard.tools-inventory-shutdown', child); } catch { /* child may already be gone */ }
2601
2604
  }
2602
2605
 
2603
- function _refreshToolsInventory() {
2604
- const now = Date.now();
2605
- if (_toolsInventoryCache && now - _toolsInventoryCacheTs < TOOLS_INVENTORY_CACHE_TTL_MS) {
2606
- return Promise.resolve(_toolsInventoryCache);
2607
- }
2606
+ // Start (or join) the single-flight child-process scan. Concurrent callers
2607
+ // share one in-flight promise so a burst of /api/tools polls never spawns
2608
+ // duplicate children. On success the cache + payload-derived ETag are swapped
2609
+ // in and the last-error is cleared; on failure the error is recorded (so a
2610
+ // stale-serve can still surface it) and re-thrown to the caller that awaited.
2611
+ function _startToolsInventoryScan() {
2608
2612
  if (_toolsInventoryRefreshPromise) return _toolsInventoryRefreshPromise;
2609
2613
 
2610
2614
  _toolsInventoryRefreshPromise = Promise.resolve()
@@ -2616,19 +2620,68 @@ function _refreshToolsInventory() {
2616
2620
  _toolsInventoryCacheJson = json;
2617
2621
  _toolsInventoryCacheEtag = `"tools-${crypto.createHash('sha256').update(json).digest('hex')}"`;
2618
2622
  _toolsInventoryCacheTs = Date.now();
2623
+ _toolsInventoryLastError = null;
2619
2624
  return next;
2620
2625
  })
2626
+ .catch(err => {
2627
+ _toolsInventoryLastError = { message: String((err && err.message) || err), ts: Date.now() };
2628
+ throw err;
2629
+ })
2621
2630
  .finally(() => {
2622
2631
  _toolsInventoryRefreshPromise = null;
2623
2632
  });
2624
2633
  return _toolsInventoryRefreshPromise;
2625
2634
  }
2626
2635
 
2636
+ function _toolsInventoryAgeMs() {
2637
+ return _toolsInventoryCacheTs ? Date.now() - _toolsInventoryCacheTs : Infinity;
2638
+ }
2639
+
2640
+ // Resolve the tools inventory. Default (no opts) preserves the original
2641
+ // block-on-expiry contract used by the CC warm path. `{ allowStale: true }`
2642
+ // opts into stale-while-revalidate: once a cache exists it is always returned
2643
+ // immediately while a background single-flight refresh runs, so the request
2644
+ // never blocks on the multi-second child scan again. Only a truly cold cache
2645
+ // (no inventory yet) blocks so callers never see missing data. A persistently
2646
+ // failing background refresh stays visible via _toolsInventoryLastError (and
2647
+ // the X-Tools-Inventory-Error header) rather than re-introducing a blocking
2648
+ // rescan — a rescan that is itself likely to fail would just restore the very
2649
+ // ~8-10s hang this path removed.
2650
+ function _refreshToolsInventory(opts) {
2651
+ const allowStale = !!(opts && opts.allowStale);
2652
+ const age = _toolsInventoryAgeMs();
2653
+
2654
+ // Fresh — serve without a scan.
2655
+ if (_toolsInventoryCache && age < TOOLS_INVENTORY_CACHE_TTL_MS) {
2656
+ return Promise.resolve(_toolsInventoryCache);
2657
+ }
2658
+
2659
+ // Stale-while-revalidate: any cache, however old, is served now while a
2660
+ // single-flight background refresh runs (errors swallowed here — the
2661
+ // recorded last-error still surfaces via the response header).
2662
+ if (allowStale && _toolsInventoryCache) {
2663
+ const bg = _startToolsInventoryScan();
2664
+ if (bg && typeof bg.catch === 'function') bg.catch(() => { /* surfaced via header */ });
2665
+ return Promise.resolve(_toolsInventoryCache);
2666
+ }
2667
+
2668
+ // Cold cache (nothing to serve) — block on a fresh scan.
2669
+ return _startToolsInventoryScan();
2670
+ }
2671
+
2627
2672
  async function handleToolsInventory(req, res) {
2628
2673
  try {
2629
- await _refreshToolsInventory();
2674
+ await _refreshToolsInventory({ allowStale: true });
2675
+ const age = _toolsInventoryAgeMs();
2676
+ const refreshing = _toolsInventoryRefreshPromise != null;
2630
2677
  res.setHeader('ETag', _toolsInventoryCacheEtag);
2631
2678
  res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
2679
+ // Freshness/error signals so the page can represent stale/refresh/error
2680
+ // state without changing the inventory payload shape.
2681
+ res.setHeader('X-Tools-Inventory-Age-Ms', String(Number.isFinite(age) ? age : 0));
2682
+ if (age >= TOOLS_INVENTORY_CACHE_TTL_MS) res.setHeader('X-Tools-Inventory-Stale', '1');
2683
+ if (refreshing) res.setHeader('X-Tools-Inventory-Refreshing', '1');
2684
+ if (_toolsInventoryLastError) res.setHeader('X-Tools-Inventory-Error', '1');
2632
2685
  if (_ifNoneMatchHasEtag(req?.headers?.['if-none-match'], _toolsInventoryCacheEtag)) {
2633
2686
  res.statusCode = 304;
2634
2687
  res.end();
@@ -2653,9 +2706,26 @@ function _resetToolsInventoryCacheForTesting() {
2653
2706
  _toolsInventoryCacheEtag = null;
2654
2707
  _toolsInventoryCacheTs = 0;
2655
2708
  _toolsInventoryRefreshPromise = null;
2709
+ _toolsInventoryLastError = null;
2656
2710
  _toolsInventoryScanner = _scanToolsInventoryInChild;
2657
2711
  }
2658
2712
 
2713
+ // Age the cache just past the fresh-TTL so the next request exercises the
2714
+ // stale-while-revalidate path (serve cached now, refresh in the background).
2715
+ function _expireToolsInventoryCacheForTesting() {
2716
+ if (_toolsInventoryCacheTs) {
2717
+ _toolsInventoryCacheTs = Date.now() - TOOLS_INVENTORY_CACHE_TTL_MS - 1;
2718
+ }
2719
+ }
2720
+
2721
+ // Age the cache back by an arbitrary amount so a test can drive it far past any
2722
+ // prior staleness window and assert the serve-cached-now contract still holds.
2723
+ function _ageToolsInventoryCacheForTesting(ageMs) {
2724
+ if (_toolsInventoryCacheTs) {
2725
+ _toolsInventoryCacheTs = Date.now() - ageMs;
2726
+ }
2727
+ }
2728
+
2659
2729
  function parsePinnedEntries(content) {
2660
2730
  if (!content) return [];
2661
2731
  const entries = [];
@@ -17170,7 +17240,7 @@ module.exports = {
17170
17240
  _resetStatusCacheForTesting,
17171
17241
  _ifNoneMatchHasEtag,
17172
17242
  _scanStatusMtimes, _refreshStatusMtimes, _resetStatusMtimeCacheForTesting, // exported for testing
17173
- handleToolsInventory, _refreshToolsInventory, _setToolsInventoryScannerForTesting, _resetToolsInventoryCacheForTesting, // exported for testing
17243
+ handleToolsInventory, _refreshToolsInventory, _setToolsInventoryScannerForTesting, _resetToolsInventoryCacheForTesting, _expireToolsInventoryCacheForTesting, _ageToolsInventoryCacheForTesting, // exported for testing
17174
17244
  collectCcProjectSkills, _mapProjectSkillsForCc, // exported for testing
17175
17245
  _ensureConfiguredProjectStateFiles: ensureConfiguredProjectStateFiles, _resetInitializedProjectStatesForTesting, // exported for testing
17176
17246
  _countWorktrees, _refreshWorktreeCount, _scanWorktreeCount, _resetWorktreeCountCacheForTesting, // exported for testing
@@ -17326,6 +17396,22 @@ if (require.main === module) {
17326
17396
  Promise.resolve(queries.getKnowledgeBaseEntries())
17327
17397
  .catch(err => console.warn(`[dashboard] KB cache warm failed: ${err && err.message}`));
17328
17398
 
17399
+ // W-msdbk1rg — warm the tools inventory (skills/commands/MCP) shortly after
17400
+ // boot so the first Skills & MCP page open renders instantly instead of
17401
+ // blocking ~8-10s on the cold child scan (the nested project-embedded skill
17402
+ // walk over large monorepos dominates). Deferred + unref'd so it never
17403
+ // competes with boot or the first /api/status snapshot, and single-flighted
17404
+ // inside _refreshToolsInventory so it can't duplicate an in-flight scan.
17405
+ const _toolsWarmTimer = setTimeout(() => {
17406
+ try {
17407
+ const pending = _refreshToolsInventory();
17408
+ if (pending && typeof pending.catch === 'function') {
17409
+ pending.catch(err => console.warn(`[dashboard] tools inventory warm failed: ${err && err.message}`));
17410
+ }
17411
+ } catch (err) { console.warn(`[dashboard] tools inventory warm failed: ${err && err.message}`); }
17412
+ }, 3000);
17413
+ if (typeof _toolsWarmTimer.unref === 'function') _toolsWarmTimer.unref();
17414
+
17329
17415
  // Auto-open the browser. `minions restart` and the upgrade path set
17330
17416
  // MINIONS_NO_AUTO_OPEN=1 because the CLI orchestrates the open itself
17331
17417
  // after observing whether an existing tab reconnected; the primitive
package/docs/README.md CHANGED
@@ -78,6 +78,7 @@ Architecture, design proposals, and lifecycle references for people working on t
78
78
  - [slim-ux/concepts.md](slim-ux/concepts.md) — Slim-UX design notes: simplified surface concepts driving the project picker, inline project link, and decoupled folder picker.
79
79
  - [slim-ux/architecture-suggestions.md](slim-ux/architecture-suggestions.md) — Slim-UX follow-up architecture suggestions paired with `concepts.md`.
80
80
  - [team-memory.md](team-memory.md) — End-to-end hybrid memory system: file-backed inputs and consolidation, SQL/FTS5 records, retrieval and fallback, prompt bounds, episodic capture, the review-learning lifecycle (capture/recall/promotion/contradiction/diagnostics) and its rollout history + acceptance measurement, security, APIs, and operations.
81
+ - [temporary-agents.md](temporary-agents.md) — Ephemeral `temp-<uid>` fallback agents: the opt-in `engine.allowTempAgents` + per-tick concurrency gates, in-memory naming/state, why they carry no charter/persona and no personal memory (but do get pinned/team/project memory), fleet-default runtime/model/tool/manifest resolution, routing/retry behavior, lifecycle/cleanup across restart, and isolation implications.
81
82
  - [timeouts-and-liveness.md](timeouts-and-liveness.md) — What kills (or doesn't kill) a live tracked agent: the wall-clock vs steering kill invariants, spawn-phase watchdog gates, steering safety nets, and stale-orphan detection ladder.
82
83
  - [visual-evidence-ci.md](visual-evidence-ci.md) — GitHub Actions before/after dashboard capture: path and label triggers, deterministic base/head fixtures, artifact/comment lifecycle, trust boundary, and local reproduction.
83
84
  - [watches.md](watches.md) — Persistent monitoring jobs: target-type registry, conditions, follow-up actions, and the `watch-plugins/` extension folder.
@@ -22,6 +22,7 @@
22
22
  | `capabilities.systemPromptFile` | **`false`** | No `--system-prompt-file` flag exists. Inject system prompt via a `<system>` block prepended to stdin. |
23
23
  | `capabilities.effortLevels` | **`true`** | `--effort` accepts `low|medium|high|xhigh` (no `max`). Adapter must map `'max' → 'xhigh'`. |
24
24
  | `capabilities.costTracking` | **`false`** | `result.usage` contains `premiumRequests` (count, not USD), no token counts, no cost. |
25
+ | `capabilities.billableUnit` | **`'premiumRequests'`** | Native billable unit. The adapter emits `usage.billable = { unit: 'premiumRequests', value, reported }` and the dashboard renders premium requests in the same position a USD runtime shows dollar cost. When the CLI does not report the field, `value` is `null` and `reported` is `false` (recorded as unavailable, never a fake `0`). See [runtime-adapters.md → Billable-unit usage accounting](runtime-adapters.md#billable-unit-usage-accounting). |
25
26
  | `capabilities.modelShorthands` | **`false`** | The Copilot CLI requires full model IDs (`claude-sonnet-4.5`, `gpt-5.4`). Minions may accept internal aliases (`haiku`, `sonnet`, `opus`), but the adapter translates them to Copilot model IDs before invoking the CLI. |
26
27
  | `capabilities.budgetCap` | **`false`** | No `--max-budget-usd` flag. |
27
28
  | `capabilities.bareMode` | **`false`** | No `--bare` equivalent. |
@@ -134,8 +134,10 @@ Stopping is deliberately two commands, because "ask the engine to shut down" and
134
134
  | `minions stop --all` | Ordered whole-stack teardown: stop-intent → supervisor → dashboard → graceful engine drain → identity-verified reap → late-respawn sweep → verification. | The daemons being verified down. It reports the `engine/state.db-shm` state but does not wait for it. |
135
135
  | `minions stop --all --wait` | The same teardown, and then waits for `engine/state.db-shm` to be released. | The database handles being released, or the budget expiring. |
136
136
 
137
- `--timeout <ms>` sets the budget for the whole sequence (default 60000, matching
138
- the internal installer's quiescence window).
137
+ `--timeout <ms>` sets the budget for the whole sequence (default 60000, the
138
+ floor of the internal installer's quiescence window — that window is derived from
139
+ the target runtime's own `engine.shutdownTimeout` and only ever widens from
140
+ here, so a stop is never shorter than the wait that follows it).
139
141
 
140
142
  Bare `minions stop` is unchanged and will stay that way: recovery paths, operator
141
143
  scripts, and the engine's own callers depend on "request shutdown and return".
@@ -164,16 +164,55 @@ by `test/unit/install-internal-minions.test.js`.
164
164
  until the database handles are actually released. Release requires **both**
165
165
  signals: the processes are dead *and* `engine/state.db-shm` is absent or
166
166
  empty — a live shared-memory index means a connection still holds the WAL. If
167
- the handles are not released within the bounded timeout the run **fails
168
- closed**, before any uninstall or package mutation, and prints which stop path
169
- was taken, the exact argv it issued, and the exact PIDs still holding the
170
- database. Whether services were actually stopped is recorded so a rollback can
171
- restart them. Skipped on a machine with no existing runtime.
167
+ the handles are not released within the derived budget (see *How long the gate
168
+ waits* below) the run **fails closed**, before any uninstall or package
169
+ mutation, and prints which stop path was taken, the exact argv it issued, the
170
+ budget that expired and where it came from, and the exact PIDs still holding
171
+ the database. Whether services were actually stopped is recorded so a rollback
172
+ can restart them. Skipped on a machine with no existing runtime.
172
173
 
173
174
  When the run does fail closed here, end the holders it names yourself — the
174
175
  refusal prints each surviving process by name and PID, and the dashboard is
175
176
  the usual one — then re-run the installer.
176
177
 
178
+ ### How long the gate waits
179
+
180
+ The wait budget is **derived from the runtime being stopped**, not fixed. It is
181
+ the pinned root's own `engine.shutdownTimeout` — the budget the engine gives
182
+ itself to drain pooled leases before exiting — plus the same grace
183
+ `minions restart` applies when it derives a teardown deadline the same way
184
+ (`engine/recovery/stop-stack.js#DRAIN_GRACE_MS`, 5 s).
185
+
186
+ | Input | Budget |
187
+ |-------|--------|
188
+ | `engine.shutdownTimeout` in the pinned root's `config.json` | that value + 5 s grace |
189
+ | config missing, unreadable, malformed, or nonsensical (negative, zero, non-numeric, `NaN`) | `ENGINE_DEFAULTS.shutdownTimeout` (300 s) + 5 s grace |
190
+ | derived budget below the floor | **floor 60 s** |
191
+ | derived budget above the ceiling | **ceiling 900 s (15 min)** |
192
+
193
+ A fixed window could not be right: the *runtime* decides how long a teardown
194
+ legitimately takes. The previous fixed 60 s was shorter than the **default**
195
+ drain budget of five minutes, so a busy stack was declared stuck while it was
196
+ still shutting down exactly as designed.
197
+
198
+ Bounded on both sides because the input is operator-supplied data this gate
199
+ does not own. The **floor** is `stop-stack.js#DEFAULT_STOP_TIMEOUT_MS`, so the
200
+ change can only ever widen the window — a runtime configured with a very short
201
+ drain never makes the gate stricter than it was — and the wait after a stop is
202
+ never shorter than the stop's own budget. The **ceiling** means a corrupt or
203
+ hostile `config.json` cannot hang an unattended migration indefinitely; at ~3x
204
+ the default runtime's budget it clamps nothing a real stack needs. A clamp is
205
+ named in the refusal output, so "it gave up too early" stays an answerable
206
+ question.
207
+
208
+ The budget is always read from the **pinned** root — the one `MINIONS_HOME` is
209
+ pinned to and the one whose services are being stopped — never from the
210
+ installing process's own root. The installer is deliberately standalone (Node
211
+ built-ins only, so it runs on a machine with no Minions to import from), so the
212
+ engine default and the grace are *mirrored* constants; a drift test in
213
+ `test/unit/install-internal-minions.test.js` binds both to their engine-side
214
+ originals so the copies cannot silently diverge.
215
+
177
216
  ### Which stop verb is issued
178
217
 
179
218
  The installer prefers `minions stop --all --wait`, and falls back to the bare
@@ -29,6 +29,48 @@ The `name`, `emoji`, `role`, and `expertise` fields are **human-facing metadata*
29
29
  label the agent in the dashboard and document intent. They do **not** drive dispatch (see
30
30
  §3) and the `expertise` array is not executable (see §4).
31
31
 
32
+ ### 1.1 Temporary agents and friendly call signs
33
+
34
+ When every permanent agent is busy and `engine.allowTempAgents` is set, the engine
35
+ spawns an **ephemeral agent** with a machine id of the form `temp-<uid>` (see
36
+ [`engine/orchestration/routing.js`](../engine/orchestration/routing.js)). That canonical
37
+ `temp-…` id is the **only** machine identity — it is unchanged for routing, SQL keys,
38
+ filesystem paths, live-output logs, process ownership, APIs, per-agent memory rules
39
+ (§8: `temp-*` ids are skipped for personal memory), and PR/reconciliation records.
40
+
41
+ For display only, each temp agent also gets a stable, memorable **call sign** such as
42
+ `Cosmic Comet`, rendered next to a visible `Temp` badge (`CallSign` + badge). This is
43
+ an *additive display identity*, never a rename of the machine id.
44
+
45
+ **Deterministic-from-id invariant (no persisted state).** The call sign is derived
46
+ purely from the immutable canonical id by
47
+ [`shared.tempAgentCallSign(id)`](../engine/core/shared.js): a single FNV-1a 32-bit hash of
48
+ the lower-cased id selects an adjective from its low bits (`h % ADJ`) and a name from its
49
+ high bits (`⌊h / ADJ⌋ % NAMES`), each drawn from a curated, roster-disjoint vocabulary.
50
+ Because the derivation depends only on the id and nothing else:
51
+
52
+ - **Restart- and history-stable** — the same id always yields the same call sign across
53
+ engine/dashboard restarts and for long-exited agents in archived/history views, so no
54
+ SQL migration or JSON runtime state is needed (and none is added). An already-assigned
55
+ name is preserved by construction — it cannot drift when the active set changes.
56
+ - **Collision behavior is deterministic** — the ~24×24 adjective/name space plus the
57
+ single-hash bit-slice keeps concurrently-visible temp agents well-spread; the curated
58
+ `name` list is disjoint from the shipped roster (Ripley/Dallas/Lambert/Rebecca/Ralph)
59
+ so a call sign never equals a configured display name, and the always-visible `Temp`
60
+ badge disambiguates even a rare full collision.
61
+ - **Case-insensitive** — legacy mixed-case `Temp-*` ids derive the same call sign as
62
+ their canonical lower-case form.
63
+
64
+ The client mirrors this vocabulary + algorithm in
65
+ [`dashboard/js/agent-identity.js`](../dashboard/js/agent-identity.js) (`MinionsAgentIdentity`),
66
+ the single formatter every user-facing renderer calls, so a legacy record missing the
67
+ server-stamped `agentName` still renders the identical stable call sign. Parity between
68
+ the two implementations is asserted by
69
+ [`test/unit/temp-agent-identity.test.js`](../test/unit/temp-agent-identity.test.js).
70
+ API payloads expose structured fields (`id`, friendly `name`/`callSign`, `isTemp`) rather
71
+ than replacing ids with formatted strings, and the canonical `temp-…` id stays discoverable
72
+ in detail views/tooltips for debugging.
73
+
32
74
  ## 2. How an agent is configured
33
75
 
34
76
  The full per-agent config shape supported under `config.agents.<id>`:
@@ -123,6 +165,12 @@ context; `_any_` routes to any available idle agent (lowest error rate first).
123
165
  `implement:large` is selected for items with `estimated_complexity: "large"`. If both
124
166
  preferred and fallback are busy, the engine falls back to any idle agent.
125
167
 
168
+ **Overflow → temp agents.** If no configured agent is idle and the operator has enabled
169
+ `engine.allowTempAgents` (default off), the engine may spawn an ephemeral, personaless
170
+ `temp-<uid>` agent as a last-resort fallback. Temp agents inherit fleet-default runtime/model/
171
+ tools and shared/project memory but carry no charter and no personal memory. See
172
+ [`temporary-agents.md`](temporary-agents.md).
173
+
126
174
  **Dispatch rules** (from `routing.md`):
127
175
 
128
176
  - **Eager by default** — the engine spawns every agent that can start work, not one at a time.
@@ -108,6 +108,44 @@ marker and visible sign-off, and lifecycle persists the same value as
108
108
  `minionsReview.model`. Model catalogs are never used to guess an implicit
109
109
  runtime default.
110
110
 
111
+ ### Billable-unit usage accounting
112
+
113
+ Some runtimes bill a native, non-USD unit — the GitHub Copilot CLI reports
114
+ **premium requests** per session/turn. `parseOutput().usage` therefore carries a
115
+ generic typed descriptor alongside the token/cost fields so the engine accounts
116
+ for credits without ever branching on a runtime name:
117
+
118
+ ```js
119
+ usage.billable = { unit: 'premiumRequests', value: <number|null>, reported: <bool> }
120
+ ```
121
+
122
+ - **Authoritative, never derived.** `value` is the count the CLI reported; the
123
+ engine never estimates credits from tokens, model, elapsed time, or pricing.
124
+ - **Unavailable ≠ 0.** When the installed CLI did not expose the field for a run,
125
+ the adapter emits `value: null, reported: false` (and Copilot leaves
126
+ `premiumRequests` / `costUsd` `null`, not `0`). Surfaces render unreported as
127
+ `N/A`, distinct from a genuine reported `0`.
128
+ - **Already scoped — no delta diffing.** Copilot `parseOutput` reassigns
129
+ `usage` on each terminal `result` event, so the value is the finalized count
130
+ for THIS invocation / pooled turn, not a running session total. Downstream code
131
+ therefore **sums** per-invocation values; it does not diff cumulative counters.
132
+ This per-invocation scoping is what makes streamed, resumed, retried, and
133
+ pooled dispatches safe from double-counting.
134
+
135
+ `shared.accumulateBillableUnits(target, usage.billable)` folds a descriptor into
136
+ `target.billableUnits[unit] = { total, reported, unavailable }`, used by
137
+ `engine/orchestration/lifecycle.js#updateMetrics` (per-agent / `_engine` /
138
+ `_daily`) and `engine/agents/llm.js` (Command Center / doc-chat). USD is
139
+ deliberately **excluded** from this path — dollar cost already flows through the
140
+ richer `costUsd` + token accounting, so routing it here too would double-count.
141
+ The map persists inside the existing metrics JSON blob (no migration; older rows
142
+ simply lack the field), and `engine/core/queries.js#getMetrics` resolves
143
+ `m.billableUnit` from the runtime capability so the dashboard keys off typed
144
+ metadata, never the runtime name. A future adapter can surface its own unit by
145
+ setting `capabilities.billableUnit` and emitting `usage.billable` — no call-site
146
+ changes required.
147
+
148
+
111
149
  ## Capability Flags
112
150
 
113
151
  Engine code branches on consumed flags, never on runtime names. A capability
@@ -122,6 +160,7 @@ to act on that behavioral split.
122
160
  | `systemPromptFile` | ✓ | ✗ | ✗ | sysprompt via `--system-prompt-file` (else inlined into stdin by `buildPrompt`). |
123
161
  | `effortLevels` | ✓ | ✓ | ✓ | Runtime reasoning-effort controls. Codex maps through `--config model_reasoning_effort=...`; Copilot maps `'max'` to `'xhigh'`; Claude leaves `'max'` alone. |
124
162
  | `costTracking` | ✓ | ✗ | ✗ | USD + token counts in the result event. Copilot only emits `premiumRequests`; Codex accounting is not assumed. |
163
+ | `billableUnit` | `usd` | `premiumRequests` | `usd` | Typed native billable unit for the display layer. The dashboard renders THIS unit in the same position a USD runtime shows dollar cost, switching labels/units/tooltips off the metadata — never a runtime-name branch. Adapters with no native credit unit default to `'usd'`. |
125
164
  | `modelShorthands` | ✓ | ✗ | ✗ | Bare `sonnet`/`opus`/`haiku` accepted by Claude only. |
126
165
  | `modelDiscovery` | ✗ | ✓ | ✓ | `listModels()` returns a real catalog when supported (Copilot API, Codex `debug models --bundled`). |
127
166
  | `strictModelCatalog` | — | — | ✗ | An explicit false lets the runtime validate IDs missing from its cached catalog; Codex uses this because bundled catalogs can lag staged/custom models. |