@yemi33/minions 0.1.2122 → 0.1.2124
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/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-work-items.js +100 -1
- package/dashboard/styles.css +2 -0
- package/docs/README.md +1 -0
- package/docs/harness-mode.md +92 -0
- package/docs/workspace-manifests.md +104 -0
- package/engine/ado.js +9 -0
- package/engine/github.js +4 -1
- package/engine/harness.js +592 -0
- package/engine/lifecycle.js +91 -0
- package/engine/scheduler.js +40 -3
- package/engine/shared.js +269 -2
- package/engine.js +91 -15
- package/package.json +1 -1
package/engine/lifecycle.js
CHANGED
|
@@ -14,6 +14,7 @@ const { trackEngineUsage } = require('./llm');
|
|
|
14
14
|
const { resolveRuntime } = require('./runtimes');
|
|
15
15
|
const adoGitAuth = require('./ado-git-auth');
|
|
16
16
|
const queries = require('./queries');
|
|
17
|
+
const harness = require('./harness');
|
|
17
18
|
const { isBranchActive } = require('./cooldown');
|
|
18
19
|
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
19
20
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
@@ -4040,6 +4041,82 @@ function handleDecompositionResult(stdout, meta, config, runtimeName) {
|
|
|
4040
4041
|
return 0;
|
|
4041
4042
|
}
|
|
4042
4043
|
|
|
4044
|
+
/**
|
|
4045
|
+
* Tri-agent harness mode (W-mq07a9gf000jbc2b): when an evaluator completes,
|
|
4046
|
+
* parse its verdict against the configured rubric/threshold and — if the
|
|
4047
|
+
* artifact didn't pass and the iteration cap hasn't been hit — append a
|
|
4048
|
+
* fresh Generator+Evaluator pair so the harness can iterate on its own
|
|
4049
|
+
* artifact. Returns the number of work items appended (0 = terminal stop,
|
|
4050
|
+
* either pass or cap reached).
|
|
4051
|
+
*
|
|
4052
|
+
* Called from runPostCompletionHooks after a successful run when the
|
|
4053
|
+
* dispatched item carries _harness.role === 'evaluator'.
|
|
4054
|
+
*/
|
|
4055
|
+
function handleHarnessIterationResult(stdout, structuredCompletion, meta, config) {
|
|
4056
|
+
const evaluatorItem = meta?.item;
|
|
4057
|
+
if (!evaluatorItem?._harness || evaluatorItem._harness.role !== harness.HARNESS_ROLE.EVALUATOR) return 0;
|
|
4058
|
+
|
|
4059
|
+
let verdict;
|
|
4060
|
+
try {
|
|
4061
|
+
verdict = harness.parseEvaluatorVerdict(stdout || '', structuredCompletion || null);
|
|
4062
|
+
} catch (err) {
|
|
4063
|
+
log('warn', `Harness ${evaluatorItem._harness.missionId}: verdict parse failed — ${err.message}; treating as terminal stop`);
|
|
4064
|
+
return 0;
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
if (!harness.shouldIterateAgain(evaluatorItem._harness, verdict)) {
|
|
4068
|
+
const reason = verdict.pass === true ? 'passed' :
|
|
4069
|
+
(evaluatorItem._harness.iteration >= evaluatorItem._harness.maxIterations ? 'max iterations reached' :
|
|
4070
|
+
'inconclusive verdict');
|
|
4071
|
+
log('info', `Harness mission ${evaluatorItem._harness.missionId} terminal stop (iteration ${evaluatorItem._harness.iteration}, ${reason}, score=${verdict.score ?? 'n/a'})`);
|
|
4072
|
+
return 0;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
let nextItems;
|
|
4076
|
+
try {
|
|
4077
|
+
nextItems = harness.createIterationWorkItems(evaluatorItem, verdict, {});
|
|
4078
|
+
} catch (err) {
|
|
4079
|
+
log('warn', `Harness ${evaluatorItem._harness.missionId}: iteration build failed — ${err.message}`);
|
|
4080
|
+
return 0;
|
|
4081
|
+
}
|
|
4082
|
+
if (!Array.isArray(nextItems) || nextItems.length === 0) return 0;
|
|
4083
|
+
|
|
4084
|
+
// Mirror handleDecompositionResult: scan central + per-project work-items.json
|
|
4085
|
+
// and append into the file that owns the evaluator (the trio always lands in
|
|
4086
|
+
// the central file in practice — scheduler.discoverScheduledWork writes
|
|
4087
|
+
// directly to engine/work-items.json via engine.js — but iterate defensively).
|
|
4088
|
+
const projects = shared.getProjects(config);
|
|
4089
|
+
const allPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
4090
|
+
for (const p of projects) allPaths.push(shared.projectWorkItemsPath(p));
|
|
4091
|
+
|
|
4092
|
+
let appendedTo = null;
|
|
4093
|
+
for (const wiPath of allPaths) {
|
|
4094
|
+
let found = false;
|
|
4095
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
4096
|
+
if (!Array.isArray(data)) return data;
|
|
4097
|
+
const evaluator = data.find(i => i.id === evaluatorItem.id);
|
|
4098
|
+
if (!evaluator) return data;
|
|
4099
|
+
found = true;
|
|
4100
|
+
// De-dupe by id in case a previous tick already appended the next pair.
|
|
4101
|
+
const existingIds = new Set(data.map(i => i.id));
|
|
4102
|
+
for (const it of nextItems) {
|
|
4103
|
+
if (existingIds.has(it.id)) continue;
|
|
4104
|
+
data.push(it);
|
|
4105
|
+
}
|
|
4106
|
+
return data;
|
|
4107
|
+
}, { defaultValue: [] });
|
|
4108
|
+
if (found) { appendedTo = wiPath; break; }
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
if (!appendedTo) {
|
|
4112
|
+
log('warn', `Harness ${evaluatorItem._harness.missionId}: evaluator ${evaluatorItem.id} not found in any work-items.json — iteration skipped`);
|
|
4113
|
+
return 0;
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
log('info', `Harness mission ${evaluatorItem._harness.missionId} iterating: appended ${nextItems.length} work items (next iteration: ${nextItems[0]._harness.iteration}, score=${verdict.score ?? 'n/a'})`);
|
|
4117
|
+
return nextItems.length;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4043
4120
|
/**
|
|
4044
4121
|
* W-mpg58wv3 — auto-dispatch a re-review WI when a fix-WI born from a minion
|
|
4045
4122
|
* REQUEST_CHANGES marks done. Closure-loop for the shared Yemi reviewer slot:
|
|
@@ -4386,6 +4463,19 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
4386
4463
|
}
|
|
4387
4464
|
}
|
|
4388
4465
|
|
|
4466
|
+
// Tri-agent harness iteration (W-mq07a9gf000jbc2b): if the evaluator just
|
|
4467
|
+
// completed successfully and verdict says retry, append the next Gen+Eval
|
|
4468
|
+
// pair into the same work-items.json. Engine will dispatch them on the
|
|
4469
|
+
// next tick. No interaction with skipDoneStatus — the evaluator itself
|
|
4470
|
+
// still marks DONE; iteration is a sibling write, not a parent decomp.
|
|
4471
|
+
if (effectiveSuccess && meta?.item?._harness?.role === harness.HARNESS_ROLE.EVALUATOR) {
|
|
4472
|
+
try {
|
|
4473
|
+
handleHarnessIterationResult(stdout, structuredCompletion, meta, config);
|
|
4474
|
+
} catch (err) {
|
|
4475
|
+
log('warn', `Harness iteration hook failed for ${meta.item.id}: ${err.message}`);
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4389
4479
|
// Verify review work items include a verdict — must run BEFORE updateWorkItemStatus(DONE),
|
|
4390
4480
|
// same pattern as plan-to-prd (#893): updateWorkItemStatus deletes _retryCount, so the check
|
|
4391
4481
|
// must read/increment it before that happens. Also sets skipDoneStatus so completedAt isn't
|
|
@@ -5204,6 +5294,7 @@ module.exports = {
|
|
|
5204
5294
|
isPrAttachmentRequired,
|
|
5205
5295
|
extractDecompositionJson,
|
|
5206
5296
|
handleDecompositionResult,
|
|
5297
|
+
handleHarnessIterationResult,
|
|
5207
5298
|
processCompletionFollowups,
|
|
5208
5299
|
// W-mpg58wv3 — closure-loop dispatch helpers (exported for testing).
|
|
5209
5300
|
dispatchReReviewForFix,
|
package/engine/scheduler.js
CHANGED
|
@@ -25,7 +25,8 @@ const fs = require('fs');
|
|
|
25
25
|
const path = require('path');
|
|
26
26
|
const shared = require('./shared');
|
|
27
27
|
const routing = require('./routing');
|
|
28
|
-
const
|
|
28
|
+
const harness = require('./harness');
|
|
29
|
+
const { safeJson, safeWrite, mutateJsonFileLocked, mutateScheduleRuns, ts, dateStamp, log, WI_STATUS, WORK_TYPE } = shared;
|
|
29
30
|
|
|
30
31
|
const SCHEDULE_RUNS_PATH = path.join(shared.MINIONS_DIR, 'engine', 'schedule-runs.json');
|
|
31
32
|
|
|
@@ -186,9 +187,9 @@ function createScheduledWorkItem(sched) {
|
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
function writeScheduleRunEntry(runs, scheduleId, workItemId) {
|
|
190
|
+
function writeScheduleRunEntry(runs, scheduleId, workItemId, extra) {
|
|
190
191
|
const existing = typeof runs[scheduleId] === 'object' && runs[scheduleId] ? runs[scheduleId] : {};
|
|
191
|
-
runs[scheduleId] = { ...existing, lastRun: ts(), lastWorkItemId: workItemId };
|
|
192
|
+
runs[scheduleId] = { ...existing, lastRun: ts(), lastWorkItemId: workItemId, ...(extra || {}) };
|
|
192
193
|
return runs[scheduleId];
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -222,6 +223,42 @@ function discoverScheduledWork(config) {
|
|
|
222
223
|
const lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || null);
|
|
223
224
|
if (!shouldRunNow(sched, lastRun)) continue;
|
|
224
225
|
|
|
226
|
+
// Tri-agent harness mode (W-mq07a9gf000jbc2b): a single schedule firing
|
|
227
|
+
// produces a coordinated Planner → Generator → Evaluator trio rather than
|
|
228
|
+
// a single work item. Validate config first — on bad config, skip this
|
|
229
|
+
// tick WITHOUT recording a schedule run so the operator can fix the
|
|
230
|
+
// config and the next tick will pick it up.
|
|
231
|
+
if (sched.harness_mode === harness.HARNESS_MODE.TRI_AGENT) {
|
|
232
|
+
const validation = harness.validateHarnessConfig(sched);
|
|
233
|
+
if (!validation.valid) {
|
|
234
|
+
log('warn', `Scheduler: harness config invalid for ${sched.id} — skipping (errors: ${validation.errors.join('; ')})`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
// Resolve schedule-time template variables on the title/description
|
|
239
|
+
// BEFORE handing the schedule to the harness builder so subtask
|
|
240
|
+
// prompts inherit the same substitutions as regular schedules.
|
|
241
|
+
const resolvedSched = {
|
|
242
|
+
...sched,
|
|
243
|
+
title: resolveScheduleTemplateVars(sched.title),
|
|
244
|
+
description: resolveScheduleTemplateVars(sched.description || sched.title),
|
|
245
|
+
harness_rubric: resolveScheduleTemplateVars(sched.harness_rubric),
|
|
246
|
+
};
|
|
247
|
+
const mission = harness.createTriAgentMission(resolvedSched);
|
|
248
|
+
for (const it of mission.items) work.push(it);
|
|
249
|
+
// Record the mission's planner id as lastWorkItemId for compatibility
|
|
250
|
+
// with the existing schedule-runs shape, plus lastMissionId so the
|
|
251
|
+
// dashboard and consolidation tooling can join across the trio.
|
|
252
|
+
writeScheduleRunEntry(runs, sched.id, mission.items[0].id, {
|
|
253
|
+
lastMissionId: mission.missionId,
|
|
254
|
+
harnessMode: harness.HARNESS_MODE.TRI_AGENT,
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
log('warn', `Scheduler: tri-agent mission build failed for ${sched.id}: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
225
262
|
// Substitute schedule-time template vars (e.g. {{date}}) before the work
|
|
226
263
|
// item is written — single-pass playbook rendering can't reach placeholders
|
|
227
264
|
// embedded inside task_description, so they must be resolved up front.
|
package/engine/shared.js
CHANGED
|
@@ -3503,6 +3503,9 @@ const FAILURE_CLASS = {
|
|
|
3503
3503
|
MANAGED_SPAWN_HEALTHCHECK_FAILED: 'managed-spawn-healthcheck-failed', // P-7a3b1c92: at least one managed-spawn spec was spawned but failed its healthcheck within timeout_s. Engine killed the failing PIDs; siblings stay alive. Dispatch ERROR with the failing spec name + log tail surfaced in the inbox alert.
|
|
3504
3504
|
INJECTION_FLAGGED: 'injection-flagged', // F5 (W-mpeklod3000we69c): the agent set `securityFlags.injectionAttempt:true` in its completion report after spotting a prompt-injection attempt inside an <UNTRUSTED-INPUT> fence. Engine writes a security inbox note + stamps `_securityFlag` on the WI and treats the dispatch as non-retryable so a human can review the source before the agent re-runs.
|
|
3505
3505
|
MODEL_UNAVAILABLE: 'model-unavailable', // W-mpg6isvy000xca4d: requested model returned overloaded_error / 503 / service_unavailable. Retriable — engine swaps in the runtime-appropriate fallback model on next spawn (Claude leans on --fallback-model already plumbed; Copilot overrides --model with engine.copilotFallbackModel).
|
|
3506
|
+
WORKSPACE_MANIFEST_REPO: 'workspace-manifest-repo-forbidden', // W-mq07avbk000m5543: dispatch routed an agent to a project/repo not present in its workspace_manifest.allowed_repos. Structural — never retryable until the manifest is widened or a different agent is chosen.
|
|
3507
|
+
WORKSPACE_MANIFEST_TOOL: 'workspace-manifest-tool-forbidden', // W-mq07avbk000m5543: out-of-scope tool call (manifest enforcement at the runtime gate). Non-retryable as-is.
|
|
3508
|
+
WORKSPACE_MANIFEST_URL: 'workspace-manifest-url-forbidden', // W-mq07avbk000m5543: out-of-scope external URL fetch. Non-retryable as-is.
|
|
3506
3509
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
3507
3510
|
};
|
|
3508
3511
|
const ESCALATION_POLICY = {
|
|
@@ -3594,6 +3597,243 @@ function pruneDefaultClaudeConfig(config) {
|
|
|
3594
3597
|
return changed;
|
|
3595
3598
|
}
|
|
3596
3599
|
|
|
3600
|
+
// ── Workspace Manifest (W-mq07avbk000m5543) ──────────────────────────────────
|
|
3601
|
+
//
|
|
3602
|
+
// Per-agent declarative permission manifest, inspired by OpenAI's AGENTS.md /
|
|
3603
|
+
// Workspace Manifest primitive (April 2026). Lives on each agent definition
|
|
3604
|
+
// under `workspace_manifest`:
|
|
3605
|
+
//
|
|
3606
|
+
// {
|
|
3607
|
+
// allowed_tools: string[] | null, // tool-name whitelist; null = no restriction
|
|
3608
|
+
// allowed_repos: string[] | null, // canonical PR-scope or owner/repo allow-list; null = no restriction
|
|
3609
|
+
// allowed_external_urls: string[] | null, // host allow-list (apex or *.subdomain); null = no restriction
|
|
3610
|
+
// memory_scope: 'private' | 'shared' | 'read-only-shared' // default 'shared'
|
|
3611
|
+
// }
|
|
3612
|
+
//
|
|
3613
|
+
// Backward compat: any null/missing field is permissive — a config with no
|
|
3614
|
+
// manifest behaves exactly as the pre-manifest engine did. Enforcement is
|
|
3615
|
+
// performed at three points today:
|
|
3616
|
+
//
|
|
3617
|
+
// 1. Dispatch time (engine.js spawnAgent): `agentCanUseRepo()` is checked
|
|
3618
|
+
// against the resolved project; mismatch fails with FAILURE_CLASS.WORKSPACE_MANIFEST_REPO.
|
|
3619
|
+
// 2. Spawn flags (engine.js spawnAgent): `mergeManifestAllowedTools()` narrows
|
|
3620
|
+
// the runtime adapter's `--allowedTools` flag so the CLI itself enforces it.
|
|
3621
|
+
// 3. Documentation/agent context (playbook.js): the manifest is surfaced in the
|
|
3622
|
+
// agent prompt so the agent knows its scope. URL + memory_scope enforcement
|
|
3623
|
+
// remains advisory pending future runtime intercepts.
|
|
3624
|
+
|
|
3625
|
+
const MEMORY_SCOPES = ['private', 'shared', 'read-only-shared'];
|
|
3626
|
+
|
|
3627
|
+
const WORKSPACE_MANIFEST_DEFAULTS = {
|
|
3628
|
+
allowed_tools: null,
|
|
3629
|
+
allowed_repos: null,
|
|
3630
|
+
allowed_external_urls: null,
|
|
3631
|
+
memory_scope: 'shared',
|
|
3632
|
+
};
|
|
3633
|
+
|
|
3634
|
+
function _isStringArrayOrNull(value, fieldName, errors) {
|
|
3635
|
+
if (value == null) return;
|
|
3636
|
+
if (!Array.isArray(value)) {
|
|
3637
|
+
errors.push(`workspace_manifest.${fieldName} must be an array of strings (got ${typeof value})`);
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
for (let i = 0; i < value.length; i++) {
|
|
3641
|
+
if (typeof value[i] !== 'string' || value[i].length === 0) {
|
|
3642
|
+
errors.push(`workspace_manifest.${fieldName}[${i}] must be a non-empty string`);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
/**
|
|
3648
|
+
* Validate a workspace_manifest object. Returns `{ ok, errors }`.
|
|
3649
|
+
* A `null`/`undefined` manifest is valid (it falls through to defaults).
|
|
3650
|
+
*/
|
|
3651
|
+
function validateWorkspaceManifest(manifest) {
|
|
3652
|
+
const errors = [];
|
|
3653
|
+
if (manifest == null) return { ok: true, errors };
|
|
3654
|
+
if (typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
3655
|
+
return { ok: false, errors: ['workspace_manifest must be an object'] };
|
|
3656
|
+
}
|
|
3657
|
+
_isStringArrayOrNull(manifest.allowed_tools, 'allowed_tools', errors);
|
|
3658
|
+
_isStringArrayOrNull(manifest.allowed_repos, 'allowed_repos', errors);
|
|
3659
|
+
_isStringArrayOrNull(manifest.allowed_external_urls, 'allowed_external_urls', errors);
|
|
3660
|
+
if (manifest.memory_scope != null && !MEMORY_SCOPES.includes(manifest.memory_scope)) {
|
|
3661
|
+
errors.push(`workspace_manifest.memory_scope must be one of ${MEMORY_SCOPES.join('|')} (got "${manifest.memory_scope}")`);
|
|
3662
|
+
}
|
|
3663
|
+
return { ok: errors.length === 0, errors };
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
/**
|
|
3667
|
+
* Resolve an agent's effective workspace manifest by merging the per-agent
|
|
3668
|
+
* value (if any) over `WORKSPACE_MANIFEST_DEFAULTS`. Returns a fresh copy on
|
|
3669
|
+
* every call so callers can mutate the result without leaking changes back
|
|
3670
|
+
* into the shared default reference.
|
|
3671
|
+
*
|
|
3672
|
+
* A malformed `agent.workspace_manifest` (non-object) silently falls back to
|
|
3673
|
+
* defaults — the engine logs a warning at config-load time via
|
|
3674
|
+
* `validateWorkspaceManifest`, so a strict reject here would just turn a
|
|
3675
|
+
* documented config error into a runtime crash.
|
|
3676
|
+
*/
|
|
3677
|
+
function resolveAgentManifest(agent, _config) {
|
|
3678
|
+
const out = { ...WORKSPACE_MANIFEST_DEFAULTS };
|
|
3679
|
+
if (!agent || typeof agent !== 'object') return out;
|
|
3680
|
+
const m = agent.workspace_manifest;
|
|
3681
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) return out;
|
|
3682
|
+
if (Array.isArray(m.allowed_tools)) out.allowed_tools = m.allowed_tools.slice();
|
|
3683
|
+
if (Array.isArray(m.allowed_repos)) out.allowed_repos = m.allowed_repos.slice();
|
|
3684
|
+
if (Array.isArray(m.allowed_external_urls)) out.allowed_external_urls = m.allowed_external_urls.slice();
|
|
3685
|
+
if (typeof m.memory_scope === 'string' && MEMORY_SCOPES.includes(m.memory_scope)) {
|
|
3686
|
+
out.memory_scope = m.memory_scope;
|
|
3687
|
+
}
|
|
3688
|
+
return out;
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
function _repoTargetToScopeCandidates(target) {
|
|
3692
|
+
// Accepts a project object or a string identifier. Returns an array of
|
|
3693
|
+
// lowercased canonical/shorthand strings to match against the allow-list.
|
|
3694
|
+
if (!target) return [];
|
|
3695
|
+
if (typeof target === 'string') {
|
|
3696
|
+
const raw = target.trim();
|
|
3697
|
+
if (!raw) return [];
|
|
3698
|
+
const lower = raw.toLowerCase();
|
|
3699
|
+
const candidates = new Set([lower]);
|
|
3700
|
+
// Strip host prefix to surface the bare owner/repo form too.
|
|
3701
|
+
const m = lower.match(/^(github|ado):(.+)$/);
|
|
3702
|
+
if (m) candidates.add(m[2]);
|
|
3703
|
+
return [...candidates];
|
|
3704
|
+
}
|
|
3705
|
+
if (typeof target === 'object') {
|
|
3706
|
+
try {
|
|
3707
|
+
const scope = getProjectPrScope(target);
|
|
3708
|
+
if (scope) {
|
|
3709
|
+
const lower = scope.toLowerCase();
|
|
3710
|
+
const candidates = new Set([lower]);
|
|
3711
|
+
const m = lower.match(/^(github|ado):(.+)$/);
|
|
3712
|
+
if (m) candidates.add(m[2]);
|
|
3713
|
+
return [...candidates];
|
|
3714
|
+
}
|
|
3715
|
+
} catch { /* defensive: malformed project — fall through */ }
|
|
3716
|
+
}
|
|
3717
|
+
return [];
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
/**
|
|
3721
|
+
* Check whether `agent` (with its workspace_manifest) is allowed to operate
|
|
3722
|
+
* against `repoTarget`. `repoTarget` may be a project object or a string
|
|
3723
|
+
* (canonical `github:owner/repo` / `ado:org/proj/repo` or bare `owner/repo`).
|
|
3724
|
+
*
|
|
3725
|
+
* - `null`/missing manifest or `allowed_repos: null` → permissive (returns true).
|
|
3726
|
+
* - Empty allow-list (`[]`) → denies everything (deliberate lockdown).
|
|
3727
|
+
* - Empty/null target → returns false (cannot scope a target with no identity).
|
|
3728
|
+
* - Comparison is case-insensitive; bare `owner/repo` in the manifest matches
|
|
3729
|
+
* both the host-prefixed canonical form and the bare form on the target.
|
|
3730
|
+
*/
|
|
3731
|
+
function agentCanUseRepo(agent, repoTarget) {
|
|
3732
|
+
const manifest = resolveAgentManifest(agent);
|
|
3733
|
+
if (manifest.allowed_repos == null) return true;
|
|
3734
|
+
const candidates = _repoTargetToScopeCandidates(repoTarget);
|
|
3735
|
+
if (candidates.length === 0) return false;
|
|
3736
|
+
const allowed = manifest.allowed_repos.map(r => String(r).trim().toLowerCase()).filter(Boolean);
|
|
3737
|
+
if (allowed.length === 0) return false;
|
|
3738
|
+
// Build the same candidate set for each allow-list entry so bare owner/repo
|
|
3739
|
+
// matches host-prefixed forms and vice versa.
|
|
3740
|
+
for (const entry of allowed) {
|
|
3741
|
+
const entryCandidates = _repoTargetToScopeCandidates(entry);
|
|
3742
|
+
if (entryCandidates.some(e => candidates.includes(e))) return true;
|
|
3743
|
+
}
|
|
3744
|
+
return false;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
/**
|
|
3748
|
+
* Check whether `agent` may invoke `toolName`. Case-sensitive comparison —
|
|
3749
|
+
* tool names are canonical (e.g. `Edit`, `Read`, `Bash`).
|
|
3750
|
+
*
|
|
3751
|
+
* Null/missing `allowed_tools` → permissive.
|
|
3752
|
+
* Empty array → denies everything.
|
|
3753
|
+
*/
|
|
3754
|
+
function agentCanUseTool(agent, toolName) {
|
|
3755
|
+
const manifest = resolveAgentManifest(agent);
|
|
3756
|
+
if (manifest.allowed_tools == null) return true;
|
|
3757
|
+
if (typeof toolName !== 'string' || toolName.length === 0) return false;
|
|
3758
|
+
return manifest.allowed_tools.includes(toolName);
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
/**
|
|
3762
|
+
* Check whether `agent` may fetch `urlString`. Wildcard entries of the form
|
|
3763
|
+
* `*.example.com` match any subdomain AND the apex (`example.com`). Bare
|
|
3764
|
+
* `example.com` matches only the exact host.
|
|
3765
|
+
*
|
|
3766
|
+
* Null/missing `allowed_external_urls` → permissive.
|
|
3767
|
+
* Empty array → denies everything.
|
|
3768
|
+
* Malformed URL → denied (cannot extract host to compare).
|
|
3769
|
+
*/
|
|
3770
|
+
function agentCanFetchUrl(agent, urlString) {
|
|
3771
|
+
const manifest = resolveAgentManifest(agent);
|
|
3772
|
+
if (manifest.allowed_external_urls == null) return true;
|
|
3773
|
+
let host = '';
|
|
3774
|
+
try {
|
|
3775
|
+
host = new URL(String(urlString)).hostname.toLowerCase();
|
|
3776
|
+
} catch { return false; }
|
|
3777
|
+
if (!host) return false;
|
|
3778
|
+
if (manifest.allowed_external_urls.length === 0) return false;
|
|
3779
|
+
for (const raw of manifest.allowed_external_urls) {
|
|
3780
|
+
const entry = String(raw).trim().toLowerCase();
|
|
3781
|
+
if (!entry) continue;
|
|
3782
|
+
if (entry.startsWith('*.')) {
|
|
3783
|
+
const suffix = entry.slice(2);
|
|
3784
|
+
if (host === suffix || host.endsWith('.' + suffix)) return true;
|
|
3785
|
+
} else if (host === entry) {
|
|
3786
|
+
return true;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
return false;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
/** Return the agent's effective memory scope. Unknown values fall back to 'shared'. */
|
|
3793
|
+
function agentMemoryScope(agent) {
|
|
3794
|
+
const manifest = resolveAgentManifest(agent);
|
|
3795
|
+
return MEMORY_SCOPES.includes(manifest.memory_scope) ? manifest.memory_scope : 'shared';
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
/**
|
|
3799
|
+
* Merge a manifest's `allowed_tools` list into the runtime adapter's existing
|
|
3800
|
+
* `allowedTools` CSV baseline. Returns the merged CSV string the runtime
|
|
3801
|
+
* adapter expects (Claude/Copilot/Codex all accept `--allowedTools <csv>`).
|
|
3802
|
+
*
|
|
3803
|
+
* Semantics:
|
|
3804
|
+
* - `manifestTools == null` → baseline unchanged (manifest unrestricted).
|
|
3805
|
+
* - `manifestTools == []` → empty string (deliberate deny-all lockdown).
|
|
3806
|
+
* - `baseline` empty + `manifestTools` non-empty → manifest becomes the new ceiling.
|
|
3807
|
+
* - Otherwise → intersection (manifest narrows baseline); order follows manifest.
|
|
3808
|
+
*/
|
|
3809
|
+
function mergeManifestAllowedTools(baseline, manifestTools) {
|
|
3810
|
+
if (manifestTools == null) return baseline == null ? '' : baseline;
|
|
3811
|
+
if (!Array.isArray(manifestTools)) return baseline == null ? '' : baseline;
|
|
3812
|
+
if (manifestTools.length === 0) return '';
|
|
3813
|
+
const baselineSet = (() => {
|
|
3814
|
+
if (baseline == null || baseline === '') return null; // no baseline = no ceiling
|
|
3815
|
+
const parts = String(baseline).split(',').map(s => s.trim()).filter(Boolean);
|
|
3816
|
+
return new Set(parts);
|
|
3817
|
+
})();
|
|
3818
|
+
if (baselineSet == null) return manifestTools.filter(Boolean).join(',');
|
|
3819
|
+
return manifestTools.filter(t => typeof t === 'string' && baselineSet.has(t)).join(',');
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
/**
|
|
3823
|
+
* Build a human-readable rejection message describing an out-of-scope tool/repo/url
|
|
3824
|
+
* call against an agent's manifest. Used by the dispatch repo gate (engine.js)
|
|
3825
|
+
* and surfaced into the dispatch failure record + inbox alert.
|
|
3826
|
+
*/
|
|
3827
|
+
function formatManifestRejection({ agentId, kind, target, allowed } = {}) {
|
|
3828
|
+
const agent = agentId || '<unknown-agent>';
|
|
3829
|
+
const k = kind || 'tool';
|
|
3830
|
+
const t = (typeof target === 'string' ? target : JSON.stringify(target)) || '<unknown>';
|
|
3831
|
+
const allowList = Array.isArray(allowed) && allowed.length > 0
|
|
3832
|
+
? allowed.join(', ')
|
|
3833
|
+
: '(none)';
|
|
3834
|
+
return `workspace_manifest rejection — agent "${agent}" attempted ${k} "${t}" but its workspace_manifest.allowed_${k}s does not include it. Allowed: ${allowList}.`;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3597
3837
|
// ── Project Helpers ──────────────────────────────────────────────────────────
|
|
3598
3838
|
|
|
3599
3839
|
function getProjects(config) {
|
|
@@ -5926,13 +6166,29 @@ function getPinnedItems() {
|
|
|
5926
6166
|
// Generic rate-limit tracker reusable by both ADO and GitHub integrations.
|
|
5927
6167
|
// Returns an object with recordThrottle, recordSuccess, isThrottled, getState.
|
|
5928
6168
|
|
|
5929
|
-
function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32 * 60000 } = {}) {
|
|
6169
|
+
function createThrottleTracker({ label, baseBackoffMs = 60000, maxBackoffMs = 32 * 60000, jitterRatio = 0.2 } = {}) {
|
|
6170
|
+
// Clamp jitterRatio to [0, 0.5]. Out-of-range or non-finite inputs warn and clamp.
|
|
6171
|
+
// jitter is dither on the SCHEDULED WAKE only — the deterministic 60s→32min
|
|
6172
|
+
// ladder is unchanged. Server Retry-After remains the FLOOR; jitter extends
|
|
6173
|
+
// upward only (multiplier in [1, 1 + jitterRatio]), never shortens.
|
|
6174
|
+
let _jitterRatio = jitterRatio;
|
|
6175
|
+
if (typeof _jitterRatio !== 'number' || !Number.isFinite(_jitterRatio) || _jitterRatio < 0 || _jitterRatio > 0.5) {
|
|
6176
|
+
const clamped = (typeof _jitterRatio === 'number' && Number.isFinite(_jitterRatio))
|
|
6177
|
+
? Math.max(0, Math.min(0.5, _jitterRatio))
|
|
6178
|
+
: 0.2;
|
|
6179
|
+
log('warn', `[${label}] createThrottleTracker: invalid jitterRatio ${_jitterRatio}, clamped to ${clamped}`);
|
|
6180
|
+
_jitterRatio = clamped;
|
|
6181
|
+
}
|
|
6182
|
+
|
|
5930
6183
|
let state = { throttled: false, retryAfter: 0, consecutiveHits: 0, backoffMs: baseBackoffMs };
|
|
5931
6184
|
|
|
5932
6185
|
function recordThrottle(retryAfterMs) {
|
|
5933
6186
|
state.consecutiveHits++;
|
|
5934
6187
|
state.backoffMs = Math.min(state.backoffMs * 2, maxBackoffMs);
|
|
5935
|
-
const
|
|
6188
|
+
const baseWaitMs = (retryAfterMs > 0) ? retryAfterMs : state.backoffMs;
|
|
6189
|
+
// Apply jitter on top of the base wake. Multiplier is in [1, 1 + jitterRatio]
|
|
6190
|
+
// so the wake is jittered upward only — server Retry-After stays a floor.
|
|
6191
|
+
const waitMs = baseWaitMs * (1 + (Math.random() * _jitterRatio));
|
|
5936
6192
|
state.throttled = true;
|
|
5937
6193
|
state.retryAfter = Date.now() + waitMs;
|
|
5938
6194
|
// Throttle retries are deterministic backoff math — info, not warn.
|
|
@@ -6087,6 +6343,17 @@ module.exports = {
|
|
|
6087
6343
|
DEFAULT_AGENTS,
|
|
6088
6344
|
DEFAULT_CLAUDE,
|
|
6089
6345
|
pruneDefaultClaudeConfig,
|
|
6346
|
+
// Workspace manifest (W-mq07avbk000m5543)
|
|
6347
|
+
MEMORY_SCOPES,
|
|
6348
|
+
WORKSPACE_MANIFEST_DEFAULTS,
|
|
6349
|
+
validateWorkspaceManifest,
|
|
6350
|
+
resolveAgentManifest,
|
|
6351
|
+
agentCanUseRepo,
|
|
6352
|
+
agentCanUseTool,
|
|
6353
|
+
agentCanFetchUrl,
|
|
6354
|
+
agentMemoryScope,
|
|
6355
|
+
mergeManifestAllowedTools,
|
|
6356
|
+
formatManifestRejection,
|
|
6090
6357
|
getProjects,
|
|
6091
6358
|
formatUnknownProjectError,
|
|
6092
6359
|
findProjectByName,
|
package/engine.js
CHANGED
|
@@ -1232,6 +1232,37 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1232
1232
|
}
|
|
1233
1233
|
const metaProjectFields = metaProject && typeof metaProject === 'object' ? metaProject : {};
|
|
1234
1234
|
const project = projectResolution.project ? { ...projectResolution.project, ...metaProjectFields } : {};
|
|
1235
|
+
|
|
1236
|
+
// ── Workspace manifest repo gate (W-mq07avbk000m5543) ────────────────────
|
|
1237
|
+
// Dispatch-time enforcement of `agents.<id>.workspace_manifest.allowed_repos`.
|
|
1238
|
+
// Built-in agents and any agent without a manifest are permissive (returns
|
|
1239
|
+
// true). Temp agents (temp-*) don't appear in config.agents so they fall
|
|
1240
|
+
// through to permissive as well. Mismatch fails non-retryably because the
|
|
1241
|
+
// structural answer is "widen the manifest or pick a different agent" —
|
|
1242
|
+
// re-spawning the same agent against the same project would deterministically
|
|
1243
|
+
// re-trip the gate.
|
|
1244
|
+
const _manifestAgent = (config.agents && config.agents[agentId]) || null;
|
|
1245
|
+
if (project && project.name && !shared.agentCanUseRepo(_manifestAgent, project)) {
|
|
1246
|
+
const _manifest = shared.resolveAgentManifest(_manifestAgent);
|
|
1247
|
+
const _scope = (() => { try { return shared.getProjectPrScope(project) || project.name; } catch { return project.name; } })();
|
|
1248
|
+
const msg = shared.formatManifestRejection({
|
|
1249
|
+
agentId,
|
|
1250
|
+
kind: 'repo',
|
|
1251
|
+
target: _scope,
|
|
1252
|
+
allowed: _manifest.allowed_repos,
|
|
1253
|
+
});
|
|
1254
|
+
log('warn', `spawnAgent: workspace-manifest repo gate denied ${agentId} → ${_scope} for ${id}`);
|
|
1255
|
+
completeDispatch(
|
|
1256
|
+
id,
|
|
1257
|
+
DISPATCH_RESULT.ERROR,
|
|
1258
|
+
msg.slice(0, 800),
|
|
1259
|
+
'workspace_manifest.allowed_repos blocks this agent from this repo. Widen the manifest or route the work to a different agent.',
|
|
1260
|
+
{ failureClass: FAILURE_CLASS.WORKSPACE_MANIFEST_REPO, agentRetryable: false },
|
|
1261
|
+
);
|
|
1262
|
+
cleanupTempAgent(agentId);
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1235
1266
|
// W-mp73x32w000l143d: decouple agent cwd from worktree placement.
|
|
1236
1267
|
// resolveSpawnPaths returns:
|
|
1237
1268
|
// - read-only types: { cwd: <project dir or MINIONS_DIR>, worktreeRootDir: null }
|
|
@@ -2405,7 +2436,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2405
2436
|
const args = _buildAgentSpawnFlags(runtime, {
|
|
2406
2437
|
model: effectiveModel,
|
|
2407
2438
|
maxTurns: _maxTurnsForType(type, engineConfig),
|
|
2408
|
-
allowedTools:
|
|
2439
|
+
allowedTools: shared.mergeManifestAllowedTools(
|
|
2440
|
+
claudeConfig.allowedTools,
|
|
2441
|
+
shared.resolveAgentManifest(_manifestAgent).allowed_tools,
|
|
2442
|
+
),
|
|
2409
2443
|
effort: requestedEffort,
|
|
2410
2444
|
sessionId: cachedSessionId,
|
|
2411
2445
|
maxBudget: resolvedMaxBudget,
|
|
@@ -2775,7 +2809,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2775
2809
|
const resumeArgs = _buildAgentSpawnFlags(runtime, {
|
|
2776
2810
|
model: effectiveModel,
|
|
2777
2811
|
maxTurns: engineConfig?.maxTurns || ENGINE_DEFAULTS.maxTurns,
|
|
2778
|
-
allowedTools:
|
|
2812
|
+
allowedTools: shared.mergeManifestAllowedTools(
|
|
2813
|
+
claudeConfig?.allowedTools,
|
|
2814
|
+
shared.resolveAgentManifest(_manifestAgent).allowed_tools,
|
|
2815
|
+
),
|
|
2779
2816
|
sessionId: steerSessionId,
|
|
2780
2817
|
maxBudget: resolvedMaxBudget,
|
|
2781
2818
|
bare: resolvedBare,
|
|
@@ -3944,7 +3981,7 @@ function reconcileItemsWithPrs(items, allPrs, { onlyIds } = {}) {
|
|
|
3944
3981
|
// ─── Inbox Consolidation (extracted to engine/consolidation.js) ──────────────
|
|
3945
3982
|
|
|
3946
3983
|
const { consolidateInbox } = require('./engine/consolidation');
|
|
3947
|
-
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, checkLiveBuildAndConflict: adoCheckLiveBuildAndConflict, needsAdoPollRetry, getAdoToken, isAdoThrottled } = require('./engine/ado');
|
|
3984
|
+
const { pollPrStatus, pollPrHumanComments, reconcilePrs, checkLiveReviewStatus: adoCheckLiveReview, checkLiveBuildAndConflict: adoCheckLiveBuildAndConflict, needsAdoPollRetry, getAdoToken, isAdoThrottled, getAdoThrottleStateAll } = require('./engine/ado');
|
|
3948
3985
|
const { pollPrStatus: ghPollPrStatus, pollPrHumanComments: ghPollPrHumanComments, reconcilePrs: ghReconcilePrs, checkLiveReviewStatus: ghCheckLiveReview, checkLiveBuildAndConflict: ghCheckLiveBuildAndConflict, isGhThrottled } = require('./engine/github');
|
|
3949
3986
|
|
|
3950
3987
|
// ─── State Snapshot ─────────────────────────────────────────────────────────
|
|
@@ -6878,12 +6915,35 @@ async function discoverWork(config) {
|
|
|
6878
6915
|
mutateJsonFileLocked(centralPath, (items) => {
|
|
6879
6916
|
if (!Array.isArray(items)) items = [];
|
|
6880
6917
|
let added = 0;
|
|
6918
|
+
// Snapshot active dedup keys BEFORE the loop so multiple items in the
|
|
6919
|
+
// same harness mission (same _missionId) all land in one tick. Without
|
|
6920
|
+
// this snapshot, the first item's push would block subsequent items
|
|
6921
|
+
// in the same mission from joining (W-mq07a9gf000jbc2b — tri-agent
|
|
6922
|
+
// harness mode requires Planner+Generator+Evaluator to land together).
|
|
6923
|
+
const activeMissionIds = new Set();
|
|
6924
|
+
const activeScheduleIds = new Set();
|
|
6925
|
+
for (const existing of items) {
|
|
6926
|
+
if (existing.status === WI_STATUS.DONE || existing.status === WI_STATUS.FAILED) continue;
|
|
6927
|
+
if (existing._missionId) activeMissionIds.add(existing._missionId);
|
|
6928
|
+
if (existing._scheduleId) activeScheduleIds.add(existing._scheduleId);
|
|
6929
|
+
}
|
|
6930
|
+
const addedScheduleIdsThisTick = new Set();
|
|
6881
6931
|
for (const item of taskItems) {
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6932
|
+
// Mission items dedup by _missionId against pre-existing rows only
|
|
6933
|
+
// (the trio's other items added later in this loop must not block
|
|
6934
|
+
// each other). Plain scheduled items keep the original scheduleId
|
|
6935
|
+
// dedup AND skip if a sibling item from the same tick already
|
|
6936
|
+
// claimed the schedule slot.
|
|
6937
|
+
if (item._missionId) {
|
|
6938
|
+
if (activeMissionIds.has(item._missionId)) continue;
|
|
6939
|
+
} else {
|
|
6940
|
+
if (activeScheduleIds.has(item._scheduleId)) continue;
|
|
6941
|
+
if (addedScheduleIdsThisTick.has(item._scheduleId)) continue;
|
|
6886
6942
|
}
|
|
6943
|
+
items.push(item);
|
|
6944
|
+
if (!item._missionId && item._scheduleId) addedScheduleIdsThisTick.add(item._scheduleId);
|
|
6945
|
+
added++;
|
|
6946
|
+
log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
|
|
6887
6947
|
}
|
|
6888
6948
|
return items;
|
|
6889
6949
|
}, { defaultValue: [] });
|
|
@@ -7349,10 +7409,18 @@ async function tickInner() {
|
|
|
7349
7409
|
lastPrStatusPollAt = now;
|
|
7350
7410
|
// Build promise array — enabled+unthrottled polls run concurrently via Promise.allSettled
|
|
7351
7411
|
const statusPolls = [];
|
|
7352
|
-
if (adoPollEnabled
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
log
|
|
7412
|
+
if (adoPollEnabled) {
|
|
7413
|
+
// Per-org throttle skip happens inside forEachActivePr (one log line per skipped project).
|
|
7414
|
+
// Top-level short-circuit: when every known ADO org is throttled, skip the whole phase
|
|
7415
|
+
// with one log line to avoid the per-project iteration cost.
|
|
7416
|
+
const adoThrottleStates = getAdoThrottleStateAll() || {};
|
|
7417
|
+
const adoOrgCount = Object.keys(adoThrottleStates).length;
|
|
7418
|
+
const allAdoThrottled = adoOrgCount > 0 && Object.values(adoThrottleStates).every(s => s && s.throttled);
|
|
7419
|
+
if (allAdoThrottled) {
|
|
7420
|
+
log('info', `[ado] PR status poll skipped — all ${adoOrgCount} known orgs throttled`);
|
|
7421
|
+
} else {
|
|
7422
|
+
statusPolls.push(pollPrStatus(config).catch(err => { log('warn', `ADO PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
7423
|
+
}
|
|
7356
7424
|
}
|
|
7357
7425
|
if (ghPollEnabled && !isGhThrottled()) {
|
|
7358
7426
|
statusPolls.push(ghPollPrStatus(config).catch(err => { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
@@ -7395,10 +7463,18 @@ async function tickInner() {
|
|
|
7395
7463
|
lastPrCommentsPollAt = now;
|
|
7396
7464
|
// Build promise array — enabled+unthrottled comment polls run concurrently via Promise.allSettled
|
|
7397
7465
|
const commentPolls = [];
|
|
7398
|
-
if (adoPollEnabled
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
log
|
|
7466
|
+
if (adoPollEnabled) {
|
|
7467
|
+
// Per-org throttle skip happens inside forEachActivePr (one log line per skipped project).
|
|
7468
|
+
// Top-level short-circuit: when every known ADO org is throttled, skip the whole phase
|
|
7469
|
+
// with one log line to avoid the per-project iteration cost.
|
|
7470
|
+
const adoThrottleStates = getAdoThrottleStateAll() || {};
|
|
7471
|
+
const adoOrgCount = Object.keys(adoThrottleStates).length;
|
|
7472
|
+
const allAdoThrottled = adoOrgCount > 0 && Object.values(adoThrottleStates).every(s => s && s.throttled);
|
|
7473
|
+
if (allAdoThrottled) {
|
|
7474
|
+
log('info', `[ado] PR comment poll skipped — all ${adoOrgCount} known orgs throttled`);
|
|
7475
|
+
} else {
|
|
7476
|
+
commentPolls.push(pollPrHumanComments(config).catch(err => { log('warn', `ADO PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
|
7477
|
+
}
|
|
7402
7478
|
}
|
|
7403
7479
|
if (ghPollEnabled && !isGhThrottled()) {
|
|
7404
7480
|
commentPolls.push(ghPollPrHumanComments(config).catch(err => { log('warn', `GitHub PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2124",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|