@yemi33/minions 0.1.2118 → 0.1.2120

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.
@@ -494,7 +494,7 @@ function openBugReport() {
494
494
  container.style.cssText = 'display:flex;flex-direction:column;gap:12px';
495
495
  var intro = document.createElement('p');
496
496
  intro.style.cssText = 'color:var(--muted);font-size:var(--text-md);margin:0';
497
- intro.textContent = 'File a bug on the Minions repo (yemi33/minions).';
497
+ intro.textContent = 'File a bug on the public Minions repo (opg-microsoft/minions).';
498
498
 
499
499
  var titleLabel = document.createElement('label');
500
500
  titleLabel.style.cssText = 'color:var(--text);font-size:var(--text-md)';
@@ -577,7 +577,7 @@ async function submitBugReport() {
577
577
  } else {
578
578
  var msg = document.createElement('span');
579
579
  msg.style.cssText = 'color:var(--muted);font-size:var(--text-md)';
580
- msg.textContent = 'Issue created on yemi33/minions';
580
+ msg.textContent = 'Issue created on opg-microsoft/minions';
581
581
  container.appendChild(msg);
582
582
  }
583
583
  var actions = document.createElement('div');
package/dashboard.js CHANGED
@@ -39,6 +39,7 @@ const dispatchMod = require('./engine/dispatch');
39
39
  const dispatchEvents = require('./engine/dispatch-events');
40
40
  const { wrapUntrusted, buildSource } = require('./engine/untrusted-fence');
41
41
  const steering = require('./engine/steering');
42
+ const steeringStore = require('./engine/steering-store');
42
43
  const projectDiscovery = require('./engine/project-discovery');
43
44
  const features = require('./engine/features');
44
45
  const ccWorkerPool = require('./engine/cc-worker-pool');
@@ -7795,7 +7796,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7795
7796
  title: body.title,
7796
7797
  description: body.description || '',
7797
7798
  labels: body.labels,
7798
- repo: 'yemi33/minions',
7799
+ repo: 'opg-microsoft/minions',
7799
7800
  tmpDir: path.join(ENGINE_DIR, 'tmp'),
7800
7801
  });
7801
7802
  return jsonReply(res, 200, result);
@@ -9632,6 +9633,32 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9632
9633
  } catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
9633
9634
  }
9634
9635
 
9636
+ // W-mq03l6zh0006f0a1-d — read-only diagnostics surface for the per-org ADO
9637
+ // throttle tracker. Returns { orgs: { [orgBase]: { throttled, retryAfter,
9638
+ // consecutiveHits } } }. Prefers the per-org getter ado.getAdoThrottleStateAll
9639
+ // when present (introduced by W-mq03l6zh0006f0a1-b). Falls back to the
9640
+ // process-global ado.getAdoThrottleState() under the synthetic key `global`
9641
+ // when the per-org getter is not present, so the endpoint stays live across
9642
+ // the staged rollout of the per-org isolation work.
9643
+ async function handleDiagnosticsAdoThrottle(req, res) {
9644
+ try {
9645
+ let orgs = {};
9646
+ if (typeof ado.getAdoThrottleStateAll === 'function') {
9647
+ const all = ado.getAdoThrottleStateAll() || {};
9648
+ // Defensive copy — handler must never expose internal mutable state.
9649
+ for (const [k, v] of Object.entries(all)) {
9650
+ if (v && typeof v === 'object') {
9651
+ orgs[k] = { throttled: !!v.throttled, retryAfter: Number(v.retryAfter) || 0, consecutiveHits: Number(v.consecutiveHits) || 0 };
9652
+ }
9653
+ }
9654
+ } else if (typeof ado.getAdoThrottleState === 'function') {
9655
+ const v = ado.getAdoThrottleState() || {};
9656
+ orgs.global = { throttled: !!v.throttled, retryAfter: Number(v.retryAfter) || 0, consecutiveHits: Number(v.consecutiveHits) || 0 };
9657
+ }
9658
+ return jsonReply(res, 200, { orgs });
9659
+ } catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
9660
+ }
9661
+
9635
9662
  // Slim UX surface for the experimental redesigned dashboard.
9636
9663
  // The markup/CSS/JS live as fragments under dashboard/slim/ (layout.html +
9637
9664
  // styles.css + body.html + js/*.js) and are assembled by buildSlimHtml() —
@@ -11468,14 +11495,46 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11468
11495
  const liveLogPath = path.join(agentDir, 'live-output.log');
11469
11496
  try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + text + '\n'); } catch { /* optional */ }
11470
11497
 
11498
+ // W-mq066js7000fff1f-a (Gap D): surface the observable
11499
+ // delivery-state row to the UI. steerId / status / deliveryUrl
11500
+ // let the dashboard poll /api/steering/:id without re-listing.
11501
+ // Existing fields (ok, message, file, inboxCount, ...delivery)
11502
+ // are preserved for back-compat with the current chat panel.
11503
+ const steerId = entry?.steerId || null;
11471
11504
  return jsonReply(res, 200, {
11472
11505
  ok: true,
11473
11506
  message: delivery.pendingDelivery ? 'Steering message pending delivery' : 'Steering message queued',
11474
11507
  ...delivery,
11475
11508
  file: entry?.file || null,
11476
11509
  inboxCount: steering.listUnreadSteeringMessages(agentId).length,
11510
+ steerId,
11511
+ status: steerId ? 'queued' : null,
11512
+ deliveryUrl: steerId ? `/api/steering/${steerId}` : null,
11477
11513
  });
11478
11514
  }},
11515
+ { method: 'GET', path: /^\/api\/agents\/([\w-]+)\/steering$/, template: '/api/agents/:agentId/steering', desc: 'List recent steering delivery-state rows for an agent (latest 50 by default)', params: 'limit? (default 50, max 200)', handler: (req, res, match) => {
11516
+ const agentId = match && match[1];
11517
+ if (!agentId) return jsonReply(res, 400, { error: 'agentId required' }, req);
11518
+ let limit = 50;
11519
+ try {
11520
+ const raw = new URL(req.url, 'http://localhost').searchParams.get('limit');
11521
+ if (raw != null) {
11522
+ const n = parseInt(raw, 10);
11523
+ if (Number.isFinite(n) && n > 0) limit = Math.min(n, 200);
11524
+ }
11525
+ } catch { /* default */ }
11526
+ let rows = [];
11527
+ try { rows = steeringStore.listForAgent(agentId, { limit }); } catch { rows = []; }
11528
+ return jsonReply(res, 200, { agentId, deliveries: rows, count: rows.length }, req);
11529
+ } },
11530
+ { method: 'GET', path: /^\/api\/steering\/([\w-]+)$/, template: '/api/steering/:id', desc: 'Get a single steering delivery-state row by steerId', handler: (req, res, match) => {
11531
+ const steerId = match && match[1];
11532
+ if (!steerId) return jsonReply(res, 400, { error: 'steerId required' }, req);
11533
+ let row = null;
11534
+ try { row = steeringStore.getById(steerId); } catch { row = null; }
11535
+ if (!row) return jsonReply(res, 404, { error: 'steering delivery not found' }, req);
11536
+ return jsonReply(res, 200, row, req);
11537
+ } },
11479
11538
  { method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent? or agentId?, task?', handler: handleAgentsCancel },
11480
11539
  { method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, template: '/api/agent/:id/kill', desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
11481
11540
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, template: '/api/agent/:id/live-stream', desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
@@ -11548,7 +11607,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11548
11607
  { method: 'POST', path: '/api/projects/remove', desc: 'Unlink a project: cancels WIs, drains dispatch, kills agents, cleans worktrees, archives data dir', params: 'name or path, keepData?, purge?', handler: handleProjectsRemove },
11549
11608
 
11550
11609
  // Bug Filing
11551
- { method: 'POST', path: '/api/issues/create', desc: 'File a bug on the Minions repo (yemi33/minions)', params: 'title, description?, labels?', handler: handleFileBug },
11610
+ { method: 'POST', path: '/api/issues/create', desc: 'File a bug on the public Minions repo (opg-microsoft/minions)', params: 'title, description?, labels?', handler: handleFileBug },
11552
11611
 
11553
11612
  // Command Center
11554
11613
  { method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
@@ -11837,6 +11896,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11837
11896
 
11838
11897
  // Diagnostics — refresh ring buffer persistence (W-mphejzx100081972).
11839
11898
  { method: 'POST', path: '/api/diagnostics/refresh', desc: 'Append a dashboard refresh-diagnostic ring buffer batch to engine/dashboard-diagnostics.log (rotated at 1 MB)', params: 'entries[]', handler: handleDiagnosticsRefresh },
11899
+ // Diagnostics — per-org ADO throttle state (W-mq03l6zh0006f0a1-d).
11900
+ { method: 'GET', path: '/api/diagnostics/ado-throttle', desc: 'Snapshot of per-org ADO throttle tracker state — { orgs: { [orgBase]: { throttled, retryAfter, consecutiveHits } } }. Falls back to a single `global` key when running against pre-per-org engines.', handler: handleDiagnosticsAdoThrottle },
11840
11901
  ];
11841
11902
 
11842
11903
  // ── Route Dispatcher ────────────────────────────────────────────────────────
@@ -98,5 +98,16 @@
98
98
  "removalGate": "Telemetry: pruneDefaultClaudeConfig must return false (no mutation) for every call across all known engines for >=30 consecutive days (add an `_engine.pruneDefaultClaudeConfigStrips` counter if needed to observe this), AND the parent `config-claude-binary-override` entry must have already cleared its own gate. The dependency is strict: removing the prune while users still rely on the override branch would surface the `deprecated-config-claude` warning on every stale generated default. Once both conditions hold, removal is the function definition (engine/shared.js:3126), the export at :5673, all 5 call sites (dashboard.js:202, :9116, :9331, :9450; minions.js:385), and the tests at unit.test.js:2260-2303 + runtime-fleet-helpers.test.js:546.",
99
99
  "targetRemovalDate": null,
100
100
  "notes": "Do NOT set targetRemovalDate — gating is signal-based AND ordered. This entry MUST NOT be removed before `config-claude-binary-override` clears its gate, otherwise installs with stale defaults will flood the deprecation channel until their next config save. The 5 call sites form a complete coverage net: load (dashboard.js:202 + minions.js:385) + save (dashboard.js:9116/9331/9450), so any code path that touches config.json runs the sanitizer."
101
+ },
102
+ {
103
+ "id": "ado-throttle-arg-less-shim",
104
+ "description": "Arg-less form of isAdoThrottled() in engine/ado.js. Introduced by W-mq03l6zh0006f0a1-b as a back-compat shim during the per-org ADO throttle isolation rollout: pre-rollout, isAdoThrottled() collapsed the single process-global tracker to one boolean; post-rollout, the canonical form is isAdoThrottled(orgBase) against the per-org Map. The arg-less call site is preserved transiently so engine code (and any in-process callers) that haven't yet been threaded through with a per-org `orgBase` keep returning the safe global-OR (true if ANY org is currently throttled) — preventing a regression where new poll work bypasses a still-warm throttle backoff on an unrelated noisy org.",
105
+ "deprecated": "2026-06-04",
106
+ "code": [
107
+ { "file": "engine/ado.js", "note": "isAdoThrottled() arg-less branch and the global-OR fold over the per-org Map. Single call site to migrate: shared.getAdoOrgBase(project) is already in scope at every consumer." }
108
+ ],
109
+ "removalGate": "Two conditions must hold simultaneously: (a) grep `engine/ado.js` for `isAdoThrottled\\s*\\(\\s*\\)` and confirm zero arg-less call sites remain across the engine — every caller passes a concrete `orgBase` resolved via `shared.getAdoOrgBase(project)`; (b) `GET /api/diagnostics/ado-throttle` on a live engine has been observed for >=2 consecutive weeks reporting per-org keys (proves the per-org Map is populated under load and the global-OR isn't masking a regression). Once both hold, removal deletes the arg-less branch in isAdoThrottled and the global-OR fold; callers that still pass no argument become an immediate, surfaced bug rather than a silent over-throttle.",
110
+ "targetRemovalDate": "2026-08-03",
111
+ "notes": "Introduced by W-mq03l6zh0006f0a1 (Per-org ADO throttle isolation). 60-day window (2 release cycles + buffer) gives the in-flight per-org migration time to land + observe per-org keys on the diagnostics endpoint. Observable live at GET /api/diagnostics/ado-throttle — the endpoint reports a single `global` key while the arg-less shim is still load-bearing, and per-org `<orgBase>` keys once isolation is complete; that key shape is the human-readable signal for whether this shim can retire."
101
112
  }
102
113
  ]
@@ -98,6 +98,30 @@ If an agent thinks a `knowledge/` file is wrong, the correct response is to **no
98
98
 
99
99
  The same constraint applies to `knowledge/agents/<agentId>.md` — those are curated by the sweep and should not be hand-edited.
100
100
 
101
+ ## Session State vs. Persistent Memory
102
+
103
+ The PRD that introduced sliding-window memory (W-mq07b8do000nc86a) referenced two distinct write paths — `update_session_state()` and `update_memory()` — borrowed from agent frameworks that model agents as long-lived in-process objects. Minions has neither method because it doesn't model agents that way; understanding the mapping prevents fruitless searches for non-existent APIs.
104
+
105
+ **Session state** = the dispatch's worktree + child process. Each Minions agent runs as a fresh OS process spawned by `engine.js → engine/spawn-agent.js` inside a per-work-item git worktree (`work/<wi-id>` by default; see `shared.deriveWorkItemBranchName`). When the dispatch ends, the engine deletes the worktree and the child exits. There is no persistent "session" object to update — ephemeral state lives in process memory and disk paths under the worktree, both of which are reclaimed automatically. No code is needed to "clear" session state; it never persists in the first place.
106
+
107
+ **Persistent memory** = `knowledge/agents/<agentId>.md`, the per-agent file appended to by `engine/consolidation.js` during the inbox sweep. This is the analog of `update_memory()` in PRD terms. It is written only by the consolidation sweep (the [sweep-write-only constraint](#sweep-write-only-constraint) applies), is injected into every subsequent dispatch's prompt for that same agent ID via `engine/playbook.js`, and is bounded by two complementary cuts plus an optional summary pass:
108
+
109
+ | Tunable (under `engine.*` in `config.json`) | Default | Behavior |
110
+ |---------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------|
111
+ | `agentMemoryMaxEntries` | `300` | Sliding-window entry-count cap. Older non-summary sections evicted oldest-first when exceeded. |
112
+ | (built-in) `AGENT_MEMORY_BUDGET_BYTES` | `25000` | Hard byte ceiling for prompt-injection safety. Always wins when both caps bind. Sticky summary sections obey it. |
113
+ | `agentMemorySummaryEnabled` | `false` | Master switch for the LLM-driven compression pass. Off by default to avoid surprise Haiku spend. |
114
+ | `agentMemorySummaryThreshold` | `30` | When the summary pass fires, fold this many oldest entries into one summary section. |
115
+ | `agentMemorySummaryDays` | `30` | Age trigger: if the oldest entry is older than this, fold even when under the entry cap. |
116
+
117
+ The summary pass runs fire-and-forget after every successful `appendToAgentMemory` write inside `classifyToKnowledgeBase`. It re-reads outside the lock, calls Haiku, then re-acquires the lock and verifies the same oldest sections are still in place (stale-candidate guard) before swapping. Any failure — disabled, no trigger, LLM unavailable, race detected — is a silent no-op; the consolidation pipeline is never blocked on the LLM.
118
+
119
+ The compressed summary is wrapped in an `<UNTRUSTED-INPUT source="agent-memory-summary:agent=...">` fence on disk. The source material was the inbox bodies of evicted entries, which are themselves untrusted; without the fence, any imperative laundered through summarization could later be executed by an agent reading its own memory.
120
+
121
+ Summary sections are **sticky** under the entry-count cap — they represent compressed knowledge that should outlive ordinary inbox entries. They are detected by their title prefix `Earlier learnings summary` and only the byte budget can evict them.
122
+
123
+ **Default-off rationale.** `agentMemorySummaryEnabled` defaults to `false` (intentional deviation from PRD wording that implies "always on"). Enabling it commits operators to per-agent Haiku spend on every consolidation cycle; the entry-count cap on its own already prevents unbounded growth. Operators who have weighed the cost set `engine.agentMemorySummaryEnabled: true` in `config.json` to opt in.
124
+
101
125
  ## Quick reference for agents
102
126
 
103
127
  ```
package/engine/cli.js CHANGED
@@ -159,6 +159,21 @@ function handleCommand(cmd, args) {
159
159
  if (!cmd) {
160
160
  return commands.start();
161
161
  } else if (commands[cmd]) {
162
+ // W-mq07mjzi000s1cc9: Centralized help-flag interception.
163
+ //
164
+ // `minions work --help` was creating ghost work items with title='--help'
165
+ // because the bare-string `title` was truthy and bypassed the `!title`
166
+ // usage check. Same class of bug exists in `spawn`/`plan`/`complete` —
167
+ // every command that takes a positional arg and tests it with `if (!arg)`.
168
+ //
169
+ // Intercept here so a single guard covers the whole command set. `pr` and
170
+ // `bridge` already handle `help`/`--help`/`-h` inline (see their own
171
+ // first-arg branches), so let them route through unchanged.
172
+ if (cmd !== 'pr' && cmd !== 'bridge' && isHelpToken(args && args[0])) {
173
+ console.log('Commands:');
174
+ for (const line of formatCliCommandHelpLines()) console.log(line);
175
+ return;
176
+ }
162
177
  return commands[cmd](...args);
163
178
  } else {
164
179
  console.log(`Unknown command: ${cmd}`);
@@ -168,6 +183,26 @@ function handleCommand(cmd, args) {
168
183
  }
169
184
  }
170
185
 
186
+ // W-mq07mjzi000s1cc9: Help-flag token recognition.
187
+ //
188
+ // Matches the exact tokens the user typed on the CLI (`--help`, `-h`, `help`).
189
+ // Used by handleCommand's centralized guard and by per-command defensive
190
+ // checks (work/spawn/plan/complete) for defense-in-depth.
191
+ function isHelpToken(arg) {
192
+ return arg === '--help' || arg === '-h' || arg === 'help';
193
+ }
194
+
195
+ // W-mq07mjzi000s1cc9: Stricter guard for command first-positionals.
196
+ //
197
+ // Real work-item titles, agent ids, plan source paths, and dispatch ids never
198
+ // start with `--`. Rejecting any leading-`--` token catches the exact bug
199
+ // reported (`--help`) plus typos like `--hep`, `--h`, `-help` that would
200
+ // otherwise still slip through as ghost-WI titles.
201
+ function looksLikeFlagOrHelp(arg) {
202
+ if (isHelpToken(arg)) return true;
203
+ return typeof arg === 'string' && arg.startsWith('--');
204
+ }
205
+
171
206
  // SoT for engine-CLI metadata: drives handleCommand's help text and the
172
207
  // CC preamble's CLI index in dashboard.js. Drift-checked against `commands`.
173
208
  const CLI_COMMAND_DOCS = Object.freeze({
@@ -1295,6 +1330,11 @@ const commands = {
1295
1330
  console.log('Usage: minions complete <dispatch-id>');
1296
1331
  return;
1297
1332
  }
1333
+ // W-mq07mjzi000s1cc9 — defensive guard mirrors work/spawn/plan.
1334
+ if (looksLikeFlagOrHelp(id)) {
1335
+ console.log('Usage: minions complete <dispatch-id>');
1336
+ process.exit(2);
1337
+ }
1298
1338
  const dispatch = getDispatch();
1299
1339
  const item = (dispatch.active || []).find(d => d.id === id);
1300
1340
  if (!item) {
@@ -1333,6 +1373,11 @@ const commands = {
1333
1373
  console.log('Usage: node .minions/engine.js spawn <agent-id> "<prompt>"');
1334
1374
  return;
1335
1375
  }
1376
+ // W-mq07mjzi000s1cc9 — defensive guard mirrors work/plan/complete.
1377
+ if (looksLikeFlagOrHelp(agentId)) {
1378
+ console.log('Usage: node .minions/engine.js spawn <agent-id> "<prompt>"');
1379
+ process.exit(2);
1380
+ }
1336
1381
 
1337
1382
  const config = getConfig();
1338
1383
  if (!config.agents[agentId]) {
@@ -1365,6 +1410,16 @@ const commands = {
1365
1410
  console.log(' id Optional caller-supplied work item ID. Defaults to a cuid-style W-<id>.');
1366
1411
  return;
1367
1412
  }
1413
+ // W-mq07mjzi000s1cc9 — Defense-in-depth: reject `--help`/`-h`/`help` or any
1414
+ // leading-`--` title even if a future caller bypasses handleCommand. The
1415
+ // original bug created ghost WIs with title='--help' because the truthy
1416
+ // check above let the flag through.
1417
+ if (looksLikeFlagOrHelp(title)) {
1418
+ console.log('Usage: node .minions/engine.js work "<title>" [options-json]');
1419
+ console.log('Options: {"id":"W-customid","type":"implement","priority":"high","agent":"dallas","description":"...","branch":"feature/...","project":"minions"}');
1420
+ console.log(' id Optional caller-supplied work item ID. Defaults to a cuid-style W-<id>.');
1421
+ process.exit(2);
1422
+ }
1368
1423
 
1369
1424
  let opts = {};
1370
1425
  const optStr = rest.join(' ');
@@ -1452,6 +1507,15 @@ const commands = {
1452
1507
  console.log(' node engine.js plan "Add auth middleware with JWT tokens and role-based access"');
1453
1508
  return;
1454
1509
  }
1510
+ // W-mq07mjzi000s1cc9 — defensive guard mirrors work/spawn/complete.
1511
+ if (looksLikeFlagOrHelp(source)) {
1512
+ console.log('Usage: node .minions/engine.js plan <source> [project]');
1513
+ console.log('');
1514
+ console.log('Source can be:');
1515
+ console.log(' - A file path (markdown, txt, or json)');
1516
+ console.log(' - Inline text wrapped in quotes');
1517
+ process.exit(2);
1518
+ }
1455
1519
 
1456
1520
  const config = getConfig();
1457
1521
  const { getProjects, resolveProjectSource } = require('./shared');